mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Configure SkipOrgRoleSync from OAuthInfo * Remove skipOrgRoleSync from socialbase and connectors * Add test to socialimpl.ProvideService * Deprecate AuthSettings' fields * clean up misleading init of frontendsettings.Auth
918 lines
31 KiB
Go
918 lines
31 KiB
Go
package connectors
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/grafana/grafana/pkg/login/social"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
func TestSearchJSONForEmail(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
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",
|
|
UserInfoJSONResponse: []byte(`{
|
|
"attributes": {
|
|
"email": "grafana@localhost"
|
|
}
|
|
}`),
|
|
EmailAttributePath: "attributes.email",
|
|
ExpectedResult: "grafana@localhost",
|
|
},
|
|
{
|
|
Name: "Given a user info JSON response with e-mails array and valid JMES path",
|
|
UserInfoJSONResponse: []byte(`{
|
|
"attributes": {
|
|
"emails": ["grafana@localhost", "admin@localhost"]
|
|
}
|
|
}`),
|
|
EmailAttributePath: "attributes.emails[0]",
|
|
ExpectedResult: "grafana@localhost",
|
|
},
|
|
{
|
|
Name: "Given a nested user info JSON response and valid JMES path",
|
|
UserInfoJSONResponse: []byte(`{
|
|
"identities": [
|
|
{
|
|
"userId": "grafana@localhost"
|
|
},
|
|
{
|
|
"userId": "admin@localhost"
|
|
}
|
|
]
|
|
}`),
|
|
EmailAttributePath: "identities[0].userId",
|
|
ExpectedResult: "grafana@localhost",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.emailAttributePath = test.EmailAttributePath
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
actualResult, err := provider.searchJSONForStringAttr(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)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSearchJSONForGroups(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
UserInfoJSONResponse []byte
|
|
GroupsAttributePath string
|
|
ExpectedResult []string
|
|
ExpectedError string
|
|
}{
|
|
{
|
|
Name: "Given an invalid user info JSON response",
|
|
UserInfoJSONResponse: []byte("{"),
|
|
GroupsAttributePath: "attributes.groups",
|
|
ExpectedResult: []string{},
|
|
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{},
|
|
GroupsAttributePath: "",
|
|
ExpectedResult: []string{},
|
|
ExpectedError: "no attribute path specified",
|
|
},
|
|
{
|
|
Name: "Given an empty user info JSON response and valid JMES path",
|
|
UserInfoJSONResponse: []byte{},
|
|
GroupsAttributePath: "attributes.groups",
|
|
ExpectedResult: []string{},
|
|
ExpectedError: "empty user info JSON response provided",
|
|
},
|
|
{
|
|
Name: "Given a simple user info JSON response and valid JMES path",
|
|
UserInfoJSONResponse: []byte(`{
|
|
"attributes": {
|
|
"groups": ["foo", "bar"]
|
|
}
|
|
}`),
|
|
GroupsAttributePath: "attributes.groups[]",
|
|
ExpectedResult: []string{"foo", "bar"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.groupsAttributePath = test.GroupsAttributePath
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
actualResult, err := provider.searchJSONForStringArrayAttr(test.GroupsAttributePath, 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)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSearchJSONForRole(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
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",
|
|
UserInfoJSONResponse: []byte(`{
|
|
"attributes": {
|
|
"role": "admin"
|
|
}
|
|
}`),
|
|
RoleAttributePath: "attributes.role",
|
|
ExpectedResult: "admin",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.roleAttributePath = test.RoleAttributePath
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
actualResult, err := provider.searchJSONForStringAttr(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)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
|
EmailAttributePath: "email",
|
|
}, &setting.Cfg{},
|
|
&ssosettingstests.MockService{},
|
|
featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
SkipOrgRoleSync bool
|
|
AllowAssignGrafanaAdmin bool
|
|
ResponseBody any
|
|
OAuth2Extra any
|
|
RoleAttributePath string
|
|
ExpectedEmail string
|
|
ExpectedRole org.RoleType
|
|
ExpectedError error
|
|
ExpectedGrafanaAdmin *bool
|
|
}{
|
|
{
|
|
Name: "Given a valid id_token, a valid role path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, no role path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
|
},
|
|
RoleAttributePath: "",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, an invalid role path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
|
|
},
|
|
RoleAttributePath: "invalid_path",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid role path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"role": "Admin",
|
|
"email": "john.doe@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
},
|
|
{
|
|
Name: "Given no id_token, no role path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"email": "john.doe@example.com",
|
|
},
|
|
RoleAttributePath: "",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a role path, a valid API response without a role, use API response",
|
|
ResponseBody: map[string]any{
|
|
"email": "john.doe@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid role path, no API response, no data",
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, a valid role path, a valid API response, prefer id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"role": "FromResponse",
|
|
"email": "from_response@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token and AssignGrafanaAdmin is unchecked, don't grant Server Admin",
|
|
AllowAssignGrafanaAdmin: false,
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"role": "FromResponse",
|
|
"email": "from_response@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
ExpectedGrafanaAdmin: nil,
|
|
},
|
|
{
|
|
Name: "Given a valid id_token and AssignGrafanaAdmin is checked, grant Server Admin",
|
|
AllowAssignGrafanaAdmin: true,
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"role": "FromResponse",
|
|
"email": "from_response@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
ExpectedGrafanaAdmin: trueBoolPtr(),
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"role": "FromResponse",
|
|
"email": "from_response@example.com",
|
|
},
|
|
RoleAttributePath: "invalid_path",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token with no email, a valid role path, a valid API response with no role, merge",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4ifQ.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"email": "from_response@example.com",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "from_response@example.com",
|
|
ExpectedRole: "Admin",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token with no role, a valid role path, a valid API response with no email, merge",
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"role": "FromResponse",
|
|
},
|
|
RoleAttributePath: "role",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Viewer",
|
|
ExpectedError: nil,
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role",
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com",
|
|
// "info": { "roles": [ "dev", "engineering" ] }}
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
|
|
},
|
|
RoleAttributePath: "contains(info.roles[*], 'dev') && 'Editor'",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Editor",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct role using the userinfo API response (JMESPath warning on id_token)",
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"info": map[string]any{
|
|
"roles": []string{"engineering", "SRE"},
|
|
},
|
|
},
|
|
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin'",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Admin",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, a valid advanced JMESPath role path, a valid API response, prefer ID token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com",
|
|
// "info": { "roles": [ "dev", "engineering" ] }}
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"info": map[string]any{
|
|
"roles": []string{"engineering", "SRE"},
|
|
},
|
|
},
|
|
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "Editor",
|
|
},
|
|
{
|
|
Name: "Given skip org role sync set to true, with a valid id_token, a valid advanced JMESPath role path, a valid API response, no org role should be set",
|
|
SkipOrgRoleSync: true,
|
|
OAuth2Extra: map[string]any{
|
|
// { "email": "john.doe@example.com",
|
|
// "info": { "roles": [ "dev", "engineering" ] }}
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg",
|
|
},
|
|
ResponseBody: map[string]any{
|
|
"info": map[string]any{
|
|
"roles": []string{"engineering", "SRE"},
|
|
},
|
|
},
|
|
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
|
|
ExpectedEmail: "john.doe@example.com",
|
|
ExpectedRole: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.roleAttributePath = test.RoleAttributePath
|
|
provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
|
|
provider.info.SkipOrgRoleSync = test.SkipOrgRoleSync
|
|
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
body, err := json.Marshal(test.ResponseBody)
|
|
require.NoError(t, err)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, err = w.Write(body)
|
|
require.NoError(t, err)
|
|
}))
|
|
provider.apiUrl = ts.URL
|
|
staticToken := oauth2.Token{
|
|
AccessToken: "",
|
|
TokenType: "",
|
|
RefreshToken: "",
|
|
Expiry: time.Now(),
|
|
}
|
|
|
|
token := staticToken.WithExtra(test.OAuth2Extra)
|
|
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
|
if test.ExpectedError != nil {
|
|
require.ErrorIs(t, err, test.ExpectedError)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.ExpectedEmail, actualResult.Email)
|
|
require.Equal(t, test.ExpectedEmail, actualResult.Login)
|
|
require.Equal(t, test.ExpectedRole, actualResult.Role)
|
|
require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserInfoSearchesForLogin(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"login_attribute_path": "login",
|
|
},
|
|
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
ResponseBody any
|
|
OAuth2Extra any
|
|
LoginAttributePath string
|
|
ExpectedLogin string
|
|
}{
|
|
{
|
|
Name: "Given a valid id_token, a valid login path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "login": "johndoe", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
|
},
|
|
LoginAttributePath: "role",
|
|
ExpectedLogin: "johndoe",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, no login path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "login": "johndoe", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
|
},
|
|
LoginAttributePath: "",
|
|
ExpectedLogin: "johndoe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid login path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"user_uid": "johndoe",
|
|
"email": "john.doe@example.com",
|
|
},
|
|
LoginAttributePath: "user_uid",
|
|
ExpectedLogin: "johndoe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, no login path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"login": "johndoe",
|
|
},
|
|
LoginAttributePath: "",
|
|
ExpectedLogin: "johndoe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a login path, a valid API response without a login, use API response",
|
|
ResponseBody: map[string]any{
|
|
"username": "john.doe",
|
|
},
|
|
LoginAttributePath: "login",
|
|
ExpectedLogin: "john.doe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid login path, no API response, no data",
|
|
LoginAttributePath: "login",
|
|
ExpectedLogin: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.loginAttributePath = test.LoginAttributePath
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
body, err := json.Marshal(test.ResponseBody)
|
|
require.NoError(t, err)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
t.Log("Writing fake API response body", "body", test.ResponseBody)
|
|
_, err = w.Write(body)
|
|
require.NoError(t, err)
|
|
}))
|
|
provider.apiUrl = ts.URL
|
|
staticToken := oauth2.Token{
|
|
AccessToken: "",
|
|
TokenType: "",
|
|
RefreshToken: "",
|
|
Expiry: time.Now(),
|
|
}
|
|
|
|
token := staticToken.WithExtra(test.OAuth2Extra)
|
|
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.ExpectedLogin, actualResult.Login)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserInfoSearchesForName(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"name_attribute_path": "name",
|
|
},
|
|
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
ResponseBody any
|
|
OAuth2Extra any
|
|
NameAttributePath string
|
|
ExpectedName string
|
|
}{
|
|
{
|
|
Name: "Given a valid id_token, a valid name path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
|
},
|
|
NameAttributePath: "name",
|
|
ExpectedName: "John Doe",
|
|
},
|
|
{
|
|
Name: "Given a valid id_token, no name path, no API response, use id_token",
|
|
OAuth2Extra: map[string]any{
|
|
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
|
},
|
|
NameAttributePath: "",
|
|
ExpectedName: "John Doe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid name path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"user_name": "John Doe",
|
|
"login": "johndoe",
|
|
"email": "john.doe@example.com",
|
|
},
|
|
NameAttributePath: "user_name",
|
|
ExpectedName: "John Doe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, no name path, a valid API response, use API response",
|
|
ResponseBody: map[string]any{
|
|
"display_name": "John Doe",
|
|
"login": "johndoe",
|
|
},
|
|
NameAttributePath: "",
|
|
ExpectedName: "John Doe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a name path, a valid API response without a name, use API response",
|
|
ResponseBody: map[string]any{
|
|
"display_name": "John Doe",
|
|
"username": "john.doe",
|
|
},
|
|
NameAttributePath: "name",
|
|
ExpectedName: "John Doe",
|
|
},
|
|
{
|
|
Name: "Given no id_token, a valid name path, no API response, no data",
|
|
NameAttributePath: "name",
|
|
ExpectedName: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
provider.nameAttributePath = test.NameAttributePath
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
body, err := json.Marshal(test.ResponseBody)
|
|
require.NoError(t, err)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
t.Log("Writing fake API response body", "body", test.ResponseBody)
|
|
_, err = w.Write(body)
|
|
require.NoError(t, err)
|
|
}))
|
|
provider.apiUrl = ts.URL
|
|
staticToken := oauth2.Token{
|
|
AccessToken: "",
|
|
TokenType: "",
|
|
RefreshToken: "",
|
|
Expiry: time.Now(),
|
|
}
|
|
|
|
token := staticToken.WithExtra(test.OAuth2Extra)
|
|
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.ExpectedName, actualResult.Name)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserInfoSearchesForGroup(t *testing.T) {
|
|
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
groupsAttributePath string
|
|
responseBody any
|
|
expectedResult []string
|
|
}{
|
|
{
|
|
name: "If groups are not set, user groups are nil",
|
|
groupsAttributePath: "",
|
|
expectedResult: nil,
|
|
},
|
|
{
|
|
name: "If groups are empty, user groups are nil",
|
|
groupsAttributePath: "info.groups",
|
|
responseBody: map[string]any{
|
|
"info": map[string]any{
|
|
"groups": []string{},
|
|
},
|
|
},
|
|
expectedResult: nil,
|
|
},
|
|
{
|
|
name: "If groups are set, user groups are set",
|
|
groupsAttributePath: "info.groups",
|
|
responseBody: map[string]any{
|
|
"info": map[string]any{
|
|
"groups": []string{"foo", "bar"},
|
|
},
|
|
},
|
|
expectedResult: []string{"foo", "bar"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
body, err := json.Marshal(test.responseBody)
|
|
require.NoError(t, err)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
t.Log("Writing fake API response body", "body", test.responseBody)
|
|
_, err := w.Write(body)
|
|
require.NoError(t, err)
|
|
}))
|
|
|
|
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
|
GroupsAttributePath: test.groupsAttributePath,
|
|
ApiUrl: ts.URL,
|
|
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
token := &oauth2.Token{
|
|
AccessToken: "",
|
|
TokenType: "",
|
|
RefreshToken: "",
|
|
Expiry: time.Now(),
|
|
}
|
|
|
|
userInfo, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.expectedResult, userInfo.Groups)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPayloadCompression(t *testing.T) {
|
|
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
|
EmailAttributePath: "email",
|
|
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
tests := []struct {
|
|
Name string
|
|
OAuth2Extra any
|
|
ExpectedEmail string
|
|
}{
|
|
{
|
|
Name: "Given a valid DEFLATE compressed id_token, return userInfo",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkRFRiJ9.eJyrVkrNTczMUbJSysrPyNNLyU91SK1IzC3ISdVLzs9V0lEqys9JBco6puRm5inVAgCFRw_6.XrV4ZKhw19dTcnviXanBD8lwjeALCYtDiESMmGzC-ho",
|
|
},
|
|
ExpectedEmail: "john.doe@example.com",
|
|
},
|
|
{
|
|
Name: "Given a valid DEFLATE compressed id_token with numeric header, return userInfo",
|
|
OAuth2Extra: map[string]any{
|
|
// Generated from https://token.dev/
|
|
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInZlciI6NH0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY0MjUxNjYwNSwiZXhwIjoxNjQyNTIwMjA1LCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.ANndoPWIHNjKPG8na7UUq7nan1RgF8-ze8STU31RXcA",
|
|
},
|
|
ExpectedEmail: "john.doe@example.com",
|
|
},
|
|
{
|
|
Name: "Given an invalid DEFLATE compressed id_token, return nil",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkRFRiJ9.00eJyrVkrNTczMUbJSysrPyNNLyU91SK1IzC3ISdVLzs9V0lEqys9JBco6puRm5inVAgCFRw_6.XrV4ZKhw19dTcnviXanBD8lwjeALCYtDiESMmGzC-ho",
|
|
},
|
|
ExpectedEmail: "",
|
|
},
|
|
{
|
|
Name: "Given an unsupported GZIP compressed id_token, return nil",
|
|
OAuth2Extra: map[string]any{
|
|
// { "role": "Admin", "email": "john.doe@example.com" }
|
|
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWSs1NzMxRslLKys_I00vJT3VIrUjMLchJ1UvOz1XSUSrKz0kFyjqm5GbmKdUCANotxTkvAAAA.85AXm3JOF5qflEA0goDFvlbZl2q3eFvqVcehz860W-o",
|
|
},
|
|
ExpectedEmail: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
staticToken := oauth2.Token{
|
|
AccessToken: "",
|
|
TokenType: "",
|
|
RefreshToken: "",
|
|
Expiry: time.Now(),
|
|
}
|
|
|
|
token := staticToken.WithExtra(test.OAuth2Extra)
|
|
userInfo := provider.extractFromToken(token)
|
|
|
|
if test.ExpectedEmail == "" {
|
|
require.Nil(t, userInfo, "Testing case %q", test.Name)
|
|
} else {
|
|
require.NotNil(t, userInfo, "Testing case %q", test.Name)
|
|
require.Equal(t, test.ExpectedEmail, userInfo.Email)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
|
|
type settingFields struct {
|
|
nameAttributePath string
|
|
loginAttributePath string
|
|
idTokenAttributeName string
|
|
teamIds []string
|
|
allowedOrganizations []string
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
settings *social.OAuthInfo
|
|
want settingFields
|
|
}{
|
|
{
|
|
name: "nameAttributePath is set",
|
|
settings: &social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"name_attribute_path": "name",
|
|
},
|
|
},
|
|
want: settingFields{
|
|
nameAttributePath: "name",
|
|
loginAttributePath: "",
|
|
idTokenAttributeName: "",
|
|
teamIds: []string{},
|
|
allowedOrganizations: []string{},
|
|
},
|
|
},
|
|
{
|
|
name: "loginAttributePath is set",
|
|
settings: &social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"login_attribute_path": "login",
|
|
},
|
|
},
|
|
want: settingFields{
|
|
nameAttributePath: "",
|
|
loginAttributePath: "login",
|
|
idTokenAttributeName: "",
|
|
teamIds: []string{},
|
|
allowedOrganizations: []string{},
|
|
},
|
|
},
|
|
{
|
|
name: "idTokenAttributeName is set",
|
|
settings: &social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"id_token_attribute_name": "id_token",
|
|
},
|
|
},
|
|
want: settingFields{
|
|
nameAttributePath: "",
|
|
loginAttributePath: "",
|
|
idTokenAttributeName: "id_token",
|
|
teamIds: []string{},
|
|
allowedOrganizations: []string{},
|
|
},
|
|
},
|
|
{
|
|
name: "teamIds is set",
|
|
settings: &social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"team_ids": "[\"team1\", \"team2\"]",
|
|
},
|
|
},
|
|
want: settingFields{
|
|
nameAttributePath: "",
|
|
loginAttributePath: "",
|
|
idTokenAttributeName: "",
|
|
teamIds: []string{"team1", "team2"},
|
|
allowedOrganizations: []string{},
|
|
},
|
|
},
|
|
{
|
|
name: "allowedOrganizations is set",
|
|
settings: &social.OAuthInfo{
|
|
Extra: map[string]string{
|
|
"allowed_organizations": "org1, org2",
|
|
},
|
|
},
|
|
want: settingFields{
|
|
nameAttributePath: "",
|
|
loginAttributePath: "",
|
|
idTokenAttributeName: "",
|
|
teamIds: []string{},
|
|
allowedOrganizations: []string{"org1", "org2"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
|
|
|
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
|
|
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)
|
|
require.Equal(t, tc.want.idTokenAttributeName, s.idTokenAttributeName)
|
|
require.Equal(t, tc.want.teamIds, s.teamIds)
|
|
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
|
})
|
|
}
|
|
}
|