OAuth: Make generic teams URL and JMES path configurable (#37233)

OAuth: Make generic teams URL and JMES path configurable
pull/38959/head
Djairho Geuens 4 years ago committed by GitHub
parent fbdaf56a84
commit 0383becc46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      conf/defaults.ini
  2. 2
      conf/sample.ini
  3. 4
      docs/sources/auth/generic-oauth.md
  4. 61
      pkg/login/social/generic_oauth.go
  5. 48
      pkg/login/social/social.go

@ -498,9 +498,11 @@ role_attribute_path =
role_attribute_strict = false
groups_attribute_path =
id_token_attribute_name =
team_ids_attribute_path =
auth_url =
token_url =
api_url =
teams_url =
allowed_domains =
team_ids =
allowed_organizations =

@ -483,12 +483,14 @@
;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user
;teams_url =
;allowed_domains =
;team_ids =
;allowed_organizations =
;role_attribute_path =
;role_attribute_strict = false
;groups_attribute_path =
;team_ids_attribute_path =
;tls_skip_verify_insecure = false
;tls_client_cert =
;tls_client_key =

@ -72,6 +72,8 @@ Grafana also attempts to map teams through OAuth as described below.
Check for the presence of groups using the [JMESPath](http://jmespath.org/examples.html) specified via the `groups_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the UserInfo endpoint specified via the `api_url` configuration option. After evaluating the `groups_attribute_path` JMESPath expression, the result should be a string array of groups.
Furthermore, Grafana will check for the presence of at least one of the teams specified via the `team_ids` configuration option using the [JMESPath](http://jmespath.org/examples.html) specified via the `team_ids_attribute_path` configuration option. The JSON used for the path lookup is the HTTP response obtained from querying the Teams endpoint specified via the `teams_url` configuration option (using `/teams` as a fallback endpoint). The result should be a string array of Grafana Team IDs. Using this setting ensures that only certain teams is allowed to authenticate to Grafana using your OAuth provider.
See [JMESPath examples](#jmespath-examples) for more information.
Customize user login using `login_attribute_path` configuration option. Order of operations is as follows:
@ -126,6 +128,8 @@ scopes = account email
auth_url = https://bitbucket.org/site/oauth2/authorize
token_url = https://bitbucket.org/site/oauth2/access_token
api_url = https://api.bitbucket.org/2.0/user
teams_url = https://api.bitbucket.org/2.0/user/permissions/workspaces
team_ids_attribute_path = values[*].workspace.slug
team_ids =
allowed_organizations =
```

@ -11,6 +11,7 @@ import (
"net/http"
"net/mail"
"regexp"
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util/errutil"
@ -21,6 +22,7 @@ type SocialGenericOAuth struct {
*SocialBase
allowedOrganizations []string
apiUrl string
teamsUrl string
emailAttributeName string
emailAttributePath string
loginAttributePath string
@ -29,7 +31,8 @@ type SocialGenericOAuth struct {
roleAttributeStrict bool
groupsAttributePath string
idTokenAttributeName string
teamIds []int
teamIdsAttributePath string
teamIds []string
}
func (s *SocialGenericOAuth) Type() int {
@ -41,8 +44,8 @@ func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
return true
}
teamMemberships, ok := s.FetchTeamMemberships(client)
if !ok {
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
@ -412,15 +415,36 @@ func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, err
return email, nil
}
func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, bool) {
func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]string, error) {
var err error
var ids []string
if s.teamsUrl == "" {
ids, err = s.fetchTeamMembershipsFromDeprecatedTeamsUrl(client)
} else {
ids, err = s.fetchTeamMembershipsFromTeamsUrl(client)
}
if err == nil {
s.log.Debug("Received team memberships", "ids", ids)
}
return ids, err
}
func (s *SocialGenericOAuth) fetchTeamMembershipsFromDeprecatedTeamsUrl(client *http.Client) ([]string, error) {
var response httpGetResponse
var err error
var ids []string
type Record struct {
Id int `json:"id"`
}
response, err := s.httpGet(client, fmt.Sprintf(s.apiUrl+"/teams"))
response, err = s.httpGet(client, fmt.Sprintf(s.apiUrl+"/teams"))
if err != nil {
s.log.Error("Error getting team memberships", "url", s.apiUrl+"/teams", "error", err)
return nil, false
return []string{}, err
}
var records []Record
@ -428,17 +452,32 @@ func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, b
err = json.Unmarshal(response.Body, &records)
if err != nil {
s.log.Error("Error decoding team memberships response", "raw_json", string(response.Body), "error", err)
return nil, false
return []string{}, err
}
var ids = make([]int, len(records))
ids = make([]string, len(records))
for i, record := range records {
ids[i] = record.Id
ids[i] = strconv.Itoa(record.Id)
}
return ids, nil
}
func (s *SocialGenericOAuth) fetchTeamMembershipsFromTeamsUrl(client *http.Client) ([]string, error) {
if s.teamIdsAttributePath == "" {
return []string{}, nil
}
s.log.Debug("Received team memberships", "ids", ids)
var response httpGetResponse
var err error
response, err = s.httpGet(client, fmt.Sprintf(s.teamsUrl))
if err != nil {
s.log.Error("Error getting team memberships", "url", s.teamsUrl, "error", err)
return nil, err
}
return ids, true
return s.searchJSONForStringArrayAttr(s.teamIdsAttributePath, response.Body)
}
func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string, bool) {

@ -38,9 +38,11 @@ type OAuthInfo struct {
RoleAttributePath string
RoleAttributeStrict bool
GroupsAttributePath string
TeamIdsAttributePath string
AllowedDomains []string
HostedDomain string
ApiUrl string
TeamsUrl string
AllowSignup bool
Name string
TlsClientCert string
@ -60,26 +62,28 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
sec := cfg.Raw.Section("auth." + name)
info := &OAuthInfo{
ClientId: sec.Key("client_id").String(),
ClientSecret: sec.Key("client_secret").String(),
Scopes: util.SplitString(sec.Key("scopes").String()),
AuthUrl: sec.Key("auth_url").String(),
TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(),
Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
ClientId: sec.Key("client_id").String(),
ClientSecret: sec.Key("client_secret").String(),
Scopes: util.SplitString(sec.Key("scopes").String()),
AuthUrl: sec.Key("auth_url").String(),
TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(),
TeamsUrl: sec.Key("teams_url").String(),
Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
}
// when empty_scopes parameter exists and is true, overwrite scope with empty value
@ -162,6 +166,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
ss.socialMap["generic_oauth"] = &SocialGenericOAuth{
SocialBase: newSocialBase(name, &config, info),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
nameAttributePath: sec.Key("name_attribute_path").String(),
@ -170,7 +175,8 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
teamIds: sec.Key("team_ids").Ints(","),
teamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
teamIds: sec.Key("team_ids").Strings(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
}

Loading…
Cancel
Save