diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 65bbe261615..05686ee30c2 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -62,7 +62,7 @@ func ProvideBackgroundServiceRegistry( _ serviceaccounts.Service, _ *guardian.Provider, _ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider, _ *grpcserver.HealthService, _ entity.EntityStoreServer, _ *grpcserver.ReflectionService, _ *ldapapi.Service, - _ *apiregistry.Service, + _ *apiregistry.Service, _ auth.IDService, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, diff --git a/pkg/services/auth/identity/requester.go b/pkg/services/auth/identity/requester.go index 01b72e679ab..bcf67b17ed9 100644 --- a/pkg/services/auth/identity/requester.go +++ b/pkg/services/auth/identity/requester.go @@ -62,6 +62,9 @@ type Requester interface { HasUniqueId() bool // AuthenticatedBy returns the authentication method used to authenticate the entity. GetAuthenticatedBy() string + // GetIDToken returns a signed token representing the identity that can be forwarded to plugins and external services. + // Will only be set when featuremgmt.FlagIdForwarding is enabled. + GetIDToken() string } // IntIdentifier converts a string identifier to an int64. diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index c8607b7e822..6e9fd5cf7ce 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -11,6 +11,8 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/identity" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) @@ -22,8 +24,17 @@ const ( var _ auth.IDService = (*Service)(nil) -func ProvideService(cfg *setting.Cfg, signer auth.IDSigner, cache remotecache.CacheStorage) *Service { - return &Service{cfg, log.New("id-service"), signer, cache} +func ProvideService( + cfg *setting.Cfg, signer auth.IDSigner, cache remotecache.CacheStorage, + features featuremgmt.FeatureToggles, authnService authn.Service, +) *Service { + s := &Service{cfg, log.New("id-service"), signer, cache} + + if features.IsEnabled(featuremgmt.FlagIdForwarding) { + authnService.RegisterPostAuthHook(s.hook, 140) + } + + return s } type Service struct { @@ -46,7 +57,6 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri s.logger.Debug("Sign new id token", "namespace", namespace, "id", identifier) now := time.Now() - token, err := s.signer.SignIDToken(ctx, &auth.IDClaims{ Claims: jwt.Claims{ ID: identifier, @@ -69,6 +79,21 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri return token, nil } +func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + // FIXME(kalleep): implement identity.Requester for authn.Identity + // FIXME(kalleep): we should probably lazy load this + token, err := s.SignIdentity(ctx, identity.SignedInUser()) + if err != nil { + namespace, id := identity.NamespacedID() + s.logger.Error("Failed to sign id token", "err", err, "namespace", namespace, "id", id) + // for now don't return error so we don't break authentication from this hook + return nil + } + + identity.IDToken = token + return nil +} + func prefixCacheKey(key string) string { return fmt.Sprintf("%s-%s", cachePrefix, key) } diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go new file mode 100644 index 00000000000..c9c571342d3 --- /dev/null +++ b/pkg/services/auth/idimpl/service_test.go @@ -0,0 +1,41 @@ +package idimpl + +import ( + "testing" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" +) + +func Test_ProvideService(t *testing.T) { + t.Run("should register post auth hook when feature flag is enabled", func(t *testing.T) { + features := featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding) + + var hookRegistered bool + authnService := &authntest.MockService{ + RegisterPostAuthHookFunc: func(_ authn.PostAuthHookFn, _ uint) { + hookRegistered = true + }, + } + + _ = ProvideService(setting.NewCfg(), nil, nil, features, authnService) + assert.True(t, hookRegistered) + }) + + t.Run("should not register post auth hook when feature flag is disabled", func(t *testing.T) { + features := featuremgmt.WithFeatures() + + var hookRegistered bool + authnService := &authntest.MockService{ + RegisterPostAuthHookFunc: func(_ authn.PostAuthHookFn, _ uint) { + hookRegistered = true + }, + } + + _ = ProvideService(setting.NewCfg(), nil, nil, features, authnService) + assert.False(t, hookRegistered) + }) +} diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 755f2a5a05c..2fd46dc8114 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -6,18 +6,12 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" - "golang.org/x/oauth2" - "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models/usertoken" - "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -176,164 +170,6 @@ type Redirect struct { Extra map[string]string } -const ( - NamespaceUser = identity.NamespaceUser - NamespaceAPIKey = identity.NamespaceAPIKey - NamespaceServiceAccount = identity.NamespaceServiceAccount - NamespaceAnonymous = identity.NamespaceAnonymous - NamespaceRenderService = identity.NamespaceRenderService -) - -type Identity struct { - // OrgID is the active organization for the entity. - OrgID int64 - // OrgName is the name of the active organization. - OrgName string - // OrgRoles is the list of organizations the entity is a member of and their roles. - OrgRoles map[int64]org.RoleType - // ID is the unique identifier for the entity in the Grafana database. - // It is in the format : where namespace is one of the - // Namespace* constants. For example, "user:1" or "api-key:1". - // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. - ID string - // IsAnonymous - IsAnonymous bool - // Login is the shorthand identifier of the entity. Should be unique. - Login string - // Name is the display name of the entity. It is not guaranteed to be unique. - Name string - // Email is the email address of the entity. Should be unique. - Email string - // IsGrafanaAdmin is true if the entity is a Grafana admin. - IsGrafanaAdmin *bool - // AuthenticatedBy is the name of the authentication client that was used to authenticate the current Identity. - // For example, "password", "apikey", "auth_ldap" or "auth_azuread". - AuthenticatedBy string - // AuthId is the unique identifier for the entity in the external system. - // Empty if the identity is provided by Grafana. - AuthID string - // IsDisabled is true if the entity is disabled. - IsDisabled bool - // HelpFlags1 is the help flags for the entity. - HelpFlags1 user.HelpFlags1 - // LastSeenAt is the time when the entity was last seen. - LastSeenAt time.Time - // Teams is the list of teams the entity is a member of. - Teams []int64 - // idP Groups that the entity is a member of. This is only populated if the - // identity provider supports groups. - Groups []string - // OAuthToken is the OAuth token used to authenticate the entity. - OAuthToken *oauth2.Token - // SessionToken is the session token used to authenticate the entity. - SessionToken *usertoken.UserToken - // ClientParams are hints for the auth service on how to handle the identity. - // Set by the authenticating client. - ClientParams ClientParams - // Permissions is the list of permissions the entity has. - Permissions map[int64]map[string][]string -} - -// Role returns the role of the identity in the active organization. -func (i *Identity) Role() org.RoleType { - return i.OrgRoles[i.OrgID] -} - -// NamespacedID returns the namespace, e.g. "user" and the id for that namespace -func (i *Identity) NamespacedID() (string, int64) { - split := strings.Split(i.ID, ":") - if len(split) != 2 { - return "", -1 - } - - id, err := strconv.ParseInt(split[1], 10, 64) - if err != nil { - // FIXME (kalleep): Improve error handling - return "", -1 - } - - return split[0], id -} - -// 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) -} - -// SignedInUser returns a SignedInUser from the identity. -func (i *Identity) SignedInUser() *user.SignedInUser { - var isGrafanaAdmin bool - if i.IsGrafanaAdmin != nil { - isGrafanaAdmin = *i.IsGrafanaAdmin - } - - u := &user.SignedInUser{ - UserID: 0, - OrgID: i.OrgID, - OrgName: i.OrgName, - OrgRole: i.Role(), - Login: i.Login, - Name: i.Name, - Email: i.Email, - AuthenticatedBy: i.AuthenticatedBy, - IsGrafanaAdmin: isGrafanaAdmin, - IsAnonymous: i.IsAnonymous, - IsDisabled: i.IsDisabled, - HelpFlags1: i.HelpFlags1, - LastSeenAt: i.LastSeenAt, - Teams: i.Teams, - Permissions: i.Permissions, - } - - namespace, id := i.NamespacedID() - if namespace == NamespaceAPIKey { - u.ApiKeyID = id - } else { - u.UserID = id - u.IsServiceAccount = namespace == NamespaceServiceAccount - } - - return u -} - -func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { - _, id := i.NamespacedID() - return login.ExternalUserInfo{ - OAuthToken: i.OAuthToken, - AuthModule: i.AuthenticatedBy, - AuthId: i.AuthID, - UserId: id, - Email: i.Email, - Login: i.Login, - Name: i.Name, - Groups: i.Groups, - OrgRoles: i.OrgRoles, - IsGrafanaAdmin: i.IsGrafanaAdmin, - IsDisabled: i.IsDisabled, - } -} - -// IdentityFromSignedInUser creates an identity from a SignedInUser. -func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams, authenticatedBy string) *Identity { - return &Identity{ - ID: id, - OrgID: usr.OrgID, - OrgName: usr.OrgName, - OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, - Login: usr.Login, - Name: usr.Name, - Email: usr.Email, - AuthenticatedBy: authenticatedBy, - IsGrafanaAdmin: &usr.IsGrafanaAdmin, - IsDisabled: usr.IsDisabled, - HelpFlags1: usr.HelpFlags1, - LastSeenAt: usr.LastSeenAt, - Teams: usr.Teams, - ClientParams: params, - Permissions: usr.Permissions, - } -} - // ClientWithPrefix returns a client name prefixed with "auth.client." func ClientWithPrefix(name string) string { return fmt.Sprintf("auth.client.%s", name) diff --git a/pkg/services/authn/authntest/mock.go b/pkg/services/authn/authntest/mock.go index 69abd1d8eb8..0868fad5aaa 100644 --- a/pkg/services/authn/authntest/mock.go +++ b/pkg/services/authn/authntest/mock.go @@ -10,7 +10,8 @@ var _ authn.Service = new(MockService) var _ authn.IdentitySynchronizer = new(MockService) type MockService struct { - SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error + SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error + RegisterPostAuthHookFunc func(hook authn.PostAuthHookFn, priority uint) } func (m *MockService) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -30,7 +31,9 @@ func (m *MockService) RegisterClient(c authn.Client) { } func (m *MockService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) { - panic("unimplemented") + if m.RegisterPostAuthHookFunc != nil { + m.RegisterPostAuthHookFunc(hook, priority) + } } func (m *MockService) RegisterPostLoginHook(hook authn.PostLoginHookFn, priority uint) { diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go new file mode 100644 index 00000000000..3f79cab5fbd --- /dev/null +++ b/pkg/services/authn/identity.go @@ -0,0 +1,179 @@ +package authn + +import ( + "fmt" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/models/usertoken" + "github.com/grafana/grafana/pkg/services/auth/identity" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" +) + +// 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) +} + +const ( + NamespaceUser = identity.NamespaceUser + NamespaceAPIKey = identity.NamespaceAPIKey + NamespaceServiceAccount = identity.NamespaceServiceAccount + NamespaceAnonymous = identity.NamespaceAnonymous + NamespaceRenderService = identity.NamespaceRenderService +) + +type Identity struct { + // OrgID is the active organization for the entity. + OrgID int64 + // OrgName is the name of the active organization. + OrgName string + // OrgRoles is the list of organizations the entity is a member of and their roles. + OrgRoles map[int64]org.RoleType + // ID is the unique identifier for the entity in the Grafana database. + // It is in the format : where namespace is one of the + // Namespace* constants. For example, "user:1" or "api-key:1". + // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. + ID string + // IsAnonymous + IsAnonymous bool + // Login is the shorthand identifier of the entity. Should be unique. + Login string + // Name is the display name of the entity. It is not guaranteed to be unique. + Name string + // Email is the email address of the entity. Should be unique. + Email string + // IsGrafanaAdmin is true if the entity is a Grafana admin. + IsGrafanaAdmin *bool + // AuthenticatedBy is the name of the authentication client that was used to authenticate the current Identity. + // For example, "password", "apikey", "auth_ldap" or "auth_azuread". + AuthenticatedBy string + // AuthId is the unique identifier for the entity in the external system. + // Empty if the identity is provided by Grafana. + AuthID string + // IsDisabled is true if the entity is disabled. + IsDisabled bool + // HelpFlags1 is the help flags for the entity. + HelpFlags1 user.HelpFlags1 + // LastSeenAt is the time when the entity was last seen. + LastSeenAt time.Time + // Teams is the list of teams the entity is a member of. + Teams []int64 + // idP Groups that the entity is a member of. This is only populated if the + // identity provider supports groups. + Groups []string + // OAuthToken is the OAuth token used to authenticate the entity. + OAuthToken *oauth2.Token + // SessionToken is the session token used to authenticate the entity. + SessionToken *usertoken.UserToken + // ClientParams are hints for the auth service on how to handle the identity. + // Set by the authenticating client. + ClientParams ClientParams + // Permissions is the list of permissions the entity has. + Permissions map[int64]map[string][]string + // IDToken is a signed token representing the identity that can be forwarded to plugins and external services. + // Will only be set when featuremgmt.FlagIdForwarding is enabled. + IDToken string +} + +// Role returns the role of the identity in the active organization. +func (i *Identity) Role() org.RoleType { + return i.OrgRoles[i.OrgID] +} + +// NamespacedID returns the namespace, e.g. "user" and the id for that namespace +func (i *Identity) NamespacedID() (string, int64) { + split := strings.Split(i.ID, ":") + if len(split) != 2 { + return "", -1 + } + + id, err := strconv.ParseInt(split[1], 10, 64) + if err != nil { + // FIXME (kalleep): Improve error handling + return "", -1 + } + + return split[0], id +} + +// SignedInUser returns a SignedInUser from the identity. +func (i *Identity) SignedInUser() *user.SignedInUser { + var isGrafanaAdmin bool + if i.IsGrafanaAdmin != nil { + isGrafanaAdmin = *i.IsGrafanaAdmin + } + + u := &user.SignedInUser{ + UserID: 0, + OrgID: i.OrgID, + OrgName: i.OrgName, + OrgRole: i.Role(), + Login: i.Login, + Name: i.Name, + Email: i.Email, + AuthenticatedBy: i.AuthenticatedBy, + IsGrafanaAdmin: isGrafanaAdmin, + IsAnonymous: i.IsAnonymous, + IsDisabled: i.IsDisabled, + HelpFlags1: i.HelpFlags1, + LastSeenAt: i.LastSeenAt, + Teams: i.Teams, + Permissions: i.Permissions, + IDToken: i.IDToken, + } + + namespace, id := i.NamespacedID() + if namespace == NamespaceAPIKey { + u.ApiKeyID = id + } else { + u.UserID = id + u.IsServiceAccount = namespace == NamespaceServiceAccount + } + + return u +} + +func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { + _, id := i.NamespacedID() + return login.ExternalUserInfo{ + OAuthToken: i.OAuthToken, + AuthModule: i.AuthenticatedBy, + AuthId: i.AuthID, + UserId: id, + Email: i.Email, + Login: i.Login, + Name: i.Name, + Groups: i.Groups, + OrgRoles: i.OrgRoles, + IsGrafanaAdmin: i.IsGrafanaAdmin, + IsDisabled: i.IsDisabled, + } +} + +// IdentityFromSignedInUser creates an identity from a SignedInUser. +func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams, authenticatedBy string) *Identity { + return &Identity{ + ID: id, + OrgID: usr.OrgID, + OrgName: usr.OrgName, + OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, + Login: usr.Login, + Name: usr.Name, + Email: usr.Email, + AuthenticatedBy: authenticatedBy, + IsGrafanaAdmin: &usr.IsGrafanaAdmin, + IsDisabled: usr.IsDisabled, + HelpFlags1: usr.HelpFlags1, + LastSeenAt: usr.LastSeenAt, + Teams: usr.Teams, + ClientParams: params, + Permissions: usr.Permissions, + IDToken: usr.IDToken, + } +} diff --git a/pkg/services/user/identity.go b/pkg/services/user/identity.go index f58df59f4e6..776c9775b1c 100644 --- a/pkg/services/user/identity.go +++ b/pkg/services/user/identity.go @@ -27,6 +27,9 @@ type SignedInUser struct { Teams []int64 // Permissions grouped by orgID and actions Permissions map[int64]map[string][]string `json:"-"` + // IDToken is a signed token representing the identity that can be forwarded to plugins and external services. + // Will only be set when featuremgmt.FlagIdForwarding is enabled. + IDToken string `json:"-" xorm:"-"` } func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { @@ -210,3 +213,7 @@ func (u *SignedInUser) GetDisplayName() string { func (u *SignedInUser) GetAuthenticatedBy() string { return u.AuthenticatedBy } + +func (u *SignedInUser) GetIDToken() string { + return u.IDToken +}