diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go index 11afa99ea75..c9a9e928222 100644 --- a/cmd/enterprise/server.go +++ b/cmd/enterprise/server.go @@ -9,12 +9,12 @@ import ( "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn" "github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn" "github.com/SigNoz/signoz/ee/authz/openfgaauthz" - eequerier "github.com/SigNoz/signoz/ee/querier" "github.com/SigNoz/signoz/ee/authz/openfgaschema" "github.com/SigNoz/signoz/ee/gateway/httpgateway" enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing" "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard" + eequerier "github.com/SigNoz/signoz/ee/querier" enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app" "github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index 30e0707b0a8..6222689f53b 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -1993,43 +1993,6 @@ components: userId: type: string type: object - TypesGettableAPIKey: - properties: - createdAt: - format: date-time - type: string - createdBy: - type: string - createdByUser: - $ref: '#/components/schemas/TypesUser' - expiresAt: - format: int64 - type: integer - id: - type: string - lastUsed: - format: int64 - type: integer - name: - type: string - revoked: - type: boolean - role: - type: string - token: - type: string - updatedAt: - format: date-time - type: string - updatedBy: - type: string - updatedByUser: - $ref: '#/components/schemas/TypesUser' - userId: - type: string - required: - - id - type: object TypesGettableGlobalConfig: properties: external_url: @@ -2091,16 +2054,6 @@ components: required: - id type: object - TypesPostableAPIKey: - properties: - expiresInDays: - format: int64 - type: integer - name: - type: string - role: - type: string - type: object TypesPostableAcceptInvite: properties: displayName: @@ -2165,33 +2118,6 @@ components: required: - id type: object - TypesStorableAPIKey: - properties: - createdAt: - format: date-time - type: string - createdBy: - type: string - id: - type: string - name: - type: string - revoked: - type: boolean - role: - type: string - token: - type: string - updatedAt: - format: date-time - type: string - updatedBy: - type: string - userId: - type: string - required: - - id - type: object TypesUser: properties: createdAt: @@ -3865,222 +3791,6 @@ paths: summary: Update org preference tags: - preferences - /api/v1/pats: - get: - deprecated: false - description: This endpoint lists all api keys - operationId: ListAPIKeys - responses: - "200": - content: - application/json: - schema: - properties: - data: - items: - $ref: '#/components/schemas/TypesGettableAPIKey' - type: array - status: - type: string - required: - - status - - data - type: object - description: OK - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Unauthorized - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Forbidden - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Internal Server Error - security: - - api_key: - - ADMIN - - tokenizer: - - ADMIN - summary: List api keys - tags: - - users - post: - deprecated: false - description: This endpoint creates an api key - operationId: CreateAPIKey - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TypesPostableAPIKey' - responses: - "201": - content: - application/json: - schema: - properties: - data: - $ref: '#/components/schemas/TypesGettableAPIKey' - status: - type: string - required: - - status - - data - type: object - description: Created - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Bad Request - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Unauthorized - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Forbidden - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Conflict - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Internal Server Error - security: - - api_key: - - ADMIN - - tokenizer: - - ADMIN - summary: Create api key - tags: - - users - /api/v1/pats/{id}: - delete: - deprecated: false - description: This endpoint revokes an api key - operationId: RevokeAPIKey - parameters: - - in: path - name: id - required: true - schema: - type: string - responses: - "204": - description: No Content - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Unauthorized - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Forbidden - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Not Found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Internal Server Error - security: - - api_key: - - ADMIN - - tokenizer: - - ADMIN - summary: Revoke api key - tags: - - users - put: - deprecated: false - description: This endpoint updates an api key - operationId: UpdateAPIKey - parameters: - - in: path - name: id - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TypesStorableAPIKey' - responses: - "204": - content: - application/json: - schema: - type: string - description: No Content - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Bad Request - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Unauthorized - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Forbidden - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Not Found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/RenderErrorResponse' - description: Internal Server Error - security: - - api_key: - - ADMIN - - tokenizer: - - ADMIN - summary: Update api key - tags: - - users /api/v1/public/dashboards/{id}: get: deprecated: false diff --git a/ee/authz/openfgaserver/server.go b/ee/authz/openfgaserver/server.go index b3d8da22d46..8f77ba313cf 100644 --- a/ee/authz/openfgaserver/server.go +++ b/ee/authz/openfgaserver/server.go @@ -31,9 +31,22 @@ func (server *Server) Stop(ctx context.Context) error { } func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error { - subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) - if err != nil { - return err + subject := "" + switch claims.Principal { + case authtypes.PrincipalUser.StringValue(): + user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) + if err != nil { + return err + } + + subject = user + case authtypes.PrincipalServiceAccount.StringValue(): + serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil) + if err != nil { + return err + } + + subject = serviceAccount } tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID) diff --git a/ee/modules/dashboard/impldashboard/module.go b/ee/modules/dashboard/impldashboard/module.go index 06a5759a440..c8b920b6969 100644 --- a/ee/modules/dashboard/impldashboard/module.go +++ b/ee/modules/dashboard/impldashboard/module.go @@ -214,8 +214,8 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff) } -func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error { - return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock) +func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error { + return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, lock) } func (module *module) MustGetTypeables() []authtypes.Typeable { diff --git a/ee/querier/handler.go b/ee/querier/handler.go index c6f5dc4abfb..1a2566b84ca 100644 --- a/ee/querier/handler.go +++ b/ee/querier/handler.go @@ -63,6 +63,8 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { h.set.Logger.ErrorContext(ctx, "panic in QueryRange", "error", r, "user", claims.UserID, + "principal", claims.Principal, + "service_account", claims.ServiceAccountID, "payload", string(queryJSON), "stacktrace", stackTrace, ) diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index 209591f0e73..1ea929d7b33 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,28 @@ 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 { + err := key.IsExpired() + if err == nil { + return key.Key, nil + } } } @@ -143,46 +139,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)) - - cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive) - if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err)) - } - - password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue()) +) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) { + serviceAccountName := fmt.Sprintf("%s-integration", cloudProvider) + email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", serviceAccountName)) - cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password)) + 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 look for integration user: %w", err)) + return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration service account: %w", err)) } - return cloudIntegrationUser, nil + return serviceAccount, nil } func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) ( diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 389d08d3e46..da520b9fd6e 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -216,8 +216,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h }), otelmux.WithPublicEndpoint(), )) - r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.Instrumentation.Logger()).Wrap) - r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap) + r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, []string{"SIGNOZ-API-KEY"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.ServiceAccountTokenizer, s.signoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), s.config.APIServer.Timeout.ExcludedRoutes, s.config.APIServer.Timeout.Default, diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index 56d6953e87b..dee8a3b32bf 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -2345,63 +2345,6 @@ export interface TypesChangePasswordRequestDTO { userId?: string; } -export interface TypesGettableAPIKeyDTO { - /** - * @type string - * @format date-time - */ - createdAt?: Date; - /** - * @type string - */ - createdBy?: string; - createdByUser?: TypesUserDTO; - /** - * @type integer - * @format int64 - */ - expiresAt?: number; - /** - * @type string - */ - id: string; - /** - * @type integer - * @format int64 - */ - lastUsed?: number; - /** - * @type string - */ - name?: string; - /** - * @type boolean - */ - revoked?: boolean; - /** - * @type string - */ - role?: string; - /** - * @type string - */ - token?: string; - /** - * @type string - * @format date-time - */ - updatedAt?: Date; - /** - * @type string - */ - updatedBy?: string; - updatedByUser?: TypesUserDTO; - /** - * @type string - */ - userId?: string; -} - export interface TypesGettableGlobalConfigDTO { /** * @type string @@ -2495,22 +2438,6 @@ export interface TypesOrganizationDTO { updatedAt?: Date; } -export interface TypesPostableAPIKeyDTO { - /** - * @type integer - * @format int64 - */ - expiresInDays?: number; - /** - * @type string - */ - name?: string; - /** - * @type string - */ - role?: string; -} - export interface TypesPostableAcceptInviteDTO { /** * @type string @@ -2602,51 +2529,6 @@ export interface TypesResetPasswordTokenDTO { token?: string; } -export interface TypesStorableAPIKeyDTO { - /** - * @type string - * @format date-time - */ - createdAt?: Date; - /** - * @type string - */ - createdBy?: string; - /** - * @type string - */ - id: string; - /** - * @type string - */ - name?: string; - /** - * @type boolean - */ - revoked?: boolean; - /** - * @type string - */ - role?: string; - /** - * @type string - */ - token?: string; - /** - * @type string - * @format date-time - */ - updatedAt?: Date; - /** - * @type string - */ - updatedBy?: string; - /** - * @type string - */ - userId?: string; -} - export interface TypesUserDTO { /** * @type string @@ -3111,31 +2993,6 @@ export type GetOrgPreference200 = { export type UpdateOrgPreferencePathParameters = { name: string; }; -export type ListAPIKeys200 = { - /** - * @type array - */ - data: TypesGettableAPIKeyDTO[]; - /** - * @type string - */ - status: string; -}; - -export type CreateAPIKey201 = { - data: TypesGettableAPIKeyDTO; - /** - * @type string - */ - status: string; -}; - -export type RevokeAPIKeyPathParameters = { - id: string; -}; -export type UpdateAPIKeyPathParameters = { - id: string; -}; export type GetPublicDashboardDataPathParameters = { id: string; }; diff --git a/frontend/src/api/generated/services/users/index.ts b/frontend/src/api/generated/services/users/index.ts index d9f3ed9f4e6..4c6a89ca8a0 100644 --- a/frontend/src/api/generated/services/users/index.ts +++ b/frontend/src/api/generated/services/users/index.ts @@ -22,7 +22,6 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance'; import type { AcceptInvite201, ChangePasswordPathParameters, - CreateAPIKey201, CreateInvite201, DeleteInvitePathParameters, DeleteUserPathParameters, @@ -33,21 +32,16 @@ import type { GetResetPasswordTokenPathParameters, GetUser200, GetUserPathParameters, - ListAPIKeys200, ListInvite200, ListUsers200, RenderErrorResponseDTO, - RevokeAPIKeyPathParameters, TypesChangePasswordRequestDTO, TypesPostableAcceptInviteDTO, - TypesPostableAPIKeyDTO, TypesPostableBulkInviteRequestDTO, TypesPostableForgotPasswordDTO, TypesPostableInviteDTO, TypesPostableResetPasswordDTO, - TypesStorableAPIKeyDTO, TypesUserDTO, - UpdateAPIKeyPathParameters, UpdateUser200, UpdateUserPathParameters, } from '../sigNoz.schemas'; @@ -750,349 +744,6 @@ export const useCreateBulkInvite = < return useMutation(mutationOptions); }; -/** - * This endpoint lists all api keys - * @summary List api keys - */ -export const listAPIKeys = (signal?: AbortSignal) => { - return GeneratedAPIInstance({ - url: `/api/v1/pats`, - method: 'GET', - signal, - }); -}; - -export const getListAPIKeysQueryKey = () => { - return [`/api/v1/pats`] as const; -}; - -export const getListAPIKeysQueryOptions = < - TData = Awaited>, - TError = ErrorType ->(options?: { - query?: UseQueryOptions< - Awaited>, - TError, - TData - >; -}) => { - const { query: queryOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getListAPIKeysQueryKey(); - - const queryFn: QueryFunction>> = ({ - signal, - }) => listAPIKeys(signal); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: QueryKey }; -}; - -export type ListAPIKeysQueryResult = NonNullable< - Awaited> ->; -export type ListAPIKeysQueryError = ErrorType; - -/** - * @summary List api keys - */ - -export function useListAPIKeys< - TData = Awaited>, - TError = ErrorType ->(options?: { - query?: UseQueryOptions< - Awaited>, - TError, - TData - >; -}): UseQueryResult & { queryKey: QueryKey } { - const queryOptions = getListAPIKeysQueryOptions(options); - - const query = useQuery(queryOptions) as UseQueryResult & { - queryKey: QueryKey; - }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List api keys - */ -export const invalidateListAPIKeys = async ( - queryClient: QueryClient, - options?: InvalidateOptions, -): Promise => { - await queryClient.invalidateQueries( - { queryKey: getListAPIKeysQueryKey() }, - options, - ); - - return queryClient; -}; - -/** - * This endpoint creates an api key - * @summary Create api key - */ -export const createAPIKey = ( - typesPostableAPIKeyDTO: BodyType, - signal?: AbortSignal, -) => { - return GeneratedAPIInstance({ - url: `/api/v1/pats`, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - data: typesPostableAPIKeyDTO, - signal, - }); -}; - -export const getCreateAPIKeyMutationOptions = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyType }, - TContext - >; -}): UseMutationOptions< - Awaited>, - TError, - { data: BodyType }, - TContext -> => { - const mutationKey = ['createAPIKey']; - const { mutation: mutationOptions } = options - ? options.mutation && - 'mutationKey' in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey } }; - - const mutationFn: MutationFunction< - Awaited>, - { data: BodyType } - > = (props) => { - const { data } = props ?? {}; - - return createAPIKey(data); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type CreateAPIKeyMutationResult = NonNullable< - Awaited> ->; -export type CreateAPIKeyMutationBody = BodyType; -export type CreateAPIKeyMutationError = ErrorType; - -/** - * @summary Create api key - */ -export const useCreateAPIKey = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyType }, - TContext - >; -}): UseMutationResult< - Awaited>, - TError, - { data: BodyType }, - TContext -> => { - const mutationOptions = getCreateAPIKeyMutationOptions(options); - - return useMutation(mutationOptions); -}; -/** - * This endpoint revokes an api key - * @summary Revoke api key - */ -export const revokeAPIKey = ({ id }: RevokeAPIKeyPathParameters) => { - return GeneratedAPIInstance({ - url: `/api/v1/pats/${id}`, - method: 'DELETE', - }); -}; - -export const getRevokeAPIKeyMutationOptions = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { pathParams: RevokeAPIKeyPathParameters }, - TContext - >; -}): UseMutationOptions< - Awaited>, - TError, - { pathParams: RevokeAPIKeyPathParameters }, - TContext -> => { - const mutationKey = ['revokeAPIKey']; - const { mutation: mutationOptions } = options - ? options.mutation && - 'mutationKey' in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey } }; - - const mutationFn: MutationFunction< - Awaited>, - { pathParams: RevokeAPIKeyPathParameters } - > = (props) => { - const { pathParams } = props ?? {}; - - return revokeAPIKey(pathParams); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type RevokeAPIKeyMutationResult = NonNullable< - Awaited> ->; - -export type RevokeAPIKeyMutationError = ErrorType; - -/** - * @summary Revoke api key - */ -export const useRevokeAPIKey = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { pathParams: RevokeAPIKeyPathParameters }, - TContext - >; -}): UseMutationResult< - Awaited>, - TError, - { pathParams: RevokeAPIKeyPathParameters }, - TContext -> => { - const mutationOptions = getRevokeAPIKeyMutationOptions(options); - - return useMutation(mutationOptions); -}; -/** - * This endpoint updates an api key - * @summary Update api key - */ -export const updateAPIKey = ( - { id }: UpdateAPIKeyPathParameters, - typesStorableAPIKeyDTO: BodyType, -) => { - return GeneratedAPIInstance({ - url: `/api/v1/pats/${id}`, - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - data: typesStorableAPIKeyDTO, - }); -}; - -export const getUpdateAPIKeyMutationOptions = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - pathParams: UpdateAPIKeyPathParameters; - data: BodyType; - }, - TContext - >; -}): UseMutationOptions< - Awaited>, - TError, - { - pathParams: UpdateAPIKeyPathParameters; - data: BodyType; - }, - TContext -> => { - const mutationKey = ['updateAPIKey']; - const { mutation: mutationOptions } = options - ? options.mutation && - 'mutationKey' in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey } }; - - const mutationFn: MutationFunction< - Awaited>, - { - pathParams: UpdateAPIKeyPathParameters; - data: BodyType; - } - > = (props) => { - const { pathParams, data } = props ?? {}; - - return updateAPIKey(pathParams, data); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type UpdateAPIKeyMutationResult = NonNullable< - Awaited> ->; -export type UpdateAPIKeyMutationBody = BodyType; -export type UpdateAPIKeyMutationError = ErrorType; - -/** - * @summary Update api key - */ -export const useUpdateAPIKey = < - TError = ErrorType, - TContext = unknown ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - pathParams: UpdateAPIKeyPathParameters; - data: BodyType; - }, - TContext - >; -}): UseMutationResult< - Awaited>, - TError, - { - pathParams: UpdateAPIKeyPathParameters; - data: BodyType; - }, - TContext -> => { - const mutationOptions = getUpdateAPIKeyMutationOptions(options); - - return useMutation(mutationOptions); -}; /** * This endpoint resets the password by token * @summary Reset password diff --git a/pkg/apiserver/signozapiserver/user.go b/pkg/apiserver/signozapiserver/user.go index 91c0c215dfe..150373da08b 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/authn/passwordauthn/emailpasswordauthn/authn.go b/pkg/authn/passwordauthn/emailpasswordauthn/authn.go index b965dbaeb99..8de52c4806e 100644 --- a/pkg/authn/passwordauthn/emailpasswordauthn/authn.go +++ b/pkg/authn/passwordauthn/emailpasswordauthn/authn.go @@ -30,5 +30,5 @@ func (a *AuthN) Authenticate(ctx context.Context, email string, password string, return nil, errors.New(errors.TypeUnauthenticated, types.ErrCodeIncorrectPassword, "invalid email or password") } - return authtypes.NewIdentity(user.ID, orgID, user.Email, user.Role), nil + return authtypes.NewIdentity(user.ID, valuer.UUID{}, authtypes.PrincipalUser, orgID, user.Email), nil } diff --git a/pkg/authz/authzstore/sqlauthzstore/store.go b/pkg/authz/authzstore/sqlauthzstore/store.go index b21272fa61b..90b24faee5e 100644 --- a/pkg/authz/authzstore/sqlauthzstore/store.go +++ b/pkg/authz/authzstore/sqlauthzstore/store.go @@ -97,11 +97,7 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, } if len(roles) != len(names) { - return nil, store.sqlstore.WrapNotFoundErrf( - nil, - roletypes.ErrCodeRoleNotFound, - "not all roles found for the provided names: %v", names, - ) + return nil, errors.Newf(errors.TypeInvalidInput, roletypes.ErrCodeRoleNotFound, "not all roles found for the provided names: %v", names) } return roles, nil @@ -122,11 +118,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id } if len(roles) != len(ids) { - return nil, store.sqlstore.WrapNotFoundErrf( - nil, - roletypes.ErrCodeRoleNotFound, - "not all roles found for the provided ids: %v", ids, - ) + return nil, errors.Newf(errors.TypeInvalidInput, roletypes.ErrCodeRoleNotFound, "not all roles found for the provided ids: %v", ids) } return roles, nil diff --git a/pkg/authz/openfgaserver/server.go b/pkg/authz/openfgaserver/server.go index 3e7cf2b33af..48136af1804 100644 --- a/pkg/authz/openfgaserver/server.go +++ b/pkg/authz/openfgaserver/server.go @@ -128,9 +128,22 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf } func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error { - subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) - if err != nil { - return err + subject := "" + switch claims.Principal { + case authtypes.PrincipalUser.StringValue(): + user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) + if err != nil { + return err + } + + subject = user + case authtypes.PrincipalServiceAccount.StringValue(): + serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil) + if err != nil { + return err + } + + subject = serviceAccount } tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID) diff --git a/pkg/http/middleware/api_key.go b/pkg/http/middleware/api_key.go deleted file mode 100644 index 8965efece8d..00000000000 --- a/pkg/http/middleware/api_key.go +++ /dev/null @@ -1,143 +0,0 @@ -package middleware - -import ( - "context" - "log/slog" - "net/http" - "time" - - "github.com/SigNoz/signoz/pkg/sharder" - "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" - "github.com/SigNoz/signoz/pkg/types/authtypes" - "github.com/SigNoz/signoz/pkg/types/ctxtypes" - "github.com/SigNoz/signoz/pkg/valuer" - "golang.org/x/sync/singleflight" -) - -const ( - apiKeyCrossOrgMessage string = "::API-KEY-CROSS-ORG::" -) - -type APIKey struct { - store sqlstore.SQLStore - uuid *authtypes.UUID - headers []string - logger *slog.Logger - sharder sharder.Sharder - sfGroup *singleflight.Group -} - -func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger, sharder sharder.Sharder) *APIKey { - return &APIKey{ - store: store, - uuid: authtypes.NewUUID(), - headers: headers, - logger: logger, - sharder: sharder, - sfGroup: &singleflight.Group{}, - } -} - -func (a *APIKey) Wrap(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var values []string - var apiKeyToken string - var apiKey types.StorableAPIKey - - for _, header := range a.headers { - values = append(values, r.Header.Get(header)) - } - - ctx, err := a.uuid.ContextFromRequest(r.Context(), values...) - if err != nil { - next.ServeHTTP(w, r) - return - } - - apiKeyToken, ok := authtypes.UUIDFromContext(ctx) - if !ok { - next.ServeHTTP(w, r) - return - } - - err = a. - store. - BunDB(). - NewSelect(). - Model(&apiKey). - Where("token = ?", apiKeyToken). - Scan(r.Context()) - if err != nil { - next.ServeHTTP(w, r) - return - } - - // allow the APIKey if expires_at is not set - if apiKey.ExpiresAt.Before(time.Now()) && !apiKey.ExpiresAt.Equal(types.NEVER_EXPIRES) { - next.ServeHTTP(w, r) - return - } - - // get user from db - user := types.User{} - err = a.store.BunDB().NewSelect().Model(&user).Where("id = ?", apiKey.UserID).Scan(r.Context()) - if err != nil { - next.ServeHTTP(w, r) - return - } - - jwt := authtypes.Claims{ - UserID: user.ID.String(), - Role: apiKey.Role, - Email: user.Email.String(), - OrgID: user.OrgID.String(), - } - - ctx = authtypes.NewContextWithClaims(ctx, jwt) - - claims, err := authtypes.ClaimsFromContext(ctx) - if err != nil { - next.ServeHTTP(w, r) - return - } - - if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil { - a.logger.ErrorContext(r.Context(), apiKeyCrossOrgMessage, "claims", claims, "error", err) - next.ServeHTTP(w, r) - return - } - - ctx = ctxtypes.SetAuthType(ctx, ctxtypes.AuthTypeAPIKey) - - comment := ctxtypes.CommentFromContext(ctx) - comment.Set("auth_type", ctxtypes.AuthTypeAPIKey.StringValue()) - comment.Set("user_id", claims.UserID) - comment.Set("org_id", claims.OrgID) - - r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment)) - - next.ServeHTTP(w, r) - - lastUsedCtx := context.WithoutCancel(r.Context()) - _, _, _ = a.sfGroup.Do(apiKey.ID.StringValue(), func() (any, error) { - apiKey.LastUsed = time.Now() - _, err = a. - store. - BunDB(). - NewUpdate(). - Model(&apiKey). - Column("last_used"). - Where("token = ?", apiKeyToken). - Where("revoked = false"). - Exec(lastUsedCtx) - if err != nil { - a.logger.ErrorContext(lastUsedCtx, "failed to update last used of api key", "error", err) - } - - return true, nil - }) - - }) - -} diff --git a/pkg/http/middleware/authn.go b/pkg/http/middleware/authn.go index f2ae10d261d..dd704b3bae5 100644 --- a/pkg/http/middleware/authn.go +++ b/pkg/http/middleware/authn.go @@ -22,31 +22,50 @@ const ( ) type AuthN struct { - tokenizer tokenizer.Tokenizer - headers []string - sharder sharder.Sharder - logger *slog.Logger - sfGroup *singleflight.Group + tokenizer tokenizer.Tokenizer + serviceAccountTokenizer tokenizer.Tokenizer + headers []string + serviceAccountHeaders []string + sharder sharder.Sharder + logger *slog.Logger + sfGroup *singleflight.Group } -func NewAuthN(headers []string, sharder sharder.Sharder, tokenizer tokenizer.Tokenizer, logger *slog.Logger) *AuthN { +func NewAuthN( + headers []string, + serviceAccountHeaders []string, + sharder sharder.Sharder, + tokenizer tokenizer.Tokenizer, + serviceAccountTokenizer tokenizer.Tokenizer, + logger *slog.Logger, +) *AuthN { return &AuthN{ - headers: headers, - sharder: sharder, - tokenizer: tokenizer, - logger: logger, - sfGroup: &singleflight.Group{}, + headers: headers, + serviceAccountHeaders: serviceAccountHeaders, + sharder: sharder, + tokenizer: tokenizer, + serviceAccountTokenizer: serviceAccountTokenizer, + logger: logger, + sfGroup: &singleflight.Group{}, } } func (a *AuthN) Wrap(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var values []string + var userHeaderValues []string for _, header := range a.headers { - values = append(values, r.Header.Get(header)) + userHeaderValues = append(userHeaderValues, r.Header.Get(header)) + } + + ctx, authType, activeTokenizer, err := a.authenticateUser(r.Context(), userHeaderValues...) + if err != nil { + var saHeaderValues []string + for _, header := range a.serviceAccountHeaders { + saHeaderValues = append(saHeaderValues, r.Header.Get(header)) + } + ctx, authType, activeTokenizer, err = a.authenticateServiceAccount(ctx, saHeaderValues...) } - ctx, err := a.contextFromRequest(r.Context(), values...) if err != nil { r = r.WithContext(ctx) next.ServeHTTP(w, r) @@ -67,27 +86,34 @@ func (a *AuthN) Wrap(next http.Handler) http.Handler { return } - ctx = ctxtypes.SetAuthType(ctx, ctxtypes.AuthTypeTokenizer) + ctx = ctxtypes.SetAuthType(ctx, authType) comment := ctxtypes.CommentFromContext(ctx) - comment.Set("auth_type", ctxtypes.AuthTypeTokenizer.StringValue()) - comment.Set("tokenizer_provider", a.tokenizer.Config().Provider) + comment.Set("auth_type", authType.StringValue()) + comment.Set("tokenizer_provider", activeTokenizer.Config().Provider) comment.Set("user_id", claims.UserID) + comment.Set("service_account_id", claims.ServiceAccountID) + comment.Set("principal", claims.Principal) comment.Set("org_id", claims.OrgID) r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment)) next.ServeHTTP(w, r) - accessToken, err := authtypes.AccessTokenFromContext(r.Context()) + // Track last observed at for the active tokenizer. + var token string + if authType == ctxtypes.AuthTypeAPIKey { + token, err = authtypes.ServiceAccountAPIKeyFromContext(r.Context()) + } else { + token, err = authtypes.AccessTokenFromContext(r.Context()) + } if err != nil { - next.ServeHTTP(w, r) return } lastObservedAtCtx := context.WithoutCancel(r.Context()) - _, _, _ = a.sfGroup.Do(accessToken, func() (any, error) { - if err := a.tokenizer.SetLastObservedAt(lastObservedAtCtx, accessToken, time.Now()); err != nil { + _, _, _ = a.sfGroup.Do(token, func() (any, error) { + if err := activeTokenizer.SetLastObservedAt(lastObservedAtCtx, token, time.Now()); err != nil { a.logger.ErrorContext(lastObservedAtCtx, "failed to set last observed at", "error", err) return false, err } @@ -97,23 +123,60 @@ func (a *AuthN) Wrap(next http.Handler) http.Handler { }) } -func (a *AuthN) contextFromRequest(ctx context.Context, values ...string) (context.Context, error) { +func (a *AuthN) authenticateUser(ctx context.Context, values ...string) (context.Context, ctxtypes.AuthType, tokenizer.Tokenizer, error) { ctx, err := a.contextFromAccessToken(ctx, values...) if err != nil { - return ctx, err + return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err } accessToken, err := authtypes.AccessTokenFromContext(ctx) if err != nil { - return ctx, err + return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err + } + + identity, err := a.tokenizer.GetIdentity(ctx, accessToken) + if err != nil { + return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err + } + + ctx = authtypes.NewContextWithClaims(ctx, identity.ToClaims()) + return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, nil +} + +func (a *AuthN) authenticateServiceAccount(ctx context.Context, values ...string) (context.Context, ctxtypes.AuthType, tokenizer.Tokenizer, error) { + ctx, err := a.contextFromServiceAccountAPIKey(ctx, values...) + if err != nil { + return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err + } + + apiKey, err := authtypes.ServiceAccountAPIKeyFromContext(ctx) + if err != nil { + return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err } - authenticatedUser, err := a.tokenizer.GetIdentity(ctx, accessToken) + identity, err := a.serviceAccountTokenizer.GetIdentity(ctx, apiKey) if err != nil { - return ctx, err + return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err + } + + ctx = authtypes.NewContextWithClaims(ctx, identity.ToClaims()) + return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, nil +} + +func (a *AuthN) contextFromServiceAccountAPIKey(ctx context.Context, values ...string) (context.Context, error) { + var value string + for _, v := range values { + if v != "" { + value = v + break + } + } + + if value == "" { + return ctx, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "missing api key header") } - return authtypes.NewContextWithClaims(ctx, authenticatedUser.ToClaims()), nil + return authtypes.NewContextWithServiceAccountAPIKey(ctx, value), nil } func (a *AuthN) contextFromAccessToken(ctx context.Context, values ...string) (context.Context, error) { diff --git a/pkg/http/middleware/authz.go b/pkg/http/middleware/authz.go index 67538e8f0ad..5877d51a5dd 100644 --- a/pkg/http/middleware/authz.go +++ b/pkg/http/middleware/authz.go @@ -9,7 +9,6 @@ import ( "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/types/authtypes" - "github.com/SigNoz/signoz/pkg/types/ctxtypes" "github.com/SigNoz/signoz/pkg/types/roletypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" @@ -42,19 +41,6 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc { return } - commentCtx := ctxtypes.CommentFromContext(ctx) - authtype, ok := commentCtx.Map()["auth_type"] - if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() { - if err := claims.IsViewer(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) - render.Error(rw, err) - return - } - - next(rw, req) - return - } - selectors := []authtypes.Selector{ authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName), authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName), @@ -94,19 +80,6 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc { return } - commentCtx := ctxtypes.CommentFromContext(ctx) - authtype, ok := commentCtx.Map()["auth_type"] - if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() { - if err := claims.IsEditor(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) - render.Error(rw, err) - return - } - - next(rw, req) - return - } - selectors := []authtypes.Selector{ authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName), authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName), @@ -145,19 +118,6 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc { return } - commentCtx := ctxtypes.CommentFromContext(ctx) - authtype, ok := commentCtx.Map()["auth_type"] - if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() { - if err := claims.IsAdmin(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) - render.Error(rw, err) - return - } - - next(rw, req) - return - } - selectors := []authtypes.Selector{ authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName), } @@ -188,17 +148,33 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc { func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - claims, err := authtypes.ClaimsFromContext(req.Context()) + ctx := req.Context() + claims, err := authtypes.ClaimsFromContext(ctx) if err != nil { render.Error(rw, err) return } - id := mux.Vars(req)["id"] - if err := claims.IsSelfAccess(id); err != nil { - middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims) - render.Error(rw, err) - return + selectors := []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName), + } + + err = middleware.authzService.CheckWithTupleCreation( + ctx, + claims, + valuer.MustNewUUID(claims.OrgID), + authtypes.RelationAssignee, + authtypes.TypeableRole, + selectors, + selectors, + ) + if err != nil { + id := mux.Vars(req)["id"] + if err := claims.IsSelfAccess(id); err != nil { + middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims) + render.Error(rw, err) + return + } } next(rw, req) diff --git a/pkg/modules/dashboard/dashboard.go b/pkg/modules/dashboard/dashboard.go index 78ef10d970e..42bd8d14141 100644 --- a/pkg/modules/dashboard/dashboard.go +++ b/pkg/modules/dashboard/dashboard.go @@ -43,7 +43,7 @@ type Module interface { Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) - LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error + LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error diff --git a/pkg/modules/dashboard/impldashboard/handler.go b/pkg/modules/dashboard/impldashboard/handler.go index 85ccd572ddd..55a495d2737 100644 --- a/pkg/modules/dashboard/impldashboard/handler.go +++ b/pkg/modules/dashboard/impldashboard/handler.go @@ -58,7 +58,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { dashboardMigrator.Migrate(ctx, req) } - dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.UserID), req) + dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.GetIdentityID()), req) if err != nil { render.Error(rw, err) return @@ -156,7 +156,7 @@ func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) { return } - err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, claims.Role, *req.Locked) + err = handler.module.LockUnlock(ctx, orgID, dashboardID, valuer.MustNewEmail(claims.Email).String(), *req.Locked) if err != nil { render.Error(rw, err) return diff --git a/pkg/modules/dashboard/impldashboard/module.go b/pkg/modules/dashboard/impldashboard/module.go index b393570c302..1d4958e793c 100644 --- a/pkg/modules/dashboard/impldashboard/module.go +++ b/pkg/modules/dashboard/impldashboard/module.go @@ -99,13 +99,13 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U return dashboard, nil } -func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error { +func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error { dashboard, err := module.Get(ctx, orgID, id) if err != nil { return err } - err = dashboard.LockUnlock(lock, role, updatedBy) + err = dashboard.LockUnlock(lock, updatedBy) if err != nil { return err } diff --git a/pkg/modules/serviceaccount/implserviceaccount/module.go b/pkg/modules/serviceaccount/implserviceaccount/module.go index d07fdcde088..7bf509412f4 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/module.go +++ b/pkg/modules/serviceaccount/implserviceaccount/module.go @@ -3,11 +3,13 @@ package implserviceaccount import ( "context" + "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/serviceaccount" + "github.com/SigNoz/signoz/pkg/tokenizer" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/emailtypes" "github.com/SigNoz/signoz/pkg/types/serviceaccounttypes" @@ -15,15 +17,17 @@ import ( ) type module struct { - store serviceaccounttypes.Store - authz authz.AuthZ - emailing emailing.Emailing - settings factory.ScopedProviderSettings + store serviceaccounttypes.Store + authz authz.AuthZ + emailing emailing.Emailing + analytics analytics.Analytics + tokenizer tokenizer.Tokenizer + settings factory.ScopedProviderSettings } -func NewModule(store serviceaccounttypes.Store, authz authz.AuthZ, emailing emailing.Emailing, providerSettings factory.ProviderSettings) serviceaccount.Module { +func NewModule(store serviceaccounttypes.Store, authz authz.AuthZ, emailing emailing.Emailing, analytics analytics.Analytics, providerSettings factory.ProviderSettings, tokenizer tokenizer.Tokenizer) serviceaccount.Module { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount") - return &module{store: store, authz: authz, emailing: emailing, settings: settings} + return &module{store: store, authz: authz, emailing: emailing, analytics: analytics, settings: settings, tokenizer: tokenizer} } func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAccount *serviceaccounttypes.ServiceAccount) error { @@ -58,6 +62,8 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAcco return err } + module.analytics.IdentifyUser(ctx, orgID.String(), serviceAccount.ID.String(), serviceAccount.Traits()) + module.analytics.TrackUser(ctx, orgID.String(), serviceAccount.ID.String(), "Service Account Created", serviceAccount.Traits()) return nil } @@ -76,6 +82,8 @@ func (module *module) GetOrCreate(ctx context.Context, serviceAccount *serviceac return nil, err } + module.analytics.IdentifyUser(ctx, serviceAccount.OrgID.String(), serviceAccount.ID.String(), serviceAccount.Traits()) + module.analytics.TrackUser(ctx, serviceAccount.OrgID.String(), serviceAccount.ID.String(), "Service Account Created", serviceAccount.Traits()) return serviceAccount, nil } @@ -186,6 +194,8 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv return err } + module.analytics.IdentifyUser(ctx, orgID.String(), input.ID.String(), input.Traits()) + module.analytics.TrackUser(ctx, orgID.String(), input.ID.String(), "Service Account Updated", input.Traits()) return nil } @@ -214,6 +224,12 @@ func (module *module) UpdateStatus(ctx context.Context, orgID valuer.UUID, input return err } + err = module.tokenizer.DeleteIdentity(ctx, input.ID) + if err != nil { + return err + } + + module.analytics.TrackUser(ctx, orgID.String(), input.ID.String(), "Service Account Deleted", map[string]any{}) return nil } @@ -276,6 +292,7 @@ func (module *module) CreateFactorAPIKey(ctx context.Context, factorAPIKey *serv module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err) } + module.analytics.TrackUser(ctx, serviceAccount.OrgID, serviceAccount.ID.String(), "API Key created", factorAPIKey.Traits()) return nil } @@ -297,12 +314,13 @@ func (module *module) ListFactorAPIKey(ctx context.Context, serviceAccountID val return serviceaccounttypes.NewFactorAPIKeyFromStorables(storables), nil } -func (module *module) UpdateFactorAPIKey(ctx context.Context, _ valuer.UUID, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error { +func (module *module) UpdateFactorAPIKey(ctx context.Context, orgID valuer.UUID, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error { err := module.store.UpdateFactorAPIKey(ctx, serviceAccountID, serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey)) if err != nil { return err } + module.analytics.TrackUser(ctx, orgID.String(), serviceAccountID.String(), "API Key updated", factorAPIKey.Traits()) return nil } @@ -331,5 +349,22 @@ func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID v module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err) } + module.analytics.TrackUser(ctx, serviceAccount.OrgID, serviceAccountID.String(), "API Key revoked", factorAPIKey.Traits()) return nil } + +func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) { + stats := make(map[string]any) + + count, err := module.store.CountByOrgID(ctx, orgID) + if err == nil { + stats["serviceaccount.count"] = count + } + + count, err = module.store.CountFactorAPIKeysByOrgID(ctx, orgID) + if err == nil { + stats["serviceaccount.keys.count"] = count + } + + return stats, nil +} diff --git a/pkg/modules/serviceaccount/implserviceaccount/store.go b/pkg/modules/serviceaccount/implserviceaccount/store.go index 82244a05120..bf5496391ad 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/store.go +++ b/pkg/modules/serviceaccount/implserviceaccount/store.go @@ -165,6 +165,23 @@ func (store *store) GetServiceAccountRoles(ctx context.Context, id valuer.UUID) return storables, nil } +func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) { + storable := new(serviceaccounttypes.StorableServiceAccount) + + count, err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storable). + Where("org_id = ?", orgID). + Count(ctx) + if err != nil { + return 0, err + } + + return int64(count), nil +} + func (store *store) ListServiceAccountRolesByOrgID(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccountRole, error) { storables := make([]*serviceaccounttypes.StorableServiceAccountRole, 0) @@ -231,6 +248,23 @@ func (store *store) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer return storable, nil } +func (store *store) GetFactorAPIKeyByKey(ctx context.Context, key string) (*serviceaccounttypes.StorableFactorAPIKey, error) { + storable := new(serviceaccounttypes.StorableFactorAPIKey) + + err := store. + sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(storable). + Where("key = ?", key). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeAPIKeytNotFound, "api key with key: %s doesn't exist", key) + } + + return storable, nil +} + func (store *store) ListFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID) ([]*serviceaccounttypes.StorableFactorAPIKey, error) { storables := make([]*serviceaccounttypes.StorableFactorAPIKey, 0) @@ -248,6 +282,44 @@ func (store *store) ListFactorAPIKey(ctx context.Context, serviceAccountID value return storables, nil } +func (store *store) ListFactorAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableFactorAPIKey, error) { + storables := make([]*serviceaccounttypes.StorableFactorAPIKey, 0) + + err := store. + sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&storables). + Join("JOIN service_account"). + JoinOn("service_account.id = factor_api_key.service_account_id"). + Where("service_account.org_id = ?", orgID). + Scan(ctx) + if err != nil { + return nil, err + } + + return storables, nil +} + +func (store *store) CountFactorAPIKeysByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) { + storable := new(serviceaccounttypes.StorableFactorAPIKey) + + count, err := store. + sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(storable). + Join("JOIN service_account"). + JoinOn("service_account.id = factor_api_key.service_account_id"). + Where("service_account.org_id = ?", orgID). + Count(ctx) + if err != nil { + return 0, err + } + + return int64(count), nil +} + func (store *store) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, storable *serviceaccounttypes.StorableFactorAPIKey) error { _, err := store. sqlstore. @@ -263,6 +335,30 @@ func (store *store) UpdateFactorAPIKey(ctx context.Context, serviceAccountID val return nil } +func (store *store) UpdateLastObservedAtByKey(ctx context.Context, apiKeyToLastObservedAt []map[string]any) error { + values := store. + sqlstore. + BunDBCtx(ctx). + NewValues(&apiKeyToLastObservedAt) + + _, err := store. + sqlstore. + BunDBCtx(ctx). + NewUpdate(). + With("update_cte", values). + Model((*serviceaccounttypes.StorableFactorAPIKey)(nil)). + TableExpr("update_cte"). + Set("last_observed_at = update_cte.last_observed_at"). + Where("factor_api_key.key = update_cte.key"). + Where("factor_api_key.service_account_id = update_cte.service_account_id"). + Exec(ctx) + if err != nil { + return err + } + + return nil +} + func (store *store) RevokeFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) error { _, err := store. sqlstore. diff --git a/pkg/modules/serviceaccount/serviceaccount.go b/pkg/modules/serviceaccount/serviceaccount.go index f1083e68518..59b38184011 100644 --- a/pkg/modules/serviceaccount/serviceaccount.go +++ b/pkg/modules/serviceaccount/serviceaccount.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/SigNoz/signoz/pkg/statsreporter" "github.com/SigNoz/signoz/pkg/types/serviceaccounttypes" "github.com/SigNoz/signoz/pkg/valuer" ) @@ -47,6 +48,8 @@ type Module interface { // Revokes an existing API key for a service account RevokeFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) error + + statsreporter.StatsCollector } type Handler interface { diff --git a/pkg/modules/session/implsession/module.go b/pkg/modules/session/implsession/module.go index 887a8fd16ab..94344932305 100644 --- a/pkg/modules/session/implsession/module.go +++ b/pkg/modules/session/implsession/module.go @@ -158,7 +158,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi return "", errors.WithAdditionalf(err, "root user can only authenticate via password") } - token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(user.ID, user.OrgID, user.Email, user.Role), map[string]string{}) + token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(user.ID, valuer.UUID{}, authtypes.PrincipalUser, user.OrgID, user.Email), map[string]string{}) if err != nil { return "", err } diff --git a/pkg/modules/spanpercentile/implspanpercentile/handler.go b/pkg/modules/spanpercentile/implspanpercentile/handler.go index fb6263886e2..a5b50e20d9d 100644 --- a/pkg/modules/spanpercentile/implspanpercentile/handler.go +++ b/pkg/modules/spanpercentile/implspanpercentile/handler.go @@ -35,7 +35,7 @@ func (h *handler) GetSpanPercentileDetails(w http.ResponseWriter, r *http.Reques return } - result, err := h.module.GetSpanPercentile(r.Context(), valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), spanPercentileRequest) + result, err := h.module.GetSpanPercentile(r.Context(), valuer.MustNewUUID(claims.OrgID), spanPercentileRequest) if err != nil { render.Error(w, err) return diff --git a/pkg/modules/spanpercentile/implspanpercentile/module.go b/pkg/modules/spanpercentile/implspanpercentile/module.go index b73e9357615..f5ee853241d 100644 --- a/pkg/modules/spanpercentile/implspanpercentile/module.go +++ b/pkg/modules/spanpercentile/implspanpercentile/module.go @@ -28,7 +28,7 @@ func NewModule( } } -func (m *module) GetSpanPercentile(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) { +func (m *module) GetSpanPercentile(ctx context.Context, orgID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) { ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{ instrumentationtypes.CodeNamespace: "spanpercentile", instrumentationtypes.CodeFunctionName: "GetSpanPercentile", diff --git a/pkg/modules/spanpercentile/spanpercentile.go b/pkg/modules/spanpercentile/spanpercentile.go index c1fd1b55c88..8030096de2e 100644 --- a/pkg/modules/spanpercentile/spanpercentile.go +++ b/pkg/modules/spanpercentile/spanpercentile.go @@ -9,7 +9,7 @@ import ( ) type Module interface { - GetSpanPercentile(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) + GetSpanPercentile(ctx context.Context, orgID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) } type Handler interface { diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go index 17b4bff20c4..08eb330312f 100644 --- a/pkg/modules/user/impluser/handler.go +++ b/pkg/modules/user/impluser/handler.go @@ -13,7 +13,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 +348,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 b328f413519..a767542949d 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 6dd242dae73..073aba6ee60 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)). @@ -302,15 +292,6 @@ func (store *store) SoftDeleteUser(ctx context.Context, orgID string, id string) 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 +438,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 +485,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 820b7fd319c..94fc32597cd 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/querier/api.go b/pkg/querier/api.go index 7aab3267562..7e4dc21924c 100644 --- a/pkg/querier/api.go +++ b/pkg/querier/api.go @@ -60,6 +60,8 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { handler.set.Logger.ErrorContext(ctx, "panic in QueryRange", "error", r, "user", claims.UserID, + "service_account", claims.ServiceAccountID, + "principal", claims.Principal, "payload", string(queryJSON), "stacktrace", stackTrace, ) @@ -160,6 +162,8 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request handler.set.Logger.ErrorContext(ctx, "panic in QueryRawStream", "error", r, "user", claims.UserID, + "service_account", claims.ServiceAccountID, + "principal", claims.Principal, "payload", string(queryJSON), "stacktrace", stackTrace, ) @@ -309,9 +313,9 @@ func (handler *handler) logEvent(ctx context.Context, referrer string, event *qb } if !event.HasData { - handler.analytics.TrackUser(ctx, claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", properties) + handler.analytics.TrackUser(ctx, claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Empty", properties) return } - handler.analytics.TrackUser(ctx, claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties) + handler.analytics.TrackUser(ctx, claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Results", properties) } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index b617e0c1434..5e237bceef2 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -512,7 +512,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { router.HandleFunc("/api/v1/dashboards/{id}", am.ViewAccess(aH.Get)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Update)).Methods(http.MethodPut) router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Delete)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/dashboards/{id}/lock", am.EditAccess(aH.Signoz.Handlers.Dashboard.LockUnlock)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/dashboards/{id}/lock", am.AdminAccess(aH.Signoz.Handlers.Dashboard.LockUnlock)).Methods(http.MethodPut) router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost) router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet) @@ -1565,7 +1565,7 @@ func (aH *APIHandler) registerEvent(w http.ResponseWriter, r *http.Request) { if errv2 == nil { switch request.EventType { case model.TrackEvent: - aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, request.EventName, request.Attributes) + aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.GetIdentityID(), request.EventName, request.Attributes) } aH.WriteJSON(w, r, map[string]string{"data": "Event Processed Successfully"}) } else { @@ -4669,7 +4669,7 @@ func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result // Check if result is empty or has no data if len(result) == 0 { - aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", properties) + aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Empty", properties) return } @@ -4679,18 +4679,18 @@ func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result if len(result[0].List) == 0 { // Check if first result has no table data if result[0].Table == nil { - aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", properties) + aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Empty", properties) return } if len(result[0].Table.Rows) == 0 { - aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", properties) + aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Empty", properties) return } } } - aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties) + aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.GetIdentityID(), "Telemetry Query Returned Results", properties) } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 965f2080eac..df60847bcc3 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -195,13 +195,12 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, }), otelmux.WithPublicEndpoint(), )) - r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.Instrumentation.Logger()).Wrap) + r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, []string{"SIGNOZ-API-KEY"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.ServiceAccountTokenizer, s.signoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), s.config.APIServer.Timeout.ExcludedRoutes, s.config.APIServer.Timeout.Default, s.config.APIServer.Timeout.Max, ).Wrap) - r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap) r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) r.Use(middleware.NewComment().Wrap) diff --git a/pkg/signoz/handler_test.go b/pkg/signoz/handler_test.go index ccc654280ee..e7d8a25d04b 100644 --- a/pkg/signoz/handler_test.go +++ b/pkg/signoz/handler_test.go @@ -43,14 +43,13 @@ func TestNewHandlers(t *testing.T) { emailing := emailingtest.New() queryParser := queryparser.New(providerSettings) require.NoError(t, err) - dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry()) require.NoError(t, err) - userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) + dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) + modules := NewModules(sqlstore, tokenizer, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) querierHandler := querier.NewHandler(providerSettings, nil, nil) handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil) diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 53693392b38..0a60b8b6658 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -74,6 +74,7 @@ type Modules struct { func NewModules( sqlstore sqlstore.SQLStore, tokenizer tokenizer.Tokenizer, + serviceAccountTokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgGetter organization.Getter, @@ -113,6 +114,6 @@ func NewModules( Services: implservices.NewModule(querier, telemetryStore), MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore), - ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings), + ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, analytics, providerSettings, serviceAccountTokenizer), } } diff --git a/pkg/signoz/module_test.go b/pkg/signoz/module_test.go index 89e2107b8ae..36ccc50f7c0 100644 --- a/pkg/signoz/module_test.go +++ b/pkg/signoz/module_test.go @@ -42,14 +42,14 @@ func TestNewModules(t *testing.T) { emailing := emailingtest.New() queryParser := queryparser.New(providerSettings) require.NoError(t, err) - dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry()) require.NoError(t, err) userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) + dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) + modules := NewModules(sqlstore, tokenizer, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) reflectVal := reflect.ValueOf(modules) for i := 0; i < reflectVal.NumField(); i++ { diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 22d5b353b4c..86bc2494898 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -27,6 +27,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/organization/implorganization" "github.com/SigNoz/signoz/pkg/modules/preference/implpreference" "github.com/SigNoz/signoz/pkg/modules/promote/implpromote" + "github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount" "github.com/SigNoz/signoz/pkg/modules/session/implsession" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user/impluser" @@ -55,6 +56,7 @@ import ( "github.com/SigNoz/signoz/pkg/tokenizer" "github.com/SigNoz/signoz/pkg/tokenizer/jwttokenizer" "github.com/SigNoz/signoz/pkg/tokenizer/opaquetokenizer" + "github.com/SigNoz/signoz/pkg/tokenizer/serviceaccounttokenizer" "github.com/SigNoz/signoz/pkg/tokenizer/tokenizerstore/sqltokenizerstore" "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/SigNoz/signoz/pkg/types/featuretypes" @@ -172,6 +174,9 @@ func NewSQLMigrationProviderFactories( sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore), sqlmigration.NewAddStatusUserFactory(sqlstore, sqlschema), sqlmigration.NewDeprecateUserInviteFactory(sqlstore, sqlschema), + sqlmigration.NewAddServiceAccountFactory(sqlstore, sqlschema), + sqlmigration.NewDeprecateAPIKeyFactory(sqlstore, sqlschema), + sqlmigration.NewServiceAccountAuthzactory(sqlstore), ) } @@ -265,9 +270,11 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.NamedMap[factory.ProviderFactory[tokenizer.Tokenizer, tokenizer.Config]] { tokenStore := sqltokenizerstore.NewStore(sqlstore) + apiKeyStore := implserviceaccount.NewStore(sqlstore) return factory.MustNewNamedMap( opaquetokenizer.NewFactory(cache, tokenStore, orgGetter), jwttokenizer.NewFactory(cache, tokenStore), + serviceaccounttokenizer.NewFactory(cache, apiKeyStore, orgGetter), ) } diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index f7f5a27f9fc..f48c560070b 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -21,6 +21,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization" + "github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount" "github.com/SigNoz/signoz/pkg/modules/user/impluser" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/querier" @@ -38,6 +39,7 @@ import ( "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/telemetrytraces" pkgtokenizer "github.com/SigNoz/signoz/pkg/tokenizer" + "github.com/SigNoz/signoz/pkg/tokenizer/serviceaccounttokenizer" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/version" @@ -48,29 +50,30 @@ import ( type SigNoz struct { *factory.Registry - Instrumentation instrumentation.Instrumentation - Analytics analytics.Analytics - Cache cache.Cache - Web web.Web - SQLStore sqlstore.SQLStore - TelemetryStore telemetrystore.TelemetryStore - TelemetryMetadataStore telemetrytypes.MetadataStore - Prometheus prometheus.Prometheus - Alertmanager alertmanager.Alertmanager - Querier querier.Querier - APIServer apiserver.APIServer - Zeus zeus.Zeus - Licensing licensing.Licensing - Emailing emailing.Emailing - Sharder sharder.Sharder - StatsReporter statsreporter.StatsReporter - Tokenizer pkgtokenizer.Tokenizer - Authz authz.AuthZ - Modules Modules - Handlers Handlers - QueryParser queryparser.QueryParser - Flagger flagger.Flagger - Gateway gateway.Gateway + Instrumentation instrumentation.Instrumentation + Analytics analytics.Analytics + Cache cache.Cache + Web web.Web + SQLStore sqlstore.SQLStore + TelemetryStore telemetrystore.TelemetryStore + TelemetryMetadataStore telemetrytypes.MetadataStore + Prometheus prometheus.Prometheus + Alertmanager alertmanager.Alertmanager + Querier querier.Querier + APIServer apiserver.APIServer + Zeus zeus.Zeus + Licensing licensing.Licensing + Emailing emailing.Emailing + Sharder sharder.Sharder + StatsReporter statsreporter.StatsReporter + Tokenizer pkgtokenizer.Tokenizer + ServiceAccountTokenizer pkgtokenizer.Tokenizer + Authz authz.AuthZ + Modules Modules + Handlers Handlers + QueryParser queryparser.QueryParser + Flagger flagger.Flagger + Gateway gateway.Gateway } func New( @@ -279,6 +282,12 @@ func New( return nil, err } + apiKeyStore := implserviceaccount.NewStore(sqlstore) + serviceAccountTokenizer, err := serviceaccounttokenizer.New(ctx, providerSettings, config.Tokenizer, cache, apiKeyStore, orgGetter) + if err != nil { + return nil, err + } + // Initialize user getter userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) @@ -388,7 +397,7 @@ func New( } // Initialize all modules - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter) + modules := NewModules(sqlstore, tokenizer, serviceAccountTokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter) userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root) @@ -421,6 +430,7 @@ func New( tokenizer, config, modules.AuthDomain, + modules.ServiceAccount, } // Initialize stats reporter from the available stats reporter provider factories @@ -443,6 +453,7 @@ func New( factory.NewNamedService(factory.MustNewName("licensing"), licensing), factory.NewNamedService(factory.MustNewName("statsreporter"), statsReporter), factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer), + factory.NewNamedService(factory.MustNewName("serviceaccounttokenizer"), serviceAccountTokenizer), factory.NewNamedService(factory.MustNewName("authz"), authz), factory.NewNamedService(factory.MustNewName("user"), userService), ) @@ -451,28 +462,29 @@ func New( } return &SigNoz{ - Registry: registry, - Analytics: analytics, - Instrumentation: instrumentation, - Cache: cache, - Web: web, - SQLStore: sqlstore, - TelemetryStore: telemetrystore, - TelemetryMetadataStore: telemetryMetadataStore, - Prometheus: prometheus, - Alertmanager: alertmanager, - Querier: querier, - APIServer: apiserver, - Zeus: zeus, - Licensing: licensing, - Emailing: emailing, - Sharder: sharder, - Tokenizer: tokenizer, - Authz: authz, - Modules: modules, - Handlers: handlers, - QueryParser: queryParser, - Flagger: flagger, - Gateway: gateway, + Registry: registry, + Analytics: analytics, + Instrumentation: instrumentation, + Cache: cache, + Web: web, + SQLStore: sqlstore, + TelemetryStore: telemetrystore, + TelemetryMetadataStore: telemetryMetadataStore, + Prometheus: prometheus, + Alertmanager: alertmanager, + Querier: querier, + APIServer: apiserver, + Zeus: zeus, + Licensing: licensing, + Emailing: emailing, + Sharder: sharder, + Tokenizer: tokenizer, + ServiceAccountTokenizer: serviceAccountTokenizer, + Authz: authz, + Modules: modules, + Handlers: handlers, + QueryParser: queryParser, + Flagger: flagger, + Gateway: gateway, }, nil } diff --git a/pkg/sqlmigration/069_add_service_account.go b/pkg/sqlmigration/069_add_service_account.go new file mode 100644 index 00000000000..b6ccfa944e3 --- /dev/null +++ b/pkg/sqlmigration/069_add_service_account.go @@ -0,0 +1,121 @@ +package sqlmigration + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type addServiceAccount struct { + sqlschema sqlschema.SQLSchema + sqlstore sqlstore.SQLStore +} + +func NewAddServiceAccountFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_service_account"), func(_ context.Context, _ factory.ProviderSettings, _ Config) (SQLMigration, error) { + return &addServiceAccount{ + sqlschema: sqlschema, + sqlstore: sqlstore, + }, nil + }) +} + +func (migration *addServiceAccount) Register(migrations *migrate.Migrations) error { + err := migrations.Register(migration.Up, migration.Down) + if err != nil { + return err + } + + return nil +} + +func (migration *addServiceAccount) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + sqls := [][]byte{} + + tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "service_account", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "deleted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "name", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "email", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "status", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ + ColumnNames: []sqlschema.ColumnName{"id"}, + }, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + { + ReferencingColumnName: sqlschema.ColumnName("org_id"), + ReferencedTableName: sqlschema.TableName("organizations"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + }, + }) + sqls = append(sqls, tableSQLs...) + + indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "service_account", ColumnNames: []sqlschema.ColumnName{"name", "org_id", "deleted_at"}}) + sqls = append(sqls, indexSQLs...) + + tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "service_account_role", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "service_account_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "role_id", DataType: sqlschema.DataTypeText, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ + ColumnNames: []sqlschema.ColumnName{"id"}, + }, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + { + ReferencingColumnName: sqlschema.ColumnName("service_account_id"), + ReferencedTableName: sqlschema.TableName("service_account"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + { + ReferencingColumnName: sqlschema.ColumnName("role_id"), + ReferencedTableName: sqlschema.TableName("role"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + }, + }) + sqls = append(sqls, tableSQLs...) + + 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 { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (a *addServiceAccount) Down(context.Context, *bun.DB) error { + return nil +} diff --git a/pkg/sqlmigration/070_deprecate_api_key.go b/pkg/sqlmigration/070_deprecate_api_key.go new file mode 100644 index 00000000000..f80a89bb6df --- /dev/null +++ b/pkg/sqlmigration/070_deprecate_api_key.go @@ -0,0 +1,325 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "time" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type oldFactorAPIKey68 struct { + bun.BaseModel `bun:"table:factor_api_key"` + + types.Identifiable + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` + Token string `bun:"token"` + Role string `bun:"role"` + Name string `bun:"name"` + ExpiresAt time.Time `bun:"expires_at"` + LastUsed time.Time `bun:"last_used"` + Revoked bool `bun:"revoked"` + UserID string `bun:"user_id"` +} + +type oldUser68 struct { + bun.BaseModel `bun:"table:users"` + + types.Identifiable + DisplayName string `bun:"display_name"` + Email string `bun:"email"` + OrgID string `bun:"org_id"` +} + +type oldRole68 struct { + bun.BaseModel `bun:"table:role"` + + types.Identifiable + Name string `bun:"name"` + OrgID string `bun:"org_id"` +} + +type newServiceAccount68 struct { + bun.BaseModel `bun:"table:service_account"` + + types.Identifiable + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` + Name string `bun:"name"` + Email string `bun:"email"` + Status string `bun:"status"` + OrgID string `bun:"org_id"` +} + +type newServiceAccountRole68 struct { + bun.BaseModel `bun:"table:service_account_role"` + + types.Identifiable + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` + ServiceAccountID string `bun:"service_account_id"` + RoleID string `bun:"role_id"` +} + +type newFactorAPIKey68 struct { + bun.BaseModel `bun:"table:factor_api_key"` + + types.Identifiable + CreatedAt time.Time `bun:"created_at"` + UpdatedAt time.Time `bun:"updated_at"` + Name string `bun:"name"` + Key string `bun:"key"` + ExpiresAt uint64 `bun:"expires_at"` + LastObservedAt time.Time `bun:"last_observed_at"` + ServiceAccountID string `bun:"service_account_id"` +} + +type deprecateAPIKey struct { + sqlstore sqlstore.SQLStore + sqlschema sqlschema.SQLSchema +} + +func NewDeprecateAPIKeyFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("deprecate_api_key"), func(_ context.Context, _ factory.ProviderSettings, c Config) (SQLMigration, error) { + return &deprecateAPIKey{ + sqlstore: sqlstore, + sqlschema: sqlschema, + }, nil + }) +} + +func (migration *deprecateAPIKey) Register(migrations *migrate.Migrations) error { + err := migrations.Register(migration.Up, migration.Down) + if err != nil { + return err + } + + return nil +} + +func (migration *deprecateAPIKey) Up(ctx context.Context, db *bun.DB) error { + table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("factor_api_key")) + if err != nil { + return err + } + + hasOldSchema := false + for _, col := range table.Columns { + if col.Name == "user_id" { + hasOldSchema = true + break + } + } + if !hasOldSchema { + return nil + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + // 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 + } + + // get all the unique users + userIDs := make(map[string]struct{}) + for _, key := range oldKeys { + userIDs[key.UserID] = struct{}{} + } + + userIDList := make([]string, 0, len(userIDs)) + for uid := range userIDs { + userIDList = append(userIDList, uid) + } + + userMap := make(map[string]*oldUser68) + if len(userIDList) > 0 { + users := make([]*oldUser68, 0) + err = tx.NewSelect().Model(&users).Where("id IN (?)", bun.In(userIDList)).Scan(ctx) + if err != nil && err != sql.ErrNoRows { + return err + } + for _, u := range users { + userMap[u.ID.String()] = u + } + } + + // get the role ids + type orgRoleKey struct { + OrgID string + RoleName string + } + roleMap := make(map[orgRoleKey]string) + if len(userMap) > 0 { + orgIDs := make(map[string]struct{}) + for _, u := range userMap { + orgIDs[u.OrgID] = struct{}{} + } + orgIDList := make([]string, 0, len(orgIDs)) + for oid := range orgIDs { + orgIDList = append(orgIDList, oid) + } + + roles := make([]*oldRole68, 0) + err = tx.NewSelect().Model(&roles).Where("org_id IN (?)", bun.In(orgIDList)).Scan(ctx) + if err != nil && err != sql.ErrNoRows { + return err + } + for _, r := range roles { + roleMap[orgRoleKey{OrgID: r.OrgID, RoleName: r.Name}] = r.ID.String() + } + } + + serviceAccounts := make([]*newServiceAccount68, 0) + serviceAccountRoles := make([]*newServiceAccountRole68, 0) + newKeys := make([]*newFactorAPIKey68, 0) + + now := time.Now() + for _, oldKey := range oldKeys { + user, ok := userMap[oldKey.UserID] + if !ok { + // this should never happen as a key cannot exist without a user + continue + } + + 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, + }) + + managedRoleName, ok := roletypes.ExistingRoleToSigNozManagedRoleMap[types.Role(oldKey.Role)] + if !ok { + managedRoleName = roletypes.SigNozViewerRoleName + } + + roleID, ok := roleMap[orgRoleKey{OrgID: user.OrgID, RoleName: managedRoleName}] + if ok { + serviceAccountRoles = append(serviceAccountRoles, &newServiceAccountRole68{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + CreatedAt: now, + UpdatedAt: now, + ServiceAccountID: saID.String(), + RoleID: roleID, + }) + } + + var expiresAtUnix uint64 + if !oldKey.ExpiresAt.IsZero() && oldKey.ExpiresAt.Unix() > 0 { + expiresAtUnix = uint64(oldKey.ExpiresAt.Unix()) + } + + // Convert last_used to last_observed_at. + lastObservedAt := oldKey.LastUsed + if lastObservedAt.IsZero() { + lastObservedAt = oldKey.CreatedAt + } + + newKeys = append(newKeys, &newFactorAPIKey68{ + Identifiable: oldKey.Identifiable, + CreatedAt: oldKey.CreatedAt, + UpdatedAt: oldKey.UpdatedAt, + Name: oldKey.Name, + Key: oldKey.Token, + ExpiresAt: expiresAtUnix, + LastObservedAt: lastObservedAt, + ServiceAccountID: saID.String(), + }) + } + + if len(serviceAccounts) > 0 { + if _, err := tx.NewInsert().Model(&serviceAccounts).Exec(ctx); err != nil { + return err + } + } + + if len(serviceAccountRoles) > 0 { + if _, err := tx.NewInsert().Model(&serviceAccountRoles).Exec(ctx); err != nil { + return err + } + } + + sqls := [][]byte{} + deprecatedFactorAPIKey, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("factor_api_key")) + if err != nil { + return err + } + + dropTableSQLS := migration.sqlschema.Operator().DropTable(deprecatedFactorAPIKey) + sqls = append(sqls, dropTableSQLS...) + + tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "factor_api_key", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "name", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "key", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "expires_at", DataType: sqlschema.DataTypeInteger, Nullable: false}, + {Name: "last_observed_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "service_account_id", DataType: sqlschema.DataTypeText, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ + ColumnNames: []sqlschema.ColumnName{"id"}, + }, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + { + ReferencingColumnName: sqlschema.ColumnName("service_account_id"), + ReferencedTableName: sqlschema.TableName("service_account"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + }, + }) + sqls = append(sqls, tableSQLs...) + + indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "factor_api_key", ColumnNames: []sqlschema.ColumnName{"key"}}) + sqls = append(sqls, indexSQLs...) + + indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "factor_api_key", ColumnNames: []sqlschema.ColumnName{"name", "service_account_id"}}) + sqls = append(sqls, indexSQLs...) + + for _, sql := range sqls { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + if len(newKeys) > 0 { + if _, err := tx.NewInsert().Model(&newKeys).Exec(ctx); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *deprecateAPIKey) Down(context.Context, *bun.DB) error { + return nil +} diff --git a/pkg/sqlmigration/071_add_service_account_authz.go b/pkg/sqlmigration/071_add_service_account_authz.go new file mode 100644 index 00000000000..14bee04aeea --- /dev/null +++ b/pkg/sqlmigration/071_add_service_account_authz.go @@ -0,0 +1,148 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "time" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/oklog/ulid/v2" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" + "github.com/uptrace/bun/migrate" +) + +type addServiceAccountAuthz struct { + sqlstore sqlstore.SQLStore +} + +func NewServiceAccountAuthzactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_service_account_authz"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return &addServiceAccountAuthz{sqlstore: sqlstore}, nil + }) +} + +func (migration *addServiceAccountAuthz) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *addServiceAccountAuthz) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + var storeID string + err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID) + if err != nil { + return err + } + + type saRoleTuple struct { + ServiceAccountID string + OrgID string + RoleName string + } + + rows, err := tx.QueryContext(ctx, ` + SELECT sa.id, sa.org_id, r.name + FROM service_account sa + JOIN service_account_role sar ON sar.service_account_id = sa.id + JOIN role r ON r.id = sar.role_id + `) + if err != nil && err != sql.ErrNoRows { + return err + } + defer rows.Close() + + tuples := make([]saRoleTuple, 0) + for rows.Next() { + var t saRoleTuple + if err := rows.Scan(&t.ServiceAccountID, &t.OrgID, &t.RoleName); err != nil { + return err + } + tuples = append(tuples, t) + } + + for _, t := range tuples { + entropy := ulid.DefaultEntropy() + now := time.Now().UTC() + tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String() + + objectID := "organization/" + t.OrgID + "/role/" + t.RoleName + saUserID := "organization/" + t.OrgID + "/serviceaccount/" + t.ServiceAccountID + + if migration.sqlstore.BunDB().Dialect().Name() == dialect.PG { + result, err := tx.ExecContext(ctx, ` + INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`, + storeID, "role", objectID, "assignee", "serviceaccount:"+saUserID, "user", tupleID, now, + ) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + if rowsAffected == 0 { + continue + } + + _, err = tx.ExecContext(ctx, ` + INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (store, ulid, object_type) DO NOTHING`, + storeID, "role", objectID, "assignee", "serviceaccount:"+saUserID, "TUPLE_OPERATION_WRITE", tupleID, now, + ) + if err != nil { + return err + } + } else { + result, err := tx.ExecContext(ctx, ` + INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`, + storeID, "role", objectID, "assignee", "serviceaccount", saUserID, "", "user", tupleID, now, + ) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + if rowsAffected == 0 { + continue + } + + _, err = tx.ExecContext(ctx, ` + INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (store, ulid, object_type) DO NOTHING`, + storeID, "role", objectID, "assignee", "serviceaccount", saUserID, "", 0, tupleID, now, + ) + if err != nil { + return err + } + } + } + + return tx.Commit() +} + +func (migration *addServiceAccountAuthz) Down(context.Context, *bun.DB) error { + return nil +} diff --git a/pkg/tokenizer/config.go b/pkg/tokenizer/config.go index ffe7fac8e52..3772b358153 100644 --- a/pkg/tokenizer/config.go +++ b/pkg/tokenizer/config.go @@ -17,6 +17,9 @@ type Config struct { // Config for the JWT tokenizer. JWT JWTConfig `mapstructure:"jwt"` + // Config for the serviceAccount tokenizer. + ServiceAccount ServiceAccountConfig `mapstructure:"service_account"` + // Rotation config Rotation RotationConfig `mapstructure:"rotation"` @@ -37,6 +40,11 @@ type JWTConfig struct { Secret string `mapstructure:"secret"` } +type ServiceAccountConfig struct { + // GC config + GC GCConfig `mapstructure:"gc"` +} + type GCConfig struct { // The interval to perform garbage collection. Interval time.Duration `mapstructure:"interval"` @@ -81,6 +89,11 @@ func newConfig() factory.Config { JWT: JWTConfig{ Secret: "", }, + ServiceAccount: ServiceAccountConfig{ + GC: GCConfig{ + Interval: 1 * time.Hour, // 1 hour + }, + }, Rotation: RotationConfig{ Interval: 30 * time.Minute, // 30 minutes Duration: 60 * time.Second, // 60 seconds diff --git a/pkg/tokenizer/jwttokenizer/claims.go b/pkg/tokenizer/jwttokenizer/claims.go index dfa39959cb6..80756f9c7dc 100644 --- a/pkg/tokenizer/jwttokenizer/claims.go +++ b/pkg/tokenizer/jwttokenizer/claims.go @@ -2,7 +2,6 @@ package jwttokenizer import ( "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/types" "github.com/golang-jwt/jwt/v5" ) @@ -10,21 +9,15 @@ var _ jwt.ClaimsValidator = (*Claims)(nil) type Claims struct { jwt.RegisteredClaims - UserID string `json:"id"` - Email string `json:"email"` - Role types.Role `json:"role"` - OrgID string `json:"orgId"` + UserID string `json:"id"` + Email string `json:"email"` + OrgID string `json:"orgId"` } func (c *Claims) Validate() error { if c.UserID == "" { return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "id is required") } - // The problem is that when the "role" field is missing entirely from the JSON (as opposed to being present but empty), the UnmarshalJSON method for Role isn't called at all. - // The JSON decoder just sets the Role field to its zero value (""). - if c.Role == "" { - return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "role is required") - } if c.OrgID == "" { return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "orgId is required") diff --git a/pkg/tokenizer/jwttokenizer/provider.go b/pkg/tokenizer/jwttokenizer/provider.go index 8a82976cf11..c0f91ba0120 100644 --- a/pkg/tokenizer/jwttokenizer/provider.go +++ b/pkg/tokenizer/jwttokenizer/provider.go @@ -76,7 +76,6 @@ func (provider *provider) Start(ctx context.Context) error { func (provider *provider) CreateToken(ctx context.Context, identity *authtypes.Identity, meta map[string]string) (*authtypes.Token, error) { accessTokenClaims := Claims{ UserID: identity.UserID.String(), - Role: identity.Role, Email: identity.Email.String(), OrgID: identity.OrgID.String(), RegisteredClaims: jwt.RegisteredClaims{ @@ -92,7 +91,6 @@ func (provider *provider) CreateToken(ctx context.Context, identity *authtypes.I refreshTokenClaims := Claims{ UserID: identity.UserID.String(), - Role: identity.Role, Email: identity.Email.String(), OrgID: identity.OrgID.String(), RegisteredClaims: jwt.RegisteredClaims{ @@ -115,17 +113,7 @@ func (provider *provider) GetIdentity(ctx context.Context, accessToken string) ( return nil, err } - // check claimed role - identity, err := provider.getOrSetIdentity(ctx, emptyOrgID, valuer.MustNewUUID(claims.UserID)) - if err != nil { - return nil, err - } - - if identity.Role != claims.Role { - return nil, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "claim role mismatch") - } - - return authtypes.NewIdentity(valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID), valuer.MustNewEmail(claims.Email), claims.Role), nil + return authtypes.NewIdentity(valuer.MustNewUUID(claims.UserID), valuer.UUID{}, authtypes.PrincipalUser, valuer.MustNewUUID(claims.OrgID), valuer.MustNewEmail(claims.Email)), nil } func (provider *provider) DeleteToken(ctx context.Context, accessToken string) error { diff --git a/pkg/tokenizer/jwttokenizer/provider_test.go b/pkg/tokenizer/jwttokenizer/provider_test.go index f15da7ea8f9..f583bce2f23 100644 --- a/pkg/tokenizer/jwttokenizer/provider_test.go +++ b/pkg/tokenizer/jwttokenizer/provider_test.go @@ -14,7 +14,6 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest" "github.com/SigNoz/signoz/pkg/tokenizer" "github.com/SigNoz/signoz/pkg/tokenizer/tokenizerstore/sqltokenizerstore" - "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/stretchr/testify/assert" @@ -62,7 +61,6 @@ func TestLastObservedAt_Concurrent(t *testing.T) { &authtypes.Identity{ UserID: valuer.GenerateUUID(), OrgID: orgID, - Role: types.RoleAdmin, Email: valuer.MustNewEmail("test@test.com"), }, map[string]string{}, @@ -74,7 +72,6 @@ func TestLastObservedAt_Concurrent(t *testing.T) { &authtypes.Identity{ UserID: valuer.GenerateUUID(), OrgID: orgID, - Role: types.RoleAdmin, Email: valuer.MustNewEmail("test@test.com"), }, map[string]string{}, diff --git a/pkg/tokenizer/serviceaccounttokenizer/provider.go b/pkg/tokenizer/serviceaccounttokenizer/provider.go new file mode 100644 index 00000000000..1072bdfdd52 --- /dev/null +++ b/pkg/tokenizer/serviceaccounttokenizer/provider.go @@ -0,0 +1,353 @@ +package serviceaccounttokenizer + +import ( + "context" + "slices" + "time" + + "github.com/SigNoz/signoz/pkg/cache" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/tokenizer" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/cachetypes" + "github.com/SigNoz/signoz/pkg/types/serviceaccounttypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/dgraph-io/ristretto/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var ( + emptyOrgID valuer.UUID = valuer.UUID{} +) + +const ( + expectedLastObservedAtCacheEntries int64 = 5000 // 1000 serviceAccounts * Max 5 keys per account +) + +type provider struct { + config tokenizer.Config + settings factory.ScopedProviderSettings + cache cache.Cache + apiKeyStore serviceaccounttypes.Store + orgGetter organization.Getter + stopC chan struct{} + lastObservedAtCache *ristretto.Cache[string, time.Time] +} + +func NewFactory(cache cache.Cache, apiKeyStore serviceaccounttypes.Store, orgGetter organization.Getter) factory.ProviderFactory[tokenizer.Tokenizer, tokenizer.Config] { + return factory.NewProviderFactory(factory.MustNewName("serviceaccount"), func(ctx context.Context, providerSettings factory.ProviderSettings, config tokenizer.Config) (tokenizer.Tokenizer, error) { + return New(ctx, providerSettings, config, cache, apiKeyStore, orgGetter) + }) +} + +func New(ctx context.Context, providerSettings factory.ProviderSettings, config tokenizer.Config, cache cache.Cache, apiKeyStore serviceaccounttypes.Store, orgGetter organization.Getter) (tokenizer.Tokenizer, error) { + settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/tokenizer/serviceaccounttokenizer") + + // * move these hardcoded values to a config based value when needed + lastObservedAtCache, err := ristretto.NewCache(&ristretto.Config[string, time.Time]{ + NumCounters: 10 * expectedLastObservedAtCacheEntries, // 10x of expected entries + MaxCost: 1 << 19, // ~ 512 KB + BufferItems: 64, + Metrics: false, + }) + if err != nil { + return nil, err + } + + return tokenizer.NewWrappedTokenizer(settings, &provider{ + config: config, + settings: settings, + cache: cache, + apiKeyStore: apiKeyStore, + orgGetter: orgGetter, + stopC: make(chan struct{}), + lastObservedAtCache: lastObservedAtCache, + }), nil +} + +func (provider *provider) Start(ctx context.Context) error { + ticker := time.NewTicker(provider.config.ServiceAccount.GC.Interval) + defer ticker.Stop() + + for { + select { + case <-provider.stopC: + return nil + case <-ticker.C: + ctx, span := provider.settings.Tracer().Start(ctx, "tokenizer.LastObservedAt", trace.WithAttributes(attribute.String("tokenizer.provider", "serviceaccount"))) + + orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx) + if err != nil { + provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", "error", err) + span.End() + continue + } + + for _, org := range orgs { + if err := provider.flushLastObservedAt(ctx, org); err != nil { + span.RecordError(err) + provider.settings.Logger().ErrorContext(ctx, "failed to flush api keys", "error", err, "org_id", org.ID) + } + } + + span.End() + } + } +} + +func (provider *provider) CreateToken(_ context.Context, _ *authtypes.Identity, _ map[string]string) (*authtypes.Token, error) { + return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented") +} + +func (provider *provider) GetIdentity(ctx context.Context, key string) (*authtypes.Identity, error) { + apiKey, err := provider.getOrGetSetAPIKey(ctx, key) + if err != nil { + return nil, err + } + + if err := apiKey.IsExpired(); err != nil { + return nil, err + } + + identity, err := provider.getOrGetSetIdentity(ctx, apiKey.ServiceAccountID) + if err != nil { + return nil, err + } + + return identity, nil +} + +func (provider *provider) RotateToken(_ context.Context, _ string, _ string) (*authtypes.Token, error) { + return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented") +} + +func (provider *provider) DeleteToken(_ context.Context, _ string) error { + return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented") +} + +func (provider *provider) DeleteTokensByUserID(_ context.Context, _ valuer.UUID) error { + return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented") +} + +func (provider *provider) DeleteIdentity(ctx context.Context, serviceAccountID valuer.UUID) error { + provider.cache.Delete(ctx, emptyOrgID, identityCacheKey(serviceAccountID)) + return nil +} + +func (provider *provider) Stop(ctx context.Context) error { + close(provider.stopC) + + orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx) + if err != nil { + return err + } + + for _, org := range orgs { + // flush api keys on stop + if err := provider.flushLastObservedAt(ctx, org); err != nil { + provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID) + } + } + + return nil +} + +func (provider *provider) SetLastObservedAt(ctx context.Context, key string, lastObservedAt time.Time) error { + apiKey, err := provider.getOrGetSetAPIKey(ctx, key) + if err != nil { + return err + } + + // If we can't update the last observed at, we return nil. + if err := apiKey.UpdateLastObservedAt(lastObservedAt); err != nil { + return nil + } + + if ok := provider.lastObservedAtCache.Set(lastObservedAtCacheKey(key, apiKey.ServiceAccountID), lastObservedAt, 24); !ok { + provider.settings.Logger().ErrorContext(ctx, "error caching last observed at timestamp", "service_account_id", apiKey.ServiceAccountID) + } + + err = provider.cache.Set(ctx, emptyOrgID, apiKeyCacheKey(key), apiKey, provider.config.Lifetime.Max) + if err != nil { + return err + } + + return nil +} + +func (provider *provider) Config() tokenizer.Config { + return provider.config +} + +func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) { + apiKeys, err := provider.apiKeyStore.ListFactorAPIKeyByOrgID(ctx, orgID) + if err != nil { + return nil, err + } + + stats := make(map[string]any) + stats["api_key.count"] = len(apiKeys) + + keyToLastObservedAt, err := provider.listLastObservedAtDesc(ctx, orgID) + if err != nil { + return nil, err + } + + if len(keyToLastObservedAt) == 0 { + return stats, nil + } + + keyToLastObservedAtMax := keyToLastObservedAt[0] + + if lastObservedAt, ok := keyToLastObservedAtMax["last_observed_at"].(time.Time); ok { + if !lastObservedAt.IsZero() { + stats["api_key.last_observed_at.max.time"] = lastObservedAt.UTC() + stats["api_key.last_observed_at.max.time_unix"] = lastObservedAt.Unix() + } + } + + return stats, nil +} + +func (provider *provider) ListMaxLastObservedAtByOrgID(ctx context.Context, orgID valuer.UUID) (map[valuer.UUID]time.Time, error) { + apiKeyToLastObservedAts, err := provider.listLastObservedAtDesc(ctx, orgID) + if err != nil { + return nil, err + } + + maxLastObservedAtPerServiceAccountID := make(map[valuer.UUID]time.Time) + + for _, apiKeyToLastObservedAt := range apiKeyToLastObservedAts { + serviceAccountID, ok := apiKeyToLastObservedAt["service_account_id"].(valuer.UUID) + if !ok { + continue + } + + lastObservedAt, ok := apiKeyToLastObservedAt["last_observed_at"].(time.Time) + if !ok { + continue + } + + if lastObservedAt.IsZero() { + continue + } + + if _, ok := maxLastObservedAtPerServiceAccountID[serviceAccountID]; !ok { + maxLastObservedAtPerServiceAccountID[serviceAccountID] = lastObservedAt.UTC() + continue + } + + if lastObservedAt.UTC().After(maxLastObservedAtPerServiceAccountID[serviceAccountID]) { + maxLastObservedAtPerServiceAccountID[serviceAccountID] = lastObservedAt.UTC() + } + } + + return maxLastObservedAtPerServiceAccountID, nil + +} + +func (provider *provider) flushLastObservedAt(ctx context.Context, org *types.Organization) error { + apiKeyToLastObservedAt, err := provider.listLastObservedAtDesc(ctx, org.ID) + if err != nil { + return err + } + + if err := provider.apiKeyStore.UpdateLastObservedAtByKey(ctx, apiKeyToLastObservedAt); err != nil { + return err + } + + return nil +} + +func (provider *provider) getOrGetSetAPIKey(ctx context.Context, key string) (*serviceaccounttypes.FactorAPIKey, error) { + apiKey := new(serviceaccounttypes.FactorAPIKey) + err := provider.cache.Get(ctx, emptyOrgID, apiKeyCacheKey(key), apiKey) + if err != nil && !errors.Ast(err, errors.TypeNotFound) { + return nil, err + } + + if err == nil { + return apiKey, nil + } + + storable, err := provider.apiKeyStore.GetFactorAPIKeyByKey(ctx, key) + if err != nil { + return nil, err + } + apiKey = serviceaccounttypes.NewFactorAPIKeyFromStorable(storable) + + err = provider.cache.Set(ctx, emptyOrgID, apiKeyCacheKey(key), apiKey, provider.config.Lifetime.Max) + if err != nil { + return nil, err + } + + return apiKey, nil +} + +func (provider *provider) getOrGetSetIdentity(ctx context.Context, serviceAccountID valuer.UUID) (*authtypes.Identity, error) { + identity := new(authtypes.Identity) + err := provider.cache.Get(ctx, emptyOrgID, identityCacheKey(serviceAccountID), identity) + if err != nil && !errors.Ast(err, errors.TypeNotFound) { + return nil, err + } + + if err == nil { + return identity, nil + } + + storableServiceAccount, err := provider.apiKeyStore.GetByID(ctx, serviceAccountID) + if err != nil { + return nil, err + } + + identity = storableServiceAccount.ToIdentity() + err = provider.cache.Set(ctx, emptyOrgID, identityCacheKey(serviceAccountID), identity, 0) + if err != nil { + return nil, err + } + + return identity, nil +} + +func (provider *provider) listLastObservedAtDesc(ctx context.Context, orgID valuer.UUID) ([]map[string]any, error) { + apiKeys, err := provider.apiKeyStore.ListFactorAPIKeyByOrgID(ctx, orgID) + if err != nil { + return nil, err + } + + var keyToLastObservedAt []map[string]any + + for _, key := range apiKeys { + keyCachedLastObservedAt, ok := provider.lastObservedAtCache.Get(lastObservedAtCacheKey(key.Key, valuer.MustNewUUID(key.ServiceAccountID))) + if ok { + keyToLastObservedAt = append(keyToLastObservedAt, map[string]any{ + "service_account_id": key.ServiceAccountID, + "key": key.Key, + "last_observed_at": keyCachedLastObservedAt, + }) + } + } + + // sort by descending order of last_observed_at + slices.SortFunc(keyToLastObservedAt, func(a, b map[string]any) int { + return b["last_observed_at"].(time.Time).Compare(a["last_observed_at"].(time.Time)) + }) + + return keyToLastObservedAt, nil +} + +func apiKeyCacheKey(apiKey string) string { + return "api_key::" + cachetypes.NewSha1CacheKey(apiKey) +} + +func identityCacheKey(serviceAccountID valuer.UUID) string { + return "identity::" + serviceAccountID.String() +} + +func lastObservedAtCacheKey(apiKey string, serviceAccountID valuer.UUID) string { + return "api_key::" + apiKey + "::" + serviceAccountID.String() +} diff --git a/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go b/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go index a93fe3c1c2f..370e170c2c5 100644 --- a/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go +++ b/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go @@ -47,7 +47,7 @@ func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID) } - return authtypes.NewIdentity(userID, user.OrgID, user.Email, types.Role(user.Role)), nil + return authtypes.NewIdentity(userID, valuer.UUID{}, authtypes.PrincipalUser, user.OrgID, user.Email), nil } func (store *store) GetByAccessToken(ctx context.Context, accessToken string) (*authtypes.StorableToken, error) { diff --git a/pkg/types/authtypes/authn.go b/pkg/types/authtypes/authn.go index 07e0c0eb10e..6760198a2ea 100644 --- a/pkg/types/authtypes/authn.go +++ b/pkg/types/authtypes/authn.go @@ -25,10 +25,11 @@ var ( type AuthNProvider struct{ valuer.String } type Identity struct { - UserID valuer.UUID `json:"userId"` - OrgID valuer.UUID `json:"orgId"` - Email valuer.Email `json:"email"` - Role types.Role `json:"role"` + UserID valuer.UUID `json:"userId"` + ServiceAccountID valuer.UUID `json:"serviceAccountId"` + Principal Principal `json:"principal"` + OrgID valuer.UUID `json:"orgId"` + Email valuer.Email `json:"email"` } type CallbackIdentity struct { @@ -78,12 +79,13 @@ func NewStateFromString(state string) (State, error) { }, nil } -func NewIdentity(userID valuer.UUID, orgID valuer.UUID, email valuer.Email, role types.Role) *Identity { +func NewIdentity(userID valuer.UUID, serviceAccountID valuer.UUID, principal Principal, orgID valuer.UUID, email valuer.Email) *Identity { return &Identity{ - UserID: userID, - OrgID: orgID, - Email: email, - Role: role, + UserID: userID, + ServiceAccountID: serviceAccountID, + Principal: principal, + OrgID: orgID, + Email: email, } } @@ -116,10 +118,11 @@ func (typ *Identity) UnmarshalBinary(data []byte) error { func (typ *Identity) ToClaims() Claims { return Claims{ - UserID: typ.UserID.String(), - Email: typ.Email.String(), - Role: typ.Role, - OrgID: typ.OrgID.String(), + UserID: typ.UserID.String(), + ServiceAccountID: typ.ServiceAccountID.String(), + Principal: typ.Principal.StringValue(), + Email: typ.Email.String(), + OrgID: typ.OrgID.String(), } } diff --git a/pkg/types/authtypes/claims.go b/pkg/types/authtypes/claims.go index dff11c17ec8..6d1644e0ec4 100644 --- a/pkg/types/authtypes/claims.go +++ b/pkg/types/authtypes/claims.go @@ -3,20 +3,20 @@ package authtypes import ( "context" "log/slog" - "slices" "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/types" ) type claimsKey struct{} type accessTokenKey struct{} +type serviceAccountAPIKeyKey struct{} type Claims struct { - UserID string - Email string - Role types.Role - OrgID string + UserID string + ServiceAccountID string + Principal string + Email string + OrgID string } // NewContextWithClaims attaches individual claims to the context. @@ -47,37 +47,35 @@ func AccessTokenFromContext(ctx context.Context) (string, error) { return accessToken, nil } -func (c *Claims) LogValue() slog.Value { - return slog.GroupValue( - slog.String("user_id", c.UserID), - slog.String("email", c.Email), - slog.String("role", c.Role.String()), - slog.String("org_id", c.OrgID), - ) +func NewContextWithServiceAccountAPIKey(ctx context.Context, apiKey string) context.Context { + return context.WithValue(ctx, serviceAccountAPIKeyKey{}, apiKey) } -func (c *Claims) IsViewer() error { - if slices.Contains([]types.Role{types.RoleViewer, types.RoleEditor, types.RoleAdmin}, c.Role) { - return nil +func ServiceAccountAPIKeyFromContext(ctx context.Context) (string, error) { + apiKey, ok := ctx.Value(serviceAccountAPIKeyKey{}).(string) + if !ok { + return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated") } - return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only viewers/editors/admins can access this resource") + return apiKey, nil } -func (c *Claims) IsEditor() error { - if slices.Contains([]types.Role{types.RoleEditor, types.RoleAdmin}, c.Role) { - return nil - } - - return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only editors/admins can access this resource") +func (c *Claims) LogValue() slog.Value { + return slog.GroupValue( + slog.String("user_id", c.UserID), + slog.String("service_account_id", c.ServiceAccountID), + slog.String("principal", c.Principal), + slog.String("email", c.Email), + slog.String("org_id", c.OrgID), + ) } -func (c *Claims) IsAdmin() error { - if c.Role == types.RoleAdmin { - return nil +func (c *Claims) GetIdentityID() string { + if c.Principal == PrincipalUser.StringValue() { + return c.UserID } - return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can access this resource") + return c.ServiceAccountID } func (c *Claims) IsSelfAccess(id string) error { @@ -85,9 +83,5 @@ func (c *Claims) IsSelfAccess(id string) error { return nil } - if c.Role == types.RoleAdmin { - return nil - } - return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only the user/admin can access their own resource") } diff --git a/pkg/types/authtypes/principal.go b/pkg/types/authtypes/principal.go new file mode 100644 index 00000000000..71c113d2537 --- /dev/null +++ b/pkg/types/authtypes/principal.go @@ -0,0 +1,10 @@ +package authtypes + +import "github.com/SigNoz/signoz/pkg/valuer" + +var ( + PrincipalUser = Principal{valuer.NewString("user")} + PrincipalServiceAccount = Principal{valuer.NewString("service_account")} +) + +type Principal struct{ valuer.String } diff --git a/pkg/types/dashboardtypes/dashboard.go b/pkg/types/dashboardtypes/dashboard.go index fb60d7681b7..dee6aa5c052 100644 --- a/pkg/types/dashboardtypes/dashboard.go +++ b/pkg/types/dashboardtypes/dashboard.go @@ -284,18 +284,7 @@ func (dashboard *Dashboard) Update(ctx context.Context, updatableDashboard Updat return nil } -func (dashboard *Dashboard) CanLockUnlock(role types.Role, updatedBy string) error { - if dashboard.CreatedBy != updatedBy && role != types.RoleAdmin { - return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard") - } - return nil -} - -func (dashboard *Dashboard) LockUnlock(lock bool, role types.Role, updatedBy string) error { - err := dashboard.CanLockUnlock(role, updatedBy) - if err != nil { - return err - } +func (dashboard *Dashboard) LockUnlock(lock bool, updatedBy string) error { dashboard.Locked = lock dashboard.UpdatedBy = updatedBy dashboard.UpdatedAt = time.Now() diff --git a/pkg/types/factor_api_key.go b/pkg/types/factor_api_key.go deleted file mode 100644 index e1eb05db69b..00000000000 --- 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/factor_api_key.go b/pkg/types/serviceaccounttypes/factor_api_key.go index 05c4de74d26..75d17d512bf 100644 --- a/pkg/types/serviceaccounttypes/factor_api_key.go +++ b/pkg/types/serviceaccounttypes/factor_api_key.go @@ -11,9 +11,9 @@ import ( ) var ( - ErrCodeAPIkeyInvalidInput = errors.MustNewCode("service_account_factor_api_key_invalid_input") - ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("service_account_factor_api_key_already_exists") - ErrCodeAPIKeytNotFound = errors.MustNewCode("service_account_factor_api_key_not_found") + ErrCodeAPIkeyInvalidInput = errors.MustNewCode("api_key_invalid_input") + ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists") + ErrCodeAPIKeytNotFound = errors.MustNewCode("api_key_not_found") ErrCodeAPIKeyExpired = errors.MustNewCode("api_key_expired") ErrCodeAPIkeyOlderLastObservedAt = errors.MustNewCode("api_key_older_last_observed_at") ) @@ -184,3 +184,18 @@ func (key *UpdatableFactorAPIKey) UnmarshalJSON(data []byte) error { *key = UpdatableFactorAPIKey(temp) return nil } + +func (key FactorAPIKey) MarshalBinary() ([]byte, error) { + return json.Marshal(key) +} + +func (key *FactorAPIKey) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, key) +} + +func (key *FactorAPIKey) Traits() map[string]any { + return map[string]any{ + "name": key.Name, + "expires_at": key.ExpiresAt, + } +} diff --git a/pkg/types/serviceaccounttypes/service_acccount.go b/pkg/types/serviceaccounttypes/service_acccount.go index 67af813a5e8..fcb2f9e5100 100644 --- a/pkg/types/serviceaccounttypes/service_acccount.go +++ b/pkg/types/serviceaccounttypes/service_acccount.go @@ -9,6 +9,7 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "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/valuer" "github.com/uptrace/bun" @@ -284,3 +285,21 @@ func (sa *UpdatableServiceAccountStatus) UnmarshalJSON(data []byte) error { *sa = UpdatableServiceAccountStatus(temp) return nil } + +func (sa *StorableServiceAccount) ToIdentity() *authtypes.Identity { + return &authtypes.Identity{ + ServiceAccountID: sa.ID, + Principal: authtypes.PrincipalServiceAccount, + OrgID: valuer.MustNewUUID(sa.OrgID), + Email: valuer.MustNewEmail(sa.Email), + } +} + +func (sa *ServiceAccount) Traits() map[string]any { + return map[string]any{ + "name": sa.Name, + "roles": sa.Roles, + "email": sa.Email.String(), + "created_at": sa.CreatedAt, + } +} diff --git a/pkg/types/serviceaccounttypes/store.go b/pkg/types/serviceaccounttypes/store.go index 64d9b9f1088..78c7acf118f 100644 --- a/pkg/types/serviceaccounttypes/store.go +++ b/pkg/types/serviceaccounttypes/store.go @@ -10,8 +10,9 @@ type Store interface { // Service Account Create(context.Context, *StorableServiceAccount) error Get(context.Context, valuer.UUID, valuer.UUID) (*StorableServiceAccount, error) - GetActiveByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableServiceAccount, error) GetByID(context.Context, valuer.UUID) (*StorableServiceAccount, error) + GetActiveByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableServiceAccount, error) + CountByOrgID(context.Context, valuer.UUID) (int64, error) List(context.Context, valuer.UUID) ([]*StorableServiceAccount, error) Update(context.Context, valuer.UUID, *StorableServiceAccount) error Delete(context.Context, valuer.UUID, valuer.UUID) error @@ -25,8 +26,12 @@ type Store interface { // Service Account Factor API Key CreateFactorAPIKey(context.Context, *StorableFactorAPIKey) error GetFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) (*StorableFactorAPIKey, error) + GetFactorAPIKeyByKey(context.Context, string) (*StorableFactorAPIKey, error) + CountFactorAPIKeysByOrgID(context.Context, valuer.UUID) (int64, error) ListFactorAPIKey(context.Context, valuer.UUID) ([]*StorableFactorAPIKey, error) + ListFactorAPIKeyByOrgID(context.Context, valuer.UUID) ([]*StorableFactorAPIKey, error) UpdateFactorAPIKey(context.Context, valuer.UUID, *StorableFactorAPIKey) error + UpdateLastObservedAtByKey(context.Context, []map[string]any) error RevokeFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) error RevokeAllFactorAPIKeys(context.Context, valuer.UUID) error diff --git a/pkg/types/user.go b/pkg/types/user.go index 4f8d53b66e2..acc72d2cc3e 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)