diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index 209591f0e7..76dc7e4773 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -12,10 +12,10 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" - "github.com/SigNoz/signoz/pkg/modules/user" basemodel "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/types/serviceaccounttypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" "go.uber.org/zap" @@ -49,7 +49,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW return } - apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider) + apiKey, apiErr := ah.getOrCreateCloudIntegrationAPIKey(r.Context(), claims.OrgID, cloudProvider) if apiErr != nil { RespondError(w, basemodel.WrapApiError( apiErr, "couldn't provision PAT for cloud integration:", @@ -109,32 +109,25 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW ah.Respond(w, result) } -func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) ( +func (ah *APIHandler) getOrCreateCloudIntegrationAPIKey(ctx context.Context, orgId string, cloudProvider string) ( string, *basemodel.ApiError, ) { integrationPATName := fmt.Sprintf("%s integration", cloudProvider) - - integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider) + integrationServiceAccount, apiErr := ah.getOrCreateCloudIntegrationServiceAccount(ctx, orgId, cloudProvider) if apiErr != nil { return "", apiErr } - orgIdUUID, err := valuer.NewUUID(orgId) + keys, err := ah.Signoz.Modules.ServiceAccount.ListFactorAPIKey(ctx, integrationServiceAccount.ID) if err != nil { return "", basemodel.InternalError(fmt.Errorf( - "couldn't parse orgId: %w", err, + "couldn't list api keys: %w", err, )) } - allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID) - if err != nil { - return "", basemodel.InternalError(fmt.Errorf( - "couldn't list PATs: %w", err, - )) - } - for _, p := range allPats { - if p.UserID == integrationUser.ID && p.Name == integrationPATName { - return p.Token, nil + for _, key := range keys { + if key.Name == integrationPATName { + return key.Key, nil } } @@ -143,46 +136,35 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId zap.String("cloudProvider", cloudProvider), ) - newPAT, err := types.NewStorableAPIKey( - integrationPATName, - integrationUser.ID, - types.RoleViewer, - 0, - ) + apiKey, err := integrationServiceAccount.NewFactorAPIKey(integrationPATName, 0) if err != nil { return "", basemodel.InternalError(fmt.Errorf( "couldn't create cloud integration PAT: %w", err, )) } - err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT) + err = ah.Signoz.Modules.ServiceAccount.CreateFactorAPIKey(ctx, apiKey) if err != nil { return "", basemodel.InternalError(fmt.Errorf( - "couldn't create cloud integration PAT: %w", err, + "couldn't create cloud integration api key: %w", err, )) } - return newPAT.Token, nil + return apiKey.Key, nil } -func (ah *APIHandler) getOrCreateCloudIntegrationUser( +func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount( ctx context.Context, orgId string, cloudProvider string, -) (*types.User, *basemodel.ApiError) { - cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider) - email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName)) +) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) { + serviceAccountName := fmt.Sprintf("%s-integration", cloudProvider) + email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", serviceAccountName)) - cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive) + serviceAccount := serviceaccounttypes.NewServiceAccount(serviceAccountName, email, []string{roletypes.SigNozViewerRoleName}, serviceaccounttypes.StatusActive, valuer.MustNewUUID(orgId)) + serviceAccount, err := ah.Signoz.Modules.ServiceAccount.GetOrCreate(ctx, serviceAccount) if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err)) + return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration service account: %w", err)) } - password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue()) - - cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password)) - if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err)) - } - - return cloudIntegrationUser, nil + return serviceAccount, nil } func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) ( diff --git a/pkg/apiserver/signozapiserver/user.go b/pkg/apiserver/signozapiserver/user.go index 91c0c215df..150373da08 100644 --- a/pkg/apiserver/signozapiserver/user.go +++ b/pkg/apiserver/signozapiserver/user.go @@ -111,74 +111,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error { return err } - if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{ - ID: "CreateAPIKey", - Tags: []string{"users"}, - Summary: "Create api key", - Description: "This endpoint creates an api key", - Request: new(types.PostableAPIKey), - RequestContentType: "application/json", - Response: new(types.GettableAPIKey), - ResponseContentType: "application/json", - SuccessStatusCode: http.StatusCreated, - ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict}, - Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleAdmin), - })).Methods(http.MethodPost).GetError(); err != nil { - return err - } - - if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListAPIKeys), handler.OpenAPIDef{ - ID: "ListAPIKeys", - Tags: []string{"users"}, - Summary: "List api keys", - Description: "This endpoint lists all api keys", - Request: nil, - RequestContentType: "", - Response: make([]*types.GettableAPIKey, 0), - ResponseContentType: "application/json", - SuccessStatusCode: http.StatusOK, - ErrorStatusCodes: []int{}, - Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleAdmin), - })).Methods(http.MethodGet).GetError(); err != nil { - return err - } - - if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.UpdateAPIKey), handler.OpenAPIDef{ - ID: "UpdateAPIKey", - Tags: []string{"users"}, - Summary: "Update api key", - Description: "This endpoint updates an api key", - Request: new(types.StorableAPIKey), - RequestContentType: "application/json", - Response: nil, - ResponseContentType: "application/json", - SuccessStatusCode: http.StatusNoContent, - ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, - Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleAdmin), - })).Methods(http.MethodPut).GetError(); err != nil { - return err - } - - if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.RevokeAPIKey), handler.OpenAPIDef{ - ID: "RevokeAPIKey", - Tags: []string{"users"}, - Summary: "Revoke api key", - Description: "This endpoint revokes an api key", - Request: nil, - RequestContentType: "", - Response: nil, - ResponseContentType: "", - SuccessStatusCode: http.StatusNoContent, - ErrorStatusCodes: []int{http.StatusNotFound}, - Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleAdmin), - })).Methods(http.MethodDelete).GetError(); err != nil { - return err - } - if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{ ID: "ListUsers", Tags: []string{"users"}, diff --git a/pkg/modules/serviceaccount/implserviceaccount/store.go b/pkg/modules/serviceaccount/implserviceaccount/store.go index 1e59d4eed4..0c53392d20 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/store.go +++ b/pkg/modules/serviceaccount/implserviceaccount/store.go @@ -84,6 +84,24 @@ func (store *store) GetByID(ctx context.Context, id valuer.UUID) (*serviceaccoun return storable, nil } +func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*serviceaccounttypes.StorableServiceAccount, error) { + storable := new(serviceaccounttypes.StorableServiceAccount) + + err := store. + sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(storable). + Where("org_id = ?", orgID). + Where("name = ?", name). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccountNotFound, "service account with name: %s doesn't exist in org: %s", name, orgID.String()) + } + + return storable, nil +} + func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccount, error) { storables := make([]*serviceaccounttypes.StorableServiceAccount, 0) diff --git a/pkg/modules/serviceaccount/serviceaccount.go b/pkg/modules/serviceaccount/serviceaccount.go index f1083e6851..df4c68e639 100644 --- a/pkg/modules/serviceaccount/serviceaccount.go +++ b/pkg/modules/serviceaccount/serviceaccount.go @@ -12,6 +12,7 @@ type Module interface { // Creates a new service account for an organization. Create(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error + GetOrCreate(context.Context, *serviceaccounttypes.ServiceAccount) (*serviceaccounttypes.ServiceAccount, error) // Gets a service account by id. Get(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go index 17b4bff20c..df45c37315 100644 --- a/pkg/modules/user/impluser/handler.go +++ b/pkg/modules/user/impluser/handler.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/http" - "slices" "time" "github.com/SigNoz/signoz/pkg/errors" @@ -13,7 +12,6 @@ import ( root "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" - "github.com/SigNoz/signoz/pkg/types/integrationtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" ) @@ -349,172 +347,3 @@ func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { render.Success(w, http.StatusNoContent, nil) } - -func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - claims, err := authtypes.ClaimsFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } - - req := new(types.PostableAPIKey) - if err := json.NewDecoder(r.Body).Decode(req); err != nil { - render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key")) - return - } - - apiKey, err := types.NewStorableAPIKey( - req.Name, - valuer.MustNewUUID(claims.UserID), - req.Role, - req.ExpiresInDays, - ) - if err != nil { - render.Error(w, err) - return - } - - err = h.module.CreateAPIKey(ctx, apiKey) - if err != nil { - render.Error(w, err) - return - } - - createdApiKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), apiKey.ID) - if err != nil { - render.Error(w, err) - return - } - - // just corrected the status code, response is same, - render.Success(w, http.StatusCreated, createdApiKey) -} - -func (h *handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - claims, err := authtypes.ClaimsFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } - - apiKeys, err := h.module.ListAPIKeys(ctx, valuer.MustNewUUID(claims.OrgID)) - if err != nil { - render.Error(w, err) - return - } - - // for backward compatibility - if len(apiKeys) == 0 { - render.Success(w, http.StatusOK, []types.GettableAPIKey{}) - return - } - - result := make([]*types.GettableAPIKey, len(apiKeys)) - for i, apiKey := range apiKeys { - result[i] = types.NewGettableAPIKeyFromStorableAPIKey(apiKey) - } - - render.Success(w, http.StatusOK, result) - -} - -func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - claims, err := authtypes.ClaimsFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } - - req := types.StorableAPIKey{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key")) - return - } - - idStr := mux.Vars(r)["id"] - id, err := valuer.NewUUID(idStr) - if err != nil { - render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7")) - return - } - - //get the API Key - existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) - if err != nil { - render.Error(w, err) - return - } - - // get the user - createdByUser, err := h.getter.Get(ctx, existingAPIKey.UserID) - if err != nil { - render.Error(w, err) - return - } - - if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) { - render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked")) - return - } - - err = h.module.UpdateAPIKey(ctx, id, &req, valuer.MustNewUUID(claims.UserID)) - if err != nil { - render.Error(w, err) - return - } - - render.Success(w, http.StatusNoContent, nil) -} - -func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - claims, err := authtypes.ClaimsFromContext(ctx) - if err != nil { - render.Error(w, err) - return - } - - idStr := mux.Vars(r)["id"] - id, err := valuer.NewUUID(idStr) - if err != nil { - render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7")) - return - } - - //get the API Key - existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) - if err != nil { - render.Error(w, err) - return - } - - // get the user - createdByUser, err := h.getter.Get(ctx, existingAPIKey.UserID) - if err != nil { - render.Error(w, err) - return - } - - if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) { - render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked")) - return - } - - if err := h.module.RevokeAPIKey(ctx, id, valuer.MustNewUUID(claims.UserID)); err != nil { - render.Error(w, err) - return - } - - render.Success(w, http.StatusNoContent, nil) -} diff --git a/pkg/modules/user/impluser/module.go b/pkg/modules/user/impluser/module.go index b328f41351..a767542949 100644 --- a/pkg/modules/user/impluser/module.go +++ b/pkg/modules/user/impluser/module.go @@ -661,26 +661,6 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt return user, nil } -func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error { - return m.store.CreateAPIKey(ctx, apiKey) -} - -func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error { - return m.store.UpdateAPIKey(ctx, id, apiKey, updaterID) -} - -func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) { - return m.store.ListAPIKeys(ctx, orgID) -} - -func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) { - return m.store.GetAPIKey(ctx, orgID, id) -} - -func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error { - return m.store.RevokeAPIKey(ctx, id, removedByUserID) -} - func (module *Module) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, passwd string) (*types.User, error) { user, err := types.NewRootUser(name, email, organization.ID) if err != nil { @@ -734,11 +714,6 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin stats["user.count.pending_invite"] = counts[types.UserStatusPendingInvite] } - count, err := module.store.CountAPIKeyByOrgID(ctx, orgID) - if err == nil { - stats["factor.api_key.count"] = count - } - return stats, nil } diff --git a/pkg/modules/user/impluser/store.go b/pkg/modules/user/impluser/store.go index 6dd242dae7..616602fe4d 100644 --- a/pkg/modules/user/impluser/store.go +++ b/pkg/modules/user/impluser/store.go @@ -3,7 +3,6 @@ package impluser import ( "context" "database/sql" - "sort" "time" "github.com/SigNoz/signoz/pkg/errors" @@ -218,15 +217,6 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete factor password") } - // delete api keys - _, err = tx.NewDelete(). - Model(&types.StorableAPIKey{}). - Where("user_id = ?", id). - Exec(ctx) - if err != nil { - return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete API keys") - } - // delete user_preference _, err = tx.NewDelete(). Model(new(preferencetypes.StorableUserPreference)). @@ -457,111 +447,6 @@ func (store *store) UpdatePassword(ctx context.Context, factorPassword *types.Fa return nil } -// --- API KEY --- -func (store *store) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error { - _, err := store.sqlstore.BunDB().NewInsert(). - Model(apiKey). - Exec(ctx) - if err != nil { - return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrAPIKeyAlreadyExists, "API key with token: %s already exists", apiKey.Token) - } - - return nil -} - -func (store *store) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error { - apiKey.UpdatedBy = updaterID.String() - apiKey.UpdatedAt = time.Now() - _, err := store.sqlstore.BunDB().NewUpdate(). - Model(apiKey). - Column("role", "name", "updated_at", "updated_by"). - Where("id = ?", id). - Where("revoked = false"). - Exec(ctx) - if err != nil { - return store.sqlstore.WrapNotFoundErrf(err, types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id) - } - return nil -} - -func (store *store) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) { - orgUserAPIKeys := new(types.OrgUserAPIKey) - - if err := store.sqlstore.BunDB().NewSelect(). - Model(orgUserAPIKeys). - Relation("Users"). - Relation("Users.APIKeys", func(q *bun.SelectQuery) *bun.SelectQuery { - return q.Where("revoked = false") - }, - ). - Relation("Users.APIKeys.CreatedByUser"). - Relation("Users.APIKeys.UpdatedByUser"). - Where("id = ?", orgID). - Scan(ctx); err != nil { - return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch API keys") - } - - // Flatten the API keys from all users - var allAPIKeys []*types.StorableAPIKeyUser - for _, user := range orgUserAPIKeys.Users { - if user.APIKeys != nil { - allAPIKeys = append(allAPIKeys, user.APIKeys...) - } - } - - // sort the API keys by updated_at - sort.Slice(allAPIKeys, func(i, j int) bool { - return allAPIKeys[i].UpdatedAt.After(allAPIKeys[j].UpdatedAt) - }) - - return allAPIKeys, nil -} - -func (store *store) RevokeAPIKey(ctx context.Context, id, revokedByUserID valuer.UUID) error { - updatedAt := time.Now().Unix() - _, err := store.sqlstore.BunDB().NewUpdate(). - Model(&types.StorableAPIKey{}). - Set("revoked = ?", true). - Set("updated_by = ?", revokedByUserID). - Set("updated_at = ?", updatedAt). - Where("id = ?", id). - Exec(ctx) - if err != nil { - return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to revoke API key") - } - return nil -} - -func (store *store) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) { - apiKey := new(types.OrgUserAPIKey) - if err := store.sqlstore.BunDB().NewSelect(). - Model(apiKey). - Relation("Users"). - Relation("Users.APIKeys", func(q *bun.SelectQuery) *bun.SelectQuery { - return q.Where("revoked = false").Where("storable_api_key.id = ?", id). - OrderExpr("storable_api_key.updated_at DESC").Limit(1) - }, - ). - Relation("Users.APIKeys.CreatedByUser"). - Relation("Users.APIKeys.UpdatedByUser"). - Scan(ctx); err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id) - } - - // flatten the API keys - flattenedAPIKeys := []*types.StorableAPIKeyUser{} - for _, user := range apiKey.Users { - if user.APIKeys != nil { - flattenedAPIKeys = append(flattenedAPIKeys, user.APIKeys...) - } - } - if len(flattenedAPIKeys) == 0 { - return nil, store.sqlstore.WrapNotFoundErrf(errors.New(errors.TypeNotFound, errors.CodeNotFound, "API key with id: %s does not exist"), types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id) - } - - return flattenedAPIKeys[0], nil -} - func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) { user := new(types.User) @@ -609,24 +494,6 @@ func (store *store) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UU return counts, nil } -func (store *store) CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) { - apiKey := new(types.StorableAPIKey) - - count, err := store. - sqlstore. - BunDB(). - NewSelect(). - Model(apiKey). - Join("JOIN users ON users.id = storable_api_key.user_id"). - Where("org_id = ?", orgID). - Count(ctx) - if err != nil { - return 0, err - } - - return int64(count), nil -} - func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error { return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error { return cb(ctx) diff --git a/pkg/modules/user/user.go b/pkg/modules/user/user.go index 820b7fd319..94fc32597c 100644 --- a/pkg/modules/user/user.go +++ b/pkg/modules/user/user.go @@ -45,13 +45,6 @@ type Module interface { AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) - // API KEY - CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error - UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error - ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) - RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error - GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error) - GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) statsreporter.StatsCollector @@ -106,10 +99,4 @@ type Handler interface { ResetPassword(http.ResponseWriter, *http.Request) ChangePassword(http.ResponseWriter, *http.Request) ForgotPassword(http.ResponseWriter, *http.Request) - - // API KEY - CreateAPIKey(http.ResponseWriter, *http.Request) - ListAPIKeys(http.ResponseWriter, *http.Request) - UpdateAPIKey(http.ResponseWriter, *http.Request) - RevokeAPIKey(http.ResponseWriter, *http.Request) } diff --git a/pkg/sqlmigration/067_add_service_account.go b/pkg/sqlmigration/067_add_service_account.go index f10ea9ffe6..d992c9d937 100644 --- a/pkg/sqlmigration/067_add_service_account.go +++ b/pkg/sqlmigration/067_add_service_account.go @@ -69,6 +69,9 @@ func (migration *addServiceAccount) Up(ctx context.Context, db *bun.DB) error { }) sqls = append(sqls, tableSQLs...) + indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "service_account", ColumnNames: []sqlschema.ColumnName{"name", "org_id"}}) + sqls = append(sqls, indexSQLs...) + tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ Name: "service_account_role", Columns: []*sqlschema.Column{ @@ -96,7 +99,7 @@ func (migration *addServiceAccount) Up(ctx context.Context, db *bun.DB) error { }) sqls = append(sqls, tableSQLs...) - indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "service_account_role", ColumnNames: []sqlschema.ColumnName{"service_account_id", "role_id"}}) + indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "service_account_role", ColumnNames: []sqlschema.ColumnName{"service_account_id", "role_id"}}) sqls = append(sqls, indexSQLs...) for _, sql := range sqls { @@ -114,4 +117,4 @@ func (migration *addServiceAccount) Up(ctx context.Context, db *bun.DB) error { func (a *addServiceAccount) Down(context.Context, *bun.DB) error { return nil -} \ No newline at end of file +} diff --git a/pkg/sqlmigration/068_deprecate_api_key.go b/pkg/sqlmigration/068_deprecate_api_key.go index 02bbd4b58b..84f8a572be 100644 --- a/pkg/sqlmigration/068_deprecate_api_key.go +++ b/pkg/sqlmigration/068_deprecate_api_key.go @@ -115,20 +115,19 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { _ = tx.Rollback() }() - // Step 1: Read all old API keys (skip revoked ones). + // get all the api keys oldKeys := make([]*oldFactorAPIKey68, 0) err = tx.NewSelect().Model(&oldKeys).Where("revoked = ?", false).Scan(ctx) if err != nil && err != sql.ErrNoRows { return err } - // Step 2: Collect unique user IDs from old keys. + // get all the unique users userIDs := make(map[string]struct{}) for _, key := range oldKeys { userIDs[key.UserID] = struct{}{} } - // Step 3: Load users that own API keys. userIDList := make([]string, 0, len(userIDs)) for uid := range userIDs { userIDList = append(userIDList, uid) @@ -146,8 +145,7 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { } } - // Step 4: Load managed roles to map old role names to role IDs. - // Build a lookup of (org_id, role_name) -> role_id. + // get the role ids type orgRoleKey struct { OrgID string RoleName string @@ -173,47 +171,29 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { } } - // Step 5: Create a service account per user that has API keys, and collect migrated data. - // One service account per user, all their API keys point to it. - userIDToServiceAccountID := make(map[string]string) serviceAccounts := make([]*newServiceAccount68, 0) serviceAccountRoles := make([]*newServiceAccountRole68, 0) newKeys := make([]*newFactorAPIKey68, 0) - // Track which (serviceAccountID, roleID) pairs we've already added. - type saRoleKey struct { - ServiceAccountID string - RoleID string - } - addedRoles := make(map[saRoleKey]struct{}) - now := time.Now() - for _, oldKey := range oldKeys { user, ok := userMap[oldKey.UserID] if !ok { - // User not found — skip this key. + // this should never happen as a key cannot exist without a user continue } - // Create service account for this user if not already created. - saID, exists := userIDToServiceAccountID[oldKey.UserID] - if !exists { - saID = valuer.GenerateUUID().String() - userIDToServiceAccountID[oldKey.UserID] = saID + saID := valuer.GenerateUUID() + serviceAccounts = append(serviceAccounts, &newServiceAccount68{ + Identifiable: types.Identifiable{ID: saID}, + CreatedAt: now, + UpdatedAt: now, + Name: oldKey.Name, + Email: user.Email, + Status: "active", + OrgID: user.OrgID, + }) - serviceAccounts = append(serviceAccounts, &newServiceAccount68{ - Identifiable: types.Identifiable{ID: valuer.MustNewUUID(saID)}, - CreatedAt: now, - UpdatedAt: now, - Name: oldKey.Name, - Email: user.Email, - Status: "active", - OrgID: user.OrgID, - }) - } - - // Map the old role (ADMIN, EDITOR, VIEWER) to the managed role name. managedRoleName, ok := roletypes.ExistingRoleToSigNozManagedRoleMap[types.Role(oldKey.Role)] if !ok { managedRoleName = roletypes.SigNozViewerRoleName @@ -221,20 +201,15 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { roleID, ok := roleMap[orgRoleKey{OrgID: user.OrgID, RoleName: managedRoleName}] if ok { - key := saRoleKey{ServiceAccountID: saID, RoleID: roleID} - if _, added := addedRoles[key]; !added { - addedRoles[key] = struct{}{} - serviceAccountRoles = append(serviceAccountRoles, &newServiceAccountRole68{ - Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, - CreatedAt: now, - UpdatedAt: now, - ServiceAccountID: saID, - RoleID: roleID, - }) - } + serviceAccountRoles = append(serviceAccountRoles, &newServiceAccountRole68{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + CreatedAt: now, + UpdatedAt: now, + ServiceAccountID: saID.String(), + RoleID: roleID, + }) } - // Convert expires_at from time.Time to unix seconds (0 = never expires). var expiresAtUnix uint64 if !oldKey.ExpiresAt.IsZero() && oldKey.ExpiresAt.Unix() > 0 { expiresAtUnix = uint64(oldKey.ExpiresAt.Unix()) @@ -254,11 +229,10 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { Key: oldKey.Token, ExpiresAt: expiresAtUnix, LastObservedAt: lastObservedAt, - ServiceAccountID: saID, + ServiceAccountID: saID.String(), }) } - // Step 6: Insert migrated service accounts and roles. if len(serviceAccounts) > 0 { if _, err := tx.NewInsert().Model(&serviceAccounts).Exec(ctx); err != nil { return err @@ -271,9 +245,7 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { } } - // Step 7: Drop old table, create new table, and insert migrated keys. sqls := [][]byte{} - deprecatedFactorAPIKey, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("factor_api_key")) if err != nil { return err @@ -319,7 +291,6 @@ func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { } } - // Step 8: Insert migrated keys into the new table. if len(newKeys) > 0 { if _, err := tx.NewInsert().Model(&newKeys).Exec(ctx); err != nil { return err diff --git a/pkg/types/factor_api_key.go b/pkg/types/factor_api_key.go deleted file mode 100644 index e1eb05db69..0000000000 --- a/pkg/types/factor_api_key.go +++ /dev/null @@ -1,144 +0,0 @@ -package types - -import ( - "crypto/rand" - "encoding/base64" - "time" - - "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/valuer" - "github.com/uptrace/bun" -) - -var NEVER_EXPIRES = time.Unix(0, 0) - -type PostableAPIKey struct { - Name string `json:"name"` - Role Role `json:"role"` - ExpiresInDays int64 `json:"expiresInDays"` -} - -type GettableAPIKey struct { - Identifiable - TimeAuditable - UserAuditable - Token string `json:"token"` - Role Role `json:"role"` - Name string `json:"name"` - ExpiresAt int64 `json:"expiresAt"` - LastUsed int64 `json:"lastUsed"` - Revoked bool `json:"revoked"` - UserID string `json:"userId"` - CreatedByUser *User `json:"createdByUser"` - UpdatedByUser *User `json:"updatedByUser"` -} - -type OrgUserAPIKey struct { - *Organization `bun:",extend"` - Users []*UserWithAPIKey `bun:"rel:has-many,join:id=org_id"` -} - -type UserWithAPIKey struct { - *User `bun:",extend"` - APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"` -} - -type StorableAPIKeyUser struct { - StorableAPIKey `bun:",extend"` - - CreatedByUser *User `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"` - UpdatedByUser *User `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"` -} - -type StorableAPIKey struct { - bun.BaseModel `bun:"table:factor_api_key"` - - Identifiable - TimeAuditable - UserAuditable - Token string `json:"token" bun:"token,type:text,notnull,unique"` - Role Role `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"` - Name string `json:"name" bun:"name,type:text,notnull"` - ExpiresAt time.Time `json:"-" bun:"expires_at,notnull,nullzero,type:timestamptz"` - LastUsed time.Time `json:"-" bun:"last_used,notnull,nullzero,type:timestamptz"` - Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"` - UserID valuer.UUID `json:"userId" bun:"user_id,type:text,notnull"` -} - -func NewStorableAPIKey(name string, userID valuer.UUID, role Role, expiresAt int64) (*StorableAPIKey, error) { - // validate - - // we allow the APIKey if expiresAt is not set, which means it never expires - if expiresAt < 0 { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "expiresAt must be greater than 0") - } - - if name == "" { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "name cannot be empty") - } - - if role == "" { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role cannot be empty") - } - - now := time.Now() - // convert expiresAt to unix timestamp from days - // expiresAt = now.Unix() + (expiresAt * 24 * 60 * 60) - expiresAtTime := now.AddDate(0, 0, int(expiresAt)) - - // if the expiresAt is 0, it means the APIKey never expires - if expiresAt == 0 { - expiresAtTime = NEVER_EXPIRES - } - - // Generate a 32-byte random token. - token := make([]byte, 32) - _, err := rand.Read(token) - if err != nil { - return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to generate token") - } - // Encode the token in base64. - encodedToken := base64.StdEncoding.EncodeToString(token) - - return &StorableAPIKey{ - Identifiable: Identifiable{ - ID: valuer.GenerateUUID(), - }, - TimeAuditable: TimeAuditable{ - CreatedAt: now, - UpdatedAt: now, - }, - UserAuditable: UserAuditable{ - CreatedBy: userID.String(), - UpdatedBy: userID.String(), - }, - Token: encodedToken, - Name: name, - Role: role, - UserID: userID, - ExpiresAt: expiresAtTime, - LastUsed: now, - Revoked: false, - }, nil -} - -func NewGettableAPIKeyFromStorableAPIKey(storableAPIKey *StorableAPIKeyUser) *GettableAPIKey { - lastUsed := storableAPIKey.LastUsed.Unix() - if storableAPIKey.LastUsed == storableAPIKey.CreatedAt { - lastUsed = 0 - } - return &GettableAPIKey{ - Identifiable: storableAPIKey.Identifiable, - TimeAuditable: storableAPIKey.TimeAuditable, - UserAuditable: storableAPIKey.UserAuditable, - Token: storableAPIKey.Token, - Role: storableAPIKey.Role, - Name: storableAPIKey.Name, - ExpiresAt: storableAPIKey.ExpiresAt.Unix(), - LastUsed: lastUsed, - Revoked: storableAPIKey.Revoked, - UserID: storableAPIKey.UserID.String(), - CreatedByUser: storableAPIKey.CreatedByUser, - UpdatedByUser: storableAPIKey.UpdatedByUser, - } -} diff --git a/pkg/types/serviceaccounttypes/store.go b/pkg/types/serviceaccounttypes/store.go index 7ba7680deb..4edc6f88b2 100644 --- a/pkg/types/serviceaccounttypes/store.go +++ b/pkg/types/serviceaccounttypes/store.go @@ -12,6 +12,7 @@ type Store interface { Get(context.Context, valuer.UUID, valuer.UUID) (*StorableServiceAccount, error) GetActiveByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableServiceAccount, error) GetByID(context.Context, valuer.UUID) (*StorableServiceAccount, error) + GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableServiceAccount, error) List(context.Context, valuer.UUID) ([]*StorableServiceAccount, error) Update(context.Context, valuer.UUID, *StorableServiceAccount) error Delete(context.Context, valuer.UUID, valuer.UUID) error diff --git a/pkg/types/user.go b/pkg/types/user.go index 4f8d53b66e..acc72d2cc3 100644 --- a/pkg/types/user.go +++ b/pkg/types/user.go @@ -255,14 +255,6 @@ type UserStore interface { DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error UpdatePassword(ctx context.Context, password *FactorPassword) error - // API KEY - CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error - UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error - ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error) - RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error - GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error) - CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) - CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error)