mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4825707853
commit
9e704fec3c
@ -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]
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
\.
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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`.
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -317,17 +317,20 @@ type Cfg struct {
|
||||
OAuthCookieMaxAge int
|
||||
|
||||
// JWT Auth
|
||||
JWTAuthEnabled bool
|
||||
JWTAuthHeaderName string
|
||||
JWTAuthURLLogin bool
|
||||
JWTAuthEmailClaim string
|
||||
JWTAuthUsernameClaim string
|
||||
JWTAuthExpectClaims string
|
||||
JWTAuthJWKSetURL string
|
||||
JWTAuthCacheTTL time.Duration
|
||||
JWTAuthKeyFile string
|
||||
JWTAuthJWKSetFile string
|
||||
JWTAuthAutoSignUp bool
|
||||
JWTAuthEnabled bool
|
||||
JWTAuthHeaderName string
|
||||
JWTAuthURLLogin bool
|
||||
JWTAuthEmailClaim string
|
||||
JWTAuthUsernameClaim string
|
||||
JWTAuthExpectClaims string
|
||||
JWTAuthJWKSetURL string
|
||||
JWTAuthCacheTTL time.Duration
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user