From 6ccdc5296e781d3b31858ed106fe6c65d8bd4195 Mon Sep 17 00:00:00 2001 From: Ahsan Barkati Date: Tue, 3 May 2022 15:26:32 +0530 Subject: [PATCH] feat(auth): Add auth and access-controls support (#874) * auth and rbac support enabled --- .gitignore | 2 +- pkg/query-service/app/dashboards/model.go | 12 +- pkg/query-service/app/dashboards/provision.go | 2 +- pkg/query-service/app/http_handler.go | 717 +++++++++++++++--- pkg/query-service/app/parser.go | 86 ++- pkg/query-service/app/server.go | 12 +- pkg/query-service/auth/auth.go | 373 +++++++++ pkg/query-service/auth/jwt.go | 94 +++ pkg/query-service/auth/rbac.go | 136 ++++ pkg/query-service/auth/utils.go | 55 ++ pkg/query-service/constants/auth.go | 7 + pkg/query-service/constants/constants.go | 9 +- pkg/query-service/dao/factory.go | 25 +- pkg/query-service/dao/interface.go | 56 ++ pkg/query-service/dao/interfaces/interface.go | 5 - .../dao/interfaces/userPreference.go | 12 - pkg/query-service/dao/sqlite/connection.go | 135 +++- pkg/query-service/dao/sqlite/rbac.go | 512 +++++++++++++ .../dao/sqlite/userPreferenceImpl.go | 91 --- pkg/query-service/go.mod | 11 +- pkg/query-service/go.sum | 15 +- pkg/query-service/main.go | 15 + pkg/query-service/model/auth.go | 51 ++ pkg/query-service/model/db.go | 46 ++ pkg/query-service/model/queryParams.go | 6 - pkg/query-service/model/response.go | 1 + pkg/query-service/model/userPreferences.go | 27 - pkg/query-service/tests/auth_test.go | 125 +++ .../tests/test-deploy/data/signoz.db | Bin 32768 -> 0 bytes .../tests/test-deploy/docker-compose.yaml | 3 + 30 files changed, 2313 insertions(+), 328 deletions(-) create mode 100644 pkg/query-service/auth/auth.go create mode 100644 pkg/query-service/auth/jwt.go create mode 100644 pkg/query-service/auth/rbac.go create mode 100644 pkg/query-service/auth/utils.go create mode 100644 pkg/query-service/constants/auth.go create mode 100644 pkg/query-service/dao/interface.go delete mode 100644 pkg/query-service/dao/interfaces/interface.go delete mode 100644 pkg/query-service/dao/interfaces/userPreference.go create mode 100644 pkg/query-service/dao/sqlite/rbac.go delete mode 100644 pkg/query-service/dao/sqlite/userPreferenceImpl.go create mode 100644 pkg/query-service/model/auth.go create mode 100644 pkg/query-service/model/db.go delete mode 100644 pkg/query-service/model/userPreferences.go create mode 100644 pkg/query-service/tests/auth_test.go delete mode 100644 pkg/query-service/tests/test-deploy/data/signoz.db diff --git a/.gitignore b/.gitignore index 01a9526908..e876823dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ frontend/cypress.env.json frontend/*.env pkg/query-service/signoz.db -pkg/query-service/tframe/test-deploy/data/ +pkg/query-service/tests/test-deploy/data/ # local data diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 5d30fecdf4..9e1b3958d3 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -103,9 +103,9 @@ func (c *Data) Scan(src interface{}) error { } // CreateDashboard creates a new dashboard -func CreateDashboard(data *map[string]interface{}) (*Dashboard, *model.ApiError) { +func CreateDashboard(data map[string]interface{}) (*Dashboard, *model.ApiError) { dash := &Dashboard{ - Data: *data, + Data: data, } dash.CreatedAt = time.Now() dash.UpdatedAt = time.Now() @@ -135,7 +135,7 @@ func CreateDashboard(data *map[string]interface{}) (*Dashboard, *model.ApiError) return dash, nil } -func GetDashboards() (*[]Dashboard, *model.ApiError) { +func GetDashboards() ([]Dashboard, *model.ApiError) { dashboards := []Dashboard{} query := fmt.Sprintf("SELECT * FROM dashboards;") @@ -145,7 +145,7 @@ func GetDashboards() (*[]Dashboard, *model.ApiError) { return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } - return &dashboards, nil + return dashboards, nil } func DeleteDashboard(uuid string) *model.ApiError { @@ -182,7 +182,7 @@ func GetDashboard(uuid string) (*Dashboard, *model.ApiError) { return &dashboard, nil } -func UpdateDashboard(uuid string, data *map[string]interface{}) (*Dashboard, *model.ApiError) { +func UpdateDashboard(uuid string, data map[string]interface{}) (*Dashboard, *model.ApiError) { map_data, err := json.Marshal(data) if err != nil { @@ -196,7 +196,7 @@ func UpdateDashboard(uuid string, data *map[string]interface{}) (*Dashboard, *mo } dashboard.UpdatedAt = time.Now() - dashboard.Data = *data + dashboard.Data = data // db.Prepare("Insert into dashboards where") _, err = db.Exec("UPDATE dashboards SET updated_at=$1, data=$2 WHERE uuid=$3 ", dashboard.UpdatedAt, map_data, dashboard.Uuid) diff --git a/pkg/query-service/app/dashboards/provision.go b/pkg/query-service/app/dashboards/provision.go index f1d5fb2ca8..5bad8ae026 100644 --- a/pkg/query-service/app/dashboards/provision.go +++ b/pkg/query-service/app/dashboards/provision.go @@ -42,7 +42,7 @@ func readCurrentDir(dir string) error { continue } - _, apiErr = CreateDashboard(&data) + _, apiErr = CreateDashboard(data) if apiErr != nil { zap.S().Errorf("Creating Dashboards: Error in file: %s\t%s", filename, apiErr.Err) continue diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 49ff79c1ed..8b07bb546f 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -3,6 +3,7 @@ package app import ( "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -13,7 +14,9 @@ import ( "github.com/prometheus/prometheus/promql" "go.signoz.io/query-service/app/dashboards" "go.signoz.io/query-service/app/parser" - "go.signoz.io/query-service/dao/interfaces" + "go.signoz.io/query-service/auth" + "go.signoz.io/query-service/constants" + "go.signoz.io/query-service/dao" am "go.signoz.io/query-service/integrations/alertManager" "go.signoz.io/query-service/model" "go.signoz.io/query-service/telemetry" @@ -40,13 +43,13 @@ type APIHandler struct { basePath string apiPrefix string reader *Reader + relationalDB dao.ModelDao alertManager am.Manager - relationalDB *interfaces.ModelDao ready func(http.HandlerFunc) http.HandlerFunc } // NewAPIHandler returns an APIHandler -func NewAPIHandler(reader *Reader, relationalDB *interfaces.ModelDao) (*APIHandler, error) { +func NewAPIHandler(reader *Reader, relationalDB dao.ModelDao) (*APIHandler, error) { alertManager := am.New("") aH := &APIHandler{ @@ -109,7 +112,7 @@ type response struct { Error string `json:"error,omitempty"` } -func (aH *APIHandler) respondError(w http.ResponseWriter, apiErr *model.ApiError, data interface{}) { +func respondError(w http.ResponseWriter, apiErr *model.ApiError, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&response{ Status: statusError, @@ -137,6 +140,8 @@ func (aH *APIHandler) respondError(w http.ResponseWriter, apiErr *model.ApiError code = http.StatusNotFound case model.ErrorNotImplemented: code = http.StatusNotImplemented + case model.ErrorUnauthorized: + code = http.StatusUnauthorized default: code = http.StatusInternalServerError } @@ -148,7 +153,7 @@ func (aH *APIHandler) respondError(w http.ResponseWriter, apiErr *model.ApiError } } -func (aH *APIHandler) respond(w http.ResponseWriter, data interface{}) { +func writeHttpResponse(w http.ResponseWriter, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&response{ Status: statusSuccess, @@ -174,57 +179,143 @@ func (aH *APIHandler) RegisterMetricsRoutes(router *mux.Router) { subRouter.HandleFunc("/autocomplete/tagValue", aH.metricAutocompleteTagValue).Methods(http.MethodGet) } +func (aH *APIHandler) respond(w http.ResponseWriter, data interface{}) { + writeHttpResponse(w, data) +} + +func OpenAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + f(w, r) + } +} + +func ViewAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !(auth.IsViewer(r) || auth.IsEditor(r) || auth.IsAdmin(r)) { + respondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: errors.New("API accessible only to the admins"), + }, nil) + return + } + f(w, r) + } +} + +func EditAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !(auth.IsEditor(r) || auth.IsAdmin(r)) { + respondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: errors.New("API accessible only to the editors"), + }, nil) + return + } + f(w, r) + } +} + +func SelfAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !(auth.IsSelfAccessRequest(r) || auth.IsAdmin(r)) { + respondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: errors.New("API accessible only for self userId"), + }, nil) + return + } + f(w, r) + } +} + +func AdminAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !auth.IsAdmin(r) { + respondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: errors.New("API accessible only to the admins"), + }, nil) + return + } + f(w, r) + } +} + // RegisterRoutes registers routes for this handler on the given router func (aH *APIHandler) RegisterRoutes(router *mux.Router) { - router.HandleFunc("/api/v1/query_range", aH.queryRangeMetrics).Methods(http.MethodGet) - router.HandleFunc("/api/v1/query", aH.queryMetrics).Methods(http.MethodGet) - router.HandleFunc("/api/v1/channels", aH.listChannels).Methods(http.MethodGet) - router.HandleFunc("/api/v1/channels/{id}", aH.getChannel).Methods(http.MethodGet) - router.HandleFunc("/api/v1/channels/{id}", aH.editChannel).Methods(http.MethodPut) - router.HandleFunc("/api/v1/channels/{id}", aH.deleteChannel).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/channels", aH.createChannel).Methods(http.MethodPost) - router.HandleFunc("/api/v1/testChannel", aH.testChannel).Methods(http.MethodPost) - router.HandleFunc("/api/v1/rules", aH.listRulesFromProm).Methods(http.MethodGet) - router.HandleFunc("/api/v1/rules/{id}", aH.getRule).Methods(http.MethodGet) - router.HandleFunc("/api/v1/rules", aH.createRule).Methods(http.MethodPost) - router.HandleFunc("/api/v1/rules/{id}", aH.editRule).Methods(http.MethodPut) - router.HandleFunc("/api/v1/rules/{id}", aH.deleteRule).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/query_range", ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/query", ViewAccess(aH.queryMetrics)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/channels", ViewAccess(aH.listChannels)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/channels/{id}", ViewAccess(aH.getChannel)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/channels/{id}", EditAccess(aH.editChannel)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/channels/{id}", EditAccess(aH.deleteChannel)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/channels", EditAccess(aH.createChannel)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/testChannel", EditAccess(aH.testChannel)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/rules", ViewAccess(aH.listRulesFromProm)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/rules/{id}", ViewAccess(aH.getRule)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/rules", EditAccess(aH.createRule)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/rules/{id}", EditAccess(aH.editRule)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/rules/{id}", EditAccess(aH.deleteRule)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/dashboards", aH.getDashboards).Methods(http.MethodGet) - router.HandleFunc("/api/v1/dashboards", aH.createDashboards).Methods(http.MethodPost) - router.HandleFunc("/api/v1/dashboards/{uuid}", aH.getDashboard).Methods(http.MethodGet) - router.HandleFunc("/api/v1/dashboards/{uuid}", aH.updateDashboard).Methods(http.MethodPut) - router.HandleFunc("/api/v1/dashboards/{uuid}", aH.deleteDashboard).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/dashboards", ViewAccess(aH.getDashboards)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/dashboards", EditAccess(aH.createDashboards)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/user", aH.user).Methods(http.MethodPost) + router.HandleFunc("/api/v1/user", ViewAccess(aH.user)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/feedback", aH.submitFeedback).Methods(http.MethodPost) + router.HandleFunc("/api/v1/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost) // router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet) - router.HandleFunc("/api/v1/services", aH.getServices).Methods(http.MethodPost) - router.HandleFunc("/api/v1/services/list", aH.getServicesList).Methods(http.MethodGet) - router.HandleFunc("/api/v1/service/overview", aH.getServiceOverview).Methods(http.MethodPost) - router.HandleFunc("/api/v1/service/top_endpoints", aH.getTopEndpoints).Methods(http.MethodPost) - router.HandleFunc("/api/v1/traces/{traceId}", aH.searchTraces).Methods(http.MethodGet) - router.HandleFunc("/api/v1/usage", aH.getUsage).Methods(http.MethodGet) - router.HandleFunc("/api/v1/serviceMapDependencies", aH.serviceMapDependencies).Methods(http.MethodGet) - router.HandleFunc("/api/v1/settings/ttl", aH.setTTL).Methods(http.MethodPost) - router.HandleFunc("/api/v1/settings/ttl", aH.getTTL).Methods(http.MethodGet) + router.HandleFunc("/api/v1/services", ViewAccess(aH.getServices)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/services/list", ViewAccess(aH.getServicesList)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/service/overview", ViewAccess(aH.getServiceOverview)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/service/top_endpoints", ViewAccess(aH.getTopEndpoints)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/traces/{traceId}", ViewAccess(aH.searchTraces)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/usage", AdminAccess(aH.getUsage)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/serviceMapDependencies", ViewAccess(aH.serviceMapDependencies)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/settings/ttl", AdminAccess(aH.setTTL)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/userPreferences", aH.setUserPreferences).Methods(http.MethodPost) - router.HandleFunc("/api/v1/userPreferences", aH.getUserPreferences).Methods(http.MethodGet) - router.HandleFunc("/api/v1/version", aH.getVersion).Methods(http.MethodGet) + router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/getSpanFilters", aH.getSpanFilters).Methods(http.MethodPost) - router.HandleFunc("/api/v1/getTagFilters", aH.getTagFilters).Methods(http.MethodPost) - router.HandleFunc("/api/v1/getFilteredSpans", aH.getFilteredSpans).Methods(http.MethodPost) - router.HandleFunc("/api/v1/getFilteredSpans/aggregates", aH.getFilteredSpanAggregates).Methods(http.MethodPost) + router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/getFilteredSpans", ViewAccess(aH.getFilteredSpans)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/getFilteredSpans/aggregates", ViewAccess(aH.getFilteredSpanAggregates)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/getTagValues", aH.getTagValues).Methods(http.MethodPost) - router.HandleFunc("/api/v1/errors", aH.getErrors).Methods(http.MethodGet) - router.HandleFunc("/api/v1/errorWithId", aH.getErrorForId).Methods(http.MethodGet) - router.HandleFunc("/api/v1/errorWithType", aH.getErrorForType).Methods(http.MethodGet) + router.HandleFunc("/api/v1/getTagValues", ViewAccess(aH.getTagValues)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/errors", ViewAccess(aH.getErrors)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/errorWithId", ViewAccess(aH.getErrorForId)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/errorWithType", ViewAccess(aH.getErrorForType)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/disks", aH.getDisks).Methods(http.MethodGet) + router.HandleFunc("/api/v1/disks", ViewAccess(aH.getDisks)).Methods(http.MethodGet) + + // === Authentication APIs === + router.HandleFunc("/api/v1/invite", AdminAccess(aH.inviteUser)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/invite/{token}", OpenAccess(aH.getInvite)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/invite/{email}", AdminAccess(aH.revokeInvite)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/invite", AdminAccess(aH.listPendingInvites)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/register", OpenAccess(aH.registerUser)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/login", OpenAccess(aH.loginUser)).Methods(http.MethodPost) + + router.HandleFunc("/api/v1/user", AdminAccess(aH.listUsers)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/user/{id}", SelfAccess(aH.getUser)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/user/{id}", SelfAccess(aH.editUser)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/user/{id}", AdminAccess(aH.deleteUser)).Methods(http.MethodDelete) + + router.HandleFunc("/api/v1/rbac/role/{id}", SelfAccess(aH.getRole)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/rbac/role/{id}", AdminAccess(aH.editRole)).Methods(http.MethodPut) + + router.HandleFunc("/api/v1/org", AdminAccess(aH.getOrgs)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/org/{id}", AdminAccess(aH.getOrg)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/org/{id}", AdminAccess(aH.editOrg)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/orgUsers/{id}", AdminAccess(aH.getOrgUsers)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/getResetPasswordToken/{id}", AdminAccess(aH.getResetPasswordToken)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/resetPassword", OpenAccess(aH.resetPassword)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/changePassword/{id}", SelfAccess(aH.changePassword)).Methods(http.MethodPost) } func Intersection(a, b []int) (c []int) { @@ -246,7 +337,7 @@ func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] alertList, apiErrorObj := (*aH.reader).GetRule(id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, alertList) @@ -257,7 +348,7 @@ func (aH *APIHandler) metricAutocompleteMetricName(w http.ResponseWriter, r *htt metricNameList, apiErrObj := (*aH.reader).GetMetricAutocompleteMetricNames(r.Context(), matchText) if apiErrObj != nil { - aH.respondError(w, apiErrObj, nil) + respondError(w, apiErrObj, nil) return } aH.respond(w, metricNameList) @@ -267,14 +358,14 @@ func (aH *APIHandler) metricAutocompleteMetricName(w http.ResponseWriter, r *htt func (aH *APIHandler) metricAutocompleteTagKey(w http.ResponseWriter, r *http.Request) { metricsAutocompleteTagKeyParams, apiErrorObj := parser.ParseMetricAutocompleteTagParams(r) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } tagKeyList, apiErrObj := (*aH.reader).GetMetricAutocompleteTagKey(r.Context(), metricsAutocompleteTagKeyParams) if apiErrObj != nil { - aH.respondError(w, apiErrObj, nil) + respondError(w, apiErrObj, nil) return } aH.respond(w, tagKeyList) @@ -285,18 +376,18 @@ func (aH *APIHandler) metricAutocompleteTagValue(w http.ResponseWriter, r *http. if len(metricsAutocompleteTagValueParams.TagKey) == 0 { apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("tagKey not present in params")} - aH.respondError(w, apiErrObj, nil) + respondError(w, apiErrObj, nil) return } if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } tagValueList, apiErrObj := (*aH.reader).GetMetricAutocompleteTagValue(r.Context(), metricsAutocompleteTagValueParams) if apiErrObj != nil { - aH.respondError(w, apiErrObj, nil) + respondError(w, apiErrObj, nil) return } @@ -310,7 +401,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request if apiErrorObj != nil { zap.S().Errorf(apiErrorObj.Err.Error()) - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } response_data := &model.QueryDataV2{ @@ -323,7 +414,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request func (aH *APIHandler) listRulesFromProm(w http.ResponseWriter, r *http.Request) { alertList, apiErrorObj := (*aH.reader).ListRulesFromProm() if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, alertList) @@ -334,18 +425,18 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { allDashboards, err := dashboards.GetDashboards() if err != nil { - aH.respondError(w, err, nil) + respondError(w, err, nil) return } tagsFromReq, ok := r.URL.Query()["tags"] if !ok || len(tagsFromReq) == 0 || tagsFromReq[0] == "" { - aH.respond(w, &allDashboards) + aH.respond(w, allDashboards) return } tags2Dash := make(map[string][]int) - for i := 0; i < len(*allDashboards); i++ { - tags, ok := (*allDashboards)[i].Data["tags"].([]interface{}) + for i := 0; i < len(allDashboards); i++ { + tags, ok := (allDashboards)[i].Data["tags"].([]interface{}) if !ok { continue } @@ -361,7 +452,7 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { } - inter := make([]int, len(*allDashboards)) + inter := make([]int, len(allDashboards)) for i := range inter { inter[i] = i } @@ -372,11 +463,11 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { filteredDashboards := []dashboards.Dashboard{} for _, val := range inter { - dash := (*allDashboards)[val] + dash := (allDashboards)[val] filteredDashboards = append(filteredDashboards, dash) } - aH.respond(w, &filteredDashboards) + aH.respond(w, filteredDashboards) } func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) { @@ -385,7 +476,7 @@ func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) { err := dashboards.DeleteDashboard(uuid) if err != nil { - aH.respondError(w, err, nil) + respondError(w, err, nil) return } @@ -400,19 +491,18 @@ func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) { var postData map[string]interface{} err := json.NewDecoder(r.Body).Decode(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") return } err = dashboards.IsPostDataSane(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") return } - dashboard, apiError := dashboards.UpdateDashboard(uuid, &postData) - + dashboard, apiError := dashboards.UpdateDashboard(uuid, postData) if apiError != nil { - aH.respondError(w, apiError, nil) + respondError(w, apiError, nil) return } @@ -427,7 +517,7 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) { dashboard, apiError := dashboards.GetDashboard(uuid) if apiError != nil { - aH.respondError(w, apiError, nil) + respondError(w, apiError, nil) return } @@ -438,21 +528,23 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) { var postData map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body") return } + err = dashboards.IsPostDataSane(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body") return } - dash, apiErr := dashboards.CreateDashboard(&postData) + dash, apiErr := dashboards.CreateDashboard(postData) if apiErr != nil { - aH.respondError(w, apiErr, nil) + respondError(w, apiErr, nil) return } @@ -466,7 +558,7 @@ func (aH *APIHandler) deleteRule(w http.ResponseWriter, r *http.Request) { apiErrorObj := (*aH.reader).DeleteRule(id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -479,14 +571,14 @@ func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) { var postData map[string]string err := json.NewDecoder(r.Body).Decode(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") return } apiErrorObj := (*aH.reader).EditRule(postData["data"], id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -498,7 +590,7 @@ func (aH *APIHandler) getChannel(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] channel, apiErrorObj := (*aH.reader).GetChannel(id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, channel) @@ -508,7 +600,7 @@ func (aH *APIHandler) deleteChannel(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] apiErrorObj := (*aH.reader).DeleteChannel(id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, "notification channel successfully deleted") @@ -517,7 +609,7 @@ func (aH *APIHandler) deleteChannel(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) listChannels(w http.ResponseWriter, r *http.Request) { channels, apiErrorObj := (*aH.reader).GetChannels() if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, channels) @@ -530,21 +622,21 @@ func (aH *APIHandler) testChannel(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { zap.S().Errorf("Error in getting req body of testChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } receiver := &am.Receiver{} if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer zap.S().Errorf("Error in parsing req body of testChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } // send alert apiErrorObj := aH.alertManager.TestReceiver(receiver) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } aH.respond(w, "test alert sent") @@ -558,21 +650,21 @@ func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { zap.S().Errorf("Error in getting req body of editChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } receiver := &am.Receiver{} if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer zap.S().Errorf("Error in parsing req body of editChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } _, apiErrorObj := (*aH.reader).EditChannel(receiver, id) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -586,21 +678,21 @@ func (aH *APIHandler) createChannel(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { zap.S().Errorf("Error in getting req body of createChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } receiver := &am.Receiver{} if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer zap.S().Errorf("Error in parsing req body of createChannel API\n", err) - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } _, apiErrorObj := (*aH.reader).CreateChannel(receiver) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -616,14 +708,14 @@ func (aH *APIHandler) createRule(w http.ResponseWriter, r *http.Request) { err := decoder.Decode(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } apiErrorObj := (*aH.reader).CreateRule(postData["data"]) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -638,7 +730,7 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) query, apiErrorObj := parseQueryRangeRequest(r) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -659,7 +751,7 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) res, qs, apiError := (*aH.reader).GetQueryRangeResult(ctx, query) if apiError != nil { - aH.respondError(w, apiError, nil) + respondError(w, apiError, nil) return } @@ -670,11 +762,11 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: - aH.respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil) case promql.ErrQueryTimeout: - aH.respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil) } - aH.respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil) } response_data := &model.QueryData{ @@ -692,7 +784,7 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) { queryParams, apiErrorObj := parseInstantQueryMetricsRequest(r) if apiErrorObj != nil { - aH.respondError(w, apiErrorObj, nil) + respondError(w, apiErrorObj, nil) return } @@ -713,7 +805,7 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) { res, qs, apiError := (*aH.reader).GetInstantQueryMetricsResult(ctx, queryParams) if apiError != nil { - aH.respondError(w, apiError, nil) + respondError(w, apiError, nil) return } @@ -724,11 +816,11 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) { if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: - aH.respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil) case promql.ErrQueryTimeout: - aH.respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil) } - aH.respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil) + respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil) } response_data := &model.QueryData{ @@ -746,18 +838,18 @@ func (aH *APIHandler) submitFeedback(w http.ResponseWriter, r *http.Request) { var postData map[string]interface{} err := json.NewDecoder(r.Body).Decode(&postData) if err != nil { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body") return } message, ok := postData["message"] if !ok { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("message not present in request body")}, "Error reading message from request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("message not present in request body")}, "Error reading message from request body") return } messageStr := fmt.Sprintf("%s", message) if len(messageStr) == 0 { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("empty message in request body")}, "empty message in request body") + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("empty message in request body")}, "empty message in request body") return } @@ -784,7 +876,7 @@ func (aH *APIHandler) user(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "name": user.Name, "email": user.Email, - "organizationName": user.OrganizationName, + "organizationName": user.OrgId, } telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER, data) @@ -1064,43 +1156,420 @@ func (aH *APIHandler) getDisks(w http.ResponseWriter, r *http.Request) { aH.writeJSON(w, r, result) } -func (aH *APIHandler) getUserPreferences(w http.ResponseWriter, r *http.Request) { - - result, apiError := (*aH.relationalDB).FetchUserPreference(r.Context()) - if apiError != nil { - aH.respondError(w, apiError, "Error from Fetch Dao") - return - } - - aH.writeJSON(w, r, result) +func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { + version := version.GetVersion() + aH.writeJSON(w, r, map[string]string{"version": version}) } -func (aH *APIHandler) setUserPreferences(w http.ResponseWriter, r *http.Request) { - userParams, err := parseUserPreferences(r) +// inviteUser is used to invite a user. It is used by an admin api. +func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) { + req, err := parseInviteRequest(r) if aH.handleError(w, err, http.StatusBadRequest) { return } - apiErr := (*aH.relationalDB).UpdateUserPreferece(r.Context(), userParams) - if apiErr != nil && aH.handleError(w, apiErr.Err, http.StatusInternalServerError) { + ctx := auth.AttachJwtToContext(context.Background(), r) + resp, err := auth.Invite(ctx, req) + if err != nil { + respondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + aH.writeJSON(w, r, resp) +} + +// 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"] + + resp, err := auth.GetInvite(context.Background(), token) + if err != nil { + respondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + aH.writeJSON(w, r, resp) +} + +// revokeInvite is used to revoke an invite. +func (aH *APIHandler) revokeInvite(w http.ResponseWriter, r *http.Request) { + email := mux.Vars(r)["email"] + + ctx := auth.AttachJwtToContext(context.Background(), r) + if err := auth.RevokeInvite(ctx, email); err != nil { + respondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + aH.writeJSON(w, r, map[string]string{"data": "invite revoked successfully"}) +} + +// listPendingInvites is used to list the pending invites. +func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request) { + + ctx := context.Background() + invites, err := dao.DB().GetInvites(ctx) + if err != nil { + respondError(w, err, nil) + return + } + + // TODO(Ahsan): Querying org name based on orgId for each invite is not a good idea. Either + // we should include org name field in the invite table, or do a join query. + var resp []*model.InvitationResponseObject + for _, inv := range invites { + + org, apiErr := dao.DB().GetOrg(ctx, inv.OrgId) + if apiErr != nil { + respondError(w, apiErr, nil) + } + resp = append(resp, &model.InvitationResponseObject{ + Name: inv.Name, + Email: inv.Email, + Token: inv.Token, + CreatedAt: inv.CreatedAt, + Role: inv.Role, + Organization: org.Name, + }) + } + aH.writeJSON(w, r, resp) +} + +func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) { + req, err := parseRegisterRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + apiErr := auth.Register(context.Background(), req) + if apiErr != nil { + respondError(w, apiErr, nil) + return + } + + aH.writeJSON(w, r, map[string]string{"data": "user registered successfully"}) +} + +func (aH *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) { + req, err := parseLoginRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + // c, err := r.Cookie("refresh-token") + // if err != nil { + // if err != http.ErrNoCookie { + // w.WriteHeader(http.StatusBadRequest) + // return + // } + // } + + // if c != nil { + // req.RefreshToken = c.Value + // } + + resp, err := auth.Login(context.Background(), req) + if aH.handleError(w, err, http.StatusUnauthorized) { + return + } + + // http.SetCookie(w, &http.Cookie{ + // Name: "refresh-token", + // Value: resp.RefreshJwt, + // Expires: time.Unix(resp.RefreshJwtExpiry, 0), + // HttpOnly: true, + // }) + + aH.writeJSON(w, r, resp) +} + +func (aH *APIHandler) listUsers(w http.ResponseWriter, r *http.Request) { + users, err := dao.DB().GetUsers(context.Background()) + if err != nil { + zap.S().Debugf("[listUsers] Failed to query list of users, err: %v", err) + respondError(w, err, nil) + return + } + // mask the password hash + for _, u := range users { + u.Password = "" + } + aH.writeJSON(w, r, users) +} + +func (aH *APIHandler) getUser(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + ctx := context.Background() + user, err := dao.DB().GetUser(ctx, id) + if err != nil { + zap.S().Debugf("[getUser] Failed to query user, err: %v", err) + respondError(w, err, "Failed to get user") + return + } + // No need to send password hash for the user object. + if user != nil { + user.Password = "" + } + + aH.writeJSON(w, r, user) +} + +// editUser only changes the user's Name and ProfilePictureURL. It is intentionally designed +// to not support update of orgId, Password, createdAt for the sucurity reasons. +func (aH *APIHandler) editUser(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + update, err := parseUserRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + ctx := context.Background() + old, apiErr := dao.DB().GetUser(ctx, id) + if apiErr != nil { + zap.S().Debugf("[editUser] Failed to query user, err: %v", err) + respondError(w, apiErr, nil) + return + } + + if len(update.Name) > 0 { + old.Name = update.Name + } + if len(update.ProfilePirctureURL) > 0 { + old.ProfilePirctureURL = update.ProfilePirctureURL + } + + _, apiErr = dao.DB().EditUser(ctx, &model.User{ + Id: old.Id, + Name: old.Name, + OrgId: old.OrgId, + Email: old.Email, + Password: old.Password, + CreatedAt: old.CreatedAt, + ProfilePirctureURL: old.ProfilePirctureURL, + }) + if apiErr != nil { + respondError(w, apiErr, nil) + return + } + aH.writeJSON(w, r, map[string]string{"data": "user updated successfully"}) +} + +func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + if !auth.IsAdmin(r) { + respondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: errors.New("Only admin can delete user"), + }, "Failed to get user") + return + } + + // Query for the user's group, and the admin's group. If the user belongs to the admin group + // and is the last user then don't let the deletion happen. Otherwise, the system will become + // admin less and hence inaccessible. + ctx := context.Background() + user, apiErr := dao.DB().GetUser(ctx, id) + if apiErr != nil { + respondError(w, apiErr, "Failed to get user's group") + return + } + adminGroup, apiErr := dao.DB().GetGroupByName(ctx, constants.AdminGroup) + if apiErr != nil { + respondError(w, apiErr, "Failed to get admin group") + return + } + adminUsers, apiErr := dao.DB().GetUsersByGroup(ctx, adminGroup.Id) + if apiErr != nil { + respondError(w, apiErr, "Failed to get admin group users") + return + } + + if user.GroupId == adminGroup.Id && len(adminUsers) == 1 { + respondError(w, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("cannot delete the last admin user")}, nil) + return + } + + err := dao.DB().DeleteUser(ctx, id) + if err != nil { + respondError(w, err, "Failed to delete user") + return + } + aH.writeJSON(w, r, map[string]string{"data": "user deleted successfully"}) +} + +func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + user, err := dao.DB().GetUser(context.Background(), id) + if err != nil { + respondError(w, err, "Failed to get user's group") + return + } + if user == nil { + respondError(w, &model.ApiError{ + Typ: model.ErrorNotFound, + Err: errors.New("No user found"), + }, nil) + return + } + group, err := dao.DB().GetGroup(context.Background(), user.GroupId) + if err != nil { + respondError(w, err, "Failed to get group") + return + } + + aH.writeJSON(w, r, &model.UserRole{UserId: id, GroupName: group.Name}) +} + +func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + req, err := parseUserRoleRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + ctx := context.Background() + newGroup, apiErr := dao.DB().GetGroupByName(ctx, req.GroupName) + if apiErr != nil { + respondError(w, apiErr, "Failed to get user's group") + return + } + + if newGroup == nil { + respondError(w, apiErr, "Specified group is not present") + return + } + + user, apiErr := dao.DB().GetUser(ctx, id) + if apiErr != nil { + respondError(w, apiErr, "Failed to fetch user group") + return + } + + // Make sure that the request is not demoting the last admin user. + if user.GroupId == auth.AuthCacheObj.AdminGroupId { + adminUsers, apiErr := dao.DB().GetUsersByGroup(ctx, auth.AuthCacheObj.AdminGroupId) + if apiErr != nil { + respondError(w, apiErr, "Failed to fetch adminUsers") + return + } + + if len(adminUsers) == 1 { + respondError(w, &model.ApiError{ + Err: errors.New("Cannot demote the last admin"), + Typ: model.ErrorInternal}, nil) + return + } + } + + apiErr = dao.DB().UpdateUserGroup(context.Background(), user.Id, newGroup.Id) + if apiErr != nil { + respondError(w, apiErr, "Failed to add user to group") + return + } + aH.writeJSON(w, r, map[string]string{"data": "user group updated successfully"}) +} + +func (aH *APIHandler) getOrgs(w http.ResponseWriter, r *http.Request) { + orgs, apiErr := dao.DB().GetOrgs(context.Background()) + if apiErr != nil { + respondError(w, apiErr, "Failed to fetch orgs from the DB") + return + } + aH.writeJSON(w, r, orgs) +} + +func (aH *APIHandler) getOrg(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + org, apiErr := dao.DB().GetOrg(context.Background(), id) + if apiErr != nil { + respondError(w, apiErr, "Failed to fetch org from the DB") + return + } + aH.writeJSON(w, r, org) +} + +func (aH *APIHandler) editOrg(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + req, err := parseEditOrgRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + req.Id = id + if apiErr := dao.DB().EditOrg(context.Background(), req); apiErr != nil { + respondError(w, apiErr, "Failed to update org in the DB") return } data := map[string]interface{}{ - "hasOptedUpdates": userParams.HasOptedUpdates, - "isAnonymous": userParams.IsAnonymous, + "hasOptedUpdates": req.HasOptedUpdates, + "isAnonymous": req.IsAnonymous, } telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_PREFERENCES, data) - aH.writeJSON(w, r, map[string]string{"data": "user preferences set successfully"}) - + aH.writeJSON(w, r, map[string]string{"data": "org updated successfully"}) } -func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { +func (aH *APIHandler) getOrgUsers(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + users, apiErr := dao.DB().GetUsersByOrg(context.Background(), id) + if apiErr != nil { + respondError(w, apiErr, "Failed to fetch org users from the DB") + return + } + // mask the password hash + for _, u := range users { + u.Password = "" + } + aH.writeJSON(w, r, users) +} - version := version.GetVersion() +func (aH *APIHandler) getResetPasswordToken(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + resp, err := auth.CreateResetPasswordToken(context.Background(), id) + if err != nil { + respondError(w, &model.ApiError{ + Typ: model.ErrorInternal, + Err: err}, "Failed to create reset token entry in the DB") + return + } + aH.writeJSON(w, r, resp) +} - aH.writeJSON(w, r, map[string]string{"version": version}) +func (aH *APIHandler) resetPassword(w http.ResponseWriter, r *http.Request) { + req, err := parseResetPasswordRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + if err := auth.ResetPassword(context.Background(), req); err != nil { + zap.S().Debugf("resetPassword failed, err: %v\n", err) + if aH.handleError(w, err, http.StatusInternalServerError) { + return + } + + } + aH.writeJSON(w, r, map[string]string{"data": "password reset successfully"}) +} + +func (aH *APIHandler) changePassword(w http.ResponseWriter, r *http.Request) { + req, err := parseChangePasswordRequest(r) + if aH.handleError(w, err, http.StatusBadRequest) { + return + } + + if err := auth.ChangePassword(context.Background(), req); err != nil { + if aH.handleError(w, err, http.StatusInternalServerError) { + return + } + + } + aH.writeJSON(w, r, map[string]string{"data": "password changed successfully"}) } // func (aH *APIHandler) getApplicationPercentiles(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 44964ade97..1f189a5541 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -9,7 +9,10 @@ import ( "strconv" "time" + "github.com/gorilla/mux" promModel "github.com/prometheus/common/model" + + "go.signoz.io/query-service/auth" "go.signoz.io/query-service/constants" "go.signoz.io/query-service/model" ) @@ -19,8 +22,7 @@ var allowedFunctions = []string{"count", "ratePerSec", "sum", "avg", "min", "max func parseUser(r *http.Request) (*model.User, error) { var user model.User - err := json.NewDecoder(r.Body).Decode(&user) - if err != nil { + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { return nil, err } if len(user.Email) == 0 { @@ -585,14 +587,84 @@ func parseGetTTL(r *http.Request) (*model.GetTTLParams, error) { return &model.GetTTLParams{Type: typeTTL, GetAllTTL: getAllTTL}, nil } -func parseUserPreferences(r *http.Request) (*model.UserPreferences, error) { +func parseUserRequest(r *http.Request) (*model.User, error) { + var req model.User + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return &req, nil +} - var userPreferences model.UserPreferences - err := json.NewDecoder(r.Body).Decode(&userPreferences) - if err != nil { +func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) { + var req model.InviteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return &req, nil +} + +func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) { + var req auth.RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, err } - return &userPreferences, nil + if err := auth.ValidatePassword(req.Password); err != nil { + return nil, err + } + return &req, nil +} + +func parseLoginRequest(r *http.Request) (*model.LoginRequest, error) { + var req model.LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return &req, nil +} + +func parseUserRoleRequest(r *http.Request) (*model.UserRole, error) { + var req model.UserRole + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return &req, nil +} + +func parseEditOrgRequest(r *http.Request) (*model.Organization, error) { + var req model.Organization + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return &req, nil +} + +func parseResetPasswordRequest(r *http.Request) (*model.ResetPasswordRequest, error) { + var req model.ResetPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + if err := auth.ValidatePassword(req.Password); err != nil { + return nil, err + } + + return &req, nil +} + +func parseChangePasswordRequest(r *http.Request) (*model.ChangePasswordRequest, error) { + id := mux.Vars(r)["id"] + var req model.ChangePasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + req.UserId = id + if err := auth.ValidatePassword(req.NewPassword); err != nil { + return nil, err + } + + return &req, nil } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 4864aff721..7908e79c18 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -69,6 +69,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { // return nil, err // } + if err := dao.InitDao("sqlite", constants.RELATIONAL_DATASOURCE_PATH); err != nil { + return nil, err + } + s := &Server{ // logger: logger, // querySvc: querySvc, @@ -113,12 +117,7 @@ func (s *Server) createHTTPServer() (*http.Server, error) { return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage) } - relationalDB, err := dao.FactoryDao("sqlite") - if err != nil { - return nil, err - } - - apiHandler, err := NewAPIHandler(&reader, relationalDB) + apiHandler, err := NewAPIHandler(&reader, dao.DB()) if err != nil { return nil, err } @@ -136,6 +135,7 @@ func (s *Server) createHTTPServer() (*http.Server, error) { AllowedOrigins: []string{"*"}, // AllowCredentials: true, AllowedMethods: []string{"GET", "DELETE", "POST", "PUT"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, }) handler := c.Handler(r) diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go new file mode 100644 index 0000000000..9c958ff7e8 --- /dev/null +++ b/pkg/query-service/auth/auth.go @@ -0,0 +1,373 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "github.com/pkg/errors" + "go.signoz.io/query-service/constants" + "go.signoz.io/query-service/dao" + "go.signoz.io/query-service/model" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" +) + +const ( + opaqueTokenSize = 16 + minimumPasswordLength = 8 +) + +var ( + ErrorInvalidCreds = fmt.Errorf("Invalid credentials") +) + +// The root user should be able to invite people to create account on SigNoz cluster. +func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteResponse, error) { + zap.S().Debugf("Got an invite request for email: %s\n", req.Email) + + token, err := randomHex(opaqueTokenSize) + if err != nil { + return nil, errors.Wrap(err, "failed to generate invite token") + } + + user, apiErr := dao.DB().GetUserByEmail(ctx, req.Email) + if apiErr != nil { + return nil, errors.Wrap(apiErr.Err, "Failed to check already existing user") + } + + if user != nil { + return nil, errors.New("User already exists with the same email") + } + + if err := validateInviteRequest(req); err != nil { + return nil, errors.Wrap(err, "invalid invite request") + } + + jwtAdmin, err := ExtractJwtFromContext(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to extract admin jwt token") + } + + adminUser, err := validateUser(jwtAdmin) + if err != nil { + return nil, errors.Wrap(err, "failed to validate admin jwt token") + } + + au, apiErr := dao.DB().GetUser(ctx, adminUser.Id) + if apiErr != nil { + return nil, errors.Wrap(err, "failed to query admin user from the DB") + } + inv := &model.InvitationObject{ + Name: req.Name, + Email: req.Email, + Token: token, + CreatedAt: time.Now().Unix(), + Role: req.Role, + OrgId: au.OrgId, + } + + if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil { + return nil, errors.Wrap(err.Err, "failed to write to DB") + } + + return &model.InviteResponse{Email: inv.Email, InviteToken: inv.Token}, nil +} + +// RevokeInvite is used to revoke the invitation for the given email. +func RevokeInvite(ctx context.Context, email string) error { + zap.S().Debugf("RevokeInvite method invoked for email: %s\n", email) + + if !isValidEmail(email) { + return ErrorInvalidInviteToken + } + + if err := dao.DB().DeleteInvitation(ctx, email); err != nil { + return errors.Wrap(err.Err, "failed to write to DB") + } + return nil +} + +// GetInvite returns an invitation object for the given token. +func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObject, error) { + zap.S().Debugf("GetInvite method invoked for token: %s\n", token) + + inv, apiErr := dao.DB().GetInviteFromToken(ctx, token) + if apiErr != nil { + return nil, errors.Wrap(apiErr.Err, "failed to query the DB") + } + + if inv == nil { + return nil, errors.New("user is not invited") + } + + // TODO(Ahsan): This is not the best way to add org name in the invite response. We should + // either include org name in the invite table or do a join query. + org, apiErr := dao.DB().GetOrg(ctx, inv.OrgId) + if apiErr != nil { + return nil, errors.Wrap(apiErr.Err, "failed to query the DB") + } + return &model.InvitationResponseObject{ + Name: inv.Name, + Email: inv.Email, + Token: inv.Token, + CreatedAt: inv.CreatedAt, + Role: inv.Role, + Organization: org.Name, + }, nil +} + +func validateInvite(ctx context.Context, req *RegisterRequest) (*model.InvitationObject, error) { + invitation, err := dao.DB().GetInviteFromEmail(ctx, req.Email) + if err != nil { + return nil, errors.Wrap(err.Err, "Failed to read from DB") + } + + if invitation == nil || invitation.Token != req.InviteToken { + return nil, ErrorInvalidInviteToken + } + + return invitation, nil +} + +func CreateResetPasswordToken(ctx context.Context, userId string) (*model.ResetPasswordEntry, error) { + token, err := randomHex(opaqueTokenSize) + if err != nil { + return nil, errors.Wrap(err, "failed to generate reset password token") + } + + req := &model.ResetPasswordEntry{ + UserId: userId, + Token: token, + } + if apiErr := dao.DB().CreateResetPasswordEntry(ctx, req); err != nil { + return nil, errors.Wrap(apiErr.Err, "failed to write to DB") + } + return req, nil +} + +func ResetPassword(ctx context.Context, req *model.ResetPasswordRequest) error { + entry, apiErr := dao.DB().GetResetPasswordEntry(ctx, req.Token) + if apiErr != nil { + return errors.Wrap(apiErr.Err, "failed to query the DB") + } + + if entry == nil { + return errors.New("Invalid reset password request") + } + + hash, err := passwordHash(req.Password) + if err != nil { + return errors.Wrap(err, "Failed to generate password hash") + } + + if apiErr := dao.DB().UpdateUserPassword(ctx, hash, entry.UserId); apiErr != nil { + return apiErr.Err + } + + if apiErr := dao.DB().DeleteResetPasswordEntry(ctx, req.Token); apiErr != nil { + return errors.Wrap(apiErr.Err, "failed to delete reset token from DB") + } + + return nil +} + +func ChangePassword(ctx context.Context, req *model.ChangePasswordRequest) error { + + user, apiErr := dao.DB().GetUser(ctx, req.UserId) + if apiErr != nil { + return errors.Wrap(apiErr.Err, "failed to query user from the DB") + } + + if user == nil || !passwordMatch(user.Password, req.OldPassword) { + return ErrorInvalidCreds + } + + hash, err := passwordHash(req.NewPassword) + if err != nil { + return errors.Wrap(err, "Failed to generate password hash") + } + + if apiErr := dao.DB().UpdateUserPassword(ctx, hash, user.Id); apiErr != nil { + return apiErr.Err + } + + return nil +} + +type RegisterRequest struct { + Name string `json:"name"` + OrgName string `json:"orgName"` + Email string `json:"email"` + Password string `json:"password"` + InviteToken string `json:"token"` +} + +// Register registers a new user. For the first register request, it doesn't need an invite token +// and also the first registration is an enforced ADMIN registration. Every subsequent request will +// need an invite token to go through. +func Register(ctx context.Context, req *RegisterRequest) *model.ApiError { + + zap.S().Debugf("Got a register request for email: %v\n", req.Email) + + // TODO(Ahsan): We should optimize it, shouldn't make an extra DB call everytime to know if + // this is the first register request. + users, apiErr := dao.DB().GetUsers(ctx) + if apiErr != nil { + zap.S().Debugf("GetUser failed, err: %v\n", apiErr.Err) + return apiErr + } + + var groupName, orgId string + + // If there are no user, then this first user is granted Admin role. Also, an org is created + // based on the request. Any other user can't use any other org name, if they do then + // registration will fail because of foreign key violation while create user. + // TODO(Ahsan): We need to re-work this logic for the case of multi-tenant system. + if len(users) == 0 { + org, apiErr := dao.DB().CreateOrg(ctx, &model.Organization{Name: req.OrgName}) + if apiErr != nil { + zap.S().Debugf("CreateOrg failed, err: %v\n", apiErr.Err) + return apiErr + } + groupName = constants.AdminGroup + orgId = org.Id + } + + if len(users) > 0 { + inv, err := validateInvite(ctx, req) + if err != nil { + return &model.ApiError{Err: err, Typ: model.ErrorUnauthorized} + } + org, apiErr := dao.DB().GetOrgByName(ctx, req.OrgName) + if apiErr != nil { + zap.S().Debugf("GetOrgByName failed, err: %v\n", apiErr.Err) + return apiErr + } + + groupName = inv.Role + if org != nil { + orgId = org.Id + } + } + + group, apiErr := dao.DB().GetGroupByName(ctx, groupName) + if apiErr != nil { + zap.S().Debugf("GetGroupByName failed, err: %v\n", apiErr.Err) + return apiErr + } + + hash, err := passwordHash(req.Password) + if err != nil { + return &model.ApiError{Err: err, Typ: model.ErrorUnauthorized} + } + + user := &model.User{ + Id: uuid.NewString(), + Name: req.Name, + Email: req.Email, + Password: hash, + CreatedAt: time.Now().Unix(), + ProfilePirctureURL: "", // Currently unused + GroupId: group.Id, + OrgId: orgId, + } + + // TODO(Ahsan): Ideally create user and delete invitation should happen in a txn. + _, apiErr = dao.DB().CreateUser(ctx, user) + if apiErr != nil { + zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err) + return apiErr + } + + return dao.DB().DeleteInvitation(ctx, user.Email) +} + +// Login method returns access and refresh tokens on successful login, else it errors out. +func Login(ctx context.Context, request *model.LoginRequest) (*model.LoginResponse, error) { + zap.S().Debugf("Login method called for user: %s\n", request.Email) + + user, err := authenticateLogin(ctx, request) + if err != nil { + zap.S().Debugf("Failed to authenticate login request, %v", err) + return nil, err + } + + accessJwtExpiry := time.Now().Add(JwtExpiry).Unix() + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "id": user.Id, + "gid": user.GroupId, + "email": user.Email, + "exp": accessJwtExpiry, + }) + + accessJwt, err := token.SignedString([]byte(JwtSecret)) + if err != nil { + return nil, errors.Errorf("failed to encode jwt: %v", err) + } + + refreshJwtExpiry := time.Now().Add(JwtRefresh).Unix() + token = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "id": user.Id, + "gid": user.GroupId, + "email": user.Email, + "exp": refreshJwtExpiry, + }) + + refreshJwt, err := token.SignedString([]byte(JwtSecret)) + if err != nil { + return nil, errors.Errorf("failed to encode jwt: %v", err) + } + + return &model.LoginResponse{ + AccessJwt: accessJwt, + AccessJwtExpiry: accessJwtExpiry, + RefreshJwt: refreshJwt, + RefreshJwtExpiry: refreshJwtExpiry, + UserId: user.Id, + }, nil +} + +// authenticateLogin is responsible for querying the DB and validating the credentials. +func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.UserPayload, error) { + + // If refresh token is valid, then simply authorize the login request. + if len(req.RefreshToken) > 0 { + user, err := validateUser(req.RefreshToken) + if err != nil { + return nil, errors.Wrap(err, "failed to validate refresh token") + } + + return user, nil + } + + user, err := dao.DB().GetUserByEmail(ctx, req.Email) + if err != nil { + return nil, errors.Wrap(err.Err, "user not found") + } + if user == nil || !passwordMatch(user.Password, req.Password) { + return nil, ErrorInvalidCreds + } + return user, nil +} + +// Generate hash from the password. +func passwordHash(pass string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +// Checks if the given password results in the given hash. +func passwordMatch(hash, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + if err != nil { + return false + } + return true +} diff --git a/pkg/query-service/auth/jwt.go b/pkg/query-service/auth/jwt.go new file mode 100644 index 0000000000..2f67a7b9e0 --- /dev/null +++ b/pkg/query-service/auth/jwt.go @@ -0,0 +1,94 @@ +package auth + +import ( + "context" + "net/http" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + "github.com/golang-jwt/jwt" + "github.com/pkg/errors" + "go.signoz.io/query-service/model" + "go.uber.org/zap" + "google.golang.org/grpc/metadata" +) + +var ( + JwtSecret string + JwtExpiry = 30 * time.Minute + JwtRefresh = 24 * time.Hour +) + +func ParseJWT(jwtStr string) (jwt.MapClaims, error) { + 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"]) + } + return []byte(JwtSecret), nil + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to parse jwt token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + return nil, errors.Errorf("Not a valid jwt claim") + } + return claims, nil +} + +func validateUser(tok string) (*model.UserPayload, error) { + claims, err := ParseJWT(tok) + if err != nil { + return nil, err + } + now := time.Now().Unix() + if !claims.VerifyExpiresAt(now, true) { + return nil, errors.Errorf("Token is expired") + } + return &model.UserPayload{ + User: model.User{ + Id: claims["id"].(string), + GroupId: claims["gid"].(string), + Email: claims["email"].(string), + }, + }, nil +} + +// AttachJwtToContext attached the jwt token from the request header to the context. +func AttachJwtToContext(ctx context.Context, r *http.Request) context.Context { + token, err := ExtractJwtFromRequest(r) + if err != nil { + zap.S().Debugf("Error while getting token from header, %v", err) + return ctx + } + + if len(token) > 0 { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.New(nil) + } + + md.Append("accessJwt", token) + ctx = metadata.NewIncomingContext(ctx, md) + } + return ctx +} + +func ExtractJwtFromContext(ctx context.Context) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", errors.New("No JWT metadata token found") + } + accessJwt := md.Get("accessJwt") + if len(accessJwt) == 0 { + return "", errors.New("No JWT token found") + } + + return accessJwt[0], nil +} + +func ExtractJwtFromRequest(r *http.Request) (string, error) { + return jwtmiddleware.FromAuthHeader(r) +} diff --git a/pkg/query-service/auth/rbac.go b/pkg/query-service/auth/rbac.go new file mode 100644 index 0000000000..94bcfcb276 --- /dev/null +++ b/pkg/query-service/auth/rbac.go @@ -0,0 +1,136 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + "regexp" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + "go.signoz.io/query-service/constants" + "go.signoz.io/query-service/dao" + "go.uber.org/zap" +) + +type Group struct { + GroupID string + GroupName string +} + +type AuthCache struct { + AdminGroupId string + EditorGroupId string + ViewerGroupId string +} + +var AuthCacheObj AuthCache + +// InitAuthCache reads the DB and initialize the auth cache. +func InitAuthCache(ctx context.Context) error { + + setGroupId := func(groupName string, dest *string) error { + group, err := dao.DB().GetGroupByName(ctx, groupName) + if err != nil { + return errors.Wrapf(err.Err, "failed to get group %s", groupName) + } + *dest = group.Id + return nil + } + + if err := setGroupId(constants.AdminGroup, &AuthCacheObj.AdminGroupId); err != nil { + return err + } + if err := setGroupId(constants.EditorGroup, &AuthCacheObj.EditorGroupId); err != nil { + return err + } + if err := setGroupId(constants.ViewerGroup, &AuthCacheObj.ViewerGroupId); err != nil { + return err + } + + return nil +} + +func IsAdmin(r *http.Request) bool { + accessJwt, err := ExtractJwtFromRequest(r) + if err != nil { + zap.S().Debugf("Failed to verify admin access, err: %v", err) + return false + } + + user, err := validateUser(accessJwt) + if err != nil { + return false + } + return user.GroupId == AuthCacheObj.AdminGroupId +} + +func IsSelfAccessRequest(r *http.Request) bool { + id := mux.Vars(r)["id"] + accessJwt, err := ExtractJwtFromRequest(r) + if err != nil { + zap.S().Debugf("Failed to verify self access, err: %v", err) + return false + } + + user, err := validateUser(accessJwt) + if err != nil { + zap.S().Debugf("Failed to verify self access, err: %v", err) + return false + } + zap.S().Debugf("Self access verification, userID: %s, id: %s\n", user.Id, id) + return user.Id == id +} + +func IsViewer(r *http.Request) bool { + accessJwt, err := ExtractJwtFromRequest(r) + if err != nil { + zap.S().Debugf("Failed to verify viewer access, err: %v", err) + return false + } + + user, err := validateUser(accessJwt) + if err != nil { + return false + } + + return user.GroupId == AuthCacheObj.ViewerGroupId +} + +func IsEditor(r *http.Request) bool { + accessJwt, err := ExtractJwtFromRequest(r) + if err != nil { + zap.S().Debugf("Failed to verify editor access, err: %v", err) + return false + } + + user, err := validateUser(accessJwt) + if err != nil { + return false + } + return user.GroupId == AuthCacheObj.EditorGroupId +} + +func ValidatePassword(password string) error { + if len(password) < minimumPasswordLength { + return errors.Errorf("Password should be atleast %d characters.", minimumPasswordLength) + } + + num := `[0-9]{1}` + lower := `[a-z]{1}` + upper := `[A-Z]{1}` + symbol := `[!@#$&*]{1}` + if b, err := regexp.MatchString(num, password); !b || err != nil { + return fmt.Errorf("password should have atleast one number") + } + if b, err := regexp.MatchString(lower, password); !b || err != nil { + return fmt.Errorf("password should have atleast one lower case letter") + } + if b, err := regexp.MatchString(upper, password); !b || err != nil { + return fmt.Errorf("password should have atleast one upper case letter") + } + if b, err := regexp.MatchString(symbol, password); !b || err != nil { + return fmt.Errorf("password should have atleast one special character from !@#$&* ") + } + return nil +} diff --git a/pkg/query-service/auth/utils.go b/pkg/query-service/auth/utils.go new file mode 100644 index 0000000000..5e13c32b8f --- /dev/null +++ b/pkg/query-service/auth/utils.go @@ -0,0 +1,55 @@ +package auth + +import ( + "crypto/rand" + "encoding/hex" + + "github.com/pkg/errors" + "go.signoz.io/query-service/constants" + "go.signoz.io/query-service/model" +) + +var ( + ErrorEmptyRequest = errors.New("Empty request") + ErrorInvalidEmail = errors.New("Invalid email") + ErrorInvalidRole = errors.New("Invalid role") + + ErrorInvalidInviteToken = errors.New("Invalid invite token") +) + +func randomHex(sz int) (string, error) { + bytes := make([]byte, sz) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func isValidRole(role string) bool { + switch role { + case constants.AdminGroup, constants.EditorGroup, constants.ViewerGroup: + return true + default: + return false + } + return false +} + +func validateInviteRequest(req *model.InviteRequest) error { + if req == nil { + return ErrorEmptyRequest + } + if !isValidEmail(req.Email) { + return ErrorInvalidEmail + } + + if !isValidRole(req.Role) { + return ErrorInvalidRole + } + return nil +} + +// TODO(Ahsan): Implement check on email semantic. +func isValidEmail(email string) bool { + return true +} diff --git a/pkg/query-service/constants/auth.go b/pkg/query-service/constants/auth.go new file mode 100644 index 0000000000..7248f9dc1a --- /dev/null +++ b/pkg/query-service/constants/auth.go @@ -0,0 +1,7 @@ +package constants + +const ( + AdminGroup = "ADMIN" + EditorGroup = "EDITOR" + ViewerGroup = "VIEWER" +) diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index f63b20ad7b..f4a00cec23 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -5,7 +5,9 @@ import ( "strconv" ) -const HTTPHostPort = "0.0.0.0:8080" +const ( + HTTPHostPort = "0.0.0.0:8080" +) var DEFAULT_TELEMETRY_ANONYMOUS = false @@ -28,7 +30,7 @@ func GetAlertManagerApiPrefix() string { return "http://alertmanager:9093/api/" } -// Alert manager channel subpath +// Alert manager channel subpath var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/routes") var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db") @@ -56,11 +58,10 @@ const ( ContextTimeout = 60 // seconds ) - func GetOrDefaultEnv(key string, fallback string) string { v := os.Getenv(key) if len(v) == 0 { return fallback } return v -} \ No newline at end of file +} diff --git a/pkg/query-service/dao/factory.go b/pkg/query-service/dao/factory.go index 92f2b7e534..710520421b 100644 --- a/pkg/query-service/dao/factory.go +++ b/pkg/query-service/dao/factory.go @@ -3,24 +3,31 @@ package dao import ( "fmt" - "go.signoz.io/query-service/constants" - "go.signoz.io/query-service/dao/interfaces" + "github.com/pkg/errors" "go.signoz.io/query-service/dao/sqlite" ) -func FactoryDao(engine string) (*interfaces.ModelDao, error) { - var i interfaces.ModelDao +var db ModelDao + +func InitDao(engine, path string) error { var err error switch engine { case "sqlite": - i, err = sqlite.InitDB(constants.RELATIONAL_DATASOURCE_PATH) + db, err = sqlite.InitDB(path) if err != nil { - return nil, err + return errors.Wrap(err, "failed to initialize DB") } default: - return nil, fmt.Errorf("RelationalDB type: %s is not supported in query service", engine) + return fmt.Errorf("RelationalDB type: %s is not supported in query service", engine) } - - return &i, nil + return nil +} + +func DB() ModelDao { + if db == nil { + // Should never reach here + panic("GetDB called before initialization") + } + return db } diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go new file mode 100644 index 0000000000..bf105ba30e --- /dev/null +++ b/pkg/query-service/dao/interface.go @@ -0,0 +1,56 @@ +package dao + +import ( + "context" + + "go.signoz.io/query-service/model" +) + +type ModelDao interface { + Queries + Mutations +} + +type Queries interface { + GetInviteFromEmail(ctx context.Context, email string) (*model.InvitationObject, *model.ApiError) + GetInviteFromToken(ctx context.Context, token string) (*model.InvitationObject, *model.ApiError) + GetInvites(ctx context.Context) ([]model.InvitationObject, *model.ApiError) + + 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) + + GetGroup(ctx context.Context, id string) (*model.Group, *model.ApiError) + GetGroupByName(ctx context.Context, name string) (*model.Group, *model.ApiError) + GetGroups(ctx context.Context) ([]model.Group, *model.ApiError) + + GetOrgs(ctx context.Context) ([]model.Organization, *model.ApiError) + GetOrgByName(ctx context.Context, name string) (*model.Organization, *model.ApiError) + GetOrg(ctx context.Context, id string) (*model.Organization, *model.ApiError) + + GetResetPasswordEntry(ctx context.Context, token string) (*model.ResetPasswordEntry, *model.ApiError) + GetUsersByOrg(ctx context.Context, orgId string) ([]model.UserPayload, *model.ApiError) + GetUsersByGroup(ctx context.Context, groupId string) ([]model.UserPayload, *model.ApiError) +} + +type Mutations interface { + CreateInviteEntry(ctx context.Context, req *model.InvitationObject) *model.ApiError + DeleteInvitation(ctx context.Context, email string) *model.ApiError + + CreateUser(ctx context.Context, user *model.User) (*model.User, *model.ApiError) + EditUser(ctx context.Context, update *model.User) (*model.User, *model.ApiError) + DeleteUser(ctx context.Context, id string) *model.ApiError + + CreateGroup(ctx context.Context, group *model.Group) (*model.Group, *model.ApiError) + DeleteGroup(ctx context.Context, id string) *model.ApiError + + CreateOrg(ctx context.Context, org *model.Organization) (*model.Organization, *model.ApiError) + EditOrg(ctx context.Context, org *model.Organization) *model.ApiError + DeleteOrg(ctx context.Context, id string) *model.ApiError + + CreateResetPasswordEntry(ctx context.Context, req *model.ResetPasswordEntry) *model.ApiError + DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError + + UpdateUserPassword(ctx context.Context, hash, userId string) *model.ApiError + UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError +} diff --git a/pkg/query-service/dao/interfaces/interface.go b/pkg/query-service/dao/interfaces/interface.go deleted file mode 100644 index e7d043fbf7..0000000000 --- a/pkg/query-service/dao/interfaces/interface.go +++ /dev/null @@ -1,5 +0,0 @@ -package interfaces - -type ModelDao interface { - UserPreferenceDao -} diff --git a/pkg/query-service/dao/interfaces/userPreference.go b/pkg/query-service/dao/interfaces/userPreference.go deleted file mode 100644 index c4770ae2de..0000000000 --- a/pkg/query-service/dao/interfaces/userPreference.go +++ /dev/null @@ -1,12 +0,0 @@ -package interfaces - -import ( - "context" - - "go.signoz.io/query-service/model" -) - -type UserPreferenceDao interface { - UpdateUserPreferece(ctx context.Context, userPreferences *model.UserPreferences) *model.ApiError - FetchUserPreference(ctx context.Context) (*model.UserPreferences, *model.ApiError) -} diff --git a/pkg/query-service/dao/sqlite/connection.go b/pkg/query-service/dao/sqlite/connection.go index 57e5cf648c..08bc210e4f 100644 --- a/pkg/query-service/dao/sqlite/connection.go +++ b/pkg/query-service/dao/sqlite/connection.go @@ -5,8 +5,11 @@ import ( "fmt" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" "go.signoz.io/query-service/constants" + "go.signoz.io/query-service/model" "go.signoz.io/query-service/telemetry" + "go.uber.org/zap" ) type ModelDaoSqlite struct { @@ -19,52 +22,138 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) { db, err := sqlx.Open("sqlite3", dataSourceName) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to Open sqlite3 DB") } db.SetMaxOpenConns(10) - table_schema := `CREATE TABLE IF NOT EXISTS user_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - uuid TEXT NOT NULL, - isAnonymous INTEGER NOT NULL DEFAULT 0 CHECK(isAnonymous IN (0,1)), - hasOptedUpdates INTEGER NOT NULL DEFAULT 1 CHECK(hasOptedUpdates IN (0,1)) - );` + table_schema := ` + PRAGMA foreign_keys = ON; + + CREATE TABLE IF NOT EXISTS invites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + token TEXT NOT NULL, + created_at INTEGER NOT NULL, + role TEXT NOT NULL, + org_id TEXT NOT NULL, + FOREIGN KEY(org_id) REFERENCES organizations(id) + ); + CREATE TABLE IF NOT EXISTS organizations ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at INTEGER NOT NULL, + is_anonymous INTEGER NOT NULL DEFAULT 0 CHECK(is_anonymous IN (0,1)), + has_opted_updates INTEGER NOT NULL DEFAULT 1 CHECK(has_opted_updates IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + created_at INTEGER NOT NULL, + profile_picture_url TEXT, + group_id TEXT NOT NULL, + org_id TEXT NOT NULL, + FOREIGN KEY(group_id) REFERENCES groups(id), + FOREIGN KEY(org_id) REFERENCES organizations(id) + ); + CREATE TABLE IF NOT EXISTS groups ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE + ); + CREATE TABLE IF NOT EXISTS reset_password_request ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + token TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) + ); + ` _, err = db.Exec(table_schema) if err != nil { - return nil, fmt.Errorf("Error in creating user_preferences table: %s", err.Error()) + return nil, fmt.Errorf("Error in creating tables: %v", err.Error()) } mds := &ModelDaoSqlite{db: db} - err = mds.initializeUserPreferences() - if err != nil { + ctx := context.Background() + if err := mds.initializeOrgPreferences(ctx); err != nil { + return nil, err + } + if err := mds.initializeRBAC(ctx); err != nil { return nil, err } - return mds, nil + return mds, nil } -func (mds *ModelDaoSqlite) initializeUserPreferences() error { + +// initializeOrgPreferences initializes in-memory telemetry settings. It is planned to have +// multiple orgs in the system. In case of multiple orgs, there will be separate instance +// of in-memory telemetry for each of the org, having their own settings. As of now, we only +// have one org so this method relies on the settings of this org to initialize the telemetry etc. +// TODO(Ahsan): Make it multi-tenant when we move to a system with multiple orgs. +func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error { // set anonymous setting as default in case of any failures to fetch UserPreference in below section telemetry.GetInstance().SetTelemetryAnonymous(constants.DEFAULT_TELEMETRY_ANONYMOUS) - ctx := context.Background() - userPreference, apiError := mds.FetchUserPreference(ctx) + orgs, apiError := mds.GetOrgs(ctx) + if apiError != nil { + return apiError.Err + } - if apiError != nil { - return apiError.Err + if len(orgs) > 1 { + return errors.Errorf("Found %d organizations, expected one or none.", len(orgs)) } - if userPreference == nil { - userPreference, apiError = mds.CreateDefaultUserPreference(ctx) - } - if apiError != nil { - return apiError.Err + + var org model.Organization + if len(orgs) == 1 { + org = orgs[0] } // set telemetry fields from userPreferences - telemetry.GetInstance().SetTelemetryAnonymous(userPreference.GetIsAnonymous()) - telemetry.GetInstance().SetDistinctId(userPreference.GetUUID()) + telemetry.GetInstance().SetTelemetryAnonymous(org.IsAnonymous) + telemetry.GetInstance().SetDistinctId(org.Id) return nil } + +// initializeRBAC creates the ADMIN, EDITOR and VIEWER groups if they are not present. +func (mds *ModelDaoSqlite) initializeRBAC(ctx context.Context) error { + f := func(groupName string) error { + _, err := mds.createGroupIfNotPresent(ctx, groupName) + return errors.Wrap(err, "Failed to create group") + } + + if err := f(constants.AdminGroup); err != nil { + return err + } + if err := f(constants.EditorGroup); err != nil { + return err + } + if err := f(constants.ViewerGroup); err != nil { + return err + } + + return nil +} + +func (mds *ModelDaoSqlite) createGroupIfNotPresent(ctx context.Context, + name string) (*model.Group, error) { + + group, err := mds.GetGroupByName(ctx, name) + if err != nil { + return nil, errors.Wrap(err.Err, "Failed to query for root group") + } + if group != nil { + return group, nil + } + + zap.S().Debugf("%s is not found, creating it", name) + group, cErr := mds.CreateGroup(ctx, &model.Group{Name: name}) + if cErr != nil { + return nil, cErr.Err + } + return group, nil +} diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go new file mode 100644 index 0000000000..b71d19b1cb --- /dev/null +++ b/pkg/query-service/dao/sqlite/rbac.go @@ -0,0 +1,512 @@ +package sqlite + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + "go.signoz.io/query-service/model" + "go.signoz.io/query-service/telemetry" +) + +func (mds *ModelDaoSqlite) CreateInviteEntry(ctx context.Context, + req *model.InvitationObject) *model.ApiError { + + _, err := mds.db.ExecContext(ctx, + `INSERT INTO invites (email, name, token, role, created_at, org_id) + VALUES (?, ?, ?, ?, ?, ?);`, + req.Email, req.Name, req.Token, req.Role, req.CreatedAt, req.OrgId) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) DeleteInvitation(ctx context.Context, email string) *model.ApiError { + _, err := mds.db.ExecContext(ctx, `DELETE from invites where email=?;`, email) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) GetInviteFromEmail(ctx context.Context, email string, +) (*model.InvitationObject, *model.ApiError) { + + invites := []model.InvitationObject{} + err := mds.db.Select(&invites, + `SELECT * FROM invites WHERE email=?;`, email) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(invites) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.Errorf("Found multiple invites for the email: %s", email)} + } + + if len(invites) == 0 { + return nil, nil + } + return &invites[0], nil +} + +func (mds *ModelDaoSqlite) GetInviteFromToken(ctx context.Context, token string, +) (*model.InvitationObject, *model.ApiError) { + + invites := []model.InvitationObject{} + err := mds.db.Select(&invites, + `SELECT * FROM invites WHERE token=?;`, token) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(invites) > 1 { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if len(invites) == 0 { + return nil, nil + } + return &invites[0], nil +} + +func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, +) ([]model.InvitationObject, *model.ApiError) { + + invites := []model.InvitationObject{} + err := mds.db.Select(&invites, "SELECT * FROM invites") + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return invites, nil +} + +func (mds *ModelDaoSqlite) CreateOrg(ctx context.Context, + org *model.Organization) (*model.Organization, *model.ApiError) { + + org.Id = uuid.NewString() + org.CreatedAt = time.Now().Unix() + _, err := mds.db.ExecContext(ctx, + `INSERT INTO organizations (id, name, created_at) VALUES (?, ?, ?);`, + org.Id, org.Name, org.CreatedAt) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return org, nil +} + +func (mds *ModelDaoSqlite) GetOrg(ctx context.Context, + id string) (*model.Organization, *model.ApiError) { + + orgs := []model.Organization{} + err := mds.db.Select(&orgs, `SELECT * FROM organizations WHERE id=?;`, id) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(orgs) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Found multiple org with same ID"), + } + } + + if len(orgs) == 0 { + return nil, nil + } + return &orgs[0], nil +} + +func (mds *ModelDaoSqlite) GetOrgByName(ctx context.Context, + name string) (*model.Organization, *model.ApiError) { + + orgs := []model.Organization{} + + if err := mds.db.Select(&orgs, `SELECT * FROM organizations WHERE name=?;`, name); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if len(orgs) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Multiple orgs with same ID found"), + } + } + if len(orgs) == 0 { + return nil, nil + } + return &orgs[0], nil +} + +func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]model.Organization, *model.ApiError) { + orgs := []model.Organization{} + err := mds.db.Select(&orgs, `SELECT * FROM organizations`) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return orgs, nil +} + +func (mds *ModelDaoSqlite) EditOrg(ctx context.Context, org *model.Organization) *model.ApiError { + + q := `UPDATE organizations SET name=?,has_opted_updates=?,is_anonymous=? WHERE id=?;` + + _, err := mds.db.ExecContext(ctx, q, org.Name, org.HasOptedUpdates, org.IsAnonymous, org.Id) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + telemetry.GetInstance().SetTelemetryAnonymous(org.IsAnonymous) + return nil +} + +func (mds *ModelDaoSqlite) DeleteOrg(ctx context.Context, id string) *model.ApiError { + + _, err := mds.db.ExecContext(ctx, `DELETE from organizations where id=?;`, id) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, + user *model.User) (*model.User, *model.ApiError) { + + _, err := mds.db.ExecContext(ctx, + `INSERT INTO users (id, name, email, password, created_at, profile_picture_url, group_id, org_id) + VALUES (?, ?, ?, ?, ?, ?, ?,?);`, + user.Id, user.Name, user.Email, user.Password, user.CreatedAt, + user.ProfilePirctureURL, user.GroupId, user.OrgId, + ) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return user, nil +} + +func (mds *ModelDaoSqlite) EditUser(ctx context.Context, + update *model.User) (*model.User, *model.ApiError) { + + _, err := mds.db.ExecContext(ctx, + `UPDATE users SET name=?,org_id=?,email=? WHERE id=?;`, update.Name, + update.OrgId, update.Email, update.Id) + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return update, nil +} + +func (mds *ModelDaoSqlite) UpdateUserPassword(ctx context.Context, passwordHash, + userId string) *model.ApiError { + + q := `UPDATE users SET password=? WHERE id=?;` + if _, err := mds.db.ExecContext(ctx, q, passwordHash, userId); err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError { + + q := `UPDATE users SET group_id=? WHERE id=?;` + if _, err := mds.db.ExecContext(ctx, q, groupId, userId); err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) DeleteUser(ctx context.Context, id string) *model.ApiError { + + result, err := mds.db.ExecContext(ctx, `DELETE from users where id=?;`, id) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + affectedRows, err := result.RowsAffected() + if err != nil { + return &model.ApiError{Typ: model.ErrorExec, Err: err} + } + if affectedRows == 0 { + return &model.ApiError{ + Typ: model.ErrorNotFound, + Err: fmt.Errorf("no user found with id: %s", id), + } + } + + return nil +} + +func (mds *ModelDaoSqlite) GetUser(ctx context.Context, + id string) (*model.UserPayload, *model.ApiError) { + + users := []model.UserPayload{} + query := `select + u.id, + u.name, + u.email, + u.password, + u.created_at, + u.profile_picture_url, + u.org_id, + u.group_id, + g.name as role, + o.name as organization + from users u, groups g, organizations o + where + g.id=u.group_id and + o.id = u.org_id and + u.id=?;` + + if err := mds.db.Select(&users, query, id); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(users) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Found multiple users with same ID"), + } + } + + if len(users) == 0 { + return nil, nil + } + return &users[0], nil +} + +func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context, + email string) (*model.UserPayload, *model.ApiError) { + + users := []model.UserPayload{} + query := `select + u.id, + u.name, + u.email, + u.password, + u.created_at, + u.profile_picture_url, + u.org_id, + u.group_id, + g.name as role, + o.name as organization + from users u, groups g, organizations o + where + g.id=u.group_id and + o.id = u.org_id and + u.email=?;` + + if err := mds.db.Select(&users, query, email); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(users) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Found multiple users with same ID."), + } + } + + if len(users) == 0 { + return nil, nil + } + return &users[0], nil +} + +func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) { + users := []model.UserPayload{} + + query := `select + u.id, + u.name, + u.email, + u.password, + u.created_at, + u.profile_picture_url, + u.org_id, + u.group_id, + g.name as role, + o.name as organization + from users u, groups g, organizations o + where + g.id = u.group_id and + o.id = u.org_id` + + err := mds.db.Select(&users, query) + + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return users, nil +} + +func (mds *ModelDaoSqlite) GetUsersByOrg(ctx context.Context, + orgId string) ([]model.UserPayload, *model.ApiError) { + + users := []model.UserPayload{} + query := `select + u.id, + u.name, + u.email, + u.password, + u.created_at, + u.profile_picture_url, + u.org_id, + u.group_id, + g.name as role, + o.name as organization + from users u, groups g, organizations o + where + u.group_id = g.id and + u.org_id = o.id and + u.org_id=?;` + + if err := mds.db.Select(&users, query, orgId); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return users, nil +} + +func (mds *ModelDaoSqlite) GetUsersByGroup(ctx context.Context, + groupId string) ([]model.UserPayload, *model.ApiError) { + + users := []model.UserPayload{} + query := `select + u.id, + u.name, + u.email, + u.password, + u.created_at, + u.profile_picture_url, + u.org_id, + u.group_id, + g.name as role, + o.name as organization + from users u, groups g, organizations o + where + u.group_id = g.id and + o.id = u.org_id and + u.group_id=?;` + + if err := mds.db.Select(&users, query, groupId); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return users, nil +} + +func (mds *ModelDaoSqlite) CreateGroup(ctx context.Context, + group *model.Group) (*model.Group, *model.ApiError) { + + group.Id = uuid.NewString() + + q := `INSERT INTO groups (id, name) VALUES (?, ?);` + if _, err := mds.db.ExecContext(ctx, q, group.Id, group.Name); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + return group, nil +} + +func (mds *ModelDaoSqlite) DeleteGroup(ctx context.Context, id string) *model.ApiError { + + if _, err := mds.db.ExecContext(ctx, `DELETE from groups where id=?;`, id); err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) GetGroup(ctx context.Context, + id string) (*model.Group, *model.ApiError) { + + groups := []model.Group{} + if err := mds.db.Select(&groups, `SELECT id, name FROM groups WHERE id=?`, id); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if len(groups) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Found multiple groups with same ID."), + } + } + + if len(groups) == 0 { + return nil, nil + } + return &groups[0], nil +} + +func (mds *ModelDaoSqlite) GetGroupByName(ctx context.Context, + name string) (*model.Group, *model.ApiError) { + + groups := []model.Group{} + if err := mds.db.Select(&groups, `SELECT id, name FROM groups WHERE name=?`, name); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if len(groups) > 1 { + return nil, &model.ApiError{ + Typ: model.ErrorInternal, + Err: errors.New("Found multiple groups with same name"), + } + } + + if len(groups) == 0 { + return nil, nil + } + + return &groups[0], nil +} + +func (mds *ModelDaoSqlite) GetGroups(ctx context.Context) ([]model.Group, *model.ApiError) { + + groups := []model.Group{} + if err := mds.db.Select(&groups, "SELECT * FROM groups"); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + return groups, nil +} + +func (mds *ModelDaoSqlite) CreateResetPasswordEntry(ctx context.Context, + req *model.ResetPasswordEntry) *model.ApiError { + + q := `INSERT INTO reset_password_request (user_id, token) VALUES (?, ?);` + if _, err := mds.db.ExecContext(ctx, q, req.UserId, req.Token); err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) DeleteResetPasswordEntry(ctx context.Context, + token string) *model.ApiError { + _, err := mds.db.ExecContext(ctx, `DELETE from reset_password_request where token=?;`, token) + if err != nil { + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + return nil +} + +func (mds *ModelDaoSqlite) GetResetPasswordEntry(ctx context.Context, + token string) (*model.ResetPasswordEntry, *model.ApiError) { + + entries := []model.ResetPasswordEntry{} + + q := `SELECT user_id,token FROM reset_password_request WHERE token=?;` + if err := mds.db.Select(&entries, q, token); err != nil { + return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + if len(entries) > 1 { + return nil, &model.ApiError{Typ: model.ErrorInternal, + Err: errors.New("Multiple entries for reset token is found")} + } + + if len(entries) == 0 { + return nil, nil + } + return &entries[0], nil +} diff --git a/pkg/query-service/dao/sqlite/userPreferenceImpl.go b/pkg/query-service/dao/sqlite/userPreferenceImpl.go deleted file mode 100644 index 95addcea68..0000000000 --- a/pkg/query-service/dao/sqlite/userPreferenceImpl.go +++ /dev/null @@ -1,91 +0,0 @@ -package sqlite - -import ( - "context" - "fmt" - - "github.com/google/uuid" - "go.signoz.io/query-service/model" - "go.signoz.io/query-service/telemetry" - "go.uber.org/zap" -) - -func (mds *ModelDaoSqlite) FetchUserPreference(ctx context.Context) (*model.UserPreferences, *model.ApiError) { - - userPreferences := []model.UserPreferences{} - query := fmt.Sprintf("SELECT id, uuid, isAnonymous, hasOptedUpdates FROM user_preferences;") - - err := mds.db.Select(&userPreferences, query) - - if err != nil { - zap.S().Debug("Error in processing sql query: ", err) - return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - - // zap.S().Info(query) - if len(userPreferences) > 1 { - zap.S().Debug("Error in processing sql query: ", fmt.Errorf("more than 1 row in user_preferences found")) - return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - - if len(userPreferences) == 0 { - return nil, nil - } - - return &userPreferences[0], nil - -} - -func (mds *ModelDaoSqlite) UpdateUserPreferece(ctx context.Context, userPreferences *model.UserPreferences) *model.ApiError { - - tx, err := mds.db.Begin() - if err != nil { - return &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - - userPreferencesFound, apiError := mds.FetchUserPreference(ctx) - if apiError != nil { - return apiError - } - - stmt, err := tx.Prepare(`UPDATE user_preferences SET isAnonymous=$1, hasOptedUpdates=$2 WHERE id=$3;`) - defer stmt.Close() - - if err != nil { - zap.S().Errorf("Error in preparing statement for INSERT to user_preferences\n", err) - tx.Rollback() - return &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - - query_result, err := stmt.Exec(userPreferences.GetIsAnonymous(), userPreferences.GetHasOptedUpdate(), userPreferencesFound.GetId()) - if err != nil { - zap.S().Errorf("Error in Executing prepared statement for INSERT to user_preferences\n", err) - tx.Rollback() // return an error too, we may want to wrap them - return &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - zap.S().Debug(query_result.RowsAffected()) - zap.S().Debug(userPreferences.GetIsAnonymous(), userPreferences.GetHasOptedUpdate(), userPreferencesFound.GetId()) - - err = tx.Commit() - if err != nil { - zap.S().Errorf("Error in committing transaction for INSERT to user_preferences\n", err) - return &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - telemetry.GetInstance().SetTelemetryAnonymous(userPreferences.GetIsAnonymous()) - - return nil -} - -func (mds *ModelDaoSqlite) CreateDefaultUserPreference(ctx context.Context) (*model.UserPreferences, *model.ApiError) { - - uuid := uuid.New().String() - _, err := mds.db.ExecContext(ctx, `INSERT INTO user_preferences (uuid, isAnonymous, hasOptedUpdates) VALUES (?, 0, 1);`, uuid) - - if err != nil { - zap.S().Errorf("Error in preparing statement for INSERT to user_preferences\n", err) - return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - - return mds.FetchUserPreference(ctx) - -} diff --git a/pkg/query-service/go.mod b/pkg/query-service/go.mod index 4ab7c47d33..b101f18da5 100644 --- a/pkg/query-service/go.mod +++ b/pkg/query-service/go.mod @@ -28,6 +28,7 @@ require ( ) require ( + github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect github.com/klauspost/cpuid v1.2.3 // indirect github.com/minio/md5-simd v1.1.0 // indirect github.com/minio/sha256-simd v0.1.1 // indirect @@ -42,6 +43,7 @@ require ( github.com/Azure/go-autorest v10.8.1+incompatible // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect + github.com/auth0/go-jwt-middleware v1.0.1 github.com/aws/aws-sdk-go v1.27.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect @@ -55,6 +57,7 @@ require ( github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -64,7 +67,7 @@ require ( github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.2.3-0.20180520015035-48a0ecefe2e4 // indirect github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602 // indirect - github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/gosimple/unidecode v1.0.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -104,7 +107,7 @@ require ( github.com/segmentio/backo-go v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect - github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/assertions v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.7.1 @@ -114,7 +117,7 @@ require ( go.opentelemetry.io/otel/trace v1.4.1 // indirect go.uber.org/atomic v1.6.0 // indirect go.uber.org/multierr v1.5.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/net v0.0.0-20211013171255-e13a2654a71e // indirect golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect @@ -125,7 +128,7 @@ require ( google.golang.org/api v0.51.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4 // indirect - google.golang.org/grpc v1.41.0 // indirect + google.golang.org/grpc v1.41.0 google.golang.org/grpc/examples v0.0.0-20210803221256-6ba56c814be7 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/pkg/query-service/go.sum b/pkg/query-service/go.sum index 4d6a503c76..77fd087ceb 100644 --- a/pkg/query-service/go.sum +++ b/pkg/query-service/go.sum @@ -63,6 +63,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/auth0/go-jwt-middleware v1.0.1 h1:/fsQ4vRr4zod1wKReUH+0A3ySRjGiT9G34kypO/EKwI= +github.com/auth0/go-jwt-middleware v1.0.1/go.mod h1:YSeUX3z6+TF2H+7padiEqNJ73Zy9vXW72U//IgN0BIM= github.com/aws/aws-sdk-go v1.13.44-0.20180507225419-00862f899353/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -109,6 +111,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -135,6 +139,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -224,11 +230,13 @@ github.com/googleapis/gnostic v0.2.3-0.20180520015035-48a0ecefe2e4 h1:Z09Qt6AGDt github.com/googleapis/gnostic v0.2.3-0.20180520015035-48a0ecefe2e4/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602 h1:Acc1d6mIuURCyYN6nkm1d7+Gycfq1+jUWdnBbTyGb6E= github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -396,8 +404,9 @@ github.com/shurcooL/vfsgen v0.0.0-20180711163814-62bca832be04/go.mod h1:TrYk7fJV github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -418,6 +427,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/query-service/main.go b/pkg/query-service/main.go index 90c12945bf..9cede29cd7 100644 --- a/pkg/query-service/main.go +++ b/pkg/query-service/main.go @@ -1,11 +1,13 @@ package main import ( + "context" "os" "os/signal" "syscall" "go.signoz.io/query-service/app" + "go.signoz.io/query-service/auth" "go.signoz.io/query-service/constants" "go.signoz.io/query-service/version" @@ -39,6 +41,15 @@ func main() { // DruidClientUrl: constants.DruidClientUrl, } + // Read the jwt secret key + auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET") + + if len(auth.JwtSecret) == 0 { + zap.S().Warn("No JWT secret key is specified.") + } else { + zap.S().Info("No JWT secret key set successfully.") + } + server, err := app.NewServer(serverOptions) if err != nil { logger.Fatal("Failed to create server", zap.Error(err)) @@ -48,6 +59,10 @@ func main() { logger.Fatal("Could not start servers", zap.Error(err)) } + if err := auth.InitAuthCache(context.Background()); err != nil { + logger.Fatal("Failed to initialize auth cache", zap.Error(err)) + } + signalsChannel := make(chan os.Signal, 1) signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM) diff --git a/pkg/query-service/model/auth.go b/pkg/query-service/model/auth.go new file mode 100644 index 0000000000..df86c43488 --- /dev/null +++ b/pkg/query-service/model/auth.go @@ -0,0 +1,51 @@ +package model + +type InviteRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` +} + +type InviteResponse struct { + Email string `json:"email"` + InviteToken string `json:"inviteToken"` +} + +type InvitationResponseObject struct { + Email string `json:"email" db:"email"` + Name string `json:"name" db:"name"` + Token string `json:"token" db:"token"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + Role string `json:"role" db:"role"` + Organization string `json:"organization" db:"organization"` +} + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` + RefreshToken string `json:"refreshToken"` +} + +type LoginResponse struct { + AccessJwt string `json:"accessJwt"` + AccessJwtExpiry int64 `json:"accessJwtExpiry"` + RefreshJwt string `json:"refreshJwt"` + RefreshJwtExpiry int64 `json:"refreshJwtExpiry"` + UserId string `json:"userId"` +} + +type ChangePasswordRequest struct { + UserId string `json:"userId"` + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` +} + +type ResetPasswordEntry struct { + UserId string `json:"userId" db:"user_id"` + Token string `json:"token" db:"token"` +} + +type UserRole struct { + UserId string `json:"user_id"` + GroupName string `json:"group_name"` +} diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go new file mode 100644 index 0000000000..0c198fbf28 --- /dev/null +++ b/pkg/query-service/model/db.go @@ -0,0 +1,46 @@ +package model + +type Organization struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + IsAnonymous bool `json:"isAnonymous" db:"is_anonymous"` + HasOptedUpdates bool `json:"hasOptedUpdates" db:"has_opted_updates"` +} + +type InvitationObject struct { + Id string `json:"id" db:"id"` + Email string `json:"email" db:"email"` + Name string `json:"name" db:"name"` + Token string `json:"token" db:"token"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + Role string `json:"role" db:"role"` + OrgId string `json:"orgId" db:"org_id"` +} + +type User struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Email string `json:"email" db:"email"` + Password string `json:"password,omitempty" db:"password"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ProfilePirctureURL string `json:"profilePictureURL" db:"profile_picture_url"` + OrgId string `json:"orgId,omitempty" db:"org_id"` + GroupId string `json:"groupId,omitempty" db:"group_id"` +} + +type UserPayload struct { + User + Role string `json:"role"` + Organization string `json:"organization"` +} + +type Group struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` +} + +type ResetPasswordRequest struct { + Password string `json:"password"` + Token string `json:"token"` +} diff --git a/pkg/query-service/model/queryParams.go b/pkg/query-service/model/queryParams.go index 7bde5626d5..e93989de94 100644 --- a/pkg/query-service/model/queryParams.go +++ b/pkg/query-service/model/queryParams.go @@ -4,12 +4,6 @@ import ( "time" ) -type User struct { - Name string `json:"name"` - Email string `json:"email"` - OrganizationName string `json:"organizationName"` -} - type InstantQueryMetricsParams struct { Time time.Time Query string diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index e5da74a923..cc4b90f937 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -27,6 +27,7 @@ const ( ErrorUnavailable ErrorType = "unavailable" ErrorNotFound ErrorType = "not_found" ErrorNotImplemented ErrorType = "not_implemented" + ErrorUnauthorized ErrorType = "unauthorized" ) type QueryDataV2 struct { diff --git a/pkg/query-service/model/userPreferences.go b/pkg/query-service/model/userPreferences.go deleted file mode 100644 index 0a6ca2d1e4..0000000000 --- a/pkg/query-service/model/userPreferences.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -type UserPreferences struct { - Id int `json:"id" db:"id"` - Uuid string `json:"uuid" db:"uuid"` - IsAnonymous bool `json:"isAnonymous" db:"isAnonymous"` - HasOptedUpdates bool `json:"hasOptedUpdates" db:"hasOptedUpdates"` -} - -func (up *UserPreferences) SetIsAnonymous(isAnonymous bool) { - up.IsAnonymous = isAnonymous -} -func (up *UserPreferences) SetHasOptedUpdate(hasOptedUpdates bool) { - up.HasOptedUpdates = hasOptedUpdates -} -func (up *UserPreferences) GetIsAnonymous() bool { - return up.IsAnonymous -} -func (up *UserPreferences) GetHasOptedUpdate() bool { - return up.HasOptedUpdates -} -func (up *UserPreferences) GetId() int { - return up.Id -} -func (up *UserPreferences) GetUUID() string { - return up.Uuid -} diff --git a/pkg/query-service/tests/auth_test.go b/pkg/query-service/tests/auth_test.go new file mode 100644 index 0000000000..8adde42f75 --- /dev/null +++ b/pkg/query-service/tests/auth_test.go @@ -0,0 +1,125 @@ +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.signoz.io/query-service/auth" + "go.signoz.io/query-service/model" +) + +func invite(t *testing.T, email string) *model.InviteResponse { + q := endpoint + fmt.Sprintf("/api/v1/invite?email=%s", email) + resp, err := client.Get(q) + require.NoError(t, err) + + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + var inviteResp model.InviteResponse + err = json.Unmarshal(b, &inviteResp) + require.NoError(t, err) + + return &inviteResp +} + +func register(email, password, token string) (string, error) { + q := endpoint + fmt.Sprintf("/api/v1/register") + + req := auth.RegisterRequest{ + Email: email, + Password: password, + InviteToken: token, + } + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + resp, err := client.Post(q, "application/json", bytes.NewBuffer(b)) + if err != nil { + return "", err + } + + defer resp.Body.Close() + b, err = ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(b), nil +} + +func login(email, password, refreshToken string) (*model.LoginResponse, error) { + q := endpoint + fmt.Sprintf("/api/v1/login") + + req := model.LoginRequest{ + Email: email, + Password: password, + RefreshToken: refreshToken, + } + + b, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal") + } + resp, err := client.Post(q, "application/json", bytes.NewBuffer(b)) + if err != nil { + return nil, errors.Wrap(err, "failed to post") + } + + defer resp.Body.Close() + b, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read body") + } + + loginResp := &model.LoginResponse{} + err = json.Unmarshal(b, loginResp) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal") + } + + return loginResp, nil +} + +func TestAuthInviteAPI(t *testing.T) { + t.Skip() + email := "abc@signoz.io" + resp := invite(t, email) + require.Equal(t, email, resp.Email) + require.NotNil(t, resp.InviteToken) +} + +func TestAuthRegisterAPI(t *testing.T) { + email := "alice@signoz.io" + resp, err := register(email, "password", "") + require.NoError(t, err) + require.Contains(t, resp, "user registered successfully") + +} + +func TestAuthLoginAPI(t *testing.T) { + t.Skip() + email := "abc-login@signoz.io" + password := "password123" + inv := invite(t, email) + + resp, err := register(email, password, inv.InviteToken) + require.NoError(t, err) + require.Contains(t, resp, "user registered successfully") + + loginResp, err := login(email, password, "") + require.NoError(t, err) + + loginResp2, err := login("", "", loginResp.RefreshJwt) + require.NoError(t, err) + + require.NotNil(t, loginResp2.AccessJwt) +} diff --git a/pkg/query-service/tests/test-deploy/data/signoz.db b/pkg/query-service/tests/test-deploy/data/signoz.db deleted file mode 100644 index c19319ab3476b0101d7f73bdf31e884b5e9f3590..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI(-%i?490%}Lgo+ztvWwmx?;zR@22po6<~m12RBW?(v2+J|h)tly{z>Mg_z=6^ zv+Nmsh-G_#UGB7stcVb@n>G116x#DUJ@ofEr%g_vrzZ`|XQb=6ed?2nur7$A@R|@o z5Yl{I;p=Ew<{RNUTEx-DjL)?2{Pc4!|67or91Ckd^556K=6*f(a^Lta76?E90uX=z z1Rwwb2qa!$NF+&C6!F^gX}iY)kGaOcWnJbnyTiQ6{gu6zTGLdb)!sBzG8rX>)oj)> zNxiA5`)Z3Ex9W$r))_fa&qz(zj_OT*_@Ua=ws^fD;59Y%qehxXeAOF`u+;KuwqsxR zoxra!UwmG4@5c)(q2 ztJ&gkGcCzGJK}W^5!sG!b*&EdEyp%G=hU`Y&zoA9iD6BxvXEHEWjq_x;1i!PE#K<1 zm|YNM`*_@@{D7D^(VO)XT@A_n%fUo2EHzn=@vpq^iu)djg4Cz+_PAC(laW8|h=OIC z?2C8NZ$07p=KmY;|fB*y_009U< z00Izzz&ruW|K|xI4gm;200Izz00bZa0SG_<0uV^P0G|I(ejg$O2tWV=5P$##AOHaf zKmY;|fWSO~+voppg#69CnutRH0uX=z1Rwwb2tWV=5P$##An;!a6vecluVlAbrNpX~ cD&