Compare commits

...

1 Commits

Author SHA1 Message Date
vikrantgupta25
8040740499 feat(tokenizer): add api key tokenizer 2026-03-12 17:29:24 +05:30
16 changed files with 546 additions and 41 deletions

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

@@ -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, user.Role), 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

@@ -128,11 +128,23 @@ 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)
if err != nil {
return err

View File

@@ -231,6 +231,23 @@ func (store *store) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer
return storable, nil
}
func (store *store) GetFactorAPIKeyByKey(ctx context.Context, key string) (*serviceaccounttypes.StorableFactorAPIKey, error) {
storable := new(serviceaccounttypes.StorableFactorAPIKey)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(storable).
Where("key = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeAPIKeytNotFound, "api key with key: %s doesn't exist", key)
}
return storable, nil
}
func (store *store) ListFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID) ([]*serviceaccounttypes.StorableFactorAPIKey, error) {
storables := make([]*serviceaccounttypes.StorableFactorAPIKey, 0)
@@ -248,6 +265,25 @@ 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) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, storable *serviceaccounttypes.StorableFactorAPIKey) error {
_, err := store.
sqlstore.
@@ -263,6 +299,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

@@ -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, user.Role), map[string]string{})
if err != nil {
return "", err
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
@@ -38,6 +39,7 @@ import (
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
pkgtokenizer "github.com/SigNoz/signoz/pkg/tokenizer"
"github.com/SigNoz/signoz/pkg/tokenizer/apikeytokenizer"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/version"
@@ -65,6 +67,7 @@ type SigNoz struct {
Sharder sharder.Sharder
StatsReporter statsreporter.StatsReporter
Tokenizer pkgtokenizer.Tokenizer
APIKeyTokenizer pkgtokenizer.Tokenizer
Authz authz.AuthZ
Modules Modules
Handlers Handlers
@@ -279,6 +282,13 @@ func New(
return nil, err
}
// Initialize api key tokenizer
apiKeyStore := implserviceaccount.NewStore(sqlstore)
apiKeyTokenizer, err := apikeytokenizer.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)
@@ -443,6 +453,7 @@ func New(
factory.NewNamedService(factory.MustNewName("licensing"), licensing),
factory.NewNamedService(factory.MustNewName("statsreporter"), statsReporter),
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
factory.NewNamedService(factory.MustNewName("apikeytokenizer"), apiKeyTokenizer),
factory.NewNamedService(factory.MustNewName("authz"), authz),
factory.NewNamedService(factory.MustNewName("user"), userService),
)
@@ -468,6 +479,7 @@ func New(
Emailing: emailing,
Sharder: sharder,
Tokenizer: tokenizer,
APIKeyTokenizer: apiKeyTokenizer,
Authz: authz,
Modules: modules,
Handlers: handlers,

View File

@@ -0,0 +1,353 @@
package apikeytokenizer
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("apikey"), 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/apikeytokenizer")
// * 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.Opaque.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

@@ -125,7 +125,7 @@ func (provider *provider) GetIdentity(ctx context.Context, accessToken string) (
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), claims.Role), nil
}
func (provider *provider) DeleteToken(ctx context.Context, accessToken string) error {

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, types.Role(user.Role)), nil
}
func (store *store) GetByAccessToken(ctx context.Context, accessToken string) (*authtypes.StorableToken, error) {

View File

@@ -25,10 +25,12 @@ 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"`
Role types.Role `json:"role"`
}
type CallbackIdentity struct {
@@ -78,12 +80,14 @@ 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, role types.Role) *Identity {
return &Identity{
UserID: userID,
OrgID: orgID,
Email: email,
Role: role,
UserID: userID,
ServiceAccountID: serviceAccountID,
Principal: principal,
OrgID: orgID,
Email: email,
Role: role,
}
}
@@ -116,10 +120,12 @@ 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(),
Role: typ.Role,
OrgID: typ.OrgID.String(),
}
}

View File

@@ -11,12 +11,15 @@ import (
type claimsKey struct{}
type accessTokenKey struct{}
type apiKeyKey struct{}
type Claims struct {
UserID string
Email string
Role types.Role
OrgID string
UserID string
ServiceAccountID string
Principal string
Email string
Role types.Role
OrgID string
}
// NewContextWithClaims attaches individual claims to the context.
@@ -47,15 +50,38 @@ func AccessTokenFromContext(ctx context.Context) (string, error) {
return accessToken, nil
}
func NewContextWithAPIKey(ctx context.Context, apiKey string) context.Context {
return context.WithValue(ctx, apiKeyKey{}, apiKey)
}
func APIKeyFromContext(ctx context.Context) (string, error) {
apiKey, ok := ctx.Value(apiKeyKey{}).(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) GetIdentityID() string {
if c.Principal == PrincipalUser.StringValue() {
return c.UserID
}
return c.ServiceAccountID
}
func (c *Claims) IsViewer() error {
if slices.Contains([]types.Role{types.RoleViewer, types.RoleEditor, types.RoleAdmin}, c.Role) {
return nil

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

@@ -11,9 +11,9 @@ import (
)
var (
ErrCodeAPIkeyInvalidInput = errors.MustNewCode("service_account_factor_api_key_invalid_input")
ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("service_account_factor_api_key_already_exists")
ErrCodeAPIKeytNotFound = errors.MustNewCode("service_account_factor_api_key_not_found")
ErrCodeAPIkeyInvalidInput = errors.MustNewCode("api_key_invalid_input")
ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
ErrCodeAPIKeytNotFound = errors.MustNewCode("api_key_not_found")
ErrCodeAPIKeyExpired = errors.MustNewCode("api_key_expired")
ErrCodeAPIkeyOlderLastObservedAt = errors.MustNewCode("api_key_older_last_observed_at")
)
@@ -184,3 +184,11 @@ func (key *UpdatableFactorAPIKey) UnmarshalJSON(data []byte) error {
*key = UpdatableFactorAPIKey(temp)
return nil
}
func (key FactorAPIKey) MarshalBinary() ([]byte, error) {
return json.Marshal(key)
}
func (key *FactorAPIKey) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, key)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
@@ -284,3 +285,12 @@ func (sa *UpdatableServiceAccountStatus) UnmarshalJSON(data []byte) error {
*sa = UpdatableServiceAccountStatus(temp)
return nil
}
func (sa *StorableServiceAccount) ToIdentity() *authtypes.Identity {
return &authtypes.Identity{
ServiceAccountID: sa.ID,
Principal: authtypes.PrincipalServiceAccount,
OrgID: valuer.MustNewUUID(sa.OrgID),
Email: valuer.MustNewEmail(sa.Email),
}
}

View File

@@ -25,8 +25,11 @@ 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)
ListFactorAPIKeyByOrgID(context.Context, valuer.UUID) ([]*StorableFactorAPIKey, error)
ListFactorAPIKey(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