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 <joao.guerreiro@grafana.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
pull/61711/head
linoman 2 years ago committed by GitHub
parent 3debfd0ca7
commit 4d095547f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      conf/defaults.ini
  2. 3
      conf/sample.ini
  3. 11
      docs/sources/setup-grafana/configure-security/configure-authentication/jwt/index.md
  4. 1
      packages/grafana-data/src/types/config.ts
  5. 1
      pkg/api/frontendsettings.go
  6. 49
      pkg/services/authn/clients/jwt.go
  7. 46
      pkg/services/contexthandler/auth_jwt.go
  8. 4
      pkg/setting/setting.go
  9. 3
      public/app/features/admin/UserAdminPage.tsx

@ -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]

@ -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

@ -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

@ -223,6 +223,7 @@ export interface AuthSettings {
OAuthSkipOrgRoleUpdateSync?: boolean;
SAMLSkipOrgRoleSync?: boolean;
LDAPSkipOrgRoleSync?: boolean;
JWTAuthSkipOrgRoleSync?: boolean;
GrafanaComSkipOrgRoleSync?: boolean;
AzureADSkipOrgRoleSync?: boolean;
DisableSyncLock?: boolean;

@ -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,

@ -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
}
}
}

@ -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
}
}
}

@ -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)

@ -106,6 +106,7 @@ export class UserAdminPage extends PureComponent<Props> {
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<Props> {
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));

Loading…
Cancel
Save