From 4d095547f83f7b92a32949fc620148a1d0359850 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 18 Jan 2023 13:59:50 +0100 Subject: [PATCH] Auth: Implement skip org role sync for jwt (#61647) * Add new config option * Add frontend control * Condition new auth broker with config option * Condition old auth broker with config option Co-authored-by: Jo Co-authored-by: Gabriel MABILLE --- conf/defaults.ini | 1 + conf/sample.ini | 3 ++ .../configure-authentication/jwt/index.md | 11 +++++ packages/grafana-data/src/types/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/services/authn/clients/jwt.go | 49 ++++++++++--------- pkg/services/contexthandler/auth_jwt.go | 46 +++++++++-------- pkg/setting/setting.go | 4 +- public/app/features/admin/UserAdminPage.tsx | 3 ++ 9 files changed, 75 insertions(+), 44 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index eeca9aa13d6..d9727443ed4 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -677,6 +677,7 @@ role_attribute_strict = false auto_sign_up = false url_login = false allow_assign_grafana_admin = false +skip_org_role_sync = false #################################### Auth LDAP ########################### [auth.ldap] diff --git a/conf/sample.ini b/conf/sample.ini index 284c8304f1c..748dc6c82bf 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -498,6 +498,9 @@ # Set to true to enable Azure authentication option for HTTP-based datasources. ;azure_auth_enabled = false +# Set to skip the organization role from JWT login and use system's role assignment instead. +; skip_org_role_sync = false + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md index 675c28d2a30..ec7a2725ba1 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md @@ -73,6 +73,17 @@ Grafana instance to include the JWT in the request's headers. In a scenario where it is not possible to rewrite the request headers you can use URL login instead. +## Skip organization role + +To skip the assignment of roles and permissions upon login via JWT and handle them via other mechanisms like the user interface, we can skip the organization role synchronization with the following configuration. + +```ini +[auth.jwt] +# ... + +skip_org_role_sync = true +``` + ### URL login `url_login` allows grafana to search for a JWT in the URL query parameter diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index d3919b65ce2..76281b6965f 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -223,6 +223,7 @@ export interface AuthSettings { OAuthSkipOrgRoleUpdateSync?: boolean; SAMLSkipOrgRoleSync?: boolean; LDAPSkipOrgRoleSync?: boolean; + JWTAuthSkipOrgRoleSync?: boolean; GrafanaComSkipOrgRoleSync?: boolean; AzureADSkipOrgRoleSync?: boolean; DisableSyncLock?: boolean; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 23f355753fd..1d54a81201c 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -148,6 +148,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync, "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync, + "JWTAuthSkipOrgRoleSync": hs.Cfg.JWTAuthSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, "AzureADSkipOrgRoleSync": hs.Cfg.AzureADSkipOrgRoleSync, "DisableSyncLock": hs.Cfg.DisableSyncLock, diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 5c3a23e523f..9eaf0543c8d 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -10,6 +10,7 @@ import ( "github.com/jmespath/go-jmespath" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/auth" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" @@ -83,30 +84,34 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi id.Name = name } - role, grafanaAdmin := s.extractRoleAndAdmin(claims) - if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { - s.log.Warn("extracted Role is invalid", "role", role, "auth_id", id.AuthID) - return nil, ErrJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role) - } - - if role.IsValid() { - var orgID int64 - // FIXME (jguer): GetIDForNewUser already has the auto assign information - // just neeeds the org role. Find a meaningful way to pass this default - // role to it (that doesn't involve id.OrgRoles[0] = role) - if s.cfg.AutoAssignOrg && s.cfg.AutoAssignOrgId > 0 { - orgID = int64(s.cfg.AutoAssignOrgId) - s.log.Debug("The user has a role assignment and organization membership is auto-assigned", - "role", role, "orgId", orgID) - } else { - orgID = int64(1) - s.log.Debug("The user has a role assignment and organization membership is not auto-assigned", - "role", role, "orgId", orgID) + var role roletype.RoleType + var grafanaAdmin bool + if !s.cfg.JWTAuthSkipOrgRoleSync { + role, grafanaAdmin = s.extractRoleAndAdmin(claims) + if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { + s.log.Warn("extracted Role is invalid", "role", role, "auth_id", id.AuthID) + return nil, ErrJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role) } - id.OrgRoles[orgID] = role - if s.cfg.JWTAuthAllowAssignGrafanaAdmin { - id.IsGrafanaAdmin = &grafanaAdmin + if role.IsValid() { + var orgID int64 + // FIXME (jguer): GetIDForNewUser already has the auto assign information + // just needs the org role. Find a meaningful way to pass this default + // role to it (that doesn't involve id.OrgRoles[0] = role) + if s.cfg.AutoAssignOrg && s.cfg.AutoAssignOrgId > 0 { + orgID = int64(s.cfg.AutoAssignOrgId) + s.log.Debug("The user has a role assignment and organization membership is auto-assigned", + "role", role, "orgId", orgID) + } else { + orgID = int64(1) + s.log.Debug("The user has a role assignment and organization membership is not auto-assigned", + "role", role, "orgId", orgID) + } + + id.OrgRoles[orgID] = role + if s.cfg.JWTAuthAllowAssignGrafanaAdmin { + id.IsGrafanaAdmin = &grafanaAdmin + } } } diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 79b51216eff..debc247aa95 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models/roletype" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -101,28 +102,31 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) extUser.Name = name } - role, grafanaAdmin := h.extractJWTRoleAndAdmin(claims) - if h.Cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { - ctx.Logger.Debug("Extracted Role is invalid") - ctx.JsonApiErr(http.StatusForbidden, InvalidRole, nil) - return true - } - - if role.IsValid() { - var orgID int64 - if h.Cfg.AutoAssignOrg && h.Cfg.AutoAssignOrgId > 0 { - orgID = int64(h.Cfg.AutoAssignOrgId) - ctx.Logger.Debug("The user has a role assignment and organization membership is auto-assigned", - "role", role, "orgId", orgID) - } else { - orgID = int64(1) - ctx.Logger.Debug("The user has a role assignment and organization membership is not auto-assigned", - "role", role, "orgId", orgID) + var role roletype.RoleType + var grafanaAdmin bool + if !h.Cfg.JWTAuthSkipOrgRoleSync { + role, grafanaAdmin = h.extractJWTRoleAndAdmin(claims) + if h.Cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { + ctx.Logger.Debug("Extracted Role is invalid") + ctx.JsonApiErr(http.StatusForbidden, InvalidRole, nil) + return true } - - extUser.OrgRoles[orgID] = role - if h.Cfg.JWTAuthAllowAssignGrafanaAdmin { - extUser.IsGrafanaAdmin = &grafanaAdmin + if role.IsValid() { + var orgID int64 + if h.Cfg.AutoAssignOrg && h.Cfg.AutoAssignOrgId > 0 { + orgID = int64(h.Cfg.AutoAssignOrgId) + ctx.Logger.Debug("The user has a role assignment and organization membership is auto-assigned", + "role", role, "orgId", orgID) + } else { + orgID = int64(1) + ctx.Logger.Debug("The user has a role assignment and organization membership is not auto-assigned", + "role", role, "orgId", orgID) + } + + extUser.OrgRoles[orgID] = role + if h.Cfg.JWTAuthAllowAssignGrafanaAdmin { + extUser.IsGrafanaAdmin = &grafanaAdmin + } } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 441adea7d0f..f45c028b6fc 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -348,6 +348,7 @@ type Cfg struct { JWTAuthRoleAttributePath string JWTAuthRoleAttributeStrict bool JWTAuthAllowAssignGrafanaAdmin bool + JWTAuthSkipOrgRoleSync bool // Dataproxy SendUserHeader bool @@ -1109,7 +1110,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.Logger.Warn("require_email_validation is enabled but smtp is disabled") } - // check old key name + // check old key name GrafanaComUrl = valueAsString(iniFile.Section("grafana_net"), "url", "") if GrafanaComUrl == "" { GrafanaComUrl = valueAsString(iniFile.Section("grafana_com"), "url", "https://grafana.com") @@ -1444,6 +1445,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.JWTAuthRoleAttributePath = valueAsString(authJWT, "role_attribute_path", "") cfg.JWTAuthRoleAttributeStrict = authJWT.Key("role_attribute_strict").MustBool(false) cfg.JWTAuthAllowAssignGrafanaAdmin = authJWT.Key("allow_assign_grafana_admin").MustBool(false) + cfg.JWTAuthSkipOrgRoleSync = authJWT.Key("skip_org_role_sync").MustBool(false) authProxy := iniFile.Section("auth.proxy") AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 71456012b4b..b99695af136 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -106,6 +106,7 @@ export class UserAdminPage extends PureComponent { render() { const { user, orgs, sessions, ldapSyncInfo, isLoading } = this.props; const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP'); + const isJWTUser = user?.authLabels?.includes('JWT'); const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList); const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); const isOAuthUserWithSkippableSync = @@ -125,11 +126,13 @@ export class UserAdminPage extends PureComponent { isSAMLUser || isLDAPUser || isAzureADUser || + isJWTUser || isGrafanaComUser )) || (!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) || (!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) || (!config.auth.LDAPSkipOrgRoleSync && isLDAPUser) || + (!config.auth.JWTAuthSkipOrgRoleSync && isJWTUser) || // both OAuthSkipOrgRoleUpdateSync and specific provider settings needs to be false for a user to be synced (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser));