JWT: Add support for assigning org roles (#54277)

* feat: allow jwt role to be set

* chore: update documentation

* fix: cr suggestions

* fix: lint issues

* respect org auto assign and default org ID

* add server admin to devenv

Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Nicholas Wiersma 2022-09-07 14:00:33 +02:00 committed by GitHub
parent 4825707853
commit 9e704fec3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 289 additions and 17 deletions

View File

@ -611,7 +611,10 @@ jwk_set_file =
cache_ttl = 60m
expected_claims = {}
key_file =
role_attribute_path =
role_attribute_strict = false
auto_sign_up = false
allow_assign_grafana_admin = false
#################################### Auth LDAP ###########################
[auth.ldap]

View File

@ -597,8 +597,11 @@
;cache_ttl = 60m
;expected_claims = {"aud": ["foo", "bar"]}
;key_file = /path/to/key/file
;role_attribute_path =
;role_attribute_strict = false
;auto_sign_up = false
;url_login = false
;allow_assign_grafana_admin = false
#################################### Auth LDAP ##########################
[auth.ldap]

View File

@ -2243,6 +2243,7 @@ d4b2c483-1dd3-47f6-86bf-42548009918d \N password 74e29604-ff35-42bb-a26d-4d0b81e
cb2bd4ed-94b8-4259-bcaa-9250c3fb28d3 \N password 6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d 1657026827644 \N {"value":"q3Z59Nh/5bdezDEpCwEbMPu8d+VgJ5WetafXkR8l0FlsTTkSDQgW+j6GaM3seJR93p3/jCxyfsvZl062d1pq7w==","salt":"ohuHnjLnwF9dBZ38DRJJWg==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10
b58e1964-6466-40b2-879c-982b724d7f9c \N password 88692d07-bb9a-46cf-844c-7ff5c529cd04 1657026904515 \N {"value":"+/0zWjiJyE3+dCOEf0SO6G3n1/LsFAVoDAZREKTfN4vQ5xJH8srJoCjxcgb+bI1crMr8gknDlFyGRy7CpYn2VQ==","salt":"v/2okNt3wGOZz+x4DjOCDQ==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10
3ff7dd8f-a299-4b51-bf5d-99665ccfd313 \N password 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e 1657026943075 \N {"value":"nMYodMJMiq/J8g9vRPktGc7WSWnOKr6leMDZX4p9K9KgAUYeXFDSu+d29PWWn0rFn93dL0PNdIdHWNQhfkIDMg==","salt":"rmi9WLHgarmIXGukecSIig==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10
c9582964-cfdd-49a1-99fb-847604b0c78c \N password 1a85b7e0-4baa-420b-89f8-1cea43a540dd 1662480997923 \N {"value":"ViNTHbpBUNdtH1qGSlip7WFI8Z9lvcGQdbL8Yw48zUgB46jVFbD1eNrOw68p3ovDwfDCIJKm34EFNbw9/uzHSg==","salt":"Z9P8RfnrQwCn0xUTpWC2DQ==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10
\.
@ -2599,6 +2600,7 @@ c49bddc6-ec92-4caa-bc04-57ba80a92eb9 grafana f ${role_offline-access} offline_ac
c029a218-4519-4537-ae12-d8f3c27a0003 grafana f Grafana Server Admin serveradmin grafana \N grafana
c9a776f9-2740-435f-a725-4dbcc17a6c91 grafana f Grafana Viewer viewer grafana \N grafana
c4c74006-c346-48cf-8cf1-1617e3e1cde1 grafana f Grafana Editor editor grafana \N grafana
31150c12-e9fe-4465-a792-816b7298a595 grafana f Grafana Server Administrator grafanaadmin grafana \N grafana
\.
@ -3008,7 +3010,7 @@ df78645e-c32b-4160-b79f-42e622d71982 String jsonType.label
COPY public.realm (id, access_code_lifespan, user_action_lifespan, access_token_lifespan, account_theme, admin_theme, email_theme, enabled, events_enabled, events_expiration, login_theme, name, not_before, password_policy, registration_allowed, remember_me, reset_password_allowed, social, ssl_required, sso_idle_timeout, sso_max_lifespan, update_profile_on_soc_login, verify_email, master_admin_client, login_lifespan, internationalization_enabled, default_locale, reg_email_as_username, admin_events_enabled, admin_events_details_enabled, edit_username_allowed, otp_policy_counter, otp_policy_window, otp_policy_period, otp_policy_digits, otp_policy_alg, otp_policy_type, browser_flow, registration_flow, direct_grant_flow, reset_credentials_flow, client_auth_flow, offline_session_idle_timeout, revoke_refresh_token, access_token_life_implicit, login_with_email_allowed, duplicate_emails_allowed, docker_auth_flow, refresh_token_max_reuse, allow_user_managed_access, sso_max_lifespan_remember_me, sso_idle_timeout_remember_me) FROM stdin;
master 60 300 60 \N \N \N t f 0 \N master 1643820855 \N f f f f EXTERNAL 1800 36000 f f 3cd285ea-0f6e-43b6-ab5c-d021c33a551b 1800 f \N f f f f 0 1 30 6 HmacSHA1 totp ef998ef5-ca12-45db-a252-2e71b1419039 1695e7d2-ad80-4502-8479-8121a6e2a2f0 5f6f801e-0588-4a6e-860a-35483f5c1ec7 954b046d-2b24-405e-84ee-c44ffe603df2 023dc515-c259-42bb-88a8-2e8d84abca92 2592000 f 900 t f 032b05cf-0007-44da-a370-b42039f6b762 0 f 0 0
grafana 60 300 300 \N \N \N t f 0 \N grafana 1643820879 \N f f f f EXTERNAL 1800 36000 f f ef7f6eac-9fff-44aa-a86c-5125d52acc82 1800 f \N f f f f 0 1 30 6 HmacSHA1 totp a38aeb47-f27e-4e68-82ff-7cc7371a47a7 9d02badd-cb1c-4655-bf5e-f888861433ff b478ecfb-db7e-4797-a245-8fc3b4dec884 3085fb68-fc1f-4e1c-a8be-33fb45194b04 cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2592000 f 900 t f 95e02703-f5bc-4e04-8bef-f6adc2d8173f 0 f 0 0
grafana 60 300 300 \N \N \N t f 0 \N grafana 1662482026 \N f f f f EXTERNAL 1800 36000 f f ef7f6eac-9fff-44aa-a86c-5125d52acc82 1800 f \N f f f f 0 1 30 6 HmacSHA1 totp a38aeb47-f27e-4e68-82ff-7cc7371a47a7 9d02badd-cb1c-4655-bf5e-f888861433ff b478ecfb-db7e-4797-a245-8fc3b4dec884 3085fb68-fc1f-4e1c-a8be-33fb45194b04 cbb4b3ca-ced6-4046-8b59-f1c3959c7948 2592000 f 900 t f 95e02703-f5bc-4e04-8bef-f6adc2d8173f 0 f 0 0
\.
@ -3325,6 +3327,7 @@ COPY public.user_entity (id, email, email_constraint, email_verified, enabled, f
6db3c5e5-b84b-4f9d-a7a8-8d05b03c929d jwt-admin@example.org jwt-admin@example.org f t \N Admin JWT grafana jwt-admin 1657026796311 \N 0
88692d07-bb9a-46cf-844c-7ff5c529cd04 jwt-editor@example.com jwt-editor@example.com f t \N Editor JWT grafana jwt-editor 1657026894275 \N 0
8f58cbec-6e40-4bab-bff0-1c5ff899fe2e jwt-viewer@example.com jwt-viewer@example.com f t \N Viewer JWT grafana jwt-viewer 1657026933578 \N 0
1a85b7e0-4baa-420b-89f8-1cea43a540dd jwt-grafanaadmin@example.com jwt-grafanaadmin@example.com t t \N Grafanaadmin JWT grafana jwt-grafanaadmin 1662480983434 \N 0
\.
@ -3401,6 +3404,11 @@ f1311ecb-6a6a-49d6-bb16-5132daf93a64 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e
18a7066b-fe71-410e-9581-69f78347ec29 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e
c9a776f9-2740-435f-a725-4dbcc17a6c91 8f58cbec-6e40-4bab-bff0-1c5ff899fe2e
c4c74006-c346-48cf-8cf1-1617e3e1cde1 88692d07-bb9a-46cf-844c-7ff5c529cd04
c49bddc6-ec92-4caa-bc04-57ba80a92eb9 1a85b7e0-4baa-420b-89f8-1cea43a540dd
0f3d47bb-002a-4cd0-a502-725f224308a7 1a85b7e0-4baa-420b-89f8-1cea43a540dd
f1311ecb-6a6a-49d6-bb16-5132daf93a64 1a85b7e0-4baa-420b-89f8-1cea43a540dd
18a7066b-fe71-410e-9581-69f78347ec29 1a85b7e0-4baa-420b-89f8-1cea43a540dd
31150c12-e9fe-4465-a792-816b7298a595 1a85b7e0-4baa-420b-89f8-1cea43a540dd
\.

View File

@ -22,6 +22,9 @@ jwk_set_file = devenv/docker/blocks/auth/oauth/jwks.json
cache_ttl = 60m
expected_claims = {"iss": "http://env.grafana.local:8087/auth/realms/grafana", "azp": "grafana-oauth"}
auto_sign_up = true
role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
role_attribute_strict = false
allow_assign_grafana_admin = true
```
Add *env.grafana.local* to /etc/hosts (Mac/Linux) or C:\Windows\System32\drivers\etc\hosts (Windows):

View File

@ -143,3 +143,68 @@ You might also want to validate that other claims are really what you expect the
# This can be seen as a required "subset" of a JWT Claims Set.
expect_claims = {"iss": "https://your-token-issuer", "your-custom-claim": "foo"}
```
## Roles
Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. The JMESPath is applied to JWT token claims. The result after evaluation of the `role_attribute_path` JMESPath expression should be a valid Grafana role, for example, `Viewer`, `Editor` or `Admin`.
The organization that the role is assigned to can be configured using the `X-Grafana-Org-Id` header.
### JMESPath examples
To ease configuration of a proper JMESPath expression, you can test/evaluate expressions with custom payloads at http://jmespath.org/.
### Role mapping
If the `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role by default. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned.
**Basic example:**
In the following example user will get `Editor` as role when authenticating. The value of the property `role` will be the resulting role if the role is a proper Grafana role, i.e. `Viewer`, `Editor` or `Admin`.
Payload:
```json
{
...
"role": "Editor",
...
}
```
Config:
```bash
role_attribute_path = role
```
**Advanced example:**
In the following example user will get `Admin` as role when authenticating since it has a role `admin`. If a user has a role `editor` it will get `Editor` as role, otherwise `Viewer`.
Payload:
```json
{
...
"info": {
...
"roles": [
"engineer",
"admin",
],
...
},
...
}
```
Config:
```bash
role_attribute_path = contains(info.roles[*], 'admin') && 'Admin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer'
```
### Grafana Admin Role
If the `role_attribute_path` property returns a `GrafanaAdmin` role, Grafana Admin is not assigned by default, instead the `Admin` role is assigned. To allow `Grafana Admin` role to be assigned set `allow_assign_grafana_admin = true`.

View File

@ -5,6 +5,7 @@ import (
"errors"
"testing"
"github.com/grafana/grafana/pkg/services/org"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/models"
@ -14,6 +15,7 @@ import (
)
func TestMiddlewareJWTAuth(t *testing.T) {
const myEmail = "vladimir@example.com"
const id int64 = 12
const orgID int64 = 2
@ -34,6 +36,19 @@ func TestMiddlewareJWTAuth(t *testing.T) {
cfg.JWTAuthAutoSignUp = true
}
configureRole := func(cfg *setting.Cfg) {
cfg.JWTAuthEmailClaim = "sub"
cfg.JWTAuthRoleAttributePath = "role"
}
configureRoleStrict := func(cfg *setting.Cfg) {
cfg.JWTAuthRoleAttributeStrict = true
}
configureRoleAllowAdmin := func(cfg *setting.Cfg) {
cfg.JWTAuthAllowAssignGrafanaAdmin = true
}
token := "some-token"
middlewareScenario(t, "Valid token with valid login claim", func(t *testing.T, sc *scenarioContext) {
@ -58,7 +73,6 @@ func TestMiddlewareJWTAuth(t *testing.T) {
}, configure, configureUsernameClaim)
middlewareScenario(t, "Valid token with valid email claim", func(t *testing.T, sc *scenarioContext) {
myEmail := "vladimir@example.com"
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
@ -79,7 +93,6 @@ func TestMiddlewareJWTAuth(t *testing.T) {
}, configure, configureEmailClaim)
middlewareScenario(t, "Valid token with no user and auto_sign_up disabled", func(t *testing.T, sc *scenarioContext) {
myEmail := "vladimir@example.com"
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
@ -98,7 +111,6 @@ func TestMiddlewareJWTAuth(t *testing.T) {
}, configure, configureEmailClaim)
middlewareScenario(t, "Valid token with no user and auto_sign_up enabled", func(t *testing.T, sc *scenarioContext) {
myEmail := "vladimir@example.com"
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
@ -151,6 +163,97 @@ func TestMiddlewareJWTAuth(t *testing.T) {
assert.Equal(t, contexthandler.InvalidJWT, sc.respJson["message"])
}, configure, configureEmailClaim)
middlewareScenario(t, "Valid token with role", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"role": "Editor",
}, nil
}
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: id, OrgID: orgID, Email: myEmail, OrgRole: org.RoleEditor}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, org.RoleEditor, sc.context.OrgRole)
}, configure, configureAutoSignUp, configureRole)
middlewareScenario(t, "Valid token with invalid role", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"role": "test",
}, nil
}
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: id, OrgID: orgID, Email: myEmail, OrgRole: org.RoleViewer}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, org.RoleViewer, sc.context.OrgRole)
}, configure, configureAutoSignUp, configureRole)
middlewareScenario(t, "Valid token with invalid role in strict mode", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"role": "test",
}, nil
}
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: id, OrgID: orgID, Email: myEmail, OrgRole: org.RoleViewer}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, contexthandler.InvalidRole, sc.respJson["message"])
}, configure, configureAutoSignUp, configureRole, configureRoleStrict)
middlewareScenario(t, "Valid token with grafana admin role not allowed", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"role": "GrafanaAdmin",
}, nil
}
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: id, OrgID: orgID, Email: myEmail, OrgRole: org.RoleAdmin}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, org.RoleAdmin, sc.context.OrgRole)
assert.False(t, sc.context.IsGrafanaAdmin)
}, configure, configureAutoSignUp, configureRole)
middlewareScenario(t, "Valid token with grafana admin role allowed", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"role": "GrafanaAdmin",
}, nil
}
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: id, OrgID: orgID, Email: myEmail, OrgRole: org.RoleAdmin, IsGrafanaAdmin: true}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, org.RoleAdmin, sc.context.OrgRole)
assert.True(t, sc.context.IsGrafanaAdmin)
}, configure, configureAutoSignUp, configureRole, configureRoleAllowAdmin)
middlewareScenario(t, "Invalid token", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {

View File

@ -2,15 +2,21 @@ package contexthandler
import (
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/jmespath/go-jmespath"
)
const InvalidJWT = "Invalid JWT"
const UserNotFound = "User not found"
const (
InvalidJWT = "Invalid JWT"
InvalidRole = "Invalid Role"
UserNotFound = "User not found"
)
func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool {
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
@ -45,6 +51,7 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
extUser := &models.ExternalUserInfo{
AuthModule: "jwt",
AuthId: sub,
OrgRoles: map[int64]org.RoleType{},
}
if key := h.Cfg.JWTAuthUsernameClaim; key != "" {
@ -60,6 +67,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)
}
extUser.OrgRoles[orgID] = role
if h.Cfg.JWTAuthAllowAssignGrafanaAdmin {
extUser.IsGrafanaAdmin = &grafanaAdmin
}
}
if query.Login == "" && query.Email == "" {
ctx.Logger.Debug("Failed to get an authentication claim from JWT")
ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err)
@ -105,3 +137,52 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
return true
}
const roleGrafanaAdmin = "GrafanaAdmin"
func (h *ContextHandler) extractJWTRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
if h.Cfg.JWTAuthRoleAttributePath == "" {
return "", false
}
role, err := searchClaimsForStringAttr(h.Cfg.JWTAuthRoleAttributePath, claims)
if err != nil || role == "" {
return "", false
}
if role == roleGrafanaAdmin {
return org.RoleAdmin, true
}
return org.RoleType(role), false
}
func searchClaimsForAttr(attributePath string, claims map[string]interface{}) (interface{}, error) {
if attributePath == "" {
return "", errors.New("no attribute path specified")
}
if len(claims) == 0 {
return "", errors.New("empty claims provided")
}
val, err := jmespath.Search(attributePath, claims)
if err != nil {
return "", fmt.Errorf("failed to search claims with provided path: %q: %w", attributePath, err)
}
return val, nil
}
func searchClaimsForStringAttr(attributePath string, claims map[string]interface{}) (string, error) {
val, err := searchClaimsForAttr(attributePath, claims)
if err != nil {
return "", err
}
strVal, ok := val.(string)
if ok {
return strVal, nil
}
return "", nil
}

View File

@ -328,6 +328,9 @@ type Cfg struct {
JWTAuthKeyFile string
JWTAuthJWKSetFile string
JWTAuthAutoSignUp bool
JWTAuthRoleAttributePath string
JWTAuthRoleAttributeStrict bool
JWTAuthAllowAssignGrafanaAdmin bool
// Dataproxy
SendUserHeader bool
@ -1322,6 +1325,9 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
cfg.JWTAuthKeyFile = valueAsString(authJWT, "key_file", "")
cfg.JWTAuthJWKSetFile = valueAsString(authJWT, "jwk_set_file", "")
cfg.JWTAuthAutoSignUp = authJWT.Key("auto_sign_up").MustBool(false)
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)
authProxy := iniFile.Section("auth.proxy")
AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)