grafana/pkg/login/social/connectors/generic_oauth_test.go

1286 lines
48 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/apimachinery/identity"
"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/org/orgtest"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) {
testCases := []struct {
Name string
SkipOrgRoleSync bool
AllowAssignGrafanaAdmin bool
ResponseBody any
OAuth2Extra any
Setup func(*orgtest.FakeOrgService)
RoleAttributePath string
RoleAttributeStrict bool
OrgAttributePath string
OrgMapping []string
ExpectedEmail string
ExpectedOrgRoles map[int64]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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
Name: "Given no id_token, a valid role path, no API response, no data",
RoleAttributePath: "role",
ExpectedEmail: "",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedGrafanaAdmin: nil,
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedGrafanaAdmin: trueBoolPtr(),
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedError: nil,
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
},
{
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct org roles 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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct org roles 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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
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",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
},
{
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",
ExpectedOrgRoles: nil,
},
{
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid org attribute path, a valid org mapping, a valid API response, derive the correct org roles 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",
OrgAttributePath: "info.roles",
OrgMapping: []string{"SRE:2:Viewer", "engineering:3:Editor"},
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin, 3: org.RoleAdmin},
},
{
Name: "Given a valid id_token, a role attribute path, an org roles path, an org mapping, a valid API response, prefer ID token",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "'Viewer'",
OrgAttributePath: "info.roles",
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
},
{
Name: "Should not fail when the evaluated role is invalid, role_attribute_strict is set to true and evaluated org roles are not empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "'Invalid'",
RoleAttributeStrict: true,
OrgAttributePath: "info.roles",
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
},
{
Name: "Should not fail when the evaluated role is valid, role_attribute_strict is set to true and evaluated org roles are empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "'Editor'",
RoleAttributeStrict: true,
OrgAttributePath: "info.roles",
OrgMapping: []string{"notmatching:org_dev:Viewer", "notmatching:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
},
{
Name: "Should not fail when role_attribute path is empty, role_attribute_strict is set to true and evaluated org roles are not empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "",
RoleAttributeStrict: true,
OrgAttributePath: "info.roles",
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
},
{
Name: "Should return empty when evaluated role is empty/invalid, role_attribute_strict is set to false and evaluated org roles are empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
Setup: func(orgSvc *orgtest.FakeOrgService) {
orgSvc.ExpectedError = assert.AnError
},
RoleAttributePath: "'Invalid'",
RoleAttributeStrict: false,
OrgAttributePath: "info.roles",
OrgMapping: []string{"dev:*:Viewer"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: nil,
},
{
Name: "Should fail when role_attribute_path is empty, role_attribute_strict is set to true and org_mapping is empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "",
RoleAttributeStrict: true,
OrgAttributePath: "info.invalid",
OrgMapping: []string{},
ExpectedEmail: "john.doe@example.com",
ExpectedError: errRoleAttributeStrictViolation,
},
{
Name: "Should fail when role_attribute_path evaluates to invalid role, role_attribute_strict is set to true and org_mapping is empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "'Invalid'",
RoleAttributeStrict: true,
OrgAttributePath: "info.invalid",
OrgMapping: []string{},
ExpectedEmail: "john.doe@example.com",
ExpectedError: errRoleAttributeStrictViolation,
},
{
Name: "Should fail when role_attribute path is empty, role_attribute_strict is set to true and evaluated org roles are empty",
SkipOrgRoleSync: false,
AllowAssignGrafanaAdmin: false,
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "",
RoleAttributeStrict: true,
OrgAttributePath: "info.invalid",
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedError: errRoleAttributeStrictViolation,
},
{
Name: "Should get orgs from API when not in token",
ResponseBody: map[string]any{"anotherInfo": map[string]any{"roles": []string{"fromApiOne", "fromApiTwo"}}},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "",
RoleAttributeStrict: true,
OrgAttributePath: "anotherInfo.roles",
OrgMapping: []string{"fromApiOne:org_dev:Viewer", "fromApiTwo:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
},
{
Name: "Give AutoAssignOrgRole in AutoAssignOrgId when OrgMapping returns no OrgRoles",
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{
// { "email": "john.doe@example.com",
// "info": { "roles": [ "dev", "engineering" ] }}
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
RoleAttributePath: "",
OrgAttributePath: "info.roles",
OrgMapping: []string{"foo:org_dev:Viewer", "bar:org_engineering:Editor"},
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
}
cfg := &setting.Cfg{
AutoAssignOrg: true,
AutoAssignOrgId: 2,
AutoAssignOrgRole: string(org.RoleViewer),
}
for _, tc := range testCases {
orgSvc := &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "org_dev"}, {ID: 5, Name: "org_engineering"}}}
if tc.Setup != nil {
tc.Setup(orgSvc)
}
orgRoleMapper := ProvideOrgRoleMapper(cfg, orgSvc)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
EmailAttributePath: "email",
}, cfg,
orgRoleMapper,
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
provider.info.RoleAttributePath = tc.RoleAttributePath
provider.info.OrgAttributePath = tc.OrgAttributePath
provider.info.OrgMapping = tc.OrgMapping
provider.orgMappingCfg = orgRoleMapper.ParseOrgMappingSettings(context.Background(), tc.OrgMapping, tc.RoleAttributeStrict)
provider.info.AllowAssignGrafanaAdmin = tc.AllowAssignGrafanaAdmin
provider.info.SkipOrgRoleSync = tc.SkipOrgRoleSync
provider.info.RoleAttributeStrict = tc.RoleAttributeStrict
t.Run(tc.Name, func(t *testing.T) {
body, err := json.Marshal(tc.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.info.ApiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(tc.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
if tc.ExpectedError != nil {
require.ErrorIs(t, err, tc.ExpectedError)
return
}
require.NoError(t, err)
require.Equal(t, tc.ExpectedEmail, actualResult.Email)
require.Equal(t, tc.ExpectedEmail, actualResult.Login)
require.Equal(t, tc.ExpectedOrgRoles, actualResult.OrgRoles)
require.Equal(t, tc.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
})
}
}
func TestUserInfoSearchesForLogin(t *testing.T) {
testCases := []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: "",
},
}
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"login_attribute_path": "login",
},
}, setting.NewCfg(),
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
for _, tc := range testCases {
provider.loginAttributePath = tc.LoginAttributePath
t.Run(tc.Name, func(t *testing.T) {
body, err := json.Marshal(tc.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", tc.ResponseBody)
_, err = w.Write(body)
require.NoError(t, err)
}))
provider.info.ApiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(tc.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
require.NoError(t, err)
require.Equal(t, tc.ExpectedLogin, actualResult.Login)
})
}
}
func TestUserInfoSearchesForName(t *testing.T) {
testCases := []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: "",
},
}
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"name_attribute_path": "name",
},
},
setting.NewCfg(),
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
for _, tc := range testCases {
provider.nameAttributePath = tc.NameAttributePath
t.Run(tc.Name, func(t *testing.T) {
body, err := json.Marshal(tc.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", tc.ResponseBody)
_, err = w.Write(body)
require.NoError(t, err)
}))
provider.info.ApiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(tc.OAuth2Extra)
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
require.NoError(t, err)
require.Equal(t, tc.ExpectedName, actualResult.Name)
})
}
}
func TestUserInfoSearchesForGroup(t *testing.T) {
testCases := []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 testCases {
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.NewCfg(),
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
&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{}, nil, &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{}, nil, &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)
})
}
}
func TestSocialGenericOAuth_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "passes when team_url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: nil,
},
{
name: "fails if settings map contains an invalid field",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"invalid_field": []int{1, 2, 3},
},
},
wantErr: ssosettings.ErrInvalidSettings,
},
{
name: "fails if client id is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if client id does not exist",
settings: ssoModels.SSOSettings{
Settings: map[string]any{},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if both allow assign grafana admin and skip org role sync are enabled",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "invalid_url",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "/path",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if teams url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "file://teams",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, ssoModels.SSOSettings{}, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestSocialGenericOAuth_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
expectedConfig *oauth2.Config
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
ClientSecret: "new-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: "some-new-url",
},
RedirectURL: "/login/generic_oauth",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
expectedConfig: &oauth2.Config{
ClientID: "client-id",
ClientSecret: "client-secret",
RedirectURL: "/login/generic_oauth",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.EqualValues(t, tc.expectedInfo, s.info)
require.EqualValues(t, tc.expectedConfig, s.Config)
})
}
}
func TestGenericOAuth_Reload_ExtraFields(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
info *social.OAuthInfo
expectError bool
expectedInfo *social.OAuthInfo
expectedTeamsUrl string
expectedEmailAttributeName string
expectedEmailAttributePath string
expectedNameAttributePath string
expectedGroupsAttributePath string
expectedLoginAttributePath string
expectedIdTokenAttributeName string
expectedTeamIdsAttributePath string
expectedTeamIds []string
expectedAllowedOrganizations []string
}{
{
name: "successfully reloads the settings",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
TeamsUrl: "https://host/users",
EmailAttributePath: "email-attr-path",
EmailAttributeName: "email-attr-name",
GroupsAttributePath: "groups-attr-path",
TeamIdsAttributePath: "team-ids-attr-path",
Extra: map[string]string{
teamIdsKey: "team1",
allowedOrganizationsKey: "org1",
loginAttributePathKey: "login-attr-path",
idTokenAttributeNameKey: "id-token-attr-name",
nameAttributePathKey: "name-attr-path",
},
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"teams_url": "https://host/v2/users",
"email_attribute_path": "new-email-attr-path",
"email_attribute_name": "new-email-attr-name",
"groups_attribute_path": "new-group-attr-path",
"team_ids_attribute_path": "new-team-ids-attr-path",
teamIdsKey: "team1,team2",
allowedOrganizationsKey: "org1,org2",
loginAttributePathKey: "new-login-attr-path",
idTokenAttributeNameKey: "new-id-token-attr-name",
nameAttributePathKey: "new-name-attr-path",
},
},
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
TeamsUrl: "https://host/v2/users",
EmailAttributePath: "new-email-attr-path",
EmailAttributeName: "new-email-attr-name",
GroupsAttributePath: "new-group-attr-path",
TeamIdsAttributePath: "new-team-ids-attr-path",
Extra: map[string]string{
teamIdsKey: "team1,team2",
allowedOrganizationsKey: "org1,org2",
loginAttributePathKey: "new-login-attr-path",
idTokenAttributeNameKey: "new-id-token-attr-name",
nameAttributePathKey: "new-name-attr-path",
},
},
expectedTeamsUrl: "https://host/v2/users",
expectedEmailAttributeName: "new-email-attr-name",
expectedEmailAttributePath: "new-email-attr-path",
expectedGroupsAttributePath: "new-group-attr-path",
expectedTeamIdsAttributePath: "new-team-ids-attr-path",
expectedTeamIds: []string{"team1", "team2"},
expectedAllowedOrganizations: []string{"org1", "org2"},
expectedLoginAttributePath: "new-login-attr-path",
expectedIdTokenAttributeName: "new-id-token-attr-name",
expectedNameAttributePath: "new-name-attr-path",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(tc.info, setting.NewCfg(), nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
require.NoError(t, err)
require.EqualValues(t, tc.expectedInfo, s.info)
require.EqualValues(t, tc.expectedTeamsUrl, s.teamsUrl)
require.EqualValues(t, tc.expectedEmailAttributeName, s.emailAttributeName)
require.EqualValues(t, tc.expectedEmailAttributePath, s.emailAttributePath)
require.EqualValues(t, tc.expectedGroupsAttributePath, s.groupsAttributePath)
require.EqualValues(t, tc.expectedTeamIdsAttributePath, s.teamIdsAttributePath)
require.EqualValues(t, tc.expectedTeamIds, s.teamIds)
require.EqualValues(t, tc.expectedAllowedOrganizations, s.allowedOrganizations)
require.EqualValues(t, tc.expectedLoginAttributePath, s.loginAttributePath)
require.EqualValues(t, tc.expectedIdTokenAttributeName, s.idTokenAttributeName)
require.EqualValues(t, tc.expectedNameAttributePath, s.nameAttributePath)
})
}
}