mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -175,7 +175,9 @@ func HasGlobalAccess(ac AccessControl, authnService authn.Service, c *contextmod
|
|||||||
var targetOrgID int64 = GlobalOrgID
|
var targetOrgID int64 = GlobalOrgID
|
||||||
orgUser, err := authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID())
|
orgUser, err := authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err))
|
// This will be an common error for entities that can't authenticate in global scope
|
||||||
|
c.Logger.Debug("Failed to authenticate user in global scope", "error", err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAccess, err := ac.Evaluate(c.Req.Context(), orgUser, evaluator)
|
hasAccess, err := ac.Evaluate(c.Req.Context(), orgUser, evaluator)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ type Client interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ContextAwareClient is an optional interface that auth client can implement.
|
// ContextAwareClient is an optional interface that auth client can implement.
|
||||||
// Clients that implements this interface will be tried during request authentication
|
// Clients that implements this interface will be tried during request authentication.
|
||||||
type ContextAwareClient interface {
|
type ContextAwareClient interface {
|
||||||
Client
|
Client
|
||||||
// Test should return true if client can be used to authenticate request
|
// Test should return true if client can be used to authenticate request
|
||||||
@@ -127,7 +127,7 @@ type HookClient interface {
|
|||||||
|
|
||||||
// RedirectClient is an optional interface that auth clients can implement.
|
// RedirectClient is an optional interface that auth clients can implement.
|
||||||
// Clients that implements this interface can be used to generate redirect urls
|
// Clients that implements this interface can be used to generate redirect urls
|
||||||
// for authentication flows, e.g. oauth clients
|
// for authentication flows, e.g. oauth clients.
|
||||||
type RedirectClient interface {
|
type RedirectClient interface {
|
||||||
Client
|
Client
|
||||||
RedirectURL(ctx context.Context, r *Request) (*Redirect, error)
|
RedirectURL(ctx context.Context, r *Request) (*Redirect, error)
|
||||||
@@ -150,12 +150,20 @@ type ProxyClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UsageStatClient is an optional interface that auth clients can implement.
|
// UsageStatClient is an optional interface that auth clients can implement.
|
||||||
// Clients that implements this interface can specify a usage stat collection hook
|
// Clients that implements this interface can specify a usage stat collection hook.
|
||||||
type UsageStatClient interface {
|
type UsageStatClient interface {
|
||||||
Client
|
Client
|
||||||
UsageStatFn(ctx context.Context) (map[string]any, error)
|
UsageStatFn(ctx context.Context) (map[string]any, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IdentityResolverClient is an optional interface that auth clients can implement.
|
||||||
|
// Clients that implements this interface can resolve an full identity from an orgID and namespaceID.
|
||||||
|
type IdentityResolverClient interface {
|
||||||
|
Client
|
||||||
|
Namespace() string
|
||||||
|
ResolveIdentity(ctx context.Context, orgID int64, namespaceID NamespaceID) (*Identity, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// OrgID will be populated by authn.Service
|
// OrgID will be populated by authn.Service
|
||||||
OrgID int64
|
OrgID int64
|
||||||
|
|||||||
@@ -49,15 +49,16 @@ func ProvideService(
|
|||||||
sessionService auth.UserTokenService, usageStats usagestats.Service, registerer prometheus.Registerer,
|
sessionService auth.UserTokenService, usageStats usagestats.Service, registerer prometheus.Registerer,
|
||||||
) *Service {
|
) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
log: log.New("authn.service"),
|
log: log.New("authn.service"),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
clients: make(map[string]authn.Client),
|
clients: make(map[string]authn.Client),
|
||||||
clientQueue: newQueue[authn.ContextAwareClient](),
|
clientQueue: newQueue[authn.ContextAwareClient](),
|
||||||
tracer: tracer,
|
idenityResolverClients: make(map[string]authn.IdentityResolverClient),
|
||||||
metrics: newMetrics(registerer),
|
tracer: tracer,
|
||||||
sessionService: sessionService,
|
metrics: newMetrics(registerer),
|
||||||
postAuthHooks: newQueue[authn.PostAuthHookFn](),
|
sessionService: sessionService,
|
||||||
postLoginHooks: newQueue[authn.PostLoginHookFn](),
|
postAuthHooks: newQueue[authn.PostAuthHookFn](),
|
||||||
|
postLoginHooks: newQueue[authn.PostLoginHookFn](),
|
||||||
}
|
}
|
||||||
|
|
||||||
usageStats.RegisterMetricsFunc(s.getUsageStats)
|
usageStats.RegisterMetricsFunc(s.getUsageStats)
|
||||||
@@ -71,6 +72,8 @@ type Service struct {
|
|||||||
clients map[string]authn.Client
|
clients map[string]authn.Client
|
||||||
clientQueue *queue[authn.ContextAwareClient]
|
clientQueue *queue[authn.ContextAwareClient]
|
||||||
|
|
||||||
|
idenityResolverClients map[string]authn.IdentityResolverClient
|
||||||
|
|
||||||
tracer tracing.Tracer
|
tracer tracing.Tracer
|
||||||
metrics *metrics
|
metrics *metrics
|
||||||
|
|
||||||
@@ -292,19 +295,29 @@ func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, namespaceID
|
|||||||
// hack to not update last seen
|
// hack to not update last seen
|
||||||
r.SetMeta(authn.MetaKeyIsLogin, "true")
|
r.SetMeta(authn.MetaKeyIsLogin, "true")
|
||||||
|
|
||||||
identity, err := s.authenticate(ctx, clients.ProvideIdentity(namespaceID), r)
|
id, err := authn.ParseNamespaceID(namespaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Service) RegisterClient(c authn.Client) {
|
||||||
s.clients[c.Name()] = c
|
s.clients[c.Name()] = c
|
||||||
|
|
||||||
if cac, ok := c.(authn.ContextAwareClient); ok {
|
if cac, ok := c.(authn.ContextAwareClient); ok {
|
||||||
s.clientQueue.insert(cac, cac.Priority())
|
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 {
|
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)
|
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) {
|
func (s *Service) errorLogFunc(ctx context.Context, err error) func(msg string, ctx ...any) {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return func(msg string, ctx ...any) {}
|
return func(msg string, ctx ...any) {}
|
||||||
|
|||||||
@@ -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 {
|
func mustParseURL(s string) *url.URL {
|
||||||
u, err := url.Parse(s)
|
u, err := url.Parse(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -395,14 +438,15 @@ func setupTests(t *testing.T, opts ...func(svc *Service)) *Service {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
log: log.NewNopLogger(),
|
log: log.NewNopLogger(),
|
||||||
cfg: setting.NewCfg(),
|
cfg: setting.NewCfg(),
|
||||||
clients: map[string]authn.Client{},
|
clients: make(map[string]authn.Client),
|
||||||
clientQueue: newQueue[authn.ContextAwareClient](),
|
clientQueue: newQueue[authn.ContextAwareClient](),
|
||||||
tracer: tracing.InitializeTracerForTest(),
|
idenityResolverClients: make(map[string]authn.IdentityResolverClient),
|
||||||
metrics: newMetrics(nil),
|
tracer: tracing.InitializeTracerForTest(),
|
||||||
postAuthHooks: newQueue[authn.PostAuthHookFn](),
|
metrics: newMetrics(nil),
|
||||||
postLoginHooks: newQueue[authn.PostLoginHookFn](),
|
postAuthHooks: newQueue[authn.PostAuthHookFn](),
|
||||||
|
postLoginHooks: newQueue[authn.PostLoginHookFn](),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
|||||||
@@ -60,14 +60,17 @@ func (m *MockService) SyncIdentity(ctx context.Context, identity *authn.Identity
|
|||||||
var _ authn.HookClient = new(MockClient)
|
var _ authn.HookClient = new(MockClient)
|
||||||
var _ authn.LogoutClient = new(MockClient)
|
var _ authn.LogoutClient = new(MockClient)
|
||||||
var _ authn.ContextAwareClient = new(MockClient)
|
var _ authn.ContextAwareClient = new(MockClient)
|
||||||
|
var _ authn.IdentityResolverClient = new(MockClient)
|
||||||
|
|
||||||
type MockClient struct {
|
type MockClient struct {
|
||||||
NameFunc func() string
|
NameFunc func() string
|
||||||
AuthenticateFunc func(ctx context.Context, r *authn.Request) (*authn.Identity, error)
|
AuthenticateFunc func(ctx context.Context, r *authn.Request) (*authn.Identity, error)
|
||||||
TestFunc func(ctx context.Context, r *authn.Request) bool
|
TestFunc func(ctx context.Context, r *authn.Request) bool
|
||||||
PriorityFunc func() uint
|
PriorityFunc func() uint
|
||||||
HookFunc func(ctx context.Context, identity *authn.Identity, r *authn.Request) error
|
HookFunc func(ctx context.Context, identity *authn.Identity, r *authn.Request) error
|
||||||
LogoutFunc func(ctx context.Context, user identity.Requester) (*authn.Redirect, bool)
|
LogoutFunc func(ctx context.Context, user identity.Requester) (*authn.Redirect, bool)
|
||||||
|
NamespaceFunc func() string
|
||||||
|
ResolveIdentityFunc func(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MockClient) Name() string {
|
func (m MockClient) Name() string {
|
||||||
@@ -112,6 +115,21 @@ func (m *MockClient) Logout(ctx context.Context, user identity.Requester) (*auth
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockClient) Namespace() string {
|
||||||
|
if m.NamespaceFunc != nil {
|
||||||
|
return m.NamespaceFunc()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveIdentity implements authn.IdentityResolverClient.
|
||||||
|
func (m *MockClient) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
|
||||||
|
if m.ResolveIdentityFunc != nil {
|
||||||
|
return m.ResolveIdentityFunc(ctx, orgID, namespaceID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ authn.ProxyClient = new(MockProxyClient)
|
var _ authn.ProxyClient = new(MockProxyClient)
|
||||||
|
|
||||||
type MockProxyClient struct {
|
type MockProxyClient struct {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var (
|
|||||||
|
|
||||||
var _ authn.HookClient = new(APIKey)
|
var _ authn.HookClient = new(APIKey)
|
||||||
var _ authn.ContextAwareClient = new(APIKey)
|
var _ authn.ContextAwareClient = new(APIKey)
|
||||||
|
var _ authn.IdentityResolverClient = new(APIKey)
|
||||||
|
|
||||||
func ProvideAPIKey(apiKeyService apikey.Service) *APIKey {
|
func ProvideAPIKey(apiKeyService apikey.Service) *APIKey {
|
||||||
return &APIKey{
|
return &APIKey{
|
||||||
@@ -45,7 +46,7 @@ func (s *APIKey) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r))
|
key, err := s.getAPIKey(ctx, getTokenFromRequest(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, apikeygen.ErrInvalidApiKey) {
|
if errors.Is(err, apikeygen.ErrInvalidApiKey) {
|
||||||
return nil, errAPIKeyInvalid.Errorf("API key is invalid")
|
return nil, errAPIKeyInvalid.Errorf("API key is invalid")
|
||||||
@@ -53,37 +54,20 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiKey.Expires != nil && *apiKey.Expires <= time.Now().Unix() {
|
|
||||||
return nil, errAPIKeyExpired.Errorf("API key has expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiKey.IsRevoked != nil && *apiKey.IsRevoked {
|
|
||||||
return nil, errAPIKeyRevoked.Errorf("Api key is revoked")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.OrgID == 0 {
|
if r.OrgID == 0 {
|
||||||
r.OrgID = apiKey.OrgID
|
r.OrgID = key.OrgID
|
||||||
} else if r.OrgID != apiKey.OrgID {
|
}
|
||||||
return nil, errAPIKeyOrgMismatch.Errorf("API does not belong in Organization %v", r.OrgID)
|
|
||||||
|
if err := validateApiKey(r.OrgID, key); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the api key don't belong to a service account construct the identity and return it
|
// if the api key don't belong to a service account construct the identity and return it
|
||||||
if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 {
|
if key.ServiceAccountId == nil || *key.ServiceAccountId < 1 {
|
||||||
return &authn.Identity{
|
return newAPIKeyIdentity(key), nil
|
||||||
ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID),
|
|
||||||
OrgID: apiKey.OrgID,
|
|
||||||
OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role},
|
|
||||||
ClientParams: authn.ClientParams{SyncPermissions: true},
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return newServiceAccountIdentity(key), nil
|
||||||
ID: authn.NamespacedID(authn.NamespaceServiceAccount, *apiKey.ServiceAccountId),
|
|
||||||
OrgID: apiKey.OrgID,
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
|
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
|
||||||
@@ -147,6 +131,38 @@ func (s *APIKey) Priority() uint {
|
|||||||
return 30
|
return 30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIKey) Namespace() string {
|
||||||
|
return authn.NamespaceAPIKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
|
||||||
|
if !namespaceID.IsNamespace(authn.NamespaceAPIKey) {
|
||||||
|
return nil, authn.ErrInvalidNamepsaceID.Errorf("got unspected namespace: %s", namespaceID.Namespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeyID, err := namespaceID.ParseInt()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := s.apiKeyService.GetApiKeyById(ctx, &apikey.GetByIDQuery{
|
||||||
|
ApiKeyID: apiKeyID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateApiKey(orgID, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.ServiceAccountId != nil && *key.ServiceAccountId >= 1 {
|
||||||
|
return nil, authn.ErrInvalidNamepsaceID.Errorf("api key belongs to service account")
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAPIKeyIdentity(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
||||||
id, exists := s.getAPIKeyID(ctx, identity, r)
|
id, exists := s.getAPIKeyID(ctx, identity, r)
|
||||||
|
|
||||||
@@ -217,3 +233,38 @@ func getTokenFromRequest(r *authn.Request) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateApiKey(orgID int64, key *apikey.APIKey) error {
|
||||||
|
if key.Expires != nil && *key.Expires <= time.Now().Unix() {
|
||||||
|
return errAPIKeyExpired.Errorf("API key has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.IsRevoked != nil && *key.IsRevoked {
|
||||||
|
return errAPIKeyRevoked.Errorf("Api key is revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgID != key.OrgID {
|
||||||
|
return errAPIKeyOrgMismatch.Errorf("API does not belong in Organization")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity {
|
||||||
|
return &authn.Identity{
|
||||||
|
ID: authn.NamespacedID(authn.NamespaceAPIKey, key.ID),
|
||||||
|
OrgID: key.OrgID,
|
||||||
|
OrgRoles: map[int64]org.RoleType{key.OrgID: key.Role},
|
||||||
|
ClientParams: authn.ClientParams{SyncPermissions: true},
|
||||||
|
AuthenticatedBy: login.APIKeyAuthModule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServiceAccountIdentity(key *apikey.APIKey) *authn.Identity {
|
||||||
|
return &authn.Identity{
|
||||||
|
ID: authn.NamespacedID(authn.NamespaceServiceAccount, *key.ServiceAccountId),
|
||||||
|
OrgID: key.OrgID,
|
||||||
|
AuthenticatedBy: login.APIKeyAuthModule,
|
||||||
|
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -283,6 +283,100 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIKey_ResolveIdentity(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
desc string
|
||||||
|
namespaceID authn.NamespaceID
|
||||||
|
|
||||||
|
exptedApiKey *apikey.APIKey
|
||||||
|
|
||||||
|
expectedIdenity *authn.Identity
|
||||||
|
expectedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
desc: "should return error for invalid namespace",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("user:1"),
|
||||||
|
expectedErr: authn.ErrInvalidNamepsaceID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return error when api key has expired",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("api-key:1"),
|
||||||
|
exptedApiKey: &apikey.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
Expires: intPtr(0),
|
||||||
|
},
|
||||||
|
expectedErr: errAPIKeyExpired,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return error when api key is revoked",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("api-key:1"),
|
||||||
|
exptedApiKey: &apikey.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
IsRevoked: boolPtr(true),
|
||||||
|
},
|
||||||
|
expectedErr: errAPIKeyRevoked,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return error when api key is connected to service account",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("api-key:1"),
|
||||||
|
exptedApiKey: &apikey.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
ServiceAccountId: intPtr(1),
|
||||||
|
},
|
||||||
|
expectedErr: authn.ErrInvalidNamepsaceID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return error when api key is belongs to different org",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("api-key:1"),
|
||||||
|
exptedApiKey: &apikey.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 2,
|
||||||
|
ServiceAccountId: intPtr(1),
|
||||||
|
},
|
||||||
|
expectedErr: errAPIKeyOrgMismatch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return valid idenitty",
|
||||||
|
namespaceID: authn.MustParseNamespaceID("api-key:1"),
|
||||||
|
exptedApiKey: &apikey.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
Role: org.RoleEditor,
|
||||||
|
},
|
||||||
|
expectedIdenity: &authn.Identity{
|
||||||
|
OrgID: 1,
|
||||||
|
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||||
|
ID: "api-key:1",
|
||||||
|
AuthenticatedBy: login.APIKeyAuthModule,
|
||||||
|
ClientParams: authn.ClientParams{SyncPermissions: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
c := ProvideAPIKey(&apikeytest.Service{
|
||||||
|
ExpectedAPIKey: tt.exptedApiKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
identity, err := c.ResolveIdentity(context.Background(), 1, tt.namespaceID)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
assert.Nil(t, identity)
|
||||||
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, *tt.expectedIdenity, *identity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func intPtr(n int64) *int64 {
|
func intPtr(n int64) *int64 {
|
||||||
return &n
|
return &n
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,18 @@ import (
|
|||||||
|
|
||||||
var _ authn.Client = (*IdentityClient)(nil)
|
var _ authn.Client = (*IdentityClient)(nil)
|
||||||
|
|
||||||
func ProvideIdentity(namespaceID string) *IdentityClient {
|
func ProvideIdentity(identity *authn.Identity) *IdentityClient {
|
||||||
return &IdentityClient{namespaceID}
|
return &IdentityClient{identity}
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityClient struct {
|
type IdentityClient struct {
|
||||||
namespaceID string
|
identity *authn.Identity
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *IdentityClient) Name() string {
|
func (i *IdentityClient) Name() string {
|
||||||
return "identity"
|
return "identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate implements authn.Client.
|
|
||||||
func (i *IdentityClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (i *IdentityClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
return &authn.Identity{
|
return i.identity, nil
|
||||||
OrgID: r.OrgID,
|
|
||||||
ID: i.namespaceID,
|
|
||||||
ClientParams: authn.ClientParams{
|
|
||||||
AllowGlobalOrg: true,
|
|
||||||
FetchSyncedUser: true,
|
|
||||||
SyncPermissions: true,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ var (
|
|||||||
ErrClientNotConfigured = errutil.BadRequest("auth.client.notConfigured")
|
ErrClientNotConfigured = errutil.BadRequest("auth.client.notConfigured")
|
||||||
ErrUnsupportedIdentity = errutil.NotImplemented("auth.identity.unsupported")
|
ErrUnsupportedIdentity = errutil.NotImplemented("auth.identity.unsupported")
|
||||||
ErrExpiredAccessToken = errutil.Unauthorized("oauth.expired-token", errutil.WithPublicMessage("OAuth access token expired"))
|
ErrExpiredAccessToken = errutil.Unauthorized("oauth.expired-token", errutil.WithPublicMessage("OAuth access token expired"))
|
||||||
|
ErrInvalidNamepsaceID = errutil.BadRequest("auth.identity.invalid-namespace-id")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,24 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NamespacedID builds a namespaced ID from a namespace and an ID.
|
const GlobalOrgID = int64(0)
|
||||||
func NamespacedID(namespace string, id int64) string {
|
|
||||||
return fmt.Sprintf("%s:%d", namespace, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
NamespaceUser = identity.NamespaceUser
|
|
||||||
NamespaceAPIKey = identity.NamespaceAPIKey
|
|
||||||
NamespaceServiceAccount = identity.NamespaceServiceAccount
|
|
||||||
NamespaceAnonymous = identity.NamespaceAnonymous
|
|
||||||
NamespaceRenderService = identity.NamespaceRenderService
|
|
||||||
NamespaceAccessPolicy = identity.NamespaceAccessPolicy
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
AnonymousNamespaceID = NamespaceAnonymous + ":0"
|
|
||||||
GlobalOrgID = int64(0)
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ identity.Requester = (*Identity)(nil)
|
var _ identity.Requester = (*Identity)(nil)
|
||||||
|
|
||||||
@@ -145,7 +128,6 @@ func (i *Identity) GetLogin() string {
|
|||||||
return i.Login
|
return i.Login
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrgID implements identity.Requester.
|
|
||||||
func (i *Identity) GetOrgID() int64 {
|
func (i *Identity) GetOrgID() int64 {
|
||||||
return i.OrgID
|
return i.OrgID
|
||||||
}
|
}
|
||||||
|
|||||||
89
pkg/services/authn/namespace.go
Normal file
89
pkg/services/authn/namespace.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NamespaceUser = identity.NamespaceUser
|
||||||
|
NamespaceAPIKey = identity.NamespaceAPIKey
|
||||||
|
NamespaceServiceAccount = identity.NamespaceServiceAccount
|
||||||
|
NamespaceAnonymous = identity.NamespaceAnonymous
|
||||||
|
NamespaceRenderService = identity.NamespaceRenderService
|
||||||
|
NamespaceAccessPolicy = identity.NamespaceAccessPolicy
|
||||||
|
AnonymousNamespaceID = NamespaceAnonymous + ":0"
|
||||||
|
)
|
||||||
|
|
||||||
|
var namespaceLookup = map[string]struct{}{
|
||||||
|
NamespaceUser: {},
|
||||||
|
NamespaceAPIKey: {},
|
||||||
|
NamespaceServiceAccount: {},
|
||||||
|
NamespaceAnonymous: {},
|
||||||
|
NamespaceRenderService: {},
|
||||||
|
NamespaceAccessPolicy: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespacedID builds a namespaced ID from a namespace and an ID.
|
||||||
|
func NamespacedID(namespace string, id int64) string {
|
||||||
|
return fmt.Sprintf("%s:%d", namespace, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseNamespaceID(str string) (NamespaceID, error) {
|
||||||
|
var namespaceID NamespaceID
|
||||||
|
|
||||||
|
parts := strings.Split(str, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return namespaceID, ErrInvalidNamepsaceID.Errorf("expected namespace id to have 2 parts")
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, id := parts[0], parts[1]
|
||||||
|
|
||||||
|
if _, ok := namespaceLookup[namespace]; !ok {
|
||||||
|
return namespaceID, ErrInvalidNamepsaceID.Errorf("got invalid namespace %s", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceID.id = id
|
||||||
|
namespaceID.namespace = namespace
|
||||||
|
|
||||||
|
return namespaceID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParseNamespaceID parses namespace id, it will panic it failes to do so.
|
||||||
|
// Sutable to use in tests or when we can garantuee that we pass a correct format.
|
||||||
|
func MustParseNamespaceID(str string) NamespaceID {
|
||||||
|
namespaceID, err := ParseNamespaceID(str)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return namespaceID
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: use this instead of encoded string through the codebase
|
||||||
|
type NamespaceID struct {
|
||||||
|
id string
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ni NamespaceID) ID() string {
|
||||||
|
return ni.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ni NamespaceID) ParseInt() (int64, error) {
|
||||||
|
return strconv.ParseInt(ni.id, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ni NamespaceID) Namespace() string {
|
||||||
|
return ni.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ni NamespaceID) IsNamespace(expected ...string) bool {
|
||||||
|
return identity.IsNamespace(ni.namespace, expected...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ni NamespaceID) String() string {
|
||||||
|
return fmt.Sprintf("%s:%s", ni.namespace, ni.id)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user