From ab514cc0f2baf47eb88890e3a11e5674acfc887d Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 24 Feb 2023 14:57:07 +0530 Subject: [PATCH] fix: changed ask admin message (#2215) --- ee/query-service/app/api/api.go | 11 +++++-- ee/query-service/app/api/auth.go | 29 +++++++++-------- frontend/src/container/AppLayout/index.tsx | 2 ++ frontend/src/container/Login/index.tsx | 36 +++++++++++++--------- frontend/src/store/reducers/app.ts | 4 +++ frontend/src/types/actions/app.ts | 2 ++ frontend/src/types/api/user/getVersion.ts | 3 +- frontend/src/types/reducer/app.ts | 2 ++ pkg/query-service/app/http_handler.go | 32 ++++++++++++++++++- pkg/query-service/auth/auth.go | 2 +- pkg/query-service/auth/utils.go | 2 +- pkg/query-service/dao/interface.go | 1 + pkg/query-service/dao/sqlite/rbac.go | 9 ++++++ pkg/query-service/model/response.go | 6 ++++ 14 files changed, 109 insertions(+), 32 deletions(-) diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index fd44fba29a..601bed7714 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -9,6 +9,7 @@ import ( "go.signoz.io/signoz/ee/query-service/license" baseapp "go.signoz.io/signoz/pkg/query-service/app" baseint "go.signoz.io/signoz/pkg/query-service/interfaces" + basemodel "go.signoz.io/signoz/pkg/query-service/model" rules "go.signoz.io/signoz/pkg/query-service/rules" "go.signoz.io/signoz/pkg/query-service/version" ) @@ -96,7 +97,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/complete/google", baseapp.OpenAccess(ah.receiveGoogleAuth)). Methods(http.MethodGet) - + router.HandleFunc("/api/v1/orgs/{orgId}/domains", baseapp.AdminAccess(ah.listDomainsByOrg)). Methods(http.MethodGet) @@ -127,5 +128,11 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) { func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { version := version.GetVersion() - ah.WriteJSON(w, r, map[string]string{"version": version, "ee": "Y"}) + versionResponse := basemodel.GetVersionResponse{ + Version: version, + EE: "Y", + SetupCompleted: ah.SetupCompleted, + } + + ah.WriteJSON(w, r, versionResponse) } diff --git a/ee/query-service/app/api/auth.go b/ee/query-service/app/api/auth.go index 2e622408be..8d96320778 100644 --- a/ee/query-service/app/api/auth.go +++ b/ee/query-service/app/api/auth.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "net/url" + "github.com/gorilla/mux" "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/model" @@ -87,9 +88,16 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) { // get invite object invite, err := baseauth.ValidateInvite(ctx, req) - if err != nil || invite == nil { + if err != nil { zap.S().Errorf("failed to validate invite token", err) + RespondError(w, model.BadRequest(err), nil) + return + } + + if invite == nil { + zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err) RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil) + return } // get auth domain from email domain @@ -190,7 +198,7 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) } // receiveGoogleAuth completes google OAuth response and forwards a request -// to front-end to sign user in +// to front-end to sign user in func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) { redirectUri := constants.GetDefaultSiteURL() ctx := context.Background() @@ -221,15 +229,15 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) // 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. + // 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 - + // 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) @@ -239,7 +247,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) handleSsoError(w, r, redirectUri) return } - + nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email) if err != nil { zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err)) @@ -250,15 +258,12 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) 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.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) @@ -287,13 +292,13 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) { // 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. + // 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.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err) @@ -327,6 +332,6 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) { handleSsoError(w, r, redirectUri) return } - + http.Redirect(w, r, nextPage, http.StatusSeeOther) } diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 640a7340a5..c31a2bf6f9 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -153,6 +153,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { type: UPDATE_CURRENT_VERSION, payload: { currentVersion: getUserVersionResponse.data.payload.version, + ee: getUserVersionResponse.data.payload.ee, + setupCompleted: getUserVersionResponse.data.payload.setupCompleted, }, }); } diff --git a/frontend/src/container/Login/index.tsx b/frontend/src/container/Login/index.tsx index e8c25f79f0..a8fda93908 100644 --- a/frontend/src/container/Login/index.tsx +++ b/frontend/src/container/Login/index.tsx @@ -1,4 +1,5 @@ import { Button, Input, Space, Tooltip, Typography } from 'antd'; +import getUserVersion from 'api/user/getVersion'; import loginApi from 'api/user/login'; import loginPrecheckApi from 'api/user/loginPrecheck'; import afterLogin from 'AppRoutes/utils'; @@ -7,6 +8,7 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck'; import { FormContainer, FormWrapper, Label, ParentContainer } from './styles'; @@ -45,6 +47,26 @@ function Login({ const { notifications } = useNotifications(); + const getUserVersionResponse = useQuery({ + queryFn: getUserVersion, + queryKey: 'getUserVersion', + enabled: true, + }); + + useEffect(() => { + if ( + getUserVersionResponse.isFetched && + getUserVersionResponse.data && + getUserVersionResponse.data.payload + ) { + const { setupCompleted } = getUserVersionResponse.data.payload; + if (!setupCompleted) { + // no org account registered yet, re-route user to sign up first + history.push(ROUTES.SIGN_UP); + } + } + }, [getUserVersionResponse]); + useEffect(() => { if (withPassword === 'Y') { setPrecheckComplete(true); @@ -255,20 +277,6 @@ function Login({ )} - {!canSelfRegister && ( - - {t('prompt_create_account')}{' '} - { - history.push(ROUTES.SIGN_UP); - }} - style={{ fontWeight: 700 }} - > - {t('create_an_account')} - - - )} - {canSelfRegister && ( {t('prompt_if_admin')}{' '} diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts index 343ed35941..94ccbc9687 100644 --- a/frontend/src/store/reducers/app.ts +++ b/frontend/src/store/reducers/app.ts @@ -57,6 +57,8 @@ const InitialValue: InitialValueTypes = { role: null, configs: {}, userFlags: {}, + ee: 'Y', + setupCompleted: true, }; const appReducer = ( @@ -89,6 +91,8 @@ const appReducer = ( return { ...state, currentVersion: action.payload.currentVersion, + ee: action.payload.ee, + setupCompleted: action.payload.setupCompleted, }; } diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts index 9b0df64afb..9efd43c45e 100644 --- a/frontend/src/types/actions/app.ts +++ b/frontend/src/types/actions/app.ts @@ -46,6 +46,8 @@ export interface UpdateAppVersion { type: typeof UPDATE_CURRENT_VERSION; payload: { currentVersion: AppReducer['currentVersion']; + ee: AppReducer['ee']; + setupCompleted: AppReducer['setupCompleted']; }; } diff --git a/frontend/src/types/api/user/getVersion.ts b/frontend/src/types/api/user/getVersion.ts index a729875bb6..5419a038c1 100644 --- a/frontend/src/types/api/user/getVersion.ts +++ b/frontend/src/types/api/user/getVersion.ts @@ -1,4 +1,5 @@ export interface PayloadProps { version: string; - ee: string; + ee: 'Y' | 'N'; + setupCompleted: boolean; } diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts index a89f6cb7ae..89c4724310 100644 --- a/frontend/src/types/reducer/app.ts +++ b/frontend/src/types/reducer/app.ts @@ -29,4 +29,6 @@ export default interface AppReducer { featureFlags: null | FeatureFlagPayload; configs: ConfigPayload; userFlags: null | UserFlags; + ee: 'Y' | 'N'; + setupCompleted: boolean; } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 8d49dbb0ad..130d11e368 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -61,6 +61,11 @@ type APIHandler struct { ruleManager *rules.Manager featureFlags interfaces.FeatureLookup ready func(http.HandlerFunc) http.HandlerFunc + + // SetupCompleted indicates if SigNoz is ready for general use. + // at the moment, we mark the app ready when the first user + // is registers. + SetupCompleted bool } type APIHandlerOpts struct { @@ -100,6 +105,19 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { // if errReadingDashboards != nil { // return nil, errReadingDashboards // } + + // check if at least one user is created + hasUsers, err := aH.appDao.GetUsersWithOpts(context.Background(), 1) + if err.Error() != "" { + // raise warning but no panic as this is a recoverable condition + zap.S().Warnf("unexpected error while fetch user count while initializing base api handler", err.Error()) + } + if len(hasUsers) != 0 { + // first user is already created, we can mark the app ready for general use. + // this means, we disable self-registration and expect new users + // to signup signoz through invite link only. + aH.SetupCompleted = true + } return aH, nil } @@ -1645,7 +1663,13 @@ func (aH *APIHandler) getDisks(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { version := version.GetVersion() - aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"}) + versionResponse := model.GetVersionResponse{ + Version: version, + EE: "Y", + SetupCompleted: aH.SetupCompleted, + } + + aH.WriteJSON(w, r, versionResponse) } func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { @@ -1777,6 +1801,12 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) { return } + if !aH.SetupCompleted { + // since the first user is now created, we can disable self-registration as + // from here onwards, we expect admin (owner) to invite other users. + aH.SetupCompleted = true + } + aH.Respond(w, nil) } diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go index ef83cb7fb7..b7fc34e1ed 100644 --- a/pkg/query-service/auth/auth.go +++ b/pkg/query-service/auth/auth.go @@ -267,7 +267,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword bool) (*model.User, *model.ApiError) { if req.InviteToken == "" { - return nil, model.BadRequest(fmt.Errorf("invite token is required")) + return nil, model.BadRequest(ErrorAskAdmin) } if !nopassword && req.Password == "" { diff --git a/pkg/query-service/auth/utils.go b/pkg/query-service/auth/utils.go index df96057229..d76beca5c4 100644 --- a/pkg/query-service/auth/utils.go +++ b/pkg/query-service/auth/utils.go @@ -15,7 +15,7 @@ var ( ErrorInvalidRole = errors.New("Invalid role") ErrorInvalidInviteToken = errors.New("Invalid invite token") - ErrorAskAdmin = errors.New("You are not allowed to create an account. Please ask your admin to send an invite link") + ErrorAskAdmin = errors.New("An invitation is needed to create an account. Please ask your admin (the person who has first installed SIgNoz) to send an invite.") ) func randomHex(sz int) (string, error) { diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index 974b313bf0..ceece7faef 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -19,6 +19,7 @@ type Queries interface { GetUser(ctx context.Context, id string) (*model.UserPayload, *model.ApiError) GetUserByEmail(ctx context.Context, email string) (*model.UserPayload, *model.ApiError) GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) + GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError) GetGroup(ctx context.Context, id string) (*model.Group, *model.ApiError) GetGroupByName(ctx context.Context, name string) (*model.Group, *model.ApiError) diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go index 11bed020a2..64ff7ed8ae 100644 --- a/pkg/query-service/dao/sqlite/rbac.go +++ b/pkg/query-service/dao/sqlite/rbac.go @@ -345,7 +345,13 @@ func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context, return &users[0], nil } +// GetUsers fetches total user count func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) { + return mds.GetUsersWithOpts(ctx, 0) +} + +// GetUsersWithOpts fetches users and supports additional search options +func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError) { users := []model.UserPayload{} query := `select @@ -364,6 +370,9 @@ func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, * g.id = u.group_id and o.id = u.org_id` + if limit > 0 { + query = fmt.Sprintf("%s LIMIT %d", query, limit) + } err := mds.db.Select(&users, query) if err != nil { diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 01afb950e4..8aa815bc19 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -585,3 +585,9 @@ func (ci *ClusterInfo) GetMapFromStruct() map[string]interface{} { json.Unmarshal(data, &clusterInfoMap) return clusterInfoMap } + +type GetVersionResponse struct { + Version string `json:"version"` + EE string `json:"ee"` + SetupCompleted bool `json:"setupCompleted"` +}