Skip to content

Commit 6f62d97

Browse files
authored
JWT Authentication: Add support for specifying groups in auth.jwt for teamsync (grafana#82175)
* merge JSON search logic * document public methods * improve test coverage * use separate JWT setting struct * correct use of cfg.JWTAuth * add group tests * fix DynMap typing * add settings to default ini * add groups option to devenv path * fix test * lint * revert jwt-proxy change * remove redundant check * fix parallel test
1 parent 32a1f39 commit 6f62d97

28 files changed

+601
-509
lines changed

conf/defaults.ini

+1
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ key_file =
840840
key_id =
841841
role_attribute_path =
842842
role_attribute_strict = false
843+
groups_attribute_path =
843844
auto_sign_up = false
844845
url_login = false
845846
allow_assign_grafana_admin = false

conf/sample.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@
774774
# Use in conjunction with key_file in case the JWT token's header specifies a key ID in "kid" field
775775
;key_id = some-key-id
776776
;role_attribute_path =
777+
;groups_attribute_path =
777778
;role_attribute_strict = false
778779
;auto_sign_up = false
779780
;url_login = false
@@ -1639,4 +1640,3 @@
16391640
[public_dashboards]
16401641
# Set to false to disable public dashboards
16411642
;enabled = true
1642-

devenv/docker/blocks/auth/jwt_proxy/readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ expect_claims = {"iss": "http://env.grafana.local:8087/realms/grafana", "azp": "
2424
auto_sign_up = true
2525
role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
2626
role_attribute_strict = false
27+
groups_attribute_path = groups[]
2728
allow_assign_grafana_admin = true
2829
```
2930

pkg/api/admin_users_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,9 @@ func Test_AdminUpdateUserPermissions(t *testing.T) {
320320
case login.GenericOAuthModule:
321321
socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync}
322322
case login.JWTModule:
323-
cfg.JWTAuthEnabled = tc.authEnabled
324-
cfg.JWTAuthSkipOrgRoleSync = tc.skipOrgRoleSync
325-
cfg.JWTAuthAllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
323+
cfg.JWTAuth.Enabled = tc.authEnabled
324+
cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync
325+
cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
326326
}
327327

328328
hs := &HTTPServer{

pkg/api/frontendsettings.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
173173
AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
174174
AuthProxyEnabled: hs.Cfg.AuthProxyEnabled,
175175
LdapEnabled: hs.Cfg.LDAPAuthEnabled,
176-
JwtHeaderName: hs.Cfg.JWTAuthHeaderName,
177-
JwtUrlLogin: hs.Cfg.JWTAuthURLLogin,
176+
JwtHeaderName: hs.Cfg.JWTAuth.HeaderName,
177+
JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin,
178178
AlertingErrorOrTimeout: hs.Cfg.AlertingErrorOrTimeout,
179179
AlertingNoDataOrNullValues: hs.Cfg.AlertingNoDataOrNullValues,
180180
AlertingMinInterval: hs.Cfg.AlertingMinInterval,
@@ -321,7 +321,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
321321
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
322322
SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync,
323323
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
324-
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuthSkipOrgRoleSync,
324+
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync,
325325
GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]),
326326
GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]),
327327
GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]),

pkg/api/user_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,9 @@ func Test_GetUserByID(t *testing.T) {
302302
case login.GenericOAuthModule:
303303
socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync}
304304
case login.JWTModule:
305-
cfg.JWTAuthEnabled = tc.authEnabled
306-
cfg.JWTAuthSkipOrgRoleSync = tc.skipOrgRoleSync
307-
cfg.JWTAuthAllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
305+
cfg.JWTAuth.Enabled = tc.authEnabled
306+
cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync
307+
cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
308308
}
309309

310310
hs := &HTTPServer{

pkg/login/social/connectors/common.go

-60
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package connectors
22

33
import (
44
"context"
5-
"encoding/json"
6-
"errors"
75
"fmt"
86
"io"
97
"net/http"
@@ -12,7 +10,6 @@ import (
1210
"strconv"
1311
"strings"
1412

15-
"github.com/jmespath/go-jmespath"
1613
"github.com/mitchellh/mapstructure"
1714
"golang.org/x/oauth2"
1815

@@ -96,63 +93,6 @@ func (s *SocialBase) httpGet(ctx context.Context, client *http.Client, url strin
9693
return response, nil
9794
}
9895

99-
func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (any, error) {
100-
if attributePath == "" {
101-
return "", errors.New("no attribute path specified")
102-
}
103-
104-
if len(data) == 0 {
105-
return "", errors.New("empty user info JSON response provided")
106-
}
107-
108-
var buf any
109-
if err := json.Unmarshal(data, &buf); err != nil {
110-
return "", fmt.Errorf("%v: %w", "failed to unmarshal user info JSON response", err)
111-
}
112-
113-
val, err := jmespath.Search(attributePath, buf)
114-
if err != nil {
115-
return "", fmt.Errorf("failed to search user info JSON response with provided path: %q: %w", attributePath, err)
116-
}
117-
118-
return val, nil
119-
}
120-
121-
func (s *SocialBase) searchJSONForStringAttr(attributePath string, data []byte) (string, error) {
122-
val, err := s.searchJSONForAttr(attributePath, data)
123-
if err != nil {
124-
return "", err
125-
}
126-
127-
strVal, ok := val.(string)
128-
if ok {
129-
return strVal, nil
130-
}
131-
132-
return "", nil
133-
}
134-
135-
func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []byte) ([]string, error) {
136-
val, err := s.searchJSONForAttr(attributePath, data)
137-
if err != nil {
138-
return []string{}, err
139-
}
140-
141-
ifArr, ok := val.([]any)
142-
if !ok {
143-
return []string{}, nil
144-
}
145-
146-
result := []string{}
147-
for _, v := range ifArr {
148-
if strVal, ok := v.(string); ok {
149-
result = append(result, strVal)
150-
}
151-
}
152-
153-
return result, nil
154-
}
155-
15696
func createOAuthConfig(info *social.OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config {
15797
var authStyle oauth2.AuthStyle
15898
switch strings.ToLower(info.AuthStyle) {

pkg/login/social/connectors/generic_oauth.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
363363
}
364364

365365
if s.emailAttributePath != "" {
366-
email, err := s.searchJSONForStringAttr(s.emailAttributePath, data.rawJSON)
366+
email, err := util.SearchJSONForStringAttr(s.emailAttributePath, data.rawJSON)
367367
if err != nil {
368368
s.log.Error("Failed to search JSON for attribute", "error", err)
369369
} else if email != "" {
@@ -395,7 +395,7 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
395395

396396
if s.loginAttributePath != "" {
397397
s.log.Debug("Searching for login among JSON", "loginAttributePath", s.loginAttributePath)
398-
login, err := s.searchJSONForStringAttr(s.loginAttributePath, data.rawJSON)
398+
login, err := util.SearchJSONForStringAttr(s.loginAttributePath, data.rawJSON)
399399
if err != nil {
400400
s.log.Error("Failed to search JSON for login attribute", "error", err)
401401
}
@@ -415,7 +415,7 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
415415

416416
func (s *SocialGenericOAuth) extractUserName(data *UserInfoJson) string {
417417
if s.nameAttributePath != "" {
418-
name, err := s.searchJSONForStringAttr(s.nameAttributePath, data.rawJSON)
418+
name, err := util.SearchJSONForStringAttr(s.nameAttributePath, data.rawJSON)
419419
if err != nil {
420420
s.log.Error("Failed to search JSON for attribute", "error", err)
421421
} else if name != "" {
@@ -443,7 +443,7 @@ func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error)
443443
return []string{}, nil
444444
}
445445

446-
return s.searchJSONForStringArrayAttr(s.groupsAttributePath, data.rawJSON)
446+
return util.SearchJSONForStringSliceAttr(s.groupsAttributePath, data.rawJSON)
447447
}
448448

449449
func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) {
@@ -554,7 +554,7 @@ func (s *SocialGenericOAuth) fetchTeamMembershipsFromTeamsUrl(ctx context.Contex
554554
return nil, err
555555
}
556556

557-
return s.searchJSONForStringArrayAttr(s.teamIdsAttributePath, response.Body)
557+
return util.SearchJSONForStringSliceAttr(s.teamIdsAttributePath, response.Body)
558558
}
559559

560560
func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *http.Client) ([]string, bool) {

0 commit comments

Comments
 (0)