343 lines
10 KiB
Go
343 lines
10 KiB
Go
|
package api
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
|
||
|
"github.com/gorilla/mux"
|
||
|
"go.uber.org/zap"
|
||
|
|
||
|
"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()
|
||
|
requestBody, err := io.ReadAll(r.Body)
|
||
|
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
|
||
|
resp, err := baseauth.Login(ctx, &req)
|
||
|
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()
|
||
|
requestBody, err := io.ReadAll(r.Body)
|
||
|
if err != nil {
|
||
|
zap.L().Error("received no input in api", zap.Error(err))
|
||
|
RespondError(w, model.BadRequest(err), nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(requestBody, &req)
|
||
|
|
||
|
if err != nil {
|
||
|
zap.L().Error("received invalid user registration request", zap.Error(err))
|
||
|
RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// get invite object
|
||
|
invite, err := baseauth.ValidateInvite(ctx, req)
|
||
|
if err != nil {
|
||
|
zap.L().Error("failed to validate invite token", zap.Error(err))
|
||
|
RespondError(w, model.BadRequest(err), nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if invite == nil {
|
||
|
zap.L().Error("failed to validate invite token: it is either empty or invalid", zap.Error(err))
|
||
|
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// get auth domain from email domain
|
||
|
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
|
||
|
if apierr != nil {
|
||
|
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||
|
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
|
||
|
}
|
||
|
|
||
|
precheckResp := &basemodel.PrecheckResponse{
|
||
|
SSO: false,
|
||
|
IsUser: false,
|
||
|
}
|
||
|
|
||
|
if domain != nil && domain.SsoEnabled {
|
||
|
// sso is enabled, create user and respond precheck data
|
||
|
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
|
||
|
if err := baseauth.ValidatePassword(req.Password); err != nil {
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
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
|
||
|
// to front-end to sign user in
|
||
|
func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
|
||
|
redirectUri := constants.GetDefaultSiteURL()
|
||
|
ctx := context.Background()
|
||
|
|
||
|
if !ah.CheckFeature(model.SSO) {
|
||
|
zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
|
||
|
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 != "" {
|
||
|
zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
|
||
|
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
relayState := q.Get("state")
|
||
|
zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
|
||
|
|
||
|
parsedState, err := url.Parse(relayState)
|
||
|
if err != nil || relayState == "" {
|
||
|
zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// upgrade redirect url from the relay state for better accuracy
|
||
|
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
||
|
|
||
|
// fetch domain by parsing relay state.
|
||
|
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
||
|
if err != nil {
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// now that we have domain, use domain to fetch sso settings.
|
||
|
// prepare google callback handler using parsedState -
|
||
|
// which contains redirect URL (front-end endpoint)
|
||
|
callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
|
||
|
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
|
||
|
}
|
||
|
|
||
|
identity, err := callbackHandler.HandleCallback(r)
|
||
|
if err != nil {
|
||
|
zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
|
||
|
if err != nil {
|
||
|
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
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()
|
||
|
|
||
|
if !ah.CheckFeature(model.SSO) {
|
||
|
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
|
||
|
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 {
|
||
|
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// the relay state is sent when a login request is submitted to
|
||
|
// Idp.
|
||
|
relayState := r.FormValue("RelayState")
|
||
|
zap.L().Debug("[receiveML] relay state", zap.String("relayState", relayState))
|
||
|
|
||
|
parsedState, err := url.Parse(relayState)
|
||
|
if err != nil || relayState == "" {
|
||
|
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// upgrade redirect url from the relay state for better accuracy
|
||
|
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
||
|
|
||
|
// fetch domain by parsing relay state.
|
||
|
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
||
|
if err != nil {
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
sp, err := domain.PrepareSamlRequest(parsedState)
|
||
|
if err != nil {
|
||
|
zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
|
||
|
if err != nil {
|
||
|
zap.L().Error("[receiveSAML] failed to retrieve assertion info from saml response", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if assertionInfo.WarningInfo.InvalidTime {
|
||
|
zap.L().Error("[receiveSAML] expired saml response", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
email := assertionInfo.NameID
|
||
|
if email == "" {
|
||
|
zap.L().Error("[receiveSAML] invalid email in the SSO response", zap.String("domain", domain.String()))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
|
||
|
if err != nil {
|
||
|
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||
|
handleSsoError(w, r, redirectUri)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
http.Redirect(w, r, nextPage, http.StatusSeeOther)
|
||
|
}
|