mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Add namespace ID * Refactor and add tests * Rename maxOneOption -> atMostOneOption * Add ToDo * Remove UserLogin & UserID for NamespaceID Co-authored-by: jguer <joao.guerreiro@grafana.com> * Remove unecessary import of the userSvc * Update pkg/services/accesscontrol/acimpl/service.go * fix 1 -> userID * Update pkg/services/accesscontrol/accesscontrol.go --------- Co-authored-by: jguer <joao.guerreiro@grafana.com>
626 lines
23 KiB
Go
626 lines
23 KiB
Go
package oasimpl
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/fosite/storage"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
|
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
|
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver"
|
|
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oastest"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
|
saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
|
"github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest"
|
|
"github.com/grafana/grafana/pkg/services/team/teamtest"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
const (
|
|
AppURL = "https://oauth.test/"
|
|
TokenURL = AppURL + "oauth2/token"
|
|
)
|
|
|
|
var (
|
|
pk, _ = rsa.GenerateKey(rand.Reader, 4096)
|
|
Client1Key, _ = rsa.GenerateKey(rand.Reader, 4096)
|
|
)
|
|
|
|
type TestEnv struct {
|
|
S *OAuth2ServiceImpl
|
|
Cfg *setting.Cfg
|
|
AcStore *actest.MockStore
|
|
OAuthStore *oastest.MockStore
|
|
UserService *usertest.FakeUserService
|
|
TeamService *teamtest.FakeService
|
|
SAService *saTests.MockExtSvcAccountsService
|
|
}
|
|
|
|
func setupTestEnv(t *testing.T) *TestEnv {
|
|
t.Helper()
|
|
|
|
cfg := setting.NewCfg()
|
|
cfg.AppURL = AppURL
|
|
|
|
config := &fosite.Config{
|
|
AccessTokenLifespan: time.Hour,
|
|
TokenURL: TokenURL,
|
|
AccessTokenIssuer: AppURL,
|
|
IDTokenIssuer: AppURL,
|
|
ScopeStrategy: fosite.WildcardScopeStrategy,
|
|
}
|
|
|
|
fmgt := featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth)
|
|
|
|
env := &TestEnv{
|
|
Cfg: cfg,
|
|
AcStore: &actest.MockStore{},
|
|
OAuthStore: &oastest.MockStore{},
|
|
UserService: usertest.NewUserServiceFake(),
|
|
TeamService: teamtest.NewFakeService(),
|
|
SAService: saTests.NewMockExtSvcAccountsService(t),
|
|
}
|
|
env.S = &OAuth2ServiceImpl{
|
|
cache: localcache.New(cacheExpirationTime, cacheCleanupInterval),
|
|
cfg: cfg,
|
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
|
acService: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
|
memstore: storage.NewMemoryStore(),
|
|
sqlstore: env.OAuthStore,
|
|
logger: log.New("oauthserver.test"),
|
|
userService: env.UserService,
|
|
saService: env.SAService,
|
|
teamService: env.TeamService,
|
|
publicKey: &pk.PublicKey,
|
|
}
|
|
|
|
env.S.oauthProvider = newProvider(config, env.S, &signingkeystest.FakeSigningKeysService{
|
|
ExpectedSinger: pk,
|
|
ExpectedKeyID: "default",
|
|
ExpectedError: nil,
|
|
})
|
|
|
|
return env
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) {
|
|
const serviceName = "my-ext-service"
|
|
|
|
tests := []struct {
|
|
name string
|
|
init func(*TestEnv)
|
|
cmd *extsvcauth.ExternalServiceRegistration
|
|
mockChecks func(*testing.T, *TestEnv)
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "should create a new client without permissions",
|
|
init: func(env *TestEnv) {
|
|
// No client at the beginning
|
|
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
|
env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
// Return a service account ID
|
|
env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(0), nil)
|
|
},
|
|
cmd: &extsvcauth.ExternalServiceRegistration{
|
|
Name: serviceName,
|
|
OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}},
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalServiceByName", mock.Anything, mock.MatchedBy(func(name string) bool {
|
|
return name == serviceName
|
|
}))
|
|
env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool {
|
|
return client.Name == serviceName && client.ClientID != "" && client.Secret != "" &&
|
|
len(client.GrantTypes) == 0 && len(client.PublicPem) > 0 && client.ServiceAccountID == 0 &&
|
|
len(client.ImpersonatePermissions) == 0
|
|
}))
|
|
},
|
|
},
|
|
{
|
|
name: "should allow client credentials grant with correct permissions",
|
|
init: func(env *TestEnv) {
|
|
// No client at the beginning
|
|
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
|
env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil)
|
|
|
|
// Return a service account ID
|
|
env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(10), nil)
|
|
},
|
|
cmd: &extsvcauth.ExternalServiceRegistration{
|
|
Name: serviceName,
|
|
Self: extsvcauth.SelfCfg{
|
|
Enabled: true,
|
|
Permissions: []ac.Permission{{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}},
|
|
},
|
|
OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}},
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalServiceByName", mock.Anything, mock.MatchedBy(func(name string) bool {
|
|
return name == serviceName
|
|
}))
|
|
env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool {
|
|
return client.Name == serviceName && len(client.ClientID) > 0 && len(client.Secret) > 0 &&
|
|
client.GrantTypes == string(fosite.GrantTypeClientCredentials) &&
|
|
len(client.PublicPem) > 0 && client.ServiceAccountID == 10 &&
|
|
len(client.ImpersonatePermissions) == 0 &&
|
|
len(client.SelfPermissions) > 0
|
|
}))
|
|
// Check that despite no credential_grants the service account still has a permission to impersonate users
|
|
env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything,
|
|
mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool {
|
|
return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}
|
|
}))
|
|
},
|
|
},
|
|
{
|
|
name: "should allow jwt bearer grant and set default permissions",
|
|
init: func(env *TestEnv) {
|
|
// No client at the beginning
|
|
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
|
env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil)
|
|
// The service account needs to be created with a permission to impersonate users
|
|
env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(10), nil)
|
|
},
|
|
cmd: &extsvcauth.ExternalServiceRegistration{
|
|
Name: serviceName,
|
|
OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}},
|
|
Impersonation: extsvcauth.ImpersonationCfg{
|
|
Enabled: true,
|
|
Groups: true,
|
|
Permissions: []ac.Permission{{Action: "dashboards:read", Scope: "dashboards:*"}},
|
|
},
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
// Check that the external service impersonate permissions contains the default permissions required to populate the access token
|
|
env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool {
|
|
impPerm := client.ImpersonatePermissions
|
|
return slices.Contains(impPerm, ac.Permission{Action: "dashboards:read", Scope: "dashboards:*"}) &&
|
|
slices.Contains(impPerm, ac.Permission{Action: ac.ActionUsersRead, Scope: oauthserver.ScopeGlobalUsersSelf}) &&
|
|
slices.Contains(impPerm, ac.Permission{Action: ac.ActionUsersPermissionsRead, Scope: oauthserver.ScopeUsersSelf}) &&
|
|
slices.Contains(impPerm, ac.Permission{Action: ac.ActionTeamsRead, Scope: oauthserver.ScopeTeamsSelf})
|
|
}))
|
|
// Check that despite no credential_grants the service account still has a permission to impersonate users
|
|
env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything,
|
|
mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool {
|
|
return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}
|
|
}))
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := setupTestEnv(t)
|
|
if tt.init != nil {
|
|
tt.init(env)
|
|
}
|
|
|
|
dto, err := env.S.SaveExternalService(context.Background(), tt.cmd)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// Check that we generated client ID and secret
|
|
require.NotEmpty(t, dto.ID)
|
|
require.NotEmpty(t, dto.Secret)
|
|
|
|
// Check that we have generated keys and that we correctly return them
|
|
if tt.cmd.OAuthProviderCfg.Key != nil && tt.cmd.OAuthProviderCfg.Key.Generate {
|
|
require.NotNil(t, dto.OAuthExtra.KeyResult)
|
|
require.True(t, dto.OAuthExtra.KeyResult.Generated)
|
|
require.NotEmpty(t, dto.OAuthExtra.KeyResult.PublicPem)
|
|
require.NotEmpty(t, dto.OAuthExtra.KeyResult.PrivatePem)
|
|
}
|
|
|
|
// Check that we computed grant types and created or updated the service account
|
|
if tt.cmd.Self.Enabled {
|
|
require.NotNil(t, dto.OAuthExtra.GrantTypes)
|
|
require.Contains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeClientCredentials, "grant types should contain client_credentials")
|
|
} else {
|
|
require.NotContains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeClientCredentials, "grant types should not contain client_credentials")
|
|
}
|
|
// Check that we updated grant types
|
|
if tt.cmd.Impersonation.Enabled {
|
|
require.NotNil(t, dto.OAuthExtra.GrantTypes)
|
|
require.Contains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeJWTBearer, "grant types should contain JWT Bearer grant")
|
|
} else {
|
|
require.NotContains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeJWTBearer, "grant types should not contain JWT Bearer grant")
|
|
}
|
|
|
|
// Check that mocks were called as expected
|
|
env.OAuthStore.AssertExpectations(t)
|
|
env.SAService.AssertExpectations(t)
|
|
env.AcStore.AssertExpectations(t)
|
|
|
|
// Additional checks performed
|
|
if tt.mockChecks != nil {
|
|
tt.mockChecks(t, env)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) {
|
|
const serviceName = "my-ext-service"
|
|
|
|
dummyClient := func() *oauthserver.OAuthExternalService {
|
|
return &oauthserver.OAuthExternalService{
|
|
Name: serviceName,
|
|
ClientID: "RANDOMID",
|
|
Secret: "RANDOMSECRET",
|
|
GrantTypes: "client_credentials",
|
|
PublicPem: []byte("-----BEGIN PUBLIC KEY-----"),
|
|
ServiceAccountID: 1,
|
|
}
|
|
}
|
|
cachedClient := &oauthserver.OAuthExternalService{
|
|
Name: serviceName,
|
|
ClientID: "RANDOMID",
|
|
Secret: "RANDOMSECRET",
|
|
GrantTypes: "client_credentials",
|
|
PublicPem: []byte("-----BEGIN PUBLIC KEY-----"),
|
|
ServiceAccountID: 1,
|
|
SelfPermissions: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}},
|
|
SignedInUser: &user.SignedInUser{
|
|
UserID: 1,
|
|
Permissions: map[int64]map[string][]string{
|
|
1: {
|
|
"users:impersonate": {"users:*"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
init func(*TestEnv)
|
|
mockChecks func(*testing.T, *TestEnv)
|
|
wantPerm []ac.Permission
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "should hit the cache",
|
|
init: func(env *TestEnv) {
|
|
env.S.cache.Set(serviceName, *cachedClient, time.Minute)
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertNotCalled(t, "GetExternalService", mock.Anything, mock.Anything)
|
|
},
|
|
wantPerm: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}},
|
|
},
|
|
{
|
|
name: "should return error when the client was not found",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return error when the service account was not found",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil)
|
|
env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, sa.ErrServiceAccountNotFound)
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything)
|
|
env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, 1, 1)
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return error when the service account has no permissions",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil)
|
|
env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, nil)
|
|
env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("some error"))
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything)
|
|
env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, 1, 1)
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return correctly",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil)
|
|
env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{ID: 1}, nil)
|
|
env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return([]ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}}, nil)
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything)
|
|
env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1))
|
|
},
|
|
wantPerm: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}},
|
|
},
|
|
{
|
|
name: "should return correctly when the client has no service account",
|
|
init: func(env *TestEnv) {
|
|
client := &oauthserver.OAuthExternalService{
|
|
Name: serviceName,
|
|
ClientID: "RANDOMID",
|
|
Secret: "RANDOMSECRET",
|
|
GrantTypes: "client_credentials",
|
|
PublicPem: []byte("-----BEGIN PUBLIC KEY-----"),
|
|
ServiceAccountID: oauthserver.NoServiceAccountID,
|
|
}
|
|
env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(client, nil)
|
|
},
|
|
mockChecks: func(t *testing.T, env *TestEnv) {
|
|
env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything)
|
|
},
|
|
wantPerm: []ac.Permission{},
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := setupTestEnv(t)
|
|
if tt.init != nil {
|
|
tt.init(env)
|
|
}
|
|
|
|
client, err := env.S.GetExternalService(context.Background(), serviceName)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tt.mockChecks != nil {
|
|
tt.mockChecks(t, env)
|
|
}
|
|
|
|
require.Equal(t, serviceName, client.Name)
|
|
require.ElementsMatch(t, client.SelfPermissions, tt.wantPerm)
|
|
assertArrayInMap(t, client.SignedInUser.Permissions[1], ac.GroupScopesByAction(tt.wantPerm))
|
|
|
|
env.OAuthStore.AssertExpectations(t)
|
|
env.SAService.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertArrayInMap[K comparable, V string](t *testing.T, m1 map[K][]V, m2 map[K][]V) {
|
|
for k, v := range m1 {
|
|
require.Contains(t, m2, k)
|
|
require.ElementsMatch(t, v, m2[k])
|
|
}
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_RemoveExternalService(t *testing.T) {
|
|
const serviceName = "my-ext-service"
|
|
const clientID = "RANDOMID"
|
|
|
|
dummyClient := &oauthserver.OAuthExternalService{
|
|
Name: serviceName,
|
|
ClientID: clientID,
|
|
ServiceAccountID: 1,
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
init func(*TestEnv)
|
|
}{
|
|
{
|
|
name: "should do nothing on not found",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
|
},
|
|
},
|
|
{
|
|
name: "should remove the external service and its associated service account",
|
|
init: func(env *TestEnv) {
|
|
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(dummyClient, nil)
|
|
env.OAuthStore.On("DeleteExternalService", mock.Anything, clientID).Return(nil)
|
|
env.SAService.On("RemoveExtSvcAccount", mock.Anything, oauthserver.TmpOrgID, serviceName).Return(nil)
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := setupTestEnv(t)
|
|
if tt.init != nil {
|
|
tt.init(env)
|
|
}
|
|
|
|
err := env.S.RemoveExternalService(context.Background(), serviceName)
|
|
require.NoError(t, err)
|
|
|
|
env.OAuthStore.AssertExpectations(t)
|
|
env.SAService.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTestOAuth2ServiceImpl_handleKeyOptions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
keyOption *extsvcauth.KeyOption
|
|
expectedResult *extsvcauth.KeyResult
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "should return error when the key option is nil",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return error when the key option is empty",
|
|
keyOption: &extsvcauth.KeyOption{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "should return successfully when PublicPEM is specified",
|
|
keyOption: &extsvcauth.KeyOption{
|
|
PublicPEM: base64.StdEncoding.EncodeToString([]byte(`-----BEGIN PUBLIC KEY-----
|
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+
|
|
mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw==
|
|
-----END PUBLIC KEY-----`)),
|
|
},
|
|
wantErr: false,
|
|
expectedResult: &extsvcauth.KeyResult{
|
|
PublicPem: `-----BEGIN PUBLIC KEY-----
|
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+
|
|
mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw==
|
|
-----END PUBLIC KEY-----`,
|
|
Generated: false,
|
|
PrivatePem: "",
|
|
URL: "",
|
|
},
|
|
},
|
|
}
|
|
env := setupTestEnv(t)
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, err := env.S.handleKeyOptions(context.Background(), tc.keyOption)
|
|
if tc.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expectedResult, result)
|
|
})
|
|
}
|
|
|
|
t.Run("should generate an ECDSA key pair (default) when generate key option is specified", func(t *testing.T) {
|
|
result, err := env.S.handleKeyOptions(context.Background(), &extsvcauth.KeyOption{Generate: true})
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.PrivatePem)
|
|
require.NotNil(t, result.PublicPem)
|
|
require.True(t, result.Generated)
|
|
})
|
|
|
|
t.Run("should generate an RSA key pair when generate key option is specified", func(t *testing.T) {
|
|
env.S.cfg.OAuth2ServerGeneratedKeyTypeForClient = "RSA"
|
|
result, err := env.S.handleKeyOptions(context.Background(), &extsvcauth.KeyOption{Generate: true})
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.PrivatePem)
|
|
require.NotNil(t, result.PublicPem)
|
|
require.True(t, result.Generated)
|
|
})
|
|
}
|
|
|
|
func TestOAuth2ServiceImpl_handlePluginStateChanged(t *testing.T) {
|
|
pluginID := "my-app"
|
|
clientID := "RANDOMID"
|
|
impersonatePermission := []ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}}
|
|
selfPermission := append(impersonatePermission, ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll})
|
|
saID := int64(101)
|
|
client := &oauthserver.OAuthExternalService{
|
|
ID: 11,
|
|
Name: pluginID,
|
|
ClientID: clientID,
|
|
Secret: "SECRET",
|
|
ServiceAccountID: saID,
|
|
}
|
|
clientWithImpersonate := &oauthserver.OAuthExternalService{
|
|
ID: 11,
|
|
Name: pluginID,
|
|
ClientID: clientID,
|
|
Secret: "SECRET",
|
|
ImpersonatePermissions: []ac.Permission{
|
|
{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll},
|
|
},
|
|
ServiceAccountID: saID,
|
|
}
|
|
extSvcAcc := &sa.ExtSvcAccount{
|
|
ID: saID,
|
|
Login: "sa-my-app",
|
|
Name: pluginID,
|
|
OrgID: extsvcauth.TmpOrgID,
|
|
IsDisabled: false,
|
|
Role: org.RoleNone,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
init func(*TestEnv)
|
|
cmd *pluginsettings.PluginStateChangedEvent
|
|
}{
|
|
{
|
|
name: "should do nothing with not found",
|
|
init: func(te *TestEnv) {
|
|
te.OAuthStore.On("GetExternalServiceByName", mock.Anything, "unknown").Return(nil, oauthserver.ErrClientNotFoundFn("unknown"))
|
|
},
|
|
cmd: &pluginsettings.PluginStateChangedEvent{PluginId: "unknown", OrgId: 1, Enabled: false},
|
|
},
|
|
{
|
|
name: "should remove grants",
|
|
init: func(te *TestEnv) {
|
|
te.OAuthStore.On("GetExternalServiceByName", mock.Anything, pluginID).Return(clientWithImpersonate, nil)
|
|
te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, "").Return(nil)
|
|
},
|
|
cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: false},
|
|
},
|
|
{
|
|
name: "should set both grants",
|
|
init: func(te *TestEnv) {
|
|
te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil)
|
|
te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil)
|
|
te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil)
|
|
te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID,
|
|
string(fosite.GrantTypeClientCredentials)+","+string(fosite.GrantTypeJWTBearer)).Return(nil)
|
|
},
|
|
cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true},
|
|
},
|
|
{
|
|
name: "should set impersonate grant",
|
|
init: func(te *TestEnv) {
|
|
te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil)
|
|
te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil)
|
|
te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(impersonatePermission, nil)
|
|
te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeJWTBearer)).Return(nil)
|
|
},
|
|
cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true},
|
|
},
|
|
{
|
|
name: "should set client_credentials grant",
|
|
init: func(te *TestEnv) {
|
|
te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(client, nil)
|
|
te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil)
|
|
te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil)
|
|
te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeClientCredentials)).Return(nil)
|
|
},
|
|
cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := setupTestEnv(t)
|
|
if tt.init != nil {
|
|
tt.init(env)
|
|
}
|
|
|
|
err := env.S.handlePluginStateChanged(context.Background(), tt.cmd)
|
|
require.NoError(t, err)
|
|
|
|
// Check that mocks were called as expected
|
|
env.OAuthStore.AssertExpectations(t)
|
|
env.SAService.AssertExpectations(t)
|
|
env.AcStore.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|