Commit 7afdfd2e by Alexander Zobnin Committed by GitHub

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>
parent 703476b3
......@@ -386,6 +386,21 @@ token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
allowed_domains =
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 #######################
[auth.generic_oauth]
name = OAuth
......
......@@ -376,6 +376,21 @@
;allowed_domains =
;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 ##########################
[auth.generic_oauth]
;enabled = false
......
+++
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
- link: /auth/gitlab/
name: GitLab
- link: /auth/okta/
name: Okta
- link: /auth/saml/
name: SAML
- link: /auth/team-sync/
......
......@@ -229,6 +229,11 @@ $external-services: (
borderColor: #393939,
icon: '',
),
okta: (
bgColor: #2f2f2f,
borderColor: #393939,
icon: '',
),
oauth: (
bgColor: #262628,
borderColor: #393939,
......
......@@ -7,6 +7,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2/jwt"
......@@ -14,9 +15,7 @@ import (
type SocialAzureAD struct {
*SocialBase
allowedDomains []string
allowedGroups []string
allowSignup bool
}
type azureClaims struct {
......@@ -32,34 +31,25 @@ func (s *SocialAzureAD) Type() int {
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) {
idToken := token.Extra("id_token")
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))
if err != nil {
return nil, fmt.Errorf("Error parsing id token")
return nil, errutil.Wrapf(err, "error parsing id token")
}
var claims azureClaims
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)
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)
......
......@@ -14,9 +14,7 @@ import (
func TestSocialAzureAD_UserInfo(t *testing.T) {
type fields struct {
SocialBase *SocialBase
allowedDomains []string
allowedGroups []string
allowSignup bool
}
type args struct {
client *http.Client
......@@ -226,9 +224,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s := &SocialAzureAD{
SocialBase: tt.fields.SocialBase,
allowedDomains: tt.fields.allowedDomains,
allowedGroups: tt.fields.allowedGroups,
allowSignup: tt.fields.allowSignup,
}
key := []byte("secret")
......
package social
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/jmespath/go-jmespath"
)
var (
......@@ -18,6 +22,14 @@ type HttpGetResponse struct {
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 {
if len(allowedDomains) == 0 {
return true
......@@ -57,3 +69,30 @@ func HttpGet(client *http.Client, url string) (response HttpGetResponse, err err
err = nil
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/models"
"github.com/jmespath/go-jmespath"
"golang.org/x/oauth2"
)
type SocialGenericOAuth struct {
*SocialBase
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
emailAttributeName string
emailAttributePath string
roleAttributePath string
......@@ -32,14 +29,6 @@ func (s *SocialGenericOAuth) Type() int {
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 {
if len(s.teamIds) == 0 {
return true
......@@ -141,7 +130,12 @@ func (s *SocialGenericOAuth) fillUserInfo(userInfo *BasicUserInfo, data *UserInf
userInfo.Email = s.extractEmail(data)
}
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 == "" {
userInfo.Name = s.extractName(data)
......@@ -209,8 +203,10 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
}
if s.emailAttributePath != "" {
email := s.searchJSONForAttr(s.emailAttributePath, data.rawJSON)
if email != "" {
email, err := s.searchJSONForAttr(s.emailAttributePath, data.rawJSON)
if err != nil {
s.log.Error("Failed to search JSON for attribute", "error", err)
} else if email != "" {
return email
}
}
......@@ -231,14 +227,16 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
return ""
}
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) string {
if s.roleAttributePath != "" {
role := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
if role != "" {
return role
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) {
if s.roleAttributePath == "" {
return "", nil
}
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
if err != nil {
return "", err
}
return ""
return role, nil
}
func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string {
......@@ -265,37 +263,6 @@ func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string {
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) {
type Record struct {
Email string `json:"email"`
......
......@@ -28,24 +28,28 @@ func TestSearchJSONForEmail(t *testing.T) {
UserInfoJSONResponse []byte
EmailAttributePath string
ExpectedResult string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
EmailAttributePath: "attributes.email",
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",
UserInfoJSONResponse: []byte{},
EmailAttributePath: "",
ExpectedResult: "",
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
EmailAttributePath: "attributes.email",
ExpectedResult: "",
ExpectedError: "empty user info JSON response provided",
},
{
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 {
provider.emailAttributePath = test.EmailAttributePath
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)
})
}
......@@ -107,24 +116,28 @@ func TestSearchJSONForRole(t *testing.T) {
UserInfoJSONResponse []byte
RoleAttributePath string
ExpectedResult string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
RoleAttributePath: "attributes.role",
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",
UserInfoJSONResponse: []byte{},
RoleAttributePath: "",
ExpectedResult: "",
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
RoleAttributePath: "attributes.role",
ExpectedResult: "",
ExpectedError: "empty user info JSON response provided",
},
{
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 {
provider.roleAttributePath = test.RoleAttributePath
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)
})
}
......
......@@ -14,10 +14,8 @@ import (
type SocialGithub struct {
*SocialBase
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
teamIds []int
}
......@@ -39,14 +37,6 @@ func (s *SocialGithub) Type() int {
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 {
if len(s.teamIds) == 0 {
return true
......
......@@ -13,24 +13,14 @@ import (
type SocialGitlab struct {
*SocialBase
allowedDomains []string
allowedGroups []string
apiUrl string
allowSignup bool
}
func (s *SocialGitlab) Type() int {
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 {
if len(s.allowedGroups) == 0 {
return true
......
......@@ -12,24 +12,14 @@ import (
type SocialGoogle struct {
*SocialBase
allowedDomains []string
hostedDomain string
apiUrl string
allowSignup bool
}
func (s *SocialGoogle) Type() int {
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) {
var data struct {
Id string `json:"id"`
......
......@@ -14,7 +14,6 @@ type SocialGrafanaCom struct {
*SocialBase
url string
allowedOrganizations []string
allowSignup bool
}
type OrgRecord struct {
......@@ -29,10 +28,6 @@ func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
return true
}
func (s *SocialGrafanaCom) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool {
if len(s.allowedOrganizations) == 0 {
return true
......
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
}
......@@ -38,6 +38,8 @@ type SocialConnector interface {
type SocialBase struct {
*oauth2.Config
log log.Logger
allowSignup bool
allowedDomains []string
}
type Error struct {
......@@ -55,9 +57,20 @@ const (
var (
SocialBaseUrl = "/login/"
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() {
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
......@@ -107,18 +120,11 @@ func NewOAuthService() {
Scopes: info.Scopes,
}
logger := log.New("oauth." + name)
// GitHub.
if name == "github" {
SocialMap["github"] = &SocialGithub{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains,
SocialBase: newSocialBase(name, &config, info),
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
......@@ -127,13 +133,8 @@ func NewOAuthService() {
// GitLab.
if name == "gitlab" {
SocialMap["gitlab"] = &SocialGitlab{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains,
SocialBase: newSocialBase(name, &config, info),
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
}
......@@ -141,40 +142,35 @@ func NewOAuthService() {
// Google.
if name == "google" {
SocialMap["google"] = &SocialGoogle{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains,
SocialBase: newSocialBase(name, &config, info),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
}
}
// AzureAD.
if name == "azuread" {
SocialMap["azuread"] = &SocialAzureAD{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains,
SocialBase: newSocialBase(name, &config, info),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
allowSignup: info.AllowSignup,
}
}
// Okta
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.
if name == "generic_oauth" {
SocialMap["generic_oauth"] = &SocialGenericOAuth{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains,
SocialBase: newSocialBase(name, &config, info),
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
roleAttributePath: info.RoleAttributePath,
......@@ -197,12 +193,8 @@ func NewOAuthService() {
}
SocialMap[grafanaCom] = &SocialGrafanaCom{
SocialBase: &SocialBase{
Config: &config,
log: logger,
},
SocialBase: newSocialBase(name, &config, info),
url: setting.GrafanaComUrl,
allowSignup: info.AllowSignup,
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
}
......
......@@ -10,4 +10,5 @@ const (
GRAFANA_COM
GITLAB
AZUREAD
OKTA
)
......@@ -33,6 +33,10 @@ const loginServices: () => LoginServices = () => {
hrefName: 'grafana_com',
icon: 'grafana_com',
},
okta: {
enabled: config.oauth.okta,
name: 'Okta',
},
oauth: {
enabled: oauthEnabled && config.oauth.generic_oauth,
name: oauthEnabled && config.oauth.generic_oauth ? config.oauth.generic_oauth.name : 'OAuth',
......
......@@ -232,6 +232,11 @@ $external-services: (
borderColor: #393939,
icon: '',
),
okta: (
bgColor: #2f2f2f,
borderColor: #393939,
icon: '',
),
oauth: (
bgColor: #262628,
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-btn {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment