mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* signing key wip use db keyset storage add signing_key table add testing for key storage add ES256 key tests Remove caching and implement UpdateOrCreate Stabilize interfaces * Encrypt private keys * Fixup signer * Fixup ext_jwt * Add GetOrCreatePrivate with automatic key rotation * use GetOrCreate for ext_jwt * use GetOrCreate in id * catch invalid block type * fix broken test * remove key generator * reduce public interface of signing service
748 lines
24 KiB
Go
748 lines
24 KiB
Go
package oasimpl
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ory/fosite"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/exp/maps"
|
|
"gopkg.in/square/go-jose.v2"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
|
|
"github.com/grafana/grafana/pkg/models/roletype"
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/oauthserver"
|
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
|
"github.com/grafana/grafana/pkg/services/team"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
)
|
|
|
|
func TestOAuth2ServiceImpl_handleClientCredentials(t *testing.T) {
|
|
client1 := &oauthserver.ExternalService{
|
|
Name: "testapp",
|
|
ClientID: "RANDOMID",
|
|
GrantTypes: string(fosite.GrantTypeClientCredentials),
|
|
ServiceAccountID: 2,
|
|
SignedInUser: &user.SignedInUser{
|
|
UserID: 2,
|
|
Name: "Test App",
|
|
Login: "testapp",
|
|
OrgRole: roletype.RoleViewer,
|
|
Permissions: map[int64]map[string][]string{
|
|
oauthserver.TmpOrgID: {
|
|
"dashboards:read": {"dashboards:*", "folders:*"},
|
|
"dashboards:write": {"dashboards:uid:1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
scopes []string
|
|
client *oauthserver.ExternalService
|
|
expectedClaims map[string]any
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "no claim without client_credentials grant type",
|
|
client: &oauthserver.ExternalService{
|
|
Name: "testapp",
|
|
ClientID: "RANDOMID",
|
|
GrantTypes: string(fosite.GrantTypeJWTBearer),
|
|
ServiceAccountID: 2,
|
|
SignedInUser: &user.SignedInUser{},
|
|
},
|
|
},
|
|
{
|
|
name: "no claims without scopes",
|
|
client: client1,
|
|
},
|
|
{
|
|
name: "profile claims",
|
|
client: client1,
|
|
scopes: []string{"profile"},
|
|
expectedClaims: map[string]any{"name": "Test App", "login": "testapp"},
|
|
},
|
|
{
|
|
name: "email claims should be empty",
|
|
client: client1,
|
|
scopes: []string{"email"},
|
|
},
|
|
{
|
|
name: "groups claims should be empty",
|
|
client: client1,
|
|
scopes: []string{"groups"},
|
|
},
|
|
{
|
|
name: "entitlements claims",
|
|
client: client1,
|
|
scopes: []string{"entitlements"},
|
|
expectedClaims: map[string]any{"entitlements": map[string][]string{
|
|
"dashboards:read": {"dashboards:*", "folders:*"},
|
|
"dashboards:write": {"dashboards:uid:1"},
|
|
}},
|
|
},
|
|
{
|
|
name: "scoped entitlements claims",
|
|
client: client1,
|
|
scopes: []string{"entitlements", "dashboards:write"},
|
|
expectedClaims: map[string]any{"entitlements": map[string][]string{
|
|
"dashboards:write": {"dashboards:uid:1"},
|
|
}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
env := setupTestEnv(t)
|
|
session := &fosite.DefaultSession{}
|
|
requester := fosite.NewAccessRequest(session)
|
|
requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ","))
|
|
requester.RequestedScope = fosite.Arguments(tt.scopes)
|
|
sessionData := NewAuthSession()
|
|
err := env.S.handleClientCredentials(ctx, requester, sessionData, tt.client)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tt.expectedClaims == nil {
|
|
require.Empty(t, sessionData.JWTClaims.Extra)
|
|
return
|
|
}
|
|
require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims))
|
|
for claimsKey, claimsValue := range tt.expectedClaims {
|
|
switch expected := claimsValue.(type) {
|
|
case []string:
|
|
require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
|
|
case map[string][]string:
|
|
actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string)
|
|
require.True(t, ok, "expected map[string][]string")
|
|
|
|
require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual))
|
|
for expKey, expValue := range expected {
|
|
require.ElementsMatch(t, expValue, actual[expKey])
|
|
}
|
|
default:
|
|
require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_handleJWTBearer(t *testing.T) {
|
|
now := time.Now()
|
|
client1 := &oauthserver.ExternalService{
|
|
Name: "testapp",
|
|
ClientID: "RANDOMID",
|
|
GrantTypes: string(fosite.GrantTypeJWTBearer),
|
|
ServiceAccountID: 2,
|
|
SignedInUser: &user.SignedInUser{
|
|
UserID: 2,
|
|
OrgID: oauthserver.TmpOrgID,
|
|
Name: "Test App",
|
|
Login: "testapp",
|
|
OrgRole: roletype.RoleViewer,
|
|
Permissions: map[int64]map[string][]string{
|
|
oauthserver.TmpOrgID: {
|
|
"users:impersonate": {"users:*"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
user56 := &user.User{
|
|
ID: 56,
|
|
Email: "user56@example.org",
|
|
Login: "user56",
|
|
Name: "User 56",
|
|
Updated: now,
|
|
}
|
|
teams := []*team.TeamDTO{
|
|
{ID: 1, Name: "Team 1", OrgID: 1},
|
|
{ID: 2, Name: "Team 2", OrgID: 1},
|
|
}
|
|
client1WithPerm := func(perms []ac.Permission) *oauthserver.ExternalService {
|
|
client := *client1
|
|
client.ImpersonatePermissions = perms
|
|
return &client
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
initEnv func(*TestEnv)
|
|
scopes []string
|
|
client *oauthserver.ExternalService
|
|
subject string
|
|
expectedClaims map[string]any
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "no claim without jwtbearer grant type",
|
|
client: &oauthserver.ExternalService{
|
|
Name: "testapp",
|
|
ClientID: "RANDOMID",
|
|
GrantTypes: string(fosite.GrantTypeClientCredentials),
|
|
ServiceAccountID: 2,
|
|
},
|
|
},
|
|
{
|
|
name: "err invalid subject",
|
|
client: client1,
|
|
subject: "invalid_subject",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "err client is not allowed to impersonate",
|
|
client: &oauthserver.ExternalService{
|
|
Name: "testapp",
|
|
ClientID: "RANDOMID",
|
|
GrantTypes: string(fosite.GrantTypeJWTBearer),
|
|
ServiceAccountID: 2,
|
|
SignedInUser: &user.SignedInUser{
|
|
UserID: 2,
|
|
Name: "Test App",
|
|
Login: "testapp",
|
|
OrgRole: roletype.RoleViewer,
|
|
Permissions: map[int64]map[string][]string{oauthserver.TmpOrgID: {}},
|
|
},
|
|
},
|
|
subject: "user:id:56",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "err subject not found",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedError = user.ErrUserNotFound
|
|
},
|
|
client: client1,
|
|
subject: "user:id:56",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "no claim without scope",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
},
|
|
client: client1,
|
|
subject: "user:id:56",
|
|
},
|
|
{
|
|
name: "profile claims",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
},
|
|
client: client1,
|
|
subject: "user:id:56",
|
|
scopes: []string{"profile"},
|
|
expectedClaims: map[string]any{
|
|
"name": "User 56",
|
|
"login": "user56",
|
|
"updated_at": now.Unix(),
|
|
},
|
|
},
|
|
{
|
|
name: "email claim",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
},
|
|
client: client1,
|
|
subject: "user:id:56",
|
|
scopes: []string{"email"},
|
|
expectedClaims: map[string]any{
|
|
"email": "user56@example.org",
|
|
},
|
|
},
|
|
{
|
|
name: "groups claim",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.TeamService.ExpectedTeamsByUser = teams
|
|
},
|
|
client: client1,
|
|
subject: "user:id:56",
|
|
scopes: []string{"groups"},
|
|
expectedClaims: map[string]any{
|
|
"groups": []string{"Team 1", "Team 2"},
|
|
},
|
|
},
|
|
{
|
|
name: "no entitlement without permission intersection",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
56: {"Viewer"}}, nil)
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
56: {{Action: "dashboards:read", Scope: "dashboards:uid:1"}},
|
|
}, nil)
|
|
},
|
|
client: client1WithPerm([]ac.Permission{
|
|
{Action: "datasources:read", Scope: "datasources:*"},
|
|
}),
|
|
subject: "user:id:56",
|
|
expectedClaims: map[string]any{
|
|
"entitlements": map[string][]string{},
|
|
},
|
|
scopes: []string{"entitlements"},
|
|
},
|
|
{
|
|
name: "entitlements contains only the intersection of permissions",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
56: {"Viewer"}}, nil)
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
56: {
|
|
{Action: "dashboards:read", Scope: "dashboards:uid:1"},
|
|
{Action: "datasources:read", Scope: "datasources:uid:1"},
|
|
},
|
|
}, nil)
|
|
},
|
|
client: client1WithPerm([]ac.Permission{
|
|
{Action: "datasources:read", Scope: "datasources:*"},
|
|
}),
|
|
subject: "user:id:56",
|
|
expectedClaims: map[string]any{
|
|
"entitlements": map[string][]string{
|
|
"datasources:read": {"datasources:uid:1"},
|
|
},
|
|
},
|
|
scopes: []string{"entitlements"},
|
|
},
|
|
{
|
|
name: "entitlements have correctly translated users:self permissions",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
56: {"Viewer"}}, nil)
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
56: {
|
|
{Action: "users:read", Scope: "global.users:id:*"},
|
|
{Action: "users.permissions:read", Scope: "users:id:*"},
|
|
}}, nil)
|
|
},
|
|
client: client1WithPerm([]ac.Permission{
|
|
{Action: "users:read", Scope: "global.users:self"},
|
|
{Action: "users.permissions:read", Scope: "users:self"},
|
|
}),
|
|
subject: "user:id:56",
|
|
expectedClaims: map[string]any{
|
|
"entitlements": map[string][]string{
|
|
"users:read": {"global.users:id:56"},
|
|
"users.permissions:read": {"users:id:56"},
|
|
},
|
|
},
|
|
scopes: []string{"entitlements"},
|
|
},
|
|
{
|
|
name: "entitlements have correctly translated teams:self permissions",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.TeamService.ExpectedTeamsByUser = teams
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
56: {"Viewer"}}, nil)
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
56: {{Action: "teams:read", Scope: "teams:*"}}}, nil)
|
|
},
|
|
client: client1WithPerm([]ac.Permission{
|
|
{Action: "teams:read", Scope: "teams:self"},
|
|
}),
|
|
subject: "user:id:56",
|
|
expectedClaims: map[string]any{
|
|
"entitlements": map[string][]string{"teams:read": {"teams:id:1", "teams:id:2"}},
|
|
},
|
|
scopes: []string{"entitlements"},
|
|
},
|
|
{
|
|
name: "entitlements are correctly filtered based on scopes",
|
|
initEnv: func(env *TestEnv) {
|
|
env.UserService.ExpectedUser = user56
|
|
env.TeamService.ExpectedTeamsByUser = teams
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
56: {"Viewer"}}, nil)
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
56: {
|
|
{Action: "users:read", Scope: "global.users:id:*"},
|
|
{Action: "datasources:read", Scope: "datasources:uid:1"},
|
|
}}, nil)
|
|
},
|
|
client: client1WithPerm([]ac.Permission{
|
|
{Action: "users:read", Scope: "global.users:*"},
|
|
{Action: "datasources:read", Scope: "datasources:*"},
|
|
}),
|
|
subject: "user:id:56",
|
|
expectedClaims: map[string]any{
|
|
"entitlements": map[string][]string{"users:read": {"global.users:id:*"}},
|
|
},
|
|
scopes: []string{"entitlements", "users:read"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
env := setupTestEnv(t)
|
|
session := &fosite.DefaultSession{}
|
|
requester := fosite.NewAccessRequest(session)
|
|
requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ","))
|
|
requester.RequestedScope = fosite.Arguments(tt.scopes)
|
|
requester.GrantedScope = fosite.Arguments(tt.scopes)
|
|
sessionData := NewAuthSession()
|
|
sessionData.Subject = tt.subject
|
|
|
|
if tt.initEnv != nil {
|
|
tt.initEnv(env)
|
|
}
|
|
err := env.S.handleJWTBearer(ctx, requester, sessionData, tt.client)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tt.expectedClaims == nil {
|
|
require.Empty(t, sessionData.JWTClaims.Extra)
|
|
return
|
|
}
|
|
require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims))
|
|
|
|
for claimsKey, claimsValue := range tt.expectedClaims {
|
|
switch expected := claimsValue.(type) {
|
|
case []string:
|
|
require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
|
|
case map[string][]string:
|
|
actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string)
|
|
require.True(t, ok, "expected map[string][]string")
|
|
|
|
require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual))
|
|
for expKey, expValue := range expected {
|
|
require.ElementsMatch(t, expValue, actual[expKey])
|
|
}
|
|
default:
|
|
require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey])
|
|
}
|
|
}
|
|
|
|
env.AcStore.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
type tokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
Scope string `json:"scope"`
|
|
TokenType string `json:"token_type"`
|
|
}
|
|
|
|
type claims struct {
|
|
jwt.Claims
|
|
ClientID string `json:"client_id"`
|
|
Groups []string `json:"groups"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
Login string `json:"login"`
|
|
Scopes []string `json:"scope"`
|
|
Entitlements map[string][]string `json:"entitlements"`
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_HandleTokenRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tweakTestClient func(*oauthserver.ExternalService)
|
|
reqParams url.Values
|
|
wantCode int
|
|
wantScope []string
|
|
wantClaims *claims
|
|
}{
|
|
{
|
|
name: "should allow client credentials grant",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeClientCredentials)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"CLIENT1SECRET"},
|
|
"scope": {"profile email groups entitlements"},
|
|
"audience": {AppURL},
|
|
},
|
|
wantCode: http.StatusOK,
|
|
wantScope: []string{"profile", "email", "groups", "entitlements"},
|
|
wantClaims: &claims{
|
|
Claims: jwt.Claims{
|
|
Subject: "user:id:2", // From client1.ServiceAccountID
|
|
Issuer: AppURL, // From env.S.Config.Issuer
|
|
Audience: jwt.Audience{AppURL},
|
|
},
|
|
ClientID: "CLIENT1ID",
|
|
Name: "client-1",
|
|
Login: "client-1",
|
|
Entitlements: map[string][]string{
|
|
"users:impersonate": {"users:*"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "should allow jwt-bearer grant",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeJWTBearer)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"CLIENT1SECRET"},
|
|
"assertion": {
|
|
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
|
|
},
|
|
"scope": {"profile email groups entitlements"},
|
|
},
|
|
wantCode: http.StatusOK,
|
|
wantScope: []string{"profile", "email", "groups", "entitlements"},
|
|
wantClaims: &claims{
|
|
Claims: jwt.Claims{
|
|
Subject: "user:id:56", // To match the assertion
|
|
Issuer: AppURL, // From env.S.Config.Issuer
|
|
Audience: jwt.Audience{TokenURL, AppURL},
|
|
},
|
|
ClientID: "CLIENT1ID",
|
|
Email: "user56@example.org",
|
|
Name: "User 56",
|
|
Login: "user56",
|
|
Groups: []string{"Team 1", "Team 2"},
|
|
Entitlements: map[string][]string{
|
|
"dashboards:read": {"folders:uid:UID1"},
|
|
"folders:read": {"folders:uid:UID1"},
|
|
"users:read": {"global.users:id:56"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "should deny jwt-bearer grant with wrong audience",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeJWTBearer)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"CLIENT1SECRET"},
|
|
"assertion": {
|
|
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, "invalid audience"),
|
|
},
|
|
"scope": {"profile email groups entitlements"},
|
|
},
|
|
wantCode: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "should deny jwt-bearer grant for clients without the grant",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeJWTBearer)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"CLIENT1SECRET"},
|
|
"assertion": {
|
|
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
|
|
},
|
|
"scope": {"profile email groups entitlements"},
|
|
},
|
|
tweakTestClient: func(es *oauthserver.ExternalService) {
|
|
es.GrantTypes = string(fosite.GrantTypeClientCredentials)
|
|
},
|
|
wantCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "should deny client_credentials grant for clients without the grant",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeClientCredentials)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"CLIENT1SECRET"},
|
|
"scope": {"profile email groups entitlements"},
|
|
"audience": {AppURL},
|
|
},
|
|
tweakTestClient: func(es *oauthserver.ExternalService) {
|
|
es.GrantTypes = string(fosite.GrantTypeJWTBearer)
|
|
},
|
|
wantCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "should deny client_credentials grant with wrong secret",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeClientCredentials)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"WRONG_SECRET"},
|
|
"scope": {"profile email groups entitlements"},
|
|
"audience": {AppURL},
|
|
},
|
|
tweakTestClient: func(es *oauthserver.ExternalService) {
|
|
es.GrantTypes = string(fosite.GrantTypeClientCredentials)
|
|
},
|
|
wantCode: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "should deny jwt-bearer grant with wrong secret",
|
|
reqParams: url.Values{
|
|
"grant_type": {string(fosite.GrantTypeJWTBearer)},
|
|
"client_id": {"CLIENT1ID"},
|
|
"client_secret": {"WRONG_SECRET"},
|
|
"assertion": {
|
|
genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL),
|
|
},
|
|
"scope": {"profile email groups entitlements"},
|
|
},
|
|
tweakTestClient: func(es *oauthserver.ExternalService) {
|
|
es.GrantTypes = string(fosite.GrantTypeJWTBearer)
|
|
},
|
|
wantCode: http.StatusUnauthorized,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := setupTestEnv(t)
|
|
setupHandleTokenRequestEnv(t, env, tt.tweakTestClient)
|
|
|
|
req := httptest.NewRequest("POST", "/oauth2/token", strings.NewReader(tt.reqParams.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
env.S.HandleTokenRequest(resp, req)
|
|
|
|
require.Equal(t, tt.wantCode, resp.Code, resp.Body.String())
|
|
if tt.wantCode != http.StatusOK {
|
|
return
|
|
}
|
|
|
|
body := resp.Body.Bytes()
|
|
require.NotEmpty(t, body)
|
|
|
|
var tokenResp tokenResponse
|
|
require.NoError(t, json.Unmarshal(body, &tokenResp))
|
|
|
|
// Check token response
|
|
require.NotEmpty(t, tokenResp.Scope)
|
|
require.ElementsMatch(t, tt.wantScope, strings.Split(tokenResp.Scope, " "))
|
|
require.Positive(t, tokenResp.ExpiresIn)
|
|
require.Equal(t, "bearer", tokenResp.TokenType)
|
|
require.NotEmpty(t, tokenResp.AccessToken)
|
|
|
|
// Check access token
|
|
parsedToken, err := jwt.ParseSigned(tokenResp.AccessToken)
|
|
require.NoError(t, err)
|
|
require.Len(t, parsedToken.Headers, 1)
|
|
typeHeader := parsedToken.Headers[0].ExtraHeaders["typ"]
|
|
require.Equal(t, "at+jwt", strings.ToLower(typeHeader.(string)))
|
|
require.Equal(t, "RS256", parsedToken.Headers[0].Algorithm)
|
|
// Check access token claims
|
|
var claims claims
|
|
require.NoError(t, parsedToken.Claims(pk.Public(), &claims))
|
|
// Check times and remove them
|
|
require.Positive(t, claims.IssuedAt.Time())
|
|
require.Positive(t, claims.Expiry.Time())
|
|
claims.IssuedAt = jwt.NewNumericDate(time.Time{})
|
|
claims.Expiry = jwt.NewNumericDate(time.Time{})
|
|
// Check the ID and remove it
|
|
require.NotEmpty(t, claims.ID)
|
|
claims.ID = ""
|
|
// Compare the rest
|
|
require.Equal(t, tt.wantClaims, &claims)
|
|
})
|
|
}
|
|
}
|
|
|
|
func genAssertion(t *testing.T, signKey *rsa.PrivateKey, clientID, sub string, audience ...string) string {
|
|
key := jose.SigningKey{Algorithm: jose.RS256, Key: signKey}
|
|
assertion := jwt.Claims{
|
|
ID: uuid.New().String(),
|
|
Issuer: clientID,
|
|
Subject: sub,
|
|
Audience: audience,
|
|
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
}
|
|
|
|
var signerOpts = jose.SignerOptions{}
|
|
signerOpts.WithType("JWT")
|
|
rsaSigner, errSigner := jose.NewSigner(key, &signerOpts)
|
|
require.NoError(t, errSigner)
|
|
builder := jwt.Signed(rsaSigner)
|
|
rawJWT, errSign := builder.Claims(assertion).CompactSerialize()
|
|
require.NoError(t, errSign)
|
|
return rawJWT
|
|
}
|
|
|
|
// setupHandleTokenRequestEnv creates a client and a user and sets all Mocks call for the handleTokenRequest test cases
|
|
func setupHandleTokenRequestEnv(t *testing.T, env *TestEnv, opt func(*oauthserver.ExternalService)) {
|
|
now := time.Now()
|
|
hashedSecret, err := bcrypt.GenerateFromPassword([]byte("CLIENT1SECRET"), bcrypt.DefaultCost)
|
|
require.NoError(t, err)
|
|
client1 := &oauthserver.ExternalService{
|
|
Name: "client-1",
|
|
ClientID: "CLIENT1ID",
|
|
Secret: string(hashedSecret),
|
|
GrantTypes: string(fosite.GrantTypeClientCredentials + "," + fosite.GrantTypeJWTBearer),
|
|
ServiceAccountID: 2,
|
|
ImpersonatePermissions: []ac.Permission{
|
|
{Action: "users:read", Scope: oauthserver.ScopeGlobalUsersSelf},
|
|
{Action: "users.permissions:read", Scope: oauthserver.ScopeUsersSelf},
|
|
{Action: "teams:read", Scope: oauthserver.ScopeTeamsSelf},
|
|
|
|
{Action: "folders:read", Scope: "folders:*"},
|
|
{Action: "dashboards:read", Scope: "folders:*"},
|
|
{Action: "dashboards:read", Scope: "dashboards:*"},
|
|
},
|
|
SelfPermissions: []ac.Permission{
|
|
{Action: "users:impersonate", Scope: "users:*"},
|
|
},
|
|
Audiences: AppURL,
|
|
}
|
|
|
|
// Apply any option the test case might need
|
|
if opt != nil {
|
|
opt(client1)
|
|
}
|
|
|
|
sa1 := &serviceaccounts.ServiceAccountProfileDTO{
|
|
Id: client1.ServiceAccountID,
|
|
Name: client1.Name,
|
|
Login: client1.Name,
|
|
OrgId: oauthserver.TmpOrgID,
|
|
IsDisabled: false,
|
|
Created: now,
|
|
Updated: now,
|
|
Role: "Viewer",
|
|
}
|
|
|
|
user56 := &user.User{
|
|
ID: 56,
|
|
Email: "user56@example.org",
|
|
Login: "user56",
|
|
Name: "User 56",
|
|
Updated: now,
|
|
}
|
|
user56Permissions := []ac.Permission{
|
|
{Action: "users:read", Scope: "global.users:id:56"},
|
|
{Action: "folders:read", Scope: "folders:uid:UID1"},
|
|
{Action: "dashboards:read", Scope: "folders:uid:UID1"},
|
|
{Action: "datasources:read", Scope: "datasources:uid:DS_UID2"}, // This one should be ignored when impersonating
|
|
}
|
|
user56Teams := []*team.TeamDTO{
|
|
{ID: 1, Name: "Team 1", OrgID: 1},
|
|
{ID: 2, Name: "Team 2", OrgID: 1},
|
|
}
|
|
|
|
// To retrieve the Client, its publicKey and its permissions
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, client1.ClientID).Return(client1, nil)
|
|
env.OAuthStore.On("GetExternalServicePublicKey", mock.Anything, client1.ClientID).Return(&jose.JSONWebKey{Key: Client1Key.Public(), Algorithm: "RS256"}, nil)
|
|
env.SAService.On("RetrieveServiceAccount", mock.Anything, oauthserver.TmpOrgID, client1.ServiceAccountID).Return(sa1, nil)
|
|
env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(client1.SelfPermissions, nil)
|
|
// To retrieve the user to impersonate, its permissions and its teams
|
|
env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{
|
|
user56.ID: user56Permissions}, nil)
|
|
env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{
|
|
user56.ID: {"Viewer"}}, nil)
|
|
env.TeamService.ExpectedTeamsByUser = user56Teams
|
|
env.UserService.ExpectedUser = user56
|
|
}
|