Compare commits

...

14 Commits

Author SHA1 Message Date
vikrantgupta25
de979742ec feat(authz): fix rebase issues 2026-03-10 13:13:51 +05:30
vikrantgupta25
bdd7fab955 feat(authz): fix rebase issues 2026-03-10 13:06:54 +05:30
vikrantgupta25
25e747fdac feat(authz): add support for deleted_at 2026-03-10 13:05:54 +05:30
vikrantgupta25
bc722c821b feat(serviceaccount): add analytics support for service account 2026-03-10 13:04:59 +05:30
vikrantgupta25
6daa949d42 feat(serviceaccount): fix the old role panic 2026-03-10 13:04:59 +05:30
vikrantgupta25
b7a511fb19 feat(serviceaccount): part changes for user id claims tracking 2026-03-10 13:04:59 +05:30
vikrantgupta25
6992c8e3e5 feat(serviceaccount): add domain checks and rotate sessions fix 2026-03-10 13:04:58 +05:30
vikrantgupta25
a844536275 feat(serviceaccount): migrate existing api keys 2026-03-10 13:04:58 +05:30
vikrantgupta25
2983beb255 feat(serviceaccount): update authz typeables 2026-03-10 13:04:58 +05:30
vikrantgupta25
a1ebd64cd3 feat(serviceaccount): update openapi spec 2026-03-10 13:04:55 +05:30
vikrantgupta25
624a715027 feat(serviceaccount): deprecate the api keys 2026-03-10 13:04:34 +05:30
vikrantgupta25
bc46cfeffc feat(serviceaccount): fix openapi spec 2026-03-10 13:03:40 +05:30
vikrantgupta25
a2adea53f4 feat(serviceaccount): add support for serviceaccount tokenizer in authn middleware 2026-03-10 13:03:32 +05:30
vikrantgupta25
c4ce49903e feat(serviceaccount): add service account tokenizer and clean identity 2026-03-10 13:02:27 +05:30
65 changed files with 1737 additions and 1941 deletions

View File

@@ -18,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -79,8 +80,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, userGetter user.Getter) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, userGetter)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -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"
@@ -29,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -119,8 +120,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), licensing, dashboardModule)
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, userGetter user.Getter) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, userGetter)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -1775,7 +1775,7 @@ components:
type: string
key:
type: string
last_used:
last_observed_at:
format: date-time
type: string
name:
@@ -1789,7 +1789,7 @@ components:
- id
- key
- expires_at
- last_used
- last_observed_at
- service_account_id
type: object
ServiceaccounttypesGettableFactorAPIKeyWithKey:
@@ -1833,6 +1833,9 @@ components:
createdAt:
format: date-time
type: string
deletedAt:
format: date-time
type: string
email:
type: string
id:
@@ -1857,6 +1860,7 @@ components:
- roles
- status
- orgID
- deletedAt
type: object
ServiceaccounttypesUpdatableFactorAPIKey:
properties:
@@ -1989,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:
@@ -2087,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:
@@ -2161,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:
@@ -3861,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

View File

@@ -2,39 +2,45 @@ module base
type organisation
relations
define read: [user, role#assignee]
define update: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
type user
relations
define read: [user, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type serviceaccount
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type anonymous
type role
relations
define assignee: [user, anonymous]
define assignee: [user, serviceaccount, anonymous]
define read: [user, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type metaresources
relations
define create: [user, role#assignee]
define list: [user, role#assignee]
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
type metaresource
relations
define read: [user, anonymous, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, anonymous, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define block: [user, role#assignee]
define block: [user, serviceaccount, role#assignee]
type telemetryresource
relations
define read: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]

View File

@@ -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)

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
@@ -31,9 +32,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, userGetter user.Getter) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, userGetter)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -214,8 +215,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 {

View File

@@ -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,
)

View File

@@ -12,10 +12,10 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"go.uber.org/zap"
@@ -49,7 +49,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
apiKey, apiErr := ah.getOrCreateCloudIntegrationAPIKey(r.Context(), claims.OrgID, cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
@@ -109,32 +109,25 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
ah.Respond(w, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
func (ah *APIHandler) getOrCreateCloudIntegrationAPIKey(ctx context.Context, orgId string, cloudProvider string) (
string, *basemodel.ApiError,
) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
integrationServiceAccount, apiErr := ah.getOrCreateCloudIntegrationServiceAccount(ctx, orgId, cloudProvider)
if apiErr != nil {
return "", apiErr
}
orgIdUUID, err := valuer.NewUUID(orgId)
keys, err := ah.Signoz.Modules.ServiceAccount.ListFactorAPIKey(ctx, integrationServiceAccount.ID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't parse orgId: %w", err,
"couldn't list api keys: %w", err,
))
}
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err,
))
}
for _, p := range allPats {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
return p.Token, nil
for _, key := range keys {
if key.Name == integrationPATName {
return key.Key, nil
}
}
@@ -143,46 +136,35 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
zap.String("cloudProvider", cloudProvider),
)
newPAT, err := types.NewStorableAPIKey(
integrationPATName,
integrationUser.ID,
types.RoleViewer,
0,
)
apiKey, err := integrationServiceAccount.NewFactorAPIKey(integrationPATName, 0)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
}
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
err = ah.Signoz.Modules.ServiceAccount.CreateFactorAPIKey(ctx, apiKey)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
"couldn't create cloud integration api key: %w", err,
))
}
return newPAT.Token, nil
return apiKey.Key, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(
ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) {
serviceAccountName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", serviceAccountName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
serviceAccount := serviceaccounttypes.NewServiceAccount(serviceAccountName, email, []string{roletypes.SigNozViewerRoleName}, serviceaccounttypes.StatusActive, valuer.MustNewUUID(orgId))
serviceAccount, err := ah.Signoz.Modules.ServiceAccount.GetOrCreate(ctx, serviceAccount)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration service account: %w", err))
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
}
return cloudIntegrationUser, nil
return serviceAccount, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -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,

View File

@@ -2113,7 +2113,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type string
* @format date-time
*/
last_used: Date;
last_observed_at: Date;
/**
* @type string
*/
@@ -2173,6 +2173,11 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @format date-time
*/
createdAt?: Date;
/**
* @type string
* @format date-time
*/
deletedAt: Date;
/**
* @type string
*/
@@ -2340,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
@@ -2490,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
@@ -2597,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
@@ -3106,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;
};

View File

@@ -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<ListAPIKeys200>({
url: `/api/v1/pats`,
method: 'GET',
signal,
});
};
export const getListAPIKeysQueryKey = () => {
return [`/api/v1/pats`] as const;
};
export const getListAPIKeysQueryOptions = <
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListAPIKeysQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listAPIKeys>>> = ({
signal,
}) => listAPIKeys(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListAPIKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof listAPIKeys>>
>;
export type ListAPIKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List api keys
*/
export function useListAPIKeys<
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListAPIKeysQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List api keys
*/
export const invalidateListAPIKeys = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListAPIKeysQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates an api key
* @summary Create api key
*/
export const createAPIKey = (
typesPostableAPIKeyDTO: BodyType<TypesPostableAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAPIKey201>({
url: `/api/v1/pats`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableAPIKeyDTO,
signal,
});
};
export const getCreateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
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<ReturnType<typeof createAPIKey>>,
{ data: BodyType<TypesPostableAPIKeyDTO> }
> = (props) => {
const { data } = props ?? {};
return createAPIKey(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof createAPIKey>>
>;
export type CreateAPIKeyMutationBody = BodyType<TypesPostableAPIKeyDTO>;
export type CreateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create api key
*/
export const useCreateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
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<void>({
url: `/api/v1/pats/${id}`,
method: 'DELETE',
});
};
export const getRevokeAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
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<ReturnType<typeof revokeAPIKey>>,
{ pathParams: RevokeAPIKeyPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return revokeAPIKey(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RevokeAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof revokeAPIKey>>
>;
export type RevokeAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Revoke api key
*/
export const useRevokeAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof revokeAPIKey>>,
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<TypesStorableAPIKeyDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/pats/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesStorableAPIKeyDTO,
});
};
export const getUpdateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
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<ReturnType<typeof updateAPIKey>>,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateAPIKey(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAPIKey>>
>;
export type UpdateAPIKeyMutationBody = BodyType<TypesStorableAPIKeyDTO>;
export type UpdateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update api key
*/
export const useUpdateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint resets the password by token
* @summary Reset password

View File

@@ -23,10 +23,10 @@ export default {
relations: {
assignee: ['role'],
create: ['metaresources'],
delete: ['user', 'role', 'organization', 'metaresource'],
delete: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
list: ['metaresources'],
read: ['user', 'role', 'organization', 'metaresource'],
update: ['user', 'role', 'organization', 'metaresource'],
read: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
update: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
},
},
} as const;

View File

@@ -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"},

View File

@@ -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
}

View File

@@ -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

View File

@@ -2,9 +2,11 @@ module base
type user
type serviceaccount
type role
relations
define assignee: [user]
define assignee: [user, serviceaccount]
type organisation
relations

View File

@@ -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)

View File

@@ -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
})
})
}

View File

@@ -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
}
authenticatedUser, err := a.tokenizer.GetIdentity(ctx, accessToken)
identity, err := a.tokenizer.GetIdentity(ctx, accessToken)
if err != nil {
return ctx, err
return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err
}
return authtypes.NewContextWithClaims(ctx, authenticatedUser.ToClaims()), nil
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
}
identity, err := a.serviceAccountTokenizer.GetIdentity(ctx, apiKey)
if err != nil {
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.NewContextWithServiceAccountAPIKey(ctx, value), nil
}
func (a *AuthN) contextFromAccessToken(ctx context.Context, values ...string) (context.Context, error) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -24,9 +25,10 @@ type module struct {
analytics analytics.Analytics
orgGetter organization.Getter
queryParser queryparser.QueryParser
userGetter user.Getter
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, userGetter user.Getter) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard")
return &module{
store: store,
@@ -34,6 +36,7 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
analytics: analytics,
orgGetter: orgGetter,
queryParser: queryParser,
userGetter: userGetter,
}
}
@@ -99,13 +102,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
}

View File

@@ -111,7 +111,12 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
return
}
serviceAccount.Update(req.Name, req.Email, req.Roles)
err = serviceAccount.Update(req.Name, req.Email, req.Roles)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Update(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
if err != nil {
render.Error(rw, err)
@@ -147,7 +152,12 @@ func (handler *handler) UpdateStatus(rw http.ResponseWriter, r *http.Request) {
return
}
serviceAccount.UpdateStatus(req.Status)
err = serviceAccount.UpdateStatus(req.Status)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.UpdateStatus(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
if err != nil {
render.Error(rw, err)
@@ -290,7 +300,7 @@ func (handler *handler) UpdateFactorAPIKey(rw http.ResponseWriter, r *http.Reque
}
factorAPIKey.Update(req.Name, req.ExpiresAt)
err = handler.module.UpdateFactorAPIKey(ctx, serviceAccount.ID, factorAPIKey)
err = handler.module.UpdateFactorAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount.ID, factorAPIKey)
if err != nil {
render.Error(rw, err)
return

View File

@@ -3,8 +3,10 @@ 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/types/authtypes"
@@ -14,15 +16,16 @@ 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
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) 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}
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAccount *serviceaccounttypes.ServiceAccount) error {
@@ -33,7 +36,7 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAcco
}
// authz actions cannot run in sql transactions
err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
@@ -57,9 +60,31 @@ 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
}
func (module *module) GetOrCreate(ctx context.Context, serviceAccount *serviceaccounttypes.ServiceAccount) (*serviceaccounttypes.ServiceAccount, error) {
existingServiceAccount, err := module.store.GetByOrgIDAndName(ctx, serviceAccount.OrgID, serviceAccount.Name)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingServiceAccount != nil {
return serviceAccount, nil
}
err = module.Create(ctx, serviceAccount.OrgID, serviceAccount)
if err != nil {
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
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) {
storableServiceAccount, err := module.store.Get(ctx, orgID, id)
if err != nil {
@@ -138,7 +163,7 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv
// gets the role diff if any to modify grants.
grants, revokes := serviceAccount.PatchRoles(input)
err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
@@ -167,32 +192,37 @@ 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
}
func (module *module) UpdateStatus(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
serviceAccount, err := module.Get(ctx, orgID, input.ID)
err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, input.ID.String(), orgID, nil))
if err != nil {
return err
}
if input.Status == serviceAccount.Status {
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
// revoke all the API keys on disable
err := module.store.RevokeAllFactorAPIKeys(ctx, input.ID)
if err != nil {
return err
}
// update the status but do not delete the role mappings as we will use them for audits
err = module.store.Update(ctx, orgID, serviceaccounttypes.NewStorableServiceAccount(input))
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
switch input.Status {
case serviceaccounttypes.StatusActive:
err := module.activateServiceAccount(ctx, orgID, input)
if err != nil {
return err
}
case serviceaccounttypes.StatusDisabled:
err := module.disableServiceAccount(ctx, orgID, input)
if err != nil {
return err
}
}
module.analytics.TrackUser(ctx, orgID.String(), input.ID.String(), "Service Account Deleted", map[string]any{})
return nil
}
@@ -203,7 +233,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
}
// revoke from authz first as this cannot run in sql transaction
err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
if err != nil {
return err
}
@@ -255,6 +285,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
}
@@ -276,8 +307,14 @@ func (module *module) ListFactorAPIKey(ctx context.Context, serviceAccountID val
return serviceaccounttypes.NewFactorAPIKeyFromStorables(storables), nil
}
func (module *module) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error {
return module.store.UpdateFactorAPIKey(ctx, serviceAccountID, serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey))
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
}
func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) error {
@@ -305,47 +342,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) disableServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
if err != nil {
return err
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
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
// revoke all the API keys on disable
err := module.store.RevokeAllFactorAPIKeys(ctx, input.ID)
if err != nil {
return err
}
// update the status but do not delete the role mappings as we will reuse them on activation.
err = module.Update(ctx, orgID, input)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
count, err = module.store.CountFactorAPIKeysByOrgID(ctx, orgID)
if err == nil {
stats["serviceaccount.keys.count"] = count
}
return nil
}
func (module *module) activateServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
err := module.authz.Grant(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
if err != nil {
return err
}
err = module.Update(ctx, orgID, input)
if err != nil {
return err
}
return nil
return stats, nil
}

View File

@@ -65,6 +65,25 @@ func (store *store) GetByID(ctx context.Context, id valuer.UUID) (*serviceaccoun
return storable, nil
}
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*serviceaccounttypes.StorableServiceAccount, error) {
storable := new(serviceaccounttypes.StorableServiceAccount)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("org_id = ?", orgID).
Where("name = ?", name).
Where("status = ?", serviceaccounttypes.StatusActive).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccountNotFound, "service account with name: %s doesn't exist in org: %s", name, orgID.String())
}
return storable, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*serviceaccounttypes.StorableServiceAccount, error) {
storables := make([]*serviceaccounttypes.StorableServiceAccount, 0)
@@ -146,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)
@@ -188,7 +224,7 @@ func (store *store) CreateFactorAPIKey(ctx context.Context, storable *serviceacc
Model(storable).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeServiceAccountFactorAPIKeyAlreadyExists, "api key with name: %s already exists for service account: %s", storable.Name, storable.ServiceAccountID)
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeAPIKeyAlreadyExists, "api key with name: %s already exists for service account: %s", storable.Name, storable.ServiceAccountID)
}
return nil
@@ -206,7 +242,24 @@ func (store *store) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer
Where("service_account_id = ?", serviceAccountID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccounFactorAPIKeytNotFound, "api key with id: %s doesn't exist for service account: %s", id, serviceAccountID)
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeAPIKeytNotFound, "api key with id: %s doesn't exist for service account: %s", id, serviceAccountID)
}
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
@@ -229,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.
@@ -244,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.

View File

@@ -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"
)
@@ -12,6 +13,9 @@ type Module interface {
// Creates a new service account for an organization.
Create(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
// Gets or creates a service account by name
GetOrCreate(context.Context, *serviceaccounttypes.ServiceAccount) (*serviceaccounttypes.ServiceAccount, error)
// Gets a service account by id.
Get(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error)
@@ -40,10 +44,12 @@ type Module interface {
ListFactorAPIKey(context.Context, valuer.UUID) ([]*serviceaccounttypes.FactorAPIKey, error)
// Updates an existing API key for a service account
UpdateFactorAPIKey(context.Context, valuer.UUID, *serviceaccounttypes.FactorAPIKey) error
UpdateFactorAPIKey(context.Context, valuer.UUID, valuer.UUID, *serviceaccounttypes.FactorAPIKey) error
// Revokes an existing API key for a service account
RevokeFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) error
statsreporter.StatsCollector
}
type Handler interface {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -43,12 +43,11 @@ 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, userGetter)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)

View File

@@ -113,6 +113,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),
}
}

View File

@@ -42,12 +42,12 @@ 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, userGetter)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)

View File

@@ -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),
)
}

View File

@@ -21,6 +21,8 @@ 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"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
@@ -38,6 +40,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 +51,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(
@@ -88,7 +92,7 @@ func New(
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config],
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing, user.Getter) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
) (*SigNoz, error) {
@@ -279,6 +283,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)
@@ -296,7 +306,7 @@ func New(
queryParser := queryparser.New(providerSettings)
// Initialize dashboard module (needed for authz registry)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing, userGetter)
// Initialize authz
authzProviderFactory := authzCallback(ctx, sqlstore, licensing, dashboard)
@@ -421,6 +431,7 @@ func New(
tokenizer,
config,
modules.AuthDomain,
modules.ServiceAccount,
}
// Initialize stats reporter from the available stats reporter provider factories
@@ -443,6 +454,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 +463,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
}

View File

@@ -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
}

View File

@@ -0,0 +1,309 @@
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 {
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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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{},

View File

@@ -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()
}

View File

@@ -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) {

View File

@@ -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(),
}
}

View File

@@ -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 NewContextWithServiceAccountAPIKey(ctx context.Context, apiKey string) context.Context {
return context.WithValue(ctx, serviceAccountAPIKeyKey{}, apiKey)
}
func ServiceAccountAPIKeyFromContext(ctx context.Context) (string, error) {
apiKey, ok := ctx.Value(serviceAccountAPIKeyKey{}).(string)
if !ok {
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
}
return apiKey, nil
}
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("role", c.Role.String()),
slog.String("org_id", c.OrgID),
)
}
func (c *Claims) IsViewer() error {
if slices.Contains([]types.Role{types.RoleViewer, types.RoleEditor, types.RoleAdmin}, c.Role) {
return nil
func (c *Claims) GetIdentityID() string {
if c.Principal == PrincipalUser.StringValue() {
return c.UserID
}
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only viewers/editors/admins can access this resource")
}
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) IsAdmin() error {
if c.Role == types.RoleAdmin {
return nil
}
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")
}

View File

@@ -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 }

View File

@@ -20,19 +20,20 @@ var (
)
var TypeableRelations = map[Type][]Relation{
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
TypeOrganization: {RelationRead, RelationUpdate, RelationDelete},
TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete},
TypeMetaResources: {RelationCreate, RelationList},
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
TypeServiceAccount: {RelationRead, RelationUpdate, RelationDelete},
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
TypeOrganization: {RelationRead, RelationUpdate, RelationDelete},
TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete},
TypeMetaResources: {RelationCreate, RelationList},
}
var RelationsTypeable = map[Relation][]Type{
RelationCreate: {TypeMetaResources},
RelationRead: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
RelationRead: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
RelationList: {TypeMetaResources},
RelationUpdate: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
RelationDelete: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
RelationUpdate: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
RelationDelete: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
RelationAssignee: {TypeRole},
}

View File

@@ -23,11 +23,12 @@ var (
)
var (
typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`)
typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`)
typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeServiceAccountSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`)
typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`)
typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
// metaresources selectors are used to select either all or none until we introduce some hierarchy here.
typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`)
)
@@ -98,6 +99,11 @@ func IsValidSelector(typed Type, selector string) error {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeUserSelectorRegex.String())
}
return nil
case TypeServiceAccount:
if !typeServiceAccountSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeServiceAccountSelectorRegex.String())
}
return nil
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String())

View File

@@ -15,19 +15,21 @@ var (
)
var (
TypeUser = Type{valuer.NewString("user")}
TypeAnonymous = Type{valuer.NewString("anonymous")}
TypeRole = Type{valuer.NewString("role")}
TypeOrganization = Type{valuer.NewString("organization")}
TypeMetaResource = Type{valuer.NewString("metaresource")}
TypeMetaResources = Type{valuer.NewString("metaresources")}
TypeUser = Type{valuer.NewString("user")}
TypeServiceAccount = Type{valuer.NewString("serviceaccount")}
TypeAnonymous = Type{valuer.NewString("anonymous")}
TypeRole = Type{valuer.NewString("role")}
TypeOrganization = Type{valuer.NewString("organization")}
TypeMetaResource = Type{valuer.NewString("metaresource")}
TypeMetaResources = Type{valuer.NewString("metaresources")}
)
var (
TypeableUser = &typeableUser{}
TypeableAnonymous = &typeableAnonymous{}
TypeableRole = &typeableRole{}
TypeableOrganization = &typeableOrganization{}
TypeableUser = &typeableUser{}
TypeableServiceAccount = &typeableServiceAccount{}
TypeableAnonymous = &typeableAnonymous{}
TypeableRole = &typeableRole{}
TypeableOrganization = &typeableOrganization{}
)
type Typeable interface {
@@ -53,6 +55,8 @@ func NewType(input string) (Type, error) {
switch input {
case "user":
return TypeUser, nil
case "serviceaccount":
return TypeServiceAccount, nil
case "role":
return TypeRole, nil
case "organization":
@@ -88,6 +92,8 @@ func NewTypeableFromType(typed Type, name Name) (Typeable, error) {
return TypeableRole, nil
case TypeUser:
return TypeableUser, nil
case TypeServiceAccount:
return TypeableServiceAccount, nil
case TypeOrganization:
return TypeableOrganization, nil
case TypeMetaResource:

View File

@@ -0,0 +1,38 @@
package authtypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableServiceAccount)
type typeableServiceAccount struct{}
func (typeableServiceAccount *typeableServiceAccount) Tuples(subject string, relation Relation, selectors []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selectors {
object := typeableServiceAccount.Prefix(orgID) + "/" + selector.String()
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableServiceAccount *typeableServiceAccount) Type() Type {
return TypeServiceAccount
}
func (typeableServiceAccount *typeableServiceAccount) Name() Name {
return MustNewName("serviceaccount")
}
// example: serviceaccount:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/serviceaccount
func (typeableServiceAccount *typeableServiceAccount) Prefix(orgID valuer.UUID) string {
return typeableServiceAccount.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableServiceAccount.Name().String()
}
func (typeableServiceAccount *typeableServiceAccount) Scope(relation Relation) string {
return typeableServiceAccount.Name().String() + ":" + relation.StringValue()
}

View File

@@ -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()

View File

@@ -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,
}
}

View File

@@ -11,20 +11,22 @@ import (
)
var (
ErrCodeServiceAccountFactorAPIkeyInvalidInput = errors.MustNewCode("service_account_factor_api_key_invalid_input")
ErrCodeServiceAccountFactorAPIKeyAlreadyExists = errors.MustNewCode("service_account_factor_api_key_already_exists")
ErrCodeServiceAccounFactorAPIKeytNotFound = 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")
)
type StorableFactorAPIKey struct {
bun.BaseModel `bun:"table:factor_api_key"`
bun.BaseModel `bun:"table:factor_api_key,alias:factor_api_key"`
types.Identifiable
types.TimeAuditable
Name string `bun:"name"`
Key string `bun:"key"`
ExpiresAt uint64 `bun:"expires_at"`
LastUsed time.Time `bun:"last_used"`
LastObservedAt time.Time `bun:"last_observed_at"`
ServiceAccountID string `bun:"service_account_id"`
}
@@ -34,7 +36,7 @@ type FactorAPIKey struct {
Name string `json:"name" requrired:"true"`
Key string `json:"key" required:"true"`
ExpiresAt uint64 `json:"expires_at" required:"true"`
LastUsed time.Time `json:"last_used" required:"true"`
LastObservedAt time.Time `json:"last_observed_at" required:"true"`
ServiceAccountID valuer.UUID `json:"service_account_id" required:"true"`
}
@@ -48,7 +50,7 @@ type GettableFactorAPIKey struct {
types.TimeAuditable
Name string `json:"name" requrired:"true"`
ExpiresAt uint64 `json:"expires_at" required:"true"`
LastUsed time.Time `json:"last_used" required:"true"`
LastObservedAt time.Time `json:"last_observed_at" required:"true"`
ServiceAccountID valuer.UUID `json:"service_account_id" required:"true"`
}
@@ -69,7 +71,7 @@ func NewFactorAPIKeyFromStorable(storable *StorableFactorAPIKey) *FactorAPIKey {
Name: storable.Name,
Key: storable.Key,
ExpiresAt: storable.ExpiresAt,
LastUsed: storable.LastUsed,
LastObservedAt: storable.LastObservedAt,
ServiceAccountID: valuer.MustNewUUID(storable.ServiceAccountID),
}
}
@@ -91,7 +93,7 @@ func NewStorableFactorAPIKey(factorAPIKey *FactorAPIKey) *StorableFactorAPIKey {
Name: factorAPIKey.Name,
Key: factorAPIKey.Key,
ExpiresAt: factorAPIKey.ExpiresAt,
LastUsed: factorAPIKey.LastUsed,
LastObservedAt: factorAPIKey.LastObservedAt,
ServiceAccountID: factorAPIKey.ServiceAccountID.String(),
}
}
@@ -105,7 +107,7 @@ func NewGettableFactorAPIKeys(keys []*FactorAPIKey) []*GettableFactorAPIKey {
TimeAuditable: key.TimeAuditable,
Name: key.Name,
ExpiresAt: key.ExpiresAt,
LastUsed: key.LastUsed,
LastObservedAt: key.LastObservedAt,
ServiceAccountID: key.ServiceAccountID,
}
}
@@ -128,6 +130,29 @@ func (apiKey *FactorAPIKey) Update(name string, expiresAt uint64) {
apiKey.UpdatedAt = time.Now()
}
func (apiKey *FactorAPIKey) IsExpired() error {
if apiKey.ExpiresAt == 0 {
return nil
}
if time.Now().After(time.Unix(int64(apiKey.ExpiresAt), 0)) {
return errors.New(errors.TypeUnauthenticated, ErrCodeAPIKeyExpired, "api key has been expired")
}
return nil
}
func (apiKey *FactorAPIKey) UpdateLastObservedAt(lastObservedAt time.Time) error {
if lastObservedAt.Before(apiKey.LastObservedAt) {
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyOlderLastObservedAt, "last observed at is before the current last observed at")
}
apiKey.LastObservedAt = lastObservedAt
apiKey.UpdatedAt = time.Now()
return nil
}
func (key *PostableFactorAPIKey) UnmarshalJSON(data []byte) error {
type Alias PostableFactorAPIKey
@@ -137,7 +162,7 @@ func (key *PostableFactorAPIKey) UnmarshalJSON(data []byte) error {
}
if temp.Name == "" {
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountFactorAPIkeyInvalidInput, "name cannot be empty")
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyInvalidInput, "name cannot be empty")
}
*key = PostableFactorAPIKey(temp)
@@ -153,9 +178,24 @@ func (key *UpdatableFactorAPIKey) UnmarshalJSON(data []byte) error {
}
if temp.Name == "" {
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountFactorAPIkeyInvalidInput, "name cannot be empty")
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyInvalidInput, "name cannot be empty")
}
*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,
}
}

View File

@@ -4,21 +4,22 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"slices"
"time"
"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"
)
var (
ErrCodeServiceAccountInvalidInput = errors.MustNewCode("service_account_invalid_input")
ErrCodeServiceAccountAlreadyExists = errors.MustNewCode("service_account_already_exists")
ErrCodeServiceAccountNotFound = errors.MustNewCode("service_account_not_found")
ErrCodeServiceAccountRoleAlreadyExists = errors.MustNewCode("service_account_role_already_exists")
ErrCodeServiceAccountInvalidInput = errors.MustNewCode("service_account_invalid_input")
ErrCodeServiceAccountAlreadyExists = errors.MustNewCode("service_account_already_exists")
ErrCodeServiceAccountNotFound = errors.MustNewCode("service_account_not_found")
ErrCodeServiceAccountRoleAlreadyExists = errors.MustNewCode("service_account_role_already_exists")
ErrCodeServiceAccountOperationUnsupported = errors.MustNewCode("service_account_operation_unsupported")
)
var (
@@ -32,20 +33,22 @@ type StorableServiceAccount struct {
types.Identifiable
types.TimeAuditable
Name string `bun:"name"`
Email string `bun:"email"`
Status valuer.String `bun:"status"`
OrgID string `bun:"org_id"`
Name string `bun:"name"`
Email string `bun:"email"`
Status valuer.String `bun:"status"`
OrgID string `bun:"org_id"`
DeletedAt time.Time `bun:"deleted_at"`
}
type ServiceAccount struct {
types.Identifiable
types.TimeAuditable
Name string `json:"name" required:"true"`
Email valuer.Email `json:"email" required:"true"`
Roles []string `json:"roles" required:"true" nullable:"false"`
Status valuer.String `json:"status" required:"true"`
OrgID valuer.UUID `json:"orgID" required:"true"`
Name string `json:"name" required:"true"`
Email valuer.Email `json:"email" required:"true"`
Roles []string `json:"roles" required:"true" nullable:"false"`
Status valuer.String `json:"status" required:"true"`
OrgID valuer.UUID `json:"orgID" required:"true"`
DeletedAt time.Time `json:"deletedAt" required:"true"`
}
type PostableServiceAccount struct {
@@ -73,11 +76,12 @@ func NewServiceAccount(name string, email valuer.Email, roles []string, status v
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Name: name,
Email: email,
Roles: roles,
Status: status,
OrgID: orgID,
Name: name,
Email: email,
Roles: roles,
Status: status,
OrgID: orgID,
DeletedAt: time.Time{},
}
}
@@ -90,6 +94,7 @@ func NewServiceAccountFromStorables(storableServiceAccount *StorableServiceAccou
Roles: roles,
Status: storableServiceAccount.Status,
OrgID: valuer.MustNewUUID(storableServiceAccount.OrgID),
DeletedAt: storableServiceAccount.DeletedAt,
}
}
@@ -126,22 +131,47 @@ func NewStorableServiceAccount(serviceAccount *ServiceAccount) *StorableServiceA
Email: serviceAccount.Email.String(),
Status: serviceAccount.Status,
OrgID: serviceAccount.OrgID.String(),
DeletedAt: serviceAccount.DeletedAt,
}
}
func (sa *ServiceAccount) Update(name string, email valuer.Email, roles []string) {
func (sa *ServiceAccount) Update(name string, email valuer.Email, roles []string) error {
if err := sa.ErrIfDisabled(); err != nil {
return err
}
sa.Name = name
sa.Email = email
sa.Roles = roles
sa.UpdatedAt = time.Now()
return nil
}
func (sa *ServiceAccount) UpdateStatus(status valuer.String) {
func (sa *ServiceAccount) UpdateStatus(status valuer.String) error {
if err := sa.ErrIfDisabled(); err != nil {
return err
}
sa.Status = status
sa.UpdatedAt = time.Now()
sa.DeletedAt = time.Now()
return nil
}
func (sa *ServiceAccount) ErrIfDisabled() error {
if sa.Status == StatusDisabled {
return errors.New(errors.TypeUnsupported, ErrCodeServiceAccountOperationUnsupported, "this operation is not supported for disabled service account")
}
return nil
}
func (sa *ServiceAccount) NewFactorAPIKey(name string, expiresAt uint64) (*FactorAPIKey, error) {
if err := sa.ErrIfDisabled(); err != nil {
return nil, err
}
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
@@ -161,7 +191,7 @@ func (sa *ServiceAccount) NewFactorAPIKey(name string, expiresAt uint64) (*Facto
Name: name,
Key: encodedKey,
ExpiresAt: expiresAt,
LastUsed: time.Now(),
LastObservedAt: time.Now(),
ServiceAccountID: sa.ID,
}, nil
}
@@ -244,10 +274,28 @@ func (sa *UpdatableServiceAccountStatus) UnmarshalJSON(data []byte) error {
return err
}
if !slices.Contains(ValidStatus, temp.Status) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "invalid status: %s, allowed status are: %v", temp.Status, ValidStatus)
if temp.Status != StatusDisabled {
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "invalid status: %s, allowed status are: %v", temp.Status, StatusDisabled)
}
*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,
}
}

View File

@@ -11,6 +11,8 @@ type Store interface {
Create(context.Context, *StorableServiceAccount) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableServiceAccount, error)
GetByID(context.Context, valuer.UUID) (*StorableServiceAccount, error)
GetByOrgIDAndName(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
@@ -24,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

View File

@@ -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)