Okta OAuth provider (team sync support) (#22972)
* Okta OAuth support * Chore: fix linter error * Chore: move IsEmailAllowed to SocialBase * Chore: move IsSignupAllowed to SocialBase * Chore: review fixes * Okta: support allowed_groups * Okta: default config * Chore: move extractEmail() to OktaClaims struct * Chore: review fixes * generic_oauth_test: Handle error cases Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * generic_oauth_test: Handle error cases Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Docs: Okta OAuth * Chore: don't return expected errors from searchJSONForAttr * Docs: role mapping * Chore: review fixes (searchJSONForAttr) * Docs: review fixes * Update docs/sources/auth/okta.md Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Update docs/sources/auth/okta.md Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Chore: log error if searchJSONForAttr failed * Docs: add Okta login link * Docs: review fixes * Docs: add reference to the org roles Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
co-authored by
Arve Knudsen
parent
703476b3ae
commit
7afdfd2ef4
@@ -386,6 +386,21 @@ token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
|
|||||||
allowed_domains =
|
allowed_domains =
|
||||||
allowed_groups =
|
allowed_groups =
|
||||||
|
|
||||||
|
#################################### Okta OAuth #######################
|
||||||
|
[auth.okta]
|
||||||
|
name = Okta
|
||||||
|
enabled = false
|
||||||
|
allow_sign_up = true
|
||||||
|
client_id = some_id
|
||||||
|
client_secret = some_secret
|
||||||
|
scopes = openid profile email groups
|
||||||
|
auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
|
||||||
|
token_url = https://<tenant-id>.okta.com/oauth2/v1/token
|
||||||
|
api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
|
||||||
|
allowed_domains =
|
||||||
|
allowed_groups =
|
||||||
|
role_attribute_path =
|
||||||
|
|
||||||
#################################### Generic OAuth #######################
|
#################################### Generic OAuth #######################
|
||||||
[auth.generic_oauth]
|
[auth.generic_oauth]
|
||||||
name = OAuth
|
name = OAuth
|
||||||
|
|||||||
@@ -376,6 +376,21 @@
|
|||||||
;allowed_domains =
|
;allowed_domains =
|
||||||
;allowed_groups =
|
;allowed_groups =
|
||||||
|
|
||||||
|
#################################### Okta OAuth #######################
|
||||||
|
[auth.okta]
|
||||||
|
;name = Okta
|
||||||
|
;enabled = false
|
||||||
|
;allow_sign_up = true
|
||||||
|
;client_id = some_id
|
||||||
|
;client_secret = some_secret
|
||||||
|
;scopes = openid profile email groups
|
||||||
|
;auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
|
||||||
|
;token_url = https://<tenant-id>.okta.com/oauth2/v1/token
|
||||||
|
;api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
|
||||||
|
;allowed_domains =
|
||||||
|
;allowed_groups =
|
||||||
|
;role_attribute_path =
|
||||||
|
|
||||||
#################################### Generic OAuth ##########################
|
#################################### Generic OAuth ##########################
|
||||||
[auth.generic_oauth]
|
[auth.generic_oauth]
|
||||||
;enabled = false
|
;enabled = false
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
+++
|
||||||
|
title = "Okta OAuth2 authentication"
|
||||||
|
description = "Grafana Okta OAuth Guide "
|
||||||
|
keywords = ["grafana", "configuration", "documentation", "oauth"]
|
||||||
|
type = "docs"
|
||||||
|
[menu.docs]
|
||||||
|
name = "Okta"
|
||||||
|
identifier = "okta_oauth2"
|
||||||
|
parent = "authentication"
|
||||||
|
weight = 3
|
||||||
|
+++
|
||||||
|
|
||||||
|
# Okta OAuth2 authentication
|
||||||
|
|
||||||
|
> Only available in Grafana v7.0+
|
||||||
|
|
||||||
|
The Okta authentication allows your Grafana users to log in by using an external Okta authorization server.
|
||||||
|
|
||||||
|
## Create an Okta application
|
||||||
|
|
||||||
|
Before you can sign a user in, you need to create an Okta application from the Okta Developer Console.
|
||||||
|
|
||||||
|
1. Log in to the [Okta portal](https://login.okta.com/).
|
||||||
|
|
||||||
|
1. Go to Admin and then select **Developer Console**.
|
||||||
|
|
||||||
|
1. Select **Applications**, then **Add Application**.
|
||||||
|
|
||||||
|
1. Pick **Web** as the platform.
|
||||||
|
|
||||||
|
1. Enter a name for your application (or leave the default value).
|
||||||
|
|
||||||
|
1. Add the **Base URI** of your application, such as https://grafana.example.com.
|
||||||
|
|
||||||
|
1. Enter values for the **Login redirect URI**. Use **Base URI** and append it with `/login/okta`, for example: https://grafana.example.com/login/okta.
|
||||||
|
|
||||||
|
1. Click **Done** to finish creating the Okta application.
|
||||||
|
|
||||||
|
## Enable Okta Oauth in Grafana
|
||||||
|
|
||||||
|
1. Add the following to the [Grafana configuration file]({{< relref "../installation/configuration.md#config-file-locations" >}}):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[auth.okta]
|
||||||
|
name = Okta
|
||||||
|
enabled = true
|
||||||
|
allow_sign_up = true
|
||||||
|
client_id = some_id
|
||||||
|
client_secret = some_secret
|
||||||
|
scopes = openid profile email groups
|
||||||
|
auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
|
||||||
|
token_url = https://<tenant-id>.okta.com/oauth2/v1/token
|
||||||
|
api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
|
||||||
|
allowed_domains =
|
||||||
|
allowed_groups =
|
||||||
|
role_attribute_path =
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure allowed groups and domains
|
||||||
|
|
||||||
|
To limit access to authenticated users that are members of one or more groups, set `allowed_groups`
|
||||||
|
to a comma- or space-separated list of Okta groups.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
allowed_groups = Developers, Admins
|
||||||
|
```
|
||||||
|
|
||||||
|
The `allowed_domains` option limits access to the users belonging to the specific domains. Domains should be separated by space or comma.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
allowed_domains = mycompany.com mycompany.org
|
||||||
|
```
|
||||||
|
|
||||||
|
### Map roles
|
||||||
|
|
||||||
|
Grafana can attempt to do role mapping through Okta OAuth. In order to achieve this, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option.
|
||||||
|
|
||||||
|
Grafana uses JSON obtained from querying the `/userinfo` endpoint for the path lookup. The result after evaluating the `role_attribute_path` JMESPath expression needs to be a valid Grafana role, i.e. `Viewer`, `Editor` or `Admin`. Refer to [Organization roles]({{< relref "../permissions/organization_roles.md" >}}) for more information about roles and permissions in Grafana.
|
||||||
|
|
||||||
|
Read about how to [add custom claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/add-custom-claim/) to the user info in Okta. Also, check Generic OAuth page for [JMESPath examples]({{< relref "generic-oauth.md/#jmespath-examples" >}}).
|
||||||
|
|
||||||
|
### Team Sync (Enterprise only)
|
||||||
|
|
||||||
|
Map your Okta groups to teams in Grafana so that your users will automatically be added to
|
||||||
|
the correct teams.
|
||||||
|
|
||||||
|
Okta groups can be referenced by group name, like `Admins`.
|
||||||
|
|
||||||
|
[Learn more about Team Sync]({{< relref "../enterprise/team-sync.md" >}})
|
||||||
@@ -56,6 +56,8 @@
|
|||||||
name: GitHub
|
name: GitHub
|
||||||
- link: /auth/gitlab/
|
- link: /auth/gitlab/
|
||||||
name: GitLab
|
name: GitLab
|
||||||
|
- link: /auth/okta/
|
||||||
|
name: Okta
|
||||||
- link: /auth/saml/
|
- link: /auth/saml/
|
||||||
name: SAML
|
name: SAML
|
||||||
- link: /auth/team-sync/
|
- link: /auth/team-sync/
|
||||||
|
|||||||
@@ -229,6 +229,11 @@ $external-services: (
|
|||||||
borderColor: #393939,
|
borderColor: #393939,
|
||||||
icon: '',
|
icon: '',
|
||||||
),
|
),
|
||||||
|
okta: (
|
||||||
|
bgColor: #2f2f2f,
|
||||||
|
borderColor: #393939,
|
||||||
|
icon: '',
|
||||||
|
),
|
||||||
oauth: (
|
oauth: (
|
||||||
bgColor: #262628,
|
bgColor: #262628,
|
||||||
borderColor: #393939,
|
borderColor: #393939,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
@@ -14,9 +15,7 @@ import (
|
|||||||
|
|
||||||
type SocialAzureAD struct {
|
type SocialAzureAD struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedDomains []string
|
allowedGroups []string
|
||||||
allowedGroups []string
|
|
||||||
allowSignup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type azureClaims struct {
|
type azureClaims struct {
|
||||||
@@ -32,34 +31,25 @@ func (s *SocialAzureAD) Type() int {
|
|||||||
return int(models.AZUREAD)
|
return int(models.AZUREAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialAzureAD) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialAzureAD) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialAzureAD) UserInfo(_ *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
func (s *SocialAzureAD) UserInfo(_ *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
idToken := token.Extra("id_token")
|
idToken := token.Extra("id_token")
|
||||||
if idToken == nil {
|
if idToken == nil {
|
||||||
return nil, fmt.Errorf("No id_token found")
|
return nil, fmt.Errorf("no id_token found")
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedToken, err := jwt.ParseSigned(idToken.(string))
|
parsedToken, err := jwt.ParseSigned(idToken.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing id token")
|
return nil, errutil.Wrapf(err, "error parsing id token")
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims azureClaims
|
var claims azureClaims
|
||||||
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
return nil, fmt.Errorf("Error getting claims from id token")
|
return nil, errutil.Wrapf(err, "error getting claims from id token")
|
||||||
}
|
}
|
||||||
|
|
||||||
email := extractEmail(claims)
|
email := extractEmail(claims)
|
||||||
|
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return nil, errors.New("Error getting user info: No email found in access token")
|
return nil, errors.New("error getting user info: no email found in access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
role := extractRole(claims)
|
role := extractRole(claims)
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import (
|
|||||||
|
|
||||||
func TestSocialAzureAD_UserInfo(t *testing.T) {
|
func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
SocialBase *SocialBase
|
SocialBase *SocialBase
|
||||||
allowedDomains []string
|
allowedGroups []string
|
||||||
allowedGroups []string
|
|
||||||
allowSignup bool
|
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
@@ -225,10 +223,8 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := &SocialAzureAD{
|
s := &SocialAzureAD{
|
||||||
SocialBase: tt.fields.SocialBase,
|
SocialBase: tt.fields.SocialBase,
|
||||||
allowedDomains: tt.fields.allowedDomains,
|
allowedGroups: tt.fields.allowedGroups,
|
||||||
allowedGroups: tt.fields.allowedGroups,
|
|
||||||
allowSignup: tt.fields.allowSignup,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
key := []byte("secret")
|
key := []byte("secret")
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package social
|
package social
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
"github.com/jmespath/go-jmespath"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -18,6 +22,14 @@ type HttpGetResponse struct {
|
|||||||
Headers http.Header
|
Headers http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SocialBase) IsEmailAllowed(email string) bool {
|
||||||
|
return isEmailAllowed(email, s.allowedDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialBase) IsSignupAllowed() bool {
|
||||||
|
return s.allowSignup
|
||||||
|
}
|
||||||
|
|
||||||
func isEmailAllowed(email string, allowedDomains []string) bool {
|
func isEmailAllowed(email string, allowedDomains []string) bool {
|
||||||
if len(allowedDomains) == 0 {
|
if len(allowedDomains) == 0 {
|
||||||
return true
|
return true
|
||||||
@@ -57,3 +69,30 @@ func HttpGet(client *http.Client, url string) (response HttpGetResponse, err err
|
|||||||
err = nil
|
err = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (string, error) {
|
||||||
|
if attributePath == "" {
|
||||||
|
return "", errors.New("no attribute path specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", errors.New("empty user info JSON response provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf interface{}
|
||||||
|
if err := json.Unmarshal(data, &buf); err != nil {
|
||||||
|
return "", errutil.Wrap("failed to unmarshal user info JSON response", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := jmespath.Search(attributePath, buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", errutil.Wrapf(err, "failed to search user info JSON response with provided path: %q", attributePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
strVal, ok := val.(string)
|
||||||
|
if ok {
|
||||||
|
return strVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,16 +12,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/jmespath/go-jmespath"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SocialGenericOAuth struct {
|
type SocialGenericOAuth struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedDomains []string
|
|
||||||
allowedOrganizations []string
|
allowedOrganizations []string
|
||||||
apiUrl string
|
apiUrl string
|
||||||
allowSignup bool
|
|
||||||
emailAttributeName string
|
emailAttributeName string
|
||||||
emailAttributePath string
|
emailAttributePath string
|
||||||
roleAttributePath string
|
roleAttributePath string
|
||||||
@@ -32,14 +29,6 @@ func (s *SocialGenericOAuth) Type() int {
|
|||||||
return int(models.GENERIC)
|
return int(models.GENERIC)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
|
func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
|
||||||
if len(s.teamIds) == 0 {
|
if len(s.teamIds) == 0 {
|
||||||
return true
|
return true
|
||||||
@@ -141,7 +130,12 @@ func (s *SocialGenericOAuth) fillUserInfo(userInfo *BasicUserInfo, data *UserInf
|
|||||||
userInfo.Email = s.extractEmail(data)
|
userInfo.Email = s.extractEmail(data)
|
||||||
}
|
}
|
||||||
if userInfo.Role == "" {
|
if userInfo.Role == "" {
|
||||||
userInfo.Role = s.extractRole(data)
|
role, err := s.extractRole(data)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("Failed to extract role", "error", err)
|
||||||
|
} else {
|
||||||
|
userInfo.Role = role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if userInfo.Name == "" {
|
if userInfo.Name == "" {
|
||||||
userInfo.Name = s.extractName(data)
|
userInfo.Name = s.extractName(data)
|
||||||
@@ -209,8 +203,10 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.emailAttributePath != "" {
|
if s.emailAttributePath != "" {
|
||||||
email := s.searchJSONForAttr(s.emailAttributePath, data.rawJSON)
|
email, err := s.searchJSONForAttr(s.emailAttributePath, data.rawJSON)
|
||||||
if email != "" {
|
if err != nil {
|
||||||
|
s.log.Error("Failed to search JSON for attribute", "error", err)
|
||||||
|
} else if email != "" {
|
||||||
return email
|
return email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,14 +227,16 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) string {
|
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) {
|
||||||
if s.roleAttributePath != "" {
|
if s.roleAttributePath == "" {
|
||||||
role := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
|
return "", nil
|
||||||
if role != "" {
|
|
||||||
return role
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
|
func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
|
||||||
@@ -265,37 +263,6 @@ func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchJSONForAttr searches the provided JSON response for the given attribute
|
|
||||||
// using the configured attribute path associated with the generic OAuth
|
|
||||||
// provider.
|
|
||||||
// Returns an empty string if an attribute is not found.
|
|
||||||
func (s *SocialGenericOAuth) searchJSONForAttr(attributePath string, data []byte) string {
|
|
||||||
if attributePath == "" {
|
|
||||||
s.log.Error("No attribute path specified")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
s.log.Error("Empty user info JSON response provided")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var buf interface{}
|
|
||||||
if err := json.Unmarshal(data, &buf); err != nil {
|
|
||||||
s.log.Error("Failed to unmarshal user info JSON response", "err", err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val, err := jmespath.Search(attributePath, buf)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error("Failed to search user info JSON response with provided path", "attributePath", attributePath, "err", err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
strVal, ok := val.(string)
|
|
||||||
if ok {
|
|
||||||
return strVal
|
|
||||||
}
|
|
||||||
s.log.Error("Attribute not found when searching JSON with provided path", "attributePath", attributePath)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
|
func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
|
||||||
type Record struct {
|
type Record struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|||||||
@@ -28,24 +28,28 @@ func TestSearchJSONForEmail(t *testing.T) {
|
|||||||
UserInfoJSONResponse []byte
|
UserInfoJSONResponse []byte
|
||||||
EmailAttributePath string
|
EmailAttributePath string
|
||||||
ExpectedResult string
|
ExpectedResult string
|
||||||
|
ExpectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Given an invalid user info JSON response",
|
Name: "Given an invalid user info JSON response",
|
||||||
UserInfoJSONResponse: []byte("{"),
|
UserInfoJSONResponse: []byte("{"),
|
||||||
EmailAttributePath: "attributes.email",
|
EmailAttributePath: "attributes.email",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given an empty user info JSON response and empty JMES path",
|
Name: "Given an empty user info JSON response and empty JMES path",
|
||||||
UserInfoJSONResponse: []byte{},
|
UserInfoJSONResponse: []byte{},
|
||||||
EmailAttributePath: "",
|
EmailAttributePath: "",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "no attribute path specified",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given an empty user info JSON response and valid JMES path",
|
Name: "Given an empty user info JSON response and valid JMES path",
|
||||||
UserInfoJSONResponse: []byte{},
|
UserInfoJSONResponse: []byte{},
|
||||||
EmailAttributePath: "attributes.email",
|
EmailAttributePath: "attributes.email",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "empty user info JSON response provided",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given a simple user info JSON response and valid JMES path",
|
Name: "Given a simple user info JSON response and valid JMES path",
|
||||||
@@ -87,7 +91,12 @@ func TestSearchJSONForEmail(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
provider.emailAttributePath = test.EmailAttributePath
|
provider.emailAttributePath = test.EmailAttributePath
|
||||||
t.Run(test.Name, func(t *testing.T) {
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
actualResult := provider.searchJSONForAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
|
actualResult, err := provider.searchJSONForAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
|
||||||
|
if test.ExpectedError == "" {
|
||||||
|
require.NoError(t, err, "Testing case %q", test.Name)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
|
||||||
|
}
|
||||||
require.Equal(t, test.ExpectedResult, actualResult)
|
require.Equal(t, test.ExpectedResult, actualResult)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -107,24 +116,28 @@ func TestSearchJSONForRole(t *testing.T) {
|
|||||||
UserInfoJSONResponse []byte
|
UserInfoJSONResponse []byte
|
||||||
RoleAttributePath string
|
RoleAttributePath string
|
||||||
ExpectedResult string
|
ExpectedResult string
|
||||||
|
ExpectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Given an invalid user info JSON response",
|
Name: "Given an invalid user info JSON response",
|
||||||
UserInfoJSONResponse: []byte("{"),
|
UserInfoJSONResponse: []byte("{"),
|
||||||
RoleAttributePath: "attributes.role",
|
RoleAttributePath: "attributes.role",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given an empty user info JSON response and empty JMES path",
|
Name: "Given an empty user info JSON response and empty JMES path",
|
||||||
UserInfoJSONResponse: []byte{},
|
UserInfoJSONResponse: []byte{},
|
||||||
RoleAttributePath: "",
|
RoleAttributePath: "",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "no attribute path specified",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given an empty user info JSON response and valid JMES path",
|
Name: "Given an empty user info JSON response and valid JMES path",
|
||||||
UserInfoJSONResponse: []byte{},
|
UserInfoJSONResponse: []byte{},
|
||||||
RoleAttributePath: "attributes.role",
|
RoleAttributePath: "attributes.role",
|
||||||
ExpectedResult: "",
|
ExpectedResult: "",
|
||||||
|
ExpectedError: "empty user info JSON response provided",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Given a simple user info JSON response and valid JMES path",
|
Name: "Given a simple user info JSON response and valid JMES path",
|
||||||
@@ -141,7 +154,12 @@ func TestSearchJSONForRole(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
provider.roleAttributePath = test.RoleAttributePath
|
provider.roleAttributePath = test.RoleAttributePath
|
||||||
t.Run(test.Name, func(t *testing.T) {
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
actualResult := provider.searchJSONForAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
|
actualResult, err := provider.searchJSONForAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
|
||||||
|
if test.ExpectedError == "" {
|
||||||
|
require.NoError(t, err, "Testing case %q", test.Name)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
|
||||||
|
}
|
||||||
require.Equal(t, test.ExpectedResult, actualResult)
|
require.Equal(t, test.ExpectedResult, actualResult)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import (
|
|||||||
|
|
||||||
type SocialGithub struct {
|
type SocialGithub struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedDomains []string
|
|
||||||
allowedOrganizations []string
|
allowedOrganizations []string
|
||||||
apiUrl string
|
apiUrl string
|
||||||
allowSignup bool
|
|
||||||
teamIds []int
|
teamIds []int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +37,6 @@ func (s *SocialGithub) Type() int {
|
|||||||
return int(models.GITHUB)
|
return int(models.GITHUB)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGithub) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
|
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
|
||||||
if len(s.teamIds) == 0 {
|
if len(s.teamIds) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -13,24 +13,14 @@ import (
|
|||||||
|
|
||||||
type SocialGitlab struct {
|
type SocialGitlab struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedDomains []string
|
allowedGroups []string
|
||||||
allowedGroups []string
|
apiUrl string
|
||||||
apiUrl string
|
|
||||||
allowSignup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGitlab) Type() int {
|
func (s *SocialGitlab) Type() int {
|
||||||
return int(models.GITLAB)
|
return int(models.GITLAB)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGitlab) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGitlab) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGitlab) IsGroupMember(groups []string) bool {
|
func (s *SocialGitlab) IsGroupMember(groups []string) bool {
|
||||||
if len(s.allowedGroups) == 0 {
|
if len(s.allowedGroups) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -12,24 +12,14 @@ import (
|
|||||||
|
|
||||||
type SocialGoogle struct {
|
type SocialGoogle struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedDomains []string
|
hostedDomain string
|
||||||
hostedDomain string
|
apiUrl string
|
||||||
apiUrl string
|
|
||||||
allowSignup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGoogle) Type() int {
|
func (s *SocialGoogle) Type() int {
|
||||||
return int(models.GOOGLE)
|
return int(models.GOOGLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGoogle) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type SocialGrafanaCom struct {
|
|||||||
*SocialBase
|
*SocialBase
|
||||||
url string
|
url string
|
||||||
allowedOrganizations []string
|
allowedOrganizations []string
|
||||||
allowSignup bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrgRecord struct {
|
type OrgRecord struct {
|
||||||
@@ -29,10 +28,6 @@ func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGrafanaCom) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool {
|
func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool {
|
||||||
if len(s.allowedOrganizations) == 0 {
|
if len(s.allowedOrganizations) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package social
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocialOkta struct {
|
||||||
|
*SocialBase
|
||||||
|
apiUrl string
|
||||||
|
allowedGroups []string
|
||||||
|
roleAttributePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OktaUserInfoJson struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Upn string `json:"upn"`
|
||||||
|
Attributes map[string][]string `json:"attributes"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
rawJSON []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type OktaClaims struct {
|
||||||
|
ID string `json:"sub"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (claims *OktaClaims) extractEmail() string {
|
||||||
|
if claims.Email == "" && claims.PreferredUsername != "" {
|
||||||
|
return claims.PreferredUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) Type() int {
|
||||||
|
return int(models.OKTA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
|
idToken := token.Extra("id_token")
|
||||||
|
if idToken == nil {
|
||||||
|
return nil, fmt.Errorf("no id_token found")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedToken, err := jwt.ParseSigned(idToken.(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errutil.Wrapf(err, "error parsing id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims OktaClaims
|
||||||
|
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
|
return nil, errutil.Wrapf(err, "error getting claims from id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
email := claims.extractEmail()
|
||||||
|
if email == "" {
|
||||||
|
return nil, errors.New("error getting user info: no email found in access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var data OktaUserInfoJson
|
||||||
|
err = s.extractAPI(&data, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.extractRole(&data)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("Failed to extract role", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := s.GetGroups(&data)
|
||||||
|
if !s.IsGroupMember(groups) {
|
||||||
|
return nil, ErrMissingGroupMembership
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BasicUserInfo{
|
||||||
|
Id: claims.ID,
|
||||||
|
Name: claims.Name,
|
||||||
|
Email: email,
|
||||||
|
Login: email,
|
||||||
|
Role: role,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) extractAPI(data *OktaUserInfoJson, client *http.Client) error {
|
||||||
|
rawUserInfoResponse, err := HttpGet(client, s.apiUrl)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Debug("Error getting user info response", "url", s.apiUrl, "error", err)
|
||||||
|
return errutil.Wrapf(err, "error getting user info response")
|
||||||
|
}
|
||||||
|
data.rawJSON = rawUserInfoResponse.Body
|
||||||
|
|
||||||
|
err = json.Unmarshal(data.rawJSON, data)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Debug("Error decoding user info response", "raw_json", data.rawJSON, "error", err)
|
||||||
|
data.rawJSON = []byte{}
|
||||||
|
return errutil.Wrapf(err, "error decoding user info response")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Debug("Received user info response", "raw_json", string(data.rawJSON), "data", data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) extractRole(data *OktaUserInfoJson) (string, error) {
|
||||||
|
if s.roleAttributePath == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return role, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
|
||||||
|
groups := make([]string, 0)
|
||||||
|
if len(data.Groups) > 0 {
|
||||||
|
groups = data.Groups
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialOkta) IsGroupMember(groups []string) bool {
|
||||||
|
if len(s.allowedGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedGroup := range s.allowedGroups {
|
||||||
|
for _, group := range groups {
|
||||||
|
if group == allowedGroup {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
+36
-44
@@ -37,7 +37,9 @@ type SocialConnector interface {
|
|||||||
|
|
||||||
type SocialBase struct {
|
type SocialBase struct {
|
||||||
*oauth2.Config
|
*oauth2.Config
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
allowSignup bool
|
||||||
|
allowedDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
@@ -55,9 +57,20 @@ const (
|
|||||||
var (
|
var (
|
||||||
SocialBaseUrl = "/login/"
|
SocialBaseUrl = "/login/"
|
||||||
SocialMap = make(map[string]SocialConnector)
|
SocialMap = make(map[string]SocialConnector)
|
||||||
allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom, "azuread"}
|
allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom, "azuread", "okta"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func newSocialBase(name string, config *oauth2.Config, info *setting.OAuthInfo) *SocialBase {
|
||||||
|
logger := log.New("oauth." + name)
|
||||||
|
|
||||||
|
return &SocialBase{
|
||||||
|
Config: config,
|
||||||
|
log: logger,
|
||||||
|
allowSignup: info.AllowSignup,
|
||||||
|
allowedDomains: info.AllowedDomains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewOAuthService() {
|
func NewOAuthService() {
|
||||||
setting.OAuthService = &setting.OAuther{}
|
setting.OAuthService = &setting.OAuther{}
|
||||||
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
||||||
@@ -107,18 +120,11 @@ func NewOAuthService() {
|
|||||||
Scopes: info.Scopes,
|
Scopes: info.Scopes,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := log.New("oauth." + name)
|
|
||||||
|
|
||||||
// GitHub.
|
// GitHub.
|
||||||
if name == "github" {
|
if name == "github" {
|
||||||
SocialMap["github"] = &SocialGithub{
|
SocialMap["github"] = &SocialGithub{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
|
||||||
log: logger,
|
|
||||||
},
|
|
||||||
allowedDomains: info.AllowedDomains,
|
|
||||||
apiUrl: info.ApiUrl,
|
apiUrl: info.ApiUrl,
|
||||||
allowSignup: info.AllowSignup,
|
|
||||||
teamIds: sec.Key("team_ids").Ints(","),
|
teamIds: sec.Key("team_ids").Ints(","),
|
||||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||||
}
|
}
|
||||||
@@ -127,54 +133,44 @@ func NewOAuthService() {
|
|||||||
// GitLab.
|
// GitLab.
|
||||||
if name == "gitlab" {
|
if name == "gitlab" {
|
||||||
SocialMap["gitlab"] = &SocialGitlab{
|
SocialMap["gitlab"] = &SocialGitlab{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
apiUrl: info.ApiUrl,
|
||||||
log: logger,
|
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||||
},
|
|
||||||
allowedDomains: info.AllowedDomains,
|
|
||||||
apiUrl: info.ApiUrl,
|
|
||||||
allowSignup: info.AllowSignup,
|
|
||||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google.
|
// Google.
|
||||||
if name == "google" {
|
if name == "google" {
|
||||||
SocialMap["google"] = &SocialGoogle{
|
SocialMap["google"] = &SocialGoogle{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
hostedDomain: info.HostedDomain,
|
||||||
log: logger,
|
apiUrl: info.ApiUrl,
|
||||||
},
|
|
||||||
allowedDomains: info.AllowedDomains,
|
|
||||||
hostedDomain: info.HostedDomain,
|
|
||||||
apiUrl: info.ApiUrl,
|
|
||||||
allowSignup: info.AllowSignup,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AzureAD.
|
// AzureAD.
|
||||||
if name == "azuread" {
|
if name == "azuread" {
|
||||||
SocialMap["azuread"] = &SocialAzureAD{
|
SocialMap["azuread"] = &SocialAzureAD{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||||
log: logger,
|
}
|
||||||
},
|
}
|
||||||
allowedDomains: info.AllowedDomains,
|
|
||||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
// Okta
|
||||||
allowSignup: info.AllowSignup,
|
if name == "okta" {
|
||||||
|
SocialMap["okta"] = &SocialOkta{
|
||||||
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
|
apiUrl: info.ApiUrl,
|
||||||
|
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||||
|
roleAttributePath: info.RoleAttributePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic - Uses the same scheme as Github.
|
// Generic - Uses the same scheme as Github.
|
||||||
if name == "generic_oauth" {
|
if name == "generic_oauth" {
|
||||||
SocialMap["generic_oauth"] = &SocialGenericOAuth{
|
SocialMap["generic_oauth"] = &SocialGenericOAuth{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
|
||||||
log: logger,
|
|
||||||
},
|
|
||||||
allowedDomains: info.AllowedDomains,
|
|
||||||
apiUrl: info.ApiUrl,
|
apiUrl: info.ApiUrl,
|
||||||
allowSignup: info.AllowSignup,
|
|
||||||
emailAttributeName: info.EmailAttributeName,
|
emailAttributeName: info.EmailAttributeName,
|
||||||
emailAttributePath: info.EmailAttributePath,
|
emailAttributePath: info.EmailAttributePath,
|
||||||
roleAttributePath: info.RoleAttributePath,
|
roleAttributePath: info.RoleAttributePath,
|
||||||
@@ -197,12 +193,8 @@ func NewOAuthService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SocialMap[grafanaCom] = &SocialGrafanaCom{
|
SocialMap[grafanaCom] = &SocialGrafanaCom{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: newSocialBase(name, &config, info),
|
||||||
Config: &config,
|
|
||||||
log: logger,
|
|
||||||
},
|
|
||||||
url: setting.GrafanaComUrl,
|
url: setting.GrafanaComUrl,
|
||||||
allowSignup: info.AllowSignup,
|
|
||||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ const (
|
|||||||
GRAFANA_COM
|
GRAFANA_COM
|
||||||
GITLAB
|
GITLAB
|
||||||
AZUREAD
|
AZUREAD
|
||||||
|
OKTA
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const loginServices: () => LoginServices = () => {
|
|||||||
hrefName: 'grafana_com',
|
hrefName: 'grafana_com',
|
||||||
icon: 'grafana_com',
|
icon: 'grafana_com',
|
||||||
},
|
},
|
||||||
|
okta: {
|
||||||
|
enabled: config.oauth.okta,
|
||||||
|
name: 'Okta',
|
||||||
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
enabled: oauthEnabled && config.oauth.generic_oauth,
|
enabled: oauthEnabled && config.oauth.generic_oauth,
|
||||||
name: oauthEnabled && config.oauth.generic_oauth ? config.oauth.generic_oauth.name : 'OAuth',
|
name: oauthEnabled && config.oauth.generic_oauth ? config.oauth.generic_oauth.name : 'OAuth',
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -232,6 +232,11 @@ $external-services: (
|
|||||||
borderColor: #393939,
|
borderColor: #393939,
|
||||||
icon: '',
|
icon: '',
|
||||||
),
|
),
|
||||||
|
okta: (
|
||||||
|
bgColor: #2f2f2f,
|
||||||
|
borderColor: #393939,
|
||||||
|
icon: '',
|
||||||
|
),
|
||||||
oauth: (
|
oauth: (
|
||||||
bgColor: #262628,
|
bgColor: #262628,
|
||||||
borderColor: #393939,
|
borderColor: #393939,
|
||||||
|
|||||||
@@ -225,6 +225,15 @@ $btn-service-icon-width: 35px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-service--okta {
|
||||||
|
.btn-service-icon {
|
||||||
|
background-image: url(../img/okta_logo_white.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
background-size: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Toggle button
|
//Toggle button
|
||||||
|
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
|
|||||||
Reference in New Issue
Block a user