From d00024b64aa21d440bfb3545ed88cb70b3be856b Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Mon, 29 Jul 2024 09:51:18 +0530 Subject: [PATCH] feat: preference framework qs changes (#5527) * feat: query service changes base setup for preferences * feat: added handlers for user and org preferences * chore: added base for all user and all org preferences * feat: added handlers for all user and all org preferences * feat: register the preference routes and initDB in pkg/query-service * feat: code refactor * chore: too much fun code refactor * chore: little little missing attributes * fix: handle range queries better * fix: handle range queries better * chore: address review comments * chore: use struct inheritance for the all preferences struct * chore: address review comments * chore: address review comments * chore: correct preference routes * chore: low hanging optimisations * chore: address review comments * chore: address review comments * chore: added extra validations for the check in allowed values * fix: better handling for the jwt claims * fix: better handling for the jwt claims * chore: move the error to preference apis * chore: move the error to preference apis * fix: move the 401 logic to the auth middleware --- ee/query-service/app/server.go | 19 +- pkg/query-service/app/http_handler.go | 127 +++++ pkg/query-service/app/preferences/map.go | 37 ++ pkg/query-service/app/preferences/model.go | 544 +++++++++++++++++++++ pkg/query-service/app/server.go | 22 +- pkg/query-service/auth/auth.go | 6 + pkg/query-service/auth/jwt.go | 10 + 7 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 pkg/query-service/app/preferences/map.go create mode 100644 pkg/query-service/app/preferences/model.go diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index ac941cec37..c41788e971 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -27,6 +28,7 @@ import ( "go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/interfaces" baseauth "go.signoz.io/signoz/pkg/query-service/auth" + "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" licensepkg "go.signoz.io/signoz/ee/query-service/license" @@ -40,6 +42,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/opamp" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" + "go.signoz.io/signoz/pkg/query-service/app/preferences" "go.signoz.io/signoz/pkg/query-service/cache" baseconst "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/healthcheck" @@ -109,6 +112,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH) + if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil { + return nil, err + } + localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH) if err != nil { @@ -319,7 +326,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e // add auth middleware getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { - return auth.GetUserFromRequest(r, apiHandler) + user, err := auth.GetUserFromRequest(r, apiHandler) + + if err != nil { + return nil, err + } + + if user.User.OrgId == "" { + return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims")) + } + + return user, nil } am := baseapp.NewAuthMiddleware(getUserFromRequest) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 42879123ec..3e4ddfbfcf 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -29,12 +29,14 @@ import ( logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" "go.signoz.io/signoz/pkg/query-service/app/metrics" metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" + "go.signoz.io/signoz/pkg/query-service/app/preferences" "go.signoz.io/signoz/pkg/query-service/app/querier" querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/cache" + "go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/constants" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/postprocess" @@ -398,6 +400,22 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet) + // === Preference APIs === + + // user actions + router.HandleFunc("/api/v1/user/preferences", am.ViewAccess(aH.getAllUserPreferences)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.getUserPreference)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.updateUserPreference)).Methods(http.MethodPut) + + // org actions + router.HandleFunc("/api/v1/org/preferences", am.AdminAccess(aH.getAllOrgPreferences)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.getOrgPreference)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.updateOrgPreference)).Methods(http.MethodPut) + // === Authentication APIs === router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost) router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet) @@ -2192,6 +2210,115 @@ func (aH *APIHandler) WriteJSON(w http.ResponseWriter, r *http.Request, response w.Write(resp) } +// Preferences + +func (ah *APIHandler) getUserPreference( + w http.ResponseWriter, r *http.Request, +) { + preferenceId := mux.Vars(r)["preferenceId"] + user := common.GetUserFromContext(r.Context()) + + preference, apiErr := preferences.GetUserPreference( + r.Context(), preferenceId, user.User.OrgId, user.User.Id, + ) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + +func (ah *APIHandler) updateUserPreference( + w http.ResponseWriter, r *http.Request, +) { + preferenceId := mux.Vars(r)["preferenceId"] + user := common.GetUserFromContext(r.Context()) + req := preferences.UpdatePreference{} + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.Id) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + +func (ah *APIHandler) getAllUserPreferences( + w http.ResponseWriter, r *http.Request, +) { + user := common.GetUserFromContext(r.Context()) + preference, apiErr := preferences.GetAllUserPreferences( + r.Context(), user.User.OrgId, user.User.Id, + ) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + +func (ah *APIHandler) getOrgPreference( + w http.ResponseWriter, r *http.Request, +) { + preferenceId := mux.Vars(r)["preferenceId"] + user := common.GetUserFromContext(r.Context()) + preference, apiErr := preferences.GetOrgPreference( + r.Context(), preferenceId, user.User.OrgId, + ) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + +func (ah *APIHandler) updateOrgPreference( + w http.ResponseWriter, r *http.Request, +) { + preferenceId := mux.Vars(r)["preferenceId"] + req := preferences.UpdatePreference{} + user := common.GetUserFromContext(r.Context()) + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgId) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + +func (ah *APIHandler) getAllOrgPreferences( + w http.ResponseWriter, r *http.Request, +) { + user := common.GetUserFromContext(r.Context()) + preference, apiErr := preferences.GetAllOrgPreferences( + r.Context(), user.User.OrgId, + ) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + ah.Respond(w, preference) +} + // Integrations func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) { subRouter := router.PathPrefix("/api/v1/integrations").Subrouter() diff --git a/pkg/query-service/app/preferences/map.go b/pkg/query-service/app/preferences/map.go new file mode 100644 index 0000000000..219fb6c595 --- /dev/null +++ b/pkg/query-service/app/preferences/map.go @@ -0,0 +1,37 @@ +package preferences + +var preferenceMap = map[string]Preference{ + "DASHBOARDS_LIST_VIEW": { + Key: "DASHBOARDS_LIST_VIEW", + Name: "Dashboards List View", + Description: "", + ValueType: "string", + DefaultValue: "grid", + AllowedValues: []interface{}{"grid", "list"}, + IsDiscreteValues: true, + AllowedScopes: []string{"user", "org"}, + }, + "LOGS_TOOLBAR_COLLAPSED": { + Key: "LOGS_TOOLBAR_COLLAPSED", + Name: "Logs toolbar", + Description: "", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user", "org"}, + }, + "MAX_DEPTH_ALLOWED": { + Key: "MAX_DEPTH_ALLOWED", + Name: "Max Depth Allowed", + Description: "", + ValueType: "integer", + DefaultValue: 10, + IsDiscreteValues: false, + Range: Range{ + Min: 0, + Max: 100, + }, + AllowedScopes: []string{"user", "org"}, + }, +} diff --git a/pkg/query-service/app/preferences/model.go b/pkg/query-service/app/preferences/model.go new file mode 100644 index 0000000000..82b8e9c9f6 --- /dev/null +++ b/pkg/query-service/app/preferences/model.go @@ -0,0 +1,544 @@ +package preferences + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/jmoiron/sqlx" + "go.signoz.io/signoz/ee/query-service/model" +) + +type Range struct { + Min int64 `json:"min"` + Max int64 `json:"max"` +} + +type Preference struct { + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + ValueType string `json:"valueType"` + DefaultValue interface{} `json:"defaultValue"` + AllowedValues []interface{} `json:"allowedValues"` + IsDiscreteValues bool `json:"isDiscreteValues"` + Range Range `json:"range"` + AllowedScopes []string `json:"allowedScopes"` +} + +func (p *Preference) ErrorValueTypeMismatch() *model.ApiError { + return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not of expected type: %s", p.ValueType)} +} + +const ( + PreferenceValueTypeInteger string = "integer" + PreferenceValueTypeFloat string = "float" + PreferenceValueTypeString string = "string" + PreferenceValueTypeBoolean string = "boolean" +) + +const ( + OrgAllowedScope string = "org" + UserAllowedScope string = "user" +) + +func (p *Preference) checkIfInAllowedValues(preferenceValue interface{}) (bool, *model.ApiError) { + + switch p.ValueType { + case PreferenceValueTypeInteger: + _, ok := preferenceValue.(int64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeFloat: + _, ok := preferenceValue.(float64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeString: + _, ok := preferenceValue.(string) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeBoolean: + _, ok := preferenceValue.(bool) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + } + isInAllowedValues := false + for _, value := range p.AllowedValues { + switch p.ValueType { + case PreferenceValueTypeInteger: + allowedValue, ok := value.(int64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeFloat: + allowedValue, ok := value.(float64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeString: + allowedValue, ok := value.(string) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeBoolean: + allowedValue, ok := value.(bool) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + } + } + return isInAllowedValues, nil +} + +func (p *Preference) IsValidValue(preferenceValue interface{}) *model.ApiError { + typeSafeValue := preferenceValue + switch p.ValueType { + case PreferenceValueTypeInteger: + val, ok := preferenceValue.(int64) + if !ok { + floatVal, ok := preferenceValue.(float64) + if !ok || floatVal != float64(int64(floatVal)) { + return p.ErrorValueTypeMismatch() + } + val = int64(floatVal) + typeSafeValue = val + } + if !p.IsDiscreteValues { + if val < p.Range.Min || val > p.Range.Max { + return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the range specified, min: %v , max:%v", p.Range.Min, p.Range.Max)} + } + } + case PreferenceValueTypeString: + _, ok := preferenceValue.(string) + if !ok { + return p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeFloat: + _, ok := preferenceValue.(float64) + if !ok { + return p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeBoolean: + _, ok := preferenceValue.(bool) + if !ok { + return p.ErrorValueTypeMismatch() + } + } + + // check the validity of the value being part of allowed values or the range specified if any + if p.IsDiscreteValues { + if p.AllowedValues != nil { + isInAllowedValues, valueMisMatchErr := p.checkIfInAllowedValues(typeSafeValue) + + if valueMisMatchErr != nil { + return valueMisMatchErr + } + if !isInAllowedValues { + return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the list of allowedValues: %v", p.AllowedValues)} + } + } + } + return nil +} + +func (p *Preference) IsEnabledForScope(scope string) bool { + isPreferenceEnabledForGivenScope := false + if p.AllowedScopes != nil { + for _, allowedScope := range p.AllowedScopes { + if allowedScope == strings.ToLower(scope) { + isPreferenceEnabledForGivenScope = true + } + } + } + return isPreferenceEnabledForGivenScope +} + +func (p *Preference) SanitizeValue(preferenceValue interface{}) interface{} { + switch p.ValueType { + case PreferenceValueTypeBoolean: + if preferenceValue == "1" || preferenceValue == true { + return true + } else { + return false + } + default: + return preferenceValue + } +} + +type AllPreferences struct { + Preference + Value interface{} `json:"value"` +} + +type PreferenceKV struct { + PreferenceId string `json:"preference_id" db:"preference_id"` + PreferenceValue interface{} `json:"preference_value" db:"preference_value"` +} + +type UpdatePreference struct { + PreferenceValue interface{} `json:"preference_value"` +} + +var db *sqlx.DB + +func InitDB(datasourceName string) error { + var err error + db, err = sqlx.Open("sqlite3", datasourceName) + + if err != nil { + return err + } + + // create the user preference table + tableSchema := ` + PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS user_preference( + preference_id TEXT NOT NULL, + preference_value TEXT, + user_id TEXT NOT NULL, + PRIMARY KEY (preference_id,user_id), + FOREIGN KEY (user_id) + REFERENCES users(id) + ON UPDATE CASCADE + ON DELETE CASCADE + );` + + _, err = db.Exec(tableSchema) + if err != nil { + return fmt.Errorf("error in creating user_preference table: %s", err.Error()) + } + + // create the org preference table + tableSchema = ` + PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS org_preference( + preference_id TEXT NOT NULL, + preference_value TEXT, + org_id TEXT NOT NULL, + PRIMARY KEY (preference_id,org_id), + FOREIGN KEY (org_id) + REFERENCES organizations(id) + ON UPDATE CASCADE + ON DELETE CASCADE + );` + + _, err = db.Exec(tableSchema) + if err != nil { + return fmt.Errorf("error in creating org_preference table: %s", err.Error()) + } + + return nil +} + +// org preference functions +func GetOrgPreference(ctx context.Context, preferenceId string, orgId string) (*PreferenceKV, *model.ApiError) { + // check if the preference key exists or not + preference, seen := preferenceMap[preferenceId] + if !seen { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} + } + + // check if the preference is enabled for org scope or not + isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope) + if !isPreferenceEnabled { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)} + } + + // fetch the value from the database + var orgPreference PreferenceKV + query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;` + err := db.Get(&orgPreference, query, preferenceId, orgId) + + // if the value doesn't exist in db then return the default value + if err != nil { + if err == sql.ErrNoRows { + return &PreferenceKV{ + PreferenceId: preferenceId, + PreferenceValue: preference.DefaultValue, + }, nil + } + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in fetching the org preference: %s", err.Error())} + + } + + // else return the value fetched from the org_preference table + return &PreferenceKV{ + PreferenceId: preferenceId, + PreferenceValue: preference.SanitizeValue(orgPreference.PreferenceValue), + }, nil +} + +func UpdateOrgPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, orgId string) (*PreferenceKV, *model.ApiError) { + // check if the preference key exists or not + preference, seen := preferenceMap[preferenceId] + if !seen { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} + } + + // check if the preference is enabled at org scope or not + isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope) + if !isPreferenceEnabled { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)} + } + + err := preference.IsValidValue(preferenceValue) + if err != nil { + return nil, err + } + + // update the values in the org_preference table and return the key and the value + query := `INSERT INTO org_preference(preference_id,preference_value,org_id) VALUES($1,$2,$3) + ON CONFLICT(preference_id,org_id) DO + UPDATE SET preference_value= $2 WHERE preference_id=$1 AND org_id=$3;` + + _, dberr := db.Exec(query, preferenceId, preferenceValue, orgId) + + if dberr != nil { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberr.Error())} + } + + return &PreferenceKV{ + PreferenceId: preferenceId, + PreferenceValue: preferenceValue, + }, nil +} + +func GetAllOrgPreferences(ctx context.Context, orgId string) (*[]AllPreferences, *model.ApiError) { + // filter out all the org enabled preferences from the preference variable + allOrgPreferences := []AllPreferences{} + + // fetch all the org preference values stored in org_preference table + orgPreferenceValues := []PreferenceKV{} + + query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;` + err := db.Select(&orgPreferenceValues, query, orgId) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)} + } + + // create a map of key vs values from the above response + preferenceValueMap := map[string]interface{}{} + + for _, preferenceValue := range orgPreferenceValues { + preferenceValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue + } + + // update in the above filtered list wherver value present in the map + for _, preference := range preferenceMap { + isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope) + if isEnabledForOrgScope { + preferenceWithValue := AllPreferences{} + preferenceWithValue.Key = preference.Key + preferenceWithValue.Name = preference.Name + preferenceWithValue.Description = preference.Description + preferenceWithValue.AllowedScopes = preference.AllowedScopes + preferenceWithValue.AllowedValues = preference.AllowedValues + preferenceWithValue.DefaultValue = preference.DefaultValue + preferenceWithValue.Range = preference.Range + preferenceWithValue.ValueType = preference.ValueType + preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues + value, seen := preferenceValueMap[preference.Key] + + if seen { + preferenceWithValue.Value = value + } else { + preferenceWithValue.Value = preference.DefaultValue + } + + preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) + allOrgPreferences = append(allOrgPreferences, preferenceWithValue) + } + } + return &allOrgPreferences, nil +} + +// user preference functions +func GetUserPreference(ctx context.Context, preferenceId string, orgId string, userId string) (*PreferenceKV, *model.ApiError) { + // check if the preference key exists + preference, seen := preferenceMap[preferenceId] + if !seen { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} + } + + preferenceValue := PreferenceKV{ + PreferenceId: preferenceId, + PreferenceValue: preference.DefaultValue, + } + + // check if the preference is enabled at user scope + isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope) + if !isPreferenceEnabledAtUserScope { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)} + } + + isPreferenceEnabledAtOrgScope := preference.IsEnabledForScope(OrgAllowedScope) + // get the value from the org scope if enabled at org scope + if isPreferenceEnabledAtOrgScope { + orgPreference := PreferenceKV{} + + query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;` + + err := db.Get(&orgPreference, query, preferenceId, orgId) + + // if there is error in getting values and its not an empty rows error return from here + if err != nil && err != sql.ErrNoRows { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting org preference values: %s", err.Error())} + } + + // if there is no error update the preference value with value from org preference + if err == nil { + preferenceValue.PreferenceValue = orgPreference.PreferenceValue + } + } + + // get the value from the user_preference table, if exists return this value else the one calculated in the above step + userPreference := PreferenceKV{} + + query := `SELECT preference_id, preference_value FROM user_preference WHERE preference_id=$1 AND user_id=$2;` + err := db.Get(&userPreference, query, preferenceId, userId) + + if err != nil && err != sql.ErrNoRows { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting user preference values: %s", err.Error())} + } + + if err == nil { + preferenceValue.PreferenceValue = userPreference.PreferenceValue + } + + return &PreferenceKV{ + PreferenceId: preferenceValue.PreferenceId, + PreferenceValue: preference.SanitizeValue(preferenceValue.PreferenceValue), + }, nil +} + +func UpdateUserPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, userId string) (*PreferenceKV, *model.ApiError) { + // check if the preference id is valid + preference, seen := preferenceMap[preferenceId] + if !seen { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} + } + + // check if the preference is enabled at user scope + isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope) + if !isPreferenceEnabledAtUserScope { + return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)} + } + + err := preference.IsValidValue(preferenceValue) + if err != nil { + return nil, err + } + // update the user preference values + query := `INSERT INTO user_preference(preference_id,preference_value,user_id) VALUES($1,$2,$3) + ON CONFLICT(preference_id,user_id) DO + UPDATE SET preference_value= $2 WHERE preference_id=$1 AND user_id=$3;` + + _, dberrr := db.Exec(query, preferenceId, preferenceValue, userId) + + if dberrr != nil { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberrr.Error())} + } + + return &PreferenceKV{ + PreferenceId: preferenceId, + PreferenceValue: preferenceValue, + }, nil +} + +func GetAllUserPreferences(ctx context.Context, orgId string, userId string) (*[]AllPreferences, *model.ApiError) { + allUserPreferences := []AllPreferences{} + + // fetch all the org preference values stored in org_preference table + orgPreferenceValues := []PreferenceKV{} + + query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;` + err := db.Select(&orgPreferenceValues, query, orgId) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)} + } + + // create a map of key vs values from the above response + preferenceOrgValueMap := map[string]interface{}{} + + for _, preferenceValue := range orgPreferenceValues { + preferenceOrgValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue + } + + // fetch all the user preference values stored in user_preference table + userPreferenceValues := []PreferenceKV{} + + query = `SELECT preference_id,preference_value FROM user_preference WHERE user_id=$1;` + err = db.Select(&userPreferenceValues, query, userId) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all user preference values: %s", err)} + } + + // create a map of key vs values from the above response + preferenceUserValueMap := map[string]interface{}{} + + for _, preferenceValue := range userPreferenceValues { + preferenceUserValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue + } + + // update in the above filtered list wherver value present in the map + for _, preference := range preferenceMap { + isEnabledForUserScope := preference.IsEnabledForScope(UserAllowedScope) + + if isEnabledForUserScope { + preferenceWithValue := AllPreferences{} + preferenceWithValue.Key = preference.Key + preferenceWithValue.Name = preference.Name + preferenceWithValue.Description = preference.Description + preferenceWithValue.AllowedScopes = preference.AllowedScopes + preferenceWithValue.AllowedValues = preference.AllowedValues + preferenceWithValue.DefaultValue = preference.DefaultValue + preferenceWithValue.Range = preference.Range + preferenceWithValue.ValueType = preference.ValueType + preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues + preferenceWithValue.Value = preference.DefaultValue + + isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope) + if isEnabledForOrgScope { + value, seen := preferenceOrgValueMap[preference.Key] + if seen { + preferenceWithValue.Value = value + } + } + + value, seen := preferenceUserValueMap[preference.Key] + + if seen { + preferenceWithValue.Value = value + } + + preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) + allUserPreferences = append(allUserPreferences, preferenceWithValue) + } + } + return &allUserPreferences, nil +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 2260045f4d..5120cd0039 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -27,6 +28,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/opamp" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" + "go.signoz.io/signoz/pkg/query-service/app/preferences" "go.signoz.io/signoz/pkg/query-service/common" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" @@ -94,6 +96,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } + if err := preferences.InitDB(constants.RELATIONAL_DATASOURCE_PATH); err != nil { + return nil, err + } + localDB, err := dashboards.InitDB(constants.RELATIONAL_DATASOURCE_PATH) explorer.InitWithDSN(constants.RELATIONAL_DATASOURCE_PATH) @@ -268,7 +274,21 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { r.Use(s.analyticsMiddleware) r.Use(loggingMiddleware) - am := NewAuthMiddleware(auth.GetUserFromRequest) + // add auth middleware + getUserFromRequest := func(r *http.Request) (*model.UserPayload, error) { + user, err := auth.GetUserFromRequest(r) + + if err != nil { + return nil, err + } + + if user.User.OrgId == "" { + return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims")) + } + + return user, nil + } + am := NewAuthMiddleware(getUserFromRequest) api.RegisterRoutes(r, am) api.RegisterLogsRoutes(r, am) diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go index 6041b3c1af..16eea6a5f3 100644 --- a/pkg/query-service/auth/auth.go +++ b/pkg/query-service/auth/auth.go @@ -467,6 +467,10 @@ func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.Use return nil, errors.Wrap(err, "failed to validate refresh token") } + if user.OrgId == "" { + return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims")) + } + return user, nil } @@ -505,6 +509,7 @@ func GenerateJWTForUser(user *model.User) (model.UserJwtObject, error) { "gid": user.GroupId, "email": user.Email, "exp": j.AccessJwtExpiry, + "orgId": user.OrgId, }) j.AccessJwt, err = token.SignedString([]byte(JwtSecret)) @@ -518,6 +523,7 @@ func GenerateJWTForUser(user *model.User) (model.UserJwtObject, error) { "gid": user.GroupId, "email": user.Email, "exp": j.RefreshJwtExpiry, + "orgId": user.OrgId, }) j.RefreshJwt, err = token.SignedString([]byte(JwtSecret)) diff --git a/pkg/query-service/auth/jwt.go b/pkg/query-service/auth/jwt.go index 7fe70e2c71..f57bb2ae18 100644 --- a/pkg/query-service/auth/jwt.go +++ b/pkg/query-service/auth/jwt.go @@ -20,6 +20,8 @@ var ( ) func ParseJWT(jwtStr string) (jwt.MapClaims, error) { + // TODO[@vikrantgupta25] : to update this to the claims check function for better integrity of JWT + // reference - https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Parser.ParseWithClaims token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.Errorf("unknown signing algo: %v", token.Header["alg"]) @@ -35,6 +37,7 @@ func ParseJWT(jwtStr string) (jwt.MapClaims, error) { if !ok || !token.Valid { return nil, errors.Errorf("Not a valid jwt claim") } + return claims, nil } @@ -47,11 +50,18 @@ func validateUser(tok string) (*model.UserPayload, error) { if !claims.VerifyExpiresAt(now, true) { return nil, model.ErrorTokenExpired } + + var orgId string + if claims["orgId"] != nil { + orgId = claims["orgId"].(string) + } + return &model.UserPayload{ User: model.User{ Id: claims["id"].(string), GroupId: claims["gid"].(string), Email: claims["email"].(string), + OrgId: orgId, }, }, nil }