mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-13 08:51:31 +08:00

* feat: added license manager and feature flags * feat: completed org domain api * chore: checking in saml auth handler code * feat: added signup with sso * feat: added login support for admins * feat: added pem support for certificate * ci(build-workflow): 👷 include EE query-service * fix: 🐛 update package name * chore(ee): 🔧 LD_FLAGS related changes Signed-off-by: Prashant Shahi <prashant@signoz.io> Co-authored-by: Prashant Shahi <prashant@signoz.io> Co-authored-by: nityanandagohain <nityanandagohain@gmail.com>
298 lines
8.3 KiB
Go
298 lines
8.3 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"go.signoz.io/signoz/ee/query-service/constants"
|
|
"go.signoz.io/signoz/ee/query-service/model"
|
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func parseRequest(r *http.Request, req interface{}) error {
|
|
defer r.Body.Close()
|
|
requestBody, err := ioutil.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 := auth.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 := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
zap.S().Errorf("received no input in api\n", err)
|
|
RespondError(w, model.BadRequest(err), nil)
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal(requestBody, &req)
|
|
|
|
if err != nil {
|
|
zap.S().Errorf("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 || invite == nil {
|
|
zap.S().Errorf("failed to validate invite token", err)
|
|
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
|
|
}
|
|
|
|
// get auth domain from email domain
|
|
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
|
|
if apierr != nil {
|
|
zap.S().Errorf("failed to get domain from email", apierr)
|
|
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
|
|
}
|
|
|
|
precheckResp := &model.PrecheckResponse{
|
|
SSO: false,
|
|
IsUser: false,
|
|
}
|
|
|
|
if domain != nil && domain.SsoEnabled {
|
|
// so 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 := auth.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 (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()
|
|
|
|
var apierr basemodel.BaseApiError
|
|
|
|
redirectOnError := func() {
|
|
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", redirectUri, string(dst)), http.StatusMovedPermanently)
|
|
}
|
|
|
|
if !ah.CheckFeature(model.SSO) {
|
|
zap.S().Errorf("[ReceiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
|
|
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.S().Errorf("[ReceiveSAML] failed to process response - invalid response from IDP", err, r)
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
// the relay state is sent when a login request is submitted to
|
|
// Idp.
|
|
relayState := r.FormValue("RelayState")
|
|
zap.S().Debug("[ReceiveML] relay state", zap.String("relayState", relayState))
|
|
|
|
parsedState, err := url.Parse(relayState)
|
|
if err != nil || relayState == "" {
|
|
zap.S().Errorf("[ReceiveSAML] failed to process response - invalid response from IDP", err, r)
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
// upgrade redirect url from the relay state for better accuracy
|
|
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
|
|
|
// derive domain id from relay state now
|
|
var domainIdStr string
|
|
for k, v := range parsedState.Query() {
|
|
if k == "domainId" && len(v) > 0 {
|
|
domainIdStr = strings.Replace(v[0], ":", "-", -1)
|
|
}
|
|
}
|
|
|
|
domainId, err := uuid.Parse(domainIdStr)
|
|
if err != nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to process request- failed to parse domain id ifrom relay", zap.Error(err))
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
domain, apierr := ah.AppDao().GetDomain(ctx, domainId)
|
|
if (apierr != nil) || domain == nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to process request- invalid domain", domainIdStr, zap.Error(apierr))
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
sp, err := domain.PrepareSamlRequest(parsedState)
|
|
if err != nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to prepare saml request for domain (%s): %v", domainId, err)
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
|
|
if err != nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domainId, err)
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
if assertionInfo.WarningInfo.InvalidTime {
|
|
zap.S().Errorf("[ReceiveSAML] expired saml response for organization (%s): %v", domainId, err)
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
email := assertionInfo.NameID
|
|
|
|
// user email found, now start preparing jwt response
|
|
userPayload, baseapierr := ah.AppDao().GetUserByEmail(ctx, email)
|
|
if baseapierr != nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to find or register a new user for email %s and org %s", email, domainId, zap.Error(baseapierr.Err))
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
tokenStore, err := baseauth.GenerateJWTForUser(&userPayload.User)
|
|
if err != nil {
|
|
zap.S().Errorf("[ReceiveSAML] failed to generate access token for email %s and org %s", email, domainId, zap.Error(err))
|
|
redirectOnError()
|
|
return
|
|
}
|
|
|
|
userID := userPayload.User.Id
|
|
nextPage := fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
|
redirectUri,
|
|
tokenStore.AccessJwt,
|
|
userID,
|
|
tokenStore.RefreshJwt)
|
|
|
|
http.Redirect(w, r, nextPage, http.StatusMovedPermanently)
|
|
}
|