Authn: Identity resolvers (#85930)

* AuthN: Add NamespaceID struct. We should replace the usage of encoded namespaceID with this one

* AuthN: Add optional interface that clients can implement to be able to resolve identity for a namespace

* Authn: Implement IdentityResolverClient for api keys

* AuthN: use idenity resolvers

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
Karl Persson
2024-04-12 11:38:20 +02:00
committed by GitHub
co-authored by Misi
parent c837d95677
commit 73fecc8d80
11 changed files with 409 additions and 87 deletions
+53 -11
View File
@@ -49,15 +49,16 @@ func ProvideService(
sessionService auth.UserTokenService, usageStats usagestats.Service, registerer prometheus.Registerer,
) *Service {
s := &Service{
log: log.New("authn.service"),
cfg: cfg,
clients: make(map[string]authn.Client),
clientQueue: newQueue[authn.ContextAwareClient](),
tracer: tracer,
metrics: newMetrics(registerer),
sessionService: sessionService,
postAuthHooks: newQueue[authn.PostAuthHookFn](),
postLoginHooks: newQueue[authn.PostLoginHookFn](),
log: log.New("authn.service"),
cfg: cfg,
clients: make(map[string]authn.Client),
clientQueue: newQueue[authn.ContextAwareClient](),
idenityResolverClients: make(map[string]authn.IdentityResolverClient),
tracer: tracer,
metrics: newMetrics(registerer),
sessionService: sessionService,
postAuthHooks: newQueue[authn.PostAuthHookFn](),
postLoginHooks: newQueue[authn.PostLoginHookFn](),
}
usageStats.RegisterMetricsFunc(s.getUsageStats)
@@ -71,6 +72,8 @@ type Service struct {
clients map[string]authn.Client
clientQueue *queue[authn.ContextAwareClient]
idenityResolverClients map[string]authn.IdentityResolverClient
tracer tracing.Tracer
metrics *metrics
@@ -292,19 +295,29 @@ func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, namespaceID
// hack to not update last seen
r.SetMeta(authn.MetaKeyIsLogin, "true")
identity, err := s.authenticate(ctx, clients.ProvideIdentity(namespaceID), r)
id, err := authn.ParseNamespaceID(namespaceID)
if err != nil {
return nil, err
}
return identity, nil
identity, err := s.resolveIdenity(ctx, orgID, id)
if err != nil {
return nil, err
}
return s.authenticate(ctx, clients.ProvideIdentity(identity), r)
}
func (s *Service) RegisterClient(c authn.Client) {
s.clients[c.Name()] = c
if cac, ok := c.(authn.ContextAwareClient); ok {
s.clientQueue.insert(cac, cac.Priority())
}
if rc, ok := c.(authn.IdentityResolverClient); ok {
s.idenityResolverClients[rc.Namespace()] = rc
}
}
func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
@@ -314,6 +327,35 @@ func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) er
return s.runPostAuthHooks(ctx, identity, r)
}
func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
if namespaceID.IsNamespace(authn.NamespaceUser) {
return &authn.Identity{
OrgID: orgID,
ID: namespaceID.String(),
ClientParams: authn.ClientParams{
AllowGlobalOrg: true,
FetchSyncedUser: true,
SyncPermissions: true,
}}, nil
}
if namespaceID.IsNamespace(authn.NamespaceServiceAccount) {
return &authn.Identity{
ID: namespaceID.String(),
OrgID: orgID,
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
}}, nil
}
resolver, ok := s.idenityResolverClients[namespaceID.Namespace()]
if !ok {
return nil, authn.ErrUnsupportedIdentity.Errorf("no resolver for : %s", namespaceID.Namespace())
}
return resolver.ResolveIdentity(ctx, orgID, namespaceID)
}
func (s *Service) errorLogFunc(ctx context.Context, err error) func(msg string, ctx ...any) {
if errors.Is(err, context.Canceled) {
return func(msg string, ctx ...any) {}
+52 -8
View File
@@ -383,6 +383,49 @@ func TestService_Logout(t *testing.T) {
}
}
func TestService_ResolveIdentity(t *testing.T) {
t.Run("should return error for for unknown namespace", func(t *testing.T) {
svc := setupTests(t)
_, err := svc.ResolveIdentity(context.Background(), 1, "some:1")
assert.ErrorIs(t, err, authn.ErrInvalidNamepsaceID)
})
t.Run("should return error for for namespace that don't have a resolver", func(t *testing.T) {
svc := setupTests(t)
_, err := svc.ResolveIdentity(context.Background(), 1, "api-key:1")
assert.ErrorIs(t, err, authn.ErrUnsupportedIdentity)
})
t.Run("should resolve for user", func(t *testing.T) {
svc := setupTests(t)
identity, err := svc.ResolveIdentity(context.Background(), 1, "user:1")
assert.NoError(t, err)
assert.NotNil(t, identity)
})
t.Run("should resolve for service account", func(t *testing.T) {
svc := setupTests(t)
identity, err := svc.ResolveIdentity(context.Background(), 1, "service-account:1")
assert.NoError(t, err)
assert.NotNil(t, identity)
})
t.Run("should resolve for valid namespace if client is registered", func(t *testing.T) {
svc := setupTests(t, func(svc *Service) {
svc.RegisterClient(&authntest.MockClient{
NamespaceFunc: func() string { return authn.NamespaceAPIKey },
ResolveIdentityFunc: func(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
return &authn.Identity{}, nil
},
})
})
identity, err := svc.ResolveIdentity(context.Background(), 1, "api-key:1")
assert.NoError(t, err)
assert.NotNil(t, identity)
})
}
func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
@@ -395,14 +438,15 @@ func setupTests(t *testing.T, opts ...func(svc *Service)) *Service {
t.Helper()
s := &Service{
log: log.NewNopLogger(),
cfg: setting.NewCfg(),
clients: map[string]authn.Client{},
clientQueue: newQueue[authn.ContextAwareClient](),
tracer: tracing.InitializeTracerForTest(),
metrics: newMetrics(nil),
postAuthHooks: newQueue[authn.PostAuthHookFn](),
postLoginHooks: newQueue[authn.PostLoginHookFn](),
log: log.NewNopLogger(),
cfg: setting.NewCfg(),
clients: make(map[string]authn.Client),
clientQueue: newQueue[authn.ContextAwareClient](),
idenityResolverClients: make(map[string]authn.IdentityResolverClient),
tracer: tracing.InitializeTracerForTest(),
metrics: newMetrics(nil),
postAuthHooks: newQueue[authn.PostAuthHookFn](),
postLoginHooks: newQueue[authn.PostLoginHookFn](),
}
for _, o := range opts {