Auth: Add Generic oauth skip org role sync setting (#62418)

* add: generic oauth skip org role sync

* add: docs

* add: backend login skip sync

* fix: docs typo

* add: tests

* remove public key

* fix markdown for generic oauth

* add: generic oauth to the configuration

* refactor: change debug to warn
This commit is contained in:
Eric Leijonmarck 2023-02-01 16:27:53 +00:00 committed by GitHub
parent 6b6b733229
commit 8ff19bd901
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 40 deletions

View File

@ -657,6 +657,7 @@ tls_client_ca =
use_pkce = false
auth_style =
allow_assign_grafana_admin = false
skip_org_role_sync = false
#################################### Basic Auth ##########################
[auth.basic]

View File

@ -936,10 +936,25 @@ The following table shows the OAuth provider's setting with the default value an
| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior |
| --- | --- | --- | --- |
| GitLab | false | false | User organization roles are set with `defaultRole` and cannot be changed |
| Github | true | false | User organization roles are set with `defaultRole` for GitLab, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. |
| GitLab | true | false | User organization roles are set with `defaultRole` for GitLab, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. |
| GitLab | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for GitLab synced users. |
| GitLab | true | true | User organization roles are set with `defaultRole` for GitLab. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. |
### [auth.generic_oauth] skip_org_role_sync
When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles, set the `skip_org_role_sync` option to `true`. the OAuth provider syncs organization roles and sets Grafana Admins.
This also impacts `allow_assign_grafana_admin` setting, by not syncing the grafana admin role from the OAuth provider.
> **Note:** There is a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers.
The following table shows the OAuth provider's setting with the default value and the skip org role sync setting.
| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior |
| --- | --- | --- | --- |
| Generic OAuth | false | false | User organization roles are set with `defaultRole` and cannot be changed |
| Generic OAuth | true | false | User organization roles are set with `defaultRole` for Generic OAuth, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. |
| Generic OAuth | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for Generic OAuth synced users. |
| Generic OAuth | true | true | User organization roles are set with `defaultRole` for Generic OAuth. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. |
### [auth.okta] skip_org_role_sync
When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles through Grafana's UI, set the `skip_org_role_sync` option to `true`.

View File

@ -217,7 +217,7 @@ By default, a refresh token is included in the response for the **Authorization
allow_sign_up = true
auto_login = false
client_id = <OpenID Connect Client ID from Centrify>
client_secret = <your generated OpenID Connect Client Secret"
client_secret = <your generated OpenID Connect Client Secret>
scopes = openid profile email
auth_url = https://<your domain>.my.centrify.com/OAuth2/Authorize/<Application ID>
token_url = https://<your domain>.my.centrify.com/OAuth2/Token/<Application ID>
@ -392,3 +392,16 @@ Payload:
...
}
```
## Skip organization role sync
To prevent the sync of organization roles from the OAuth provider, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana.
This also impacts the `allow_assign_grafana_admin` setting by not syncing the Grafana admin role from the OAuth provider.
```ini
[auth.generic_oauth]
# ..
# prevents the sync of org roles from the Oauth provider
skip_org_role_sync = true
``
```

View File

@ -235,5 +235,6 @@ export interface AuthSettings {
OktaSkipOrgRoleSync?: boolean;
AzureADSkipOrgRoleSync?: boolean;
GoogleSkipOrgRoleSync?: boolean;
GenericOAuthSkipOrgRoleSync?: boolean;
DisableSyncLock?: boolean;
}

View File

@ -6,17 +6,18 @@ import (
)
type FrontendSettingsAuthDTO struct {
OAuthSkipOrgRoleUpdateSync bool `json:"OAuthSkipOrgRoleUpdateSync"`
SAMLSkipOrgRoleSync bool `json:"SAMLSkipOrgRoleSync"`
LDAPSkipOrgRoleSync bool `json:"LDAPSkipOrgRoleSync"`
GoogleSkipOrgRoleSync bool `json:"GoogleSkipOrgRoleSync"`
JWTAuthSkipOrgRoleSync bool `json:"JWTAuthSkipOrgRoleSync"`
GrafanaComSkipOrgRoleSync bool `json:"GrafanaComSkipOrgRoleSync"`
AzureADSkipOrgRoleSync bool `json:"AzureADSkipOrgRoleSync"`
GithubSkipOrgRoleSync bool `json:"GithubSkipOrgRoleSync"`
GitLabSkipOrgRoleSync bool `json:"GitLabSkipOrgRoleSync"`
OktaSkipOrgRoleSync bool `json:"OktaSkipOrgRoleSync"`
DisableSyncLock bool `json:"DisableSyncLock"`
OAuthSkipOrgRoleUpdateSync bool `json:"OAuthSkipOrgRoleUpdateSync"`
SAMLSkipOrgRoleSync bool `json:"SAMLSkipOrgRoleSync"`
LDAPSkipOrgRoleSync bool `json:"LDAPSkipOrgRoleSync"`
GoogleSkipOrgRoleSync bool `json:"GoogleSkipOrgRoleSync"`
GenericOAuthSkipOrgRoleSync bool `json:"GenericOAuthSkipOrgRoleSync"`
JWTAuthSkipOrgRoleSync bool `json:"JWTAuthSkipOrgRoleSync"`
GrafanaComSkipOrgRoleSync bool `json:"GrafanaComSkipOrgRoleSync"`
AzureADSkipOrgRoleSync bool `json:"AzureADSkipOrgRoleSync"`
GithubSkipOrgRoleSync bool `json:"GithubSkipOrgRoleSync"`
GitLabSkipOrgRoleSync bool `json:"GitLabSkipOrgRoleSync"`
OktaSkipOrgRoleSync bool `json:"OktaSkipOrgRoleSync"`
DisableSyncLock bool `json:"DisableSyncLock"`
}
type FrontendSettingsBuildInfoDTO struct {

View File

@ -148,17 +148,18 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
DateFormats: hs.Cfg.DateFormats,
Auth: dtos.FrontendSettingsAuthDTO{
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
SAMLSkipOrgRoleSync: hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
GoogleSkipOrgRoleSync: hs.Cfg.GoogleSkipOrgRoleSync,
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuthSkipOrgRoleSync,
GrafanaComSkipOrgRoleSync: hs.Cfg.GrafanaComSkipOrgRoleSync,
AzureADSkipOrgRoleSync: hs.Cfg.AzureADSkipOrgRoleSync,
GithubSkipOrgRoleSync: hs.Cfg.GithubSkipOrgRoleSync,
GitLabSkipOrgRoleSync: hs.Cfg.GitLabSkipOrgRoleSync,
OktaSkipOrgRoleSync: hs.Cfg.OktaSkipOrgRoleSync,
DisableSyncLock: hs.Cfg.DisableSyncLock,
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
SAMLSkipOrgRoleSync: hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
GoogleSkipOrgRoleSync: hs.Cfg.GoogleSkipOrgRoleSync,
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuthSkipOrgRoleSync,
GrafanaComSkipOrgRoleSync: hs.Cfg.GrafanaComSkipOrgRoleSync,
GenericOAuthSkipOrgRoleSync: hs.Cfg.GenericOAuthSkipOrgRoleSync,
AzureADSkipOrgRoleSync: hs.Cfg.AzureADSkipOrgRoleSync,
GithubSkipOrgRoleSync: hs.Cfg.GithubSkipOrgRoleSync,
GitLabSkipOrgRoleSync: hs.Cfg.GitLabSkipOrgRoleSync,
OktaSkipOrgRoleSync: hs.Cfg.OktaSkipOrgRoleSync,
DisableSyncLock: hs.Cfg.DisableSyncLock,
},
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{

View File

@ -143,15 +143,20 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
}
if userInfo.Role == "" {
role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, []string{}, true)
if role != "" {
s.log.Debug("Setting user info role from extracted role")
if !s.skipOrgRoleSync {
role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, []string{}, true)
if role != "" {
s.log.Debug("Setting user info role from extracted role")
userInfo.Role = role
if s.allowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
userInfo.Role = role
if s.allowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
}
}
if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync {
s.log.Warn("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
}
}
if len(userInfo.Groups) == 0 {

View File

@ -247,6 +247,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
tests := []struct {
Name string
SkipOrgRoleSync bool
AllowAssignGrafanaAdmin bool
ResponseBody interface{}
OAuth2Extra interface{}
@ -447,11 +448,29 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Editor",
},
{
Name: "Given skip org role sync set to true, with a valid id_token, a valid advanced JMESPath role path, a valid API response, no org role should be set",
SkipOrgRoleSync: true,
OAuth2Extra: map[string]interface{}{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
},
ResponseBody: map[string]interface{}{
"info": map[string]interface{}{
"roles": []string{"engineering", "SRE"},
},
},
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "",
},
}
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
provider.skipOrgRoleSync = test.SkipOrgRoleSync
t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody)

View File

@ -46,7 +46,9 @@ func GetAuthProviderLabel(authModule string) string {
return "JWT"
case AuthProxyAuthModule:
return "Auth Proxy"
case "oauth_generic_oauth":
return "Generic OAuth"
default:
return "OAuth" // FIXME: replace with "Unknown" and handle generic oauth as a case
return "Unknown"
}
}

View File

@ -22,11 +22,12 @@ import (
"time"
"github.com/gobwas/glob"
"github.com/prometheus/common/model"
"gopkg.in/ini.v1"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/prometheus/common/model"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
@ -434,6 +435,9 @@ type Cfg struct {
// Gitlab
GitLabSkipOrgRoleSync bool
// Generic OAuth
GenericOAuthSkipOrgRoleSync bool
// LDAP
LDAPEnabled bool
LDAPSkipOrgRoleSync bool
@ -1397,6 +1401,11 @@ func readAuthGitlabSettings(iniFile *ini.File, cfg *Cfg) {
cfg.GitLabSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false)
}
func readGenericOAuthSettings(iniFile *ini.File, cfg *Cfg) {
sec := iniFile.Section("auth.generic_oauth")
cfg.GenericOAuthSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false)
}
func readAuthOktaSettings(iniFile *ini.File, cfg *Cfg) {
sec := iniFile.Section("auth.okta")
cfg.OktaSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false)
@ -1462,6 +1471,9 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
// GitLab Auth
readAuthGitlabSettings(iniFile, cfg)
// Generic OAuth
readGenericOAuthSettings(iniFile, cfg)
// Okta Auth
readAuthOktaSettings(iniFile, cfg)

View File

@ -39,8 +39,6 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {
error?: UserAdminError;
}
const SyncedOAuthLabels: string[] = ['OAuth'];
export class UserAdminPage extends PureComponent<Props> {
async componentDidMount() {
const { match, loadAdminUserPage } = this.props;
@ -109,8 +107,6 @@ export class UserAdminPage extends PureComponent<Props> {
const isJWTUser = user?.authLabels?.includes('JWT');
const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList);
const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead);
const isOAuthUserWithSkippableSync =
user?.isExternal && user?.authLabels?.some((r) => SyncedOAuthLabels.includes(r));
const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML');
const isGoogleUser = user?.isExternal && user?.authLabels?.includes('Google');
const isGithubUser = user?.isExternal && user?.authLabels?.includes('GitHub');
@ -119,6 +115,7 @@ export class UserAdminPage extends PureComponent<Props> {
const isAzureADUser = user?.isExternal && user?.authLabels?.includes('AzureAD');
const isOktaUser = user?.isExternal && user?.authLabels?.includes('Okta');
const isGrafanaComUser = user?.isExternal && user?.authLabels?.includes('grafana.com');
const isGenericOAuthUser = user?.isExternal && user?.authLabels?.includes('Generic OAuth');
const isUserSynced =
!config.auth.DisableSyncLock &&
((user?.isExternal &&
@ -126,7 +123,7 @@ export class UserAdminPage extends PureComponent<Props> {
isAuthProxyUser ||
isGoogleUser ||
isGitLabUser ||
isOAuthUserWithSkippableSync ||
isGenericOAuthUser ||
isSAMLUser ||
isOktaUser ||
isLDAPUser ||
@ -135,7 +132,6 @@ export class UserAdminPage extends PureComponent<Props> {
isJWTUser ||
isGrafanaComUser
)) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) ||
(!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) ||
(!config.auth.LDAPSkipOrgRoleSync && isLDAPUser) ||
(!config.auth.JWTAuthSkipOrgRoleSync && isJWTUser) ||
@ -145,6 +141,7 @@ export class UserAdminPage extends PureComponent<Props> {
(!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GithubSkipOrgRoleSync && isGithubUser) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GitLabSkipOrgRoleSync && isGitLabUser) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GenericOAuthSkipOrgRoleSync && isGenericOAuthUser) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GoogleSkipOrgRoleSync && isGoogleUser));
const pageNav: NavModelItem = {

View File

@ -150,7 +150,7 @@ const UserListAdminPageUnConnected = ({
<Icon name="question-circle" />
</Tooltip>
</th>
<th style={{ width: '1%' }}></th>
<th style={{ width: '1%' }}>Synced from</th>
</tr>
</thead>
<tbody>