2022-10-06 20:13:30 +05:30
package api
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
2023-10-19 14:16:20 +05:30
"io"
2022-10-06 20:13:30 +05:30
"net/http"
"net/url"
2023-02-24 14:57:07 +05:30
2022-10-06 20:13:30 +05:30
"github.com/gorilla/mux"
2023-10-19 14:16:20 +05:30
"go.uber.org/zap"
2022-10-06 20:13:30 +05:30
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
func parseRequest ( r * http . Request , req interface { } ) error {
defer r . Body . Close ( )
2023-10-19 14:16:20 +05:30
requestBody , err := io . ReadAll ( r . Body )
2022-10-06 20:13:30 +05:30
if err != nil {
return err
}
err = json . Unmarshal ( requestBody , & req )
return err
}
// loginUser overrides base handler and considers SSO case.
func ( ah * APIHandler ) loginUser ( w http . ResponseWriter , r * http . Request ) {
req := basemodel . LoginRequest { }
err := parseRequest ( r , & req )
if err != nil {
RespondError ( w , model . BadRequest ( err ) , nil )
return
}
ctx := context . Background ( )
if req . Email != "" && ah . CheckFeature ( model . SSO ) {
var apierr basemodel . BaseApiError
_ , apierr = ah . AppDao ( ) . CanUsePassword ( ctx , req . Email )
if apierr != nil && ! apierr . IsNil ( ) {
RespondError ( w , apierr , nil )
}
}
// if all looks good, call auth
2024-06-11 20:10:38 +05:30
resp , err := baseauth . Login ( ctx , & req )
2022-10-06 20:13:30 +05:30
if ah . HandleError ( w , err , http . StatusUnauthorized ) {
return
}
ah . WriteJSON ( w , r , resp )
}
// registerUser registers a user and responds with a precheck
// so the front-end can decide the login method
func ( ah * APIHandler ) registerUser ( w http . ResponseWriter , r * http . Request ) {
if ! ah . CheckFeature ( model . SSO ) {
ah . APIHandler . Register ( w , r )
return
}
ctx := context . Background ( )
var req * baseauth . RegisterRequest
defer r . Body . Close ( )
2023-10-19 14:16:20 +05:30
requestBody , err := io . ReadAll ( r . Body )
2022-10-06 20:13:30 +05:30
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "received no input in api" , zap . Error ( err ) )
2022-10-06 20:13:30 +05:30
RespondError ( w , model . BadRequest ( err ) , nil )
return
}
err = json . Unmarshal ( requestBody , & req )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "received invalid user registration request" , zap . Error ( err ) )
2022-10-06 20:13:30 +05:30
RespondError ( w , model . BadRequest ( fmt . Errorf ( "failed to register user" ) ) , nil )
return
}
// get invite object
invite , err := baseauth . ValidateInvite ( ctx , req )
2023-02-24 14:57:07 +05:30
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "failed to validate invite token" , zap . Error ( err ) )
2023-02-24 14:57:07 +05:30
RespondError ( w , model . BadRequest ( err ) , nil )
return
}
if invite == nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "failed to validate invite token: it is either empty or invalid" , zap . Error ( err ) )
2022-10-06 20:13:30 +05:30
RespondError ( w , model . BadRequest ( basemodel . ErrSignupFailed { } ) , nil )
2023-02-24 14:57:07 +05:30
return
2022-10-06 20:13:30 +05:30
}
// get auth domain from email domain
domain , apierr := ah . AppDao ( ) . GetDomainByEmail ( ctx , invite . Email )
if apierr != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "failed to get domain from email" , zap . Error ( apierr ) )
2022-10-06 20:13:30 +05:30
RespondError ( w , model . InternalError ( basemodel . ErrSignupFailed { } ) , nil )
}
2023-09-12 12:53:46 +05:30
precheckResp := & basemodel . PrecheckResponse {
2022-10-06 20:13:30 +05:30
SSO : false ,
IsUser : false ,
}
if domain != nil && domain . SsoEnabled {
2023-08-23 16:22:24 +05:30
// sso is enabled, create user and respond precheck data
2022-10-06 20:13:30 +05:30
user , apierr := baseauth . RegisterInvitedUser ( ctx , req , true )
if apierr != nil {
RespondError ( w , apierr , nil )
return
}
var precheckError basemodel . BaseApiError
precheckResp , precheckError = ah . AppDao ( ) . PrecheckLogin ( ctx , user . Email , req . SourceUrl )
if precheckError != nil {
RespondError ( w , precheckError , precheckResp )
}
} else {
// no-sso, validate password
2024-06-11 20:10:38 +05:30
if err := baseauth . ValidatePassword ( req . Password ) ; err != nil {
2022-10-06 20:13:30 +05:30
RespondError ( w , model . InternalError ( fmt . Errorf ( "password is not in a valid format" ) ) , nil )
return
}
_ , registerError := baseauth . Register ( ctx , req )
if ! registerError . IsNil ( ) {
RespondError ( w , apierr , nil )
return
}
precheckResp . IsUser = true
}
ah . Respond ( w , precheckResp )
}
// getInvite returns the invite object details for the given invite token. We do not need to
// protect this API because invite token itself is meant to be private.
func ( ah * APIHandler ) getInvite ( w http . ResponseWriter , r * http . Request ) {
token := mux . Vars ( r ) [ "token" ]
sourceUrl := r . URL . Query ( ) . Get ( "ref" )
ctx := context . Background ( )
inviteObject , err := baseauth . GetInvite ( context . Background ( ) , token )
if err != nil {
RespondError ( w , model . BadRequest ( err ) , nil )
return
}
resp := model . GettableInvitation {
InvitationResponseObject : inviteObject ,
}
precheck , apierr := ah . AppDao ( ) . PrecheckLogin ( ctx , inviteObject . Email , sourceUrl )
resp . Precheck = precheck
if apierr != nil {
RespondError ( w , apierr , resp )
}
ah . WriteJSON ( w , r , resp )
}
// PrecheckLogin enables browser login page to display appropriate
// login methods
func ( ah * APIHandler ) precheckLogin ( w http . ResponseWriter , r * http . Request ) {
ctx := context . Background ( )
email := r . URL . Query ( ) . Get ( "email" )
sourceUrl := r . URL . Query ( ) . Get ( "ref" )
resp , apierr := ah . AppDao ( ) . PrecheckLogin ( ctx , email , sourceUrl )
if apierr != nil {
RespondError ( w , apierr , resp )
}
ah . Respond ( w , resp )
}
2022-12-06 22:32:59 +05:30
func handleSsoError ( w http . ResponseWriter , r * http . Request , redirectURL string ) {
ssoError := [ ] byte ( "Login failed. Please contact your system administrator" )
dst := make ( [ ] byte , base64 . StdEncoding . EncodedLen ( len ( ssoError ) ) )
base64 . StdEncoding . Encode ( dst , ssoError )
http . Redirect ( w , r , fmt . Sprintf ( "%s?ssoerror=%s" , redirectURL , string ( dst ) ) , http . StatusSeeOther )
}
// receiveGoogleAuth completes google OAuth response and forwards a request
2023-02-24 14:57:07 +05:30
// to front-end to sign user in
2022-12-06 22:32:59 +05:30
func ( ah * APIHandler ) receiveGoogleAuth ( w http . ResponseWriter , r * http . Request ) {
2022-10-06 20:13:30 +05:30
redirectUri := constants . GetDefaultSiteURL ( )
ctx := context . Background ( )
2022-12-06 22:32:59 +05:30
if ! ah . CheckFeature ( model . SSO ) {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveGoogleAuth] sso requested but feature unavailable in org domain" )
2022-12-06 22:32:59 +05:30
http . Redirect ( w , r , fmt . Sprintf ( "%s?ssoerror=%s" , redirectUri , "feature unavailable, please upgrade your billing plan to access this feature" ) , http . StatusMovedPermanently )
return
}
q := r . URL . Query ( )
if errType := q . Get ( "error" ) ; errType != "" {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveGoogleAuth] failed to login with google auth" , zap . String ( "error" , errType ) , zap . String ( "error_description" , q . Get ( "error_description" ) ) )
2022-12-06 22:32:59 +05:30
http . Redirect ( w , r , fmt . Sprintf ( "%s?ssoerror=%s" , redirectUri , "failed to login through SSO " ) , http . StatusMovedPermanently )
return
}
relayState := q . Get ( "state" )
2024-03-27 00:07:29 +05:30
zap . L ( ) . Debug ( "[receiveGoogleAuth] relay state received" , zap . String ( "state" , relayState ) )
2022-10-06 20:13:30 +05:30
2022-12-06 22:32:59 +05:30
parsedState , err := url . Parse ( relayState )
if err != nil || relayState == "" {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveGoogleAuth] failed to process response - invalid response from IDP" , zap . Error ( err ) , zap . Any ( "request" , r ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
return
}
2022-10-06 20:13:30 +05:30
2022-12-06 22:32:59 +05:30
// upgrade redirect url from the relay state for better accuracy
redirectUri = fmt . Sprintf ( "%s://%s%s" , parsedState . Scheme , parsedState . Host , "/login" )
2023-02-24 14:57:07 +05:30
// fetch domain by parsing relay state.
2022-12-06 22:32:59 +05:30
domain , err := ah . AppDao ( ) . GetDomainFromSsoResponse ( ctx , parsedState )
if err != nil {
handleSsoError ( w , r , redirectUri )
return
2022-10-06 20:13:30 +05:30
}
2023-02-24 14:57:07 +05:30
// now that we have domain, use domain to fetch sso settings.
// prepare google callback handler using parsedState -
2022-12-06 22:32:59 +05:30
// which contains redirect URL (front-end endpoint)
callbackHandler , err := domain . PrepareGoogleOAuthProvider ( parsedState )
2024-06-11 20:10:38 +05:30
if err != nil {
zap . L ( ) . Error ( "[receiveGoogleAuth] failed to prepare google oauth provider" , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
handleSsoError ( w , r , redirectUri )
return
}
2022-12-06 22:32:59 +05:30
identity , err := callbackHandler . HandleCallback ( r )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveGoogleAuth] failed to process HandleCallback " , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
return
}
2023-02-24 14:57:07 +05:30
2022-12-06 22:32:59 +05:30
nextPage , err := ah . AppDao ( ) . PrepareSsoRedirect ( ctx , redirectUri , identity . Email )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveGoogleAuth] failed to generate redirect URI after successful login " , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
return
}
http . Redirect ( w , r , nextPage , http . StatusSeeOther )
}
// receiveSAML completes a SAML request and gets user logged in
func ( ah * APIHandler ) receiveSAML ( w http . ResponseWriter , r * http . Request ) {
// this is the source url that initiated the login request
redirectUri := constants . GetDefaultSiteURL ( )
ctx := context . Background ( )
2022-10-06 20:13:30 +05:30
if ! ah . CheckFeature ( model . SSO ) {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] sso requested but feature unavailable in org domain" )
2022-10-06 20:13:30 +05:30
http . Redirect ( w , r , fmt . Sprintf ( "%s?ssoerror=%s" , redirectUri , "feature unavailable, please upgrade your billing plan to access this feature" ) , http . StatusMovedPermanently )
return
}
err := r . ParseForm ( )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] failed to process response - invalid response from IDP" , zap . Error ( err ) , zap . Any ( "request" , r ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
// the relay state is sent when a login request is submitted to
// Idp.
relayState := r . FormValue ( "RelayState" )
2024-03-27 00:07:29 +05:30
zap . L ( ) . Debug ( "[receiveML] relay state" , zap . String ( "relayState" , relayState ) )
2022-10-06 20:13:30 +05:30
parsedState , err := url . Parse ( relayState )
if err != nil || relayState == "" {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] failed to process response - invalid response from IDP" , zap . Error ( err ) , zap . Any ( "request" , r ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
// upgrade redirect url from the relay state for better accuracy
redirectUri = fmt . Sprintf ( "%s://%s%s" , parsedState . Scheme , parsedState . Host , "/login" )
2023-02-24 14:57:07 +05:30
// fetch domain by parsing relay state.
2022-12-06 22:32:59 +05:30
domain , err := ah . AppDao ( ) . GetDomainFromSsoResponse ( ctx , parsedState )
2022-10-06 20:13:30 +05:30
if err != nil {
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
2023-02-24 14:57:07 +05:30
2022-10-06 20:13:30 +05:30
sp , err := domain . PrepareSamlRequest ( parsedState )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] failed to prepare saml request for domain" , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
assertionInfo , err := sp . RetrieveAssertionInfo ( r . FormValue ( "SAMLResponse" ) )
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] failed to retrieve assertion info from saml response" , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
if assertionInfo . WarningInfo . InvalidTime {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] expired saml response" , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
email := assertionInfo . NameID
2022-12-06 22:32:59 +05:30
if email == "" {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] invalid email in the SSO response" , zap . String ( "domain" , domain . String ( ) ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
2022-12-06 22:32:59 +05:30
nextPage , err := ah . AppDao ( ) . PrepareSsoRedirect ( ctx , redirectUri , email )
2022-10-06 20:13:30 +05:30
if err != nil {
2024-03-27 00:07:29 +05:30
zap . L ( ) . Error ( "[receiveSAML] failed to generate redirect URI after successful login " , zap . String ( "domain" , domain . String ( ) ) , zap . Error ( err ) )
2022-12-06 22:32:59 +05:30
handleSsoError ( w , r , redirectUri )
2022-10-06 20:13:30 +05:30
return
}
2023-02-24 14:57:07 +05:30
2022-12-06 22:32:59 +05:30
http . Redirect ( w , r , nextPage , http . StatusSeeOther )
2022-10-06 20:13:30 +05:30
}