grafana/pkg/services/oauthserver/store/database_test.go
Gabriel MABILLE edf1775d49
AuthN: Embed an OAuth2 server for external service authentication (#68086)
* Moving POC files from #64283 to a new branch

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Adding missing permission definition

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Force the service instantiation while client isn't merged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Merge conf with main

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Leave go-sqlite3 version unchanged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* tidy

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* User SearchUserPermissions instead of SearchUsersPermissions

* Replace DummyKeyService with signingkeys.Service

* Use user🆔<id> as subject

* Fix introspection endpoint issue

* Add X-Grafana-Org-Id to get_resources.bash script

* Regenerate toggles_gen.go
* Fix basic.go

* Add GetExternalService tests

* Add GetPublicKeyScopes tests

* Add GetScopesOnUser tests

* Add GetScopes tests

* Add ParsePublicKeyPem tests

* Add database test for GetByName

* re-add comments

* client tests added

* Add GetExternalServicePublicKey tests

* Add other test case to GetExternalServicePublicKey

* client_credentials grant test

* Add test to jwtbearer grant

* Test Comments

* Add handleKeyOptions tests

* Add RSA key generation test

* Add ECDSA by default to EmbeddedSigningKeysService

* Clean up org id scope and audiences

* Add audiences to the DB

* Fix check on Audience

* Fix double import

* Add AC Store mock and align oauthserver tests

* Fix test after rebase

* Adding missing store function to mock

* Fix double import

* Add CODEOWNER

* Fix some linting errors

* errors don't need type assertion

* Typo codeowners

* use mockery for oauthserver store

* Add feature toggle check

* Fix db tests to handle the feature flag

* Adding call to DeleteExternalServiceRole

* Fix flaky test

* Re-organize routes comments and plan futur work

* Add client_id check to Extended JWT client

* Clean up

* Fix

* Remove background service registry instantiation of the OAuth server

* Comment cleanup

* Remove unused client function

* Update go.mod to use the latest ory/fosite commit

* Remove oauth2_server related configs from defaults.ini

* Add audiences to DTO

* Fix flaky test

* Remove registration endpoint and demo scripts. Document code

* Rename packages

* Remove the OAuthService vs OAuthServer confusion

* fix incorrect import ext_jwt_test

* Comments and order

* Comment basic auth

* Remove unecessary todo

* Clean api

* Moving ParsePublicKeyPem to utils

* re ordering functions in service.go

* Fix comment

* comment on the redirect uri

* Add RBAC actions, not only scopes

* Fix tests

* re-import featuremgmt in migrations

* Fix wire

* Fix scopes in test

* Fix flaky test

* Remove todo, the intersection should always return the minimal set

* Remove unecessary check from intersection code

* Allow env overrides on settings

* remove the term app name

* Remove app keyword for client instead and use Name instead of ExternalServiceName

* LogID remove ExternalService ref

* Use Name instead of ExternalServiceName

* Imports order

* Inline

* Using ExternalService and ExternalServiceDTO

* Remove xorm tags

* comment

* Rename client files

* client -> external service

* comments

* Move test to correct package

* slimmer test

* cachedUser -> cachedExternalService

* Fix aggregate store test

* PluginAuthSession -> AuthSession

* Revert the nil cehcks

* Remove unecessary extra

* Removing custom session

* fix typo in test

* Use constants for tests

* Simplify HandleToken tests

* Refactor the HandleTokenRequest test

* test message

* Review test

* Prevent flacky test on client as well

* go imports

* Revert changes from 526e48ad45

* AuthN: Change the External Service registration form (#68649)

* AuthN: change the External Service registration form

* Gen default permissions

* Change demo script registration form

* Remove unecessary comment

* Nit.

* Reduce cyclomatic complexity

* Remove demo_scripts

* Handle case with no service account

* Comments

* Group key gen

* Nit.

* Check the SaveExternalService test

* Rename cachedUser to cachedClient in test

* One more test case to database test

* Comments

* Remove last org scope

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Update pkg/services/oauthserver/utils/utils_test.go

* Update pkg/services/sqlstore/migrations/oauthserver/migrations.go

Remove comment

* Update pkg/setting/setting.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

---------

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>
2023-05-25 15:38:30 +02:00

377 lines
12 KiB
Go

package store
import (
"context"
"testing"
"github.com/go-jose/go-jose/v3"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthserver"
)
func TestStore_RegisterAndGetClient(t *testing.T) {
s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})}
tests := []struct {
name string
client oauthserver.ExternalService
wantErr bool
}{
{
name: "register and get",
client: oauthserver.ExternalService{
Name: "The Worst App Ever",
ClientID: "ANonRandomClientID",
Secret: "ICouldKeepSecrets",
GrantTypes: "clients_credentials",
PublicPem: []byte(`------BEGIN FAKE PUBLIC KEY-----
VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO
b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB
IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp
cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3Qg
QW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtl
eS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJ
cyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4g
UlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4g
VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO
b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB
IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp
cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3Qg
QW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtl
eS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJ
cyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4g
UlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4g
VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO
b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB
IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp
cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4uLi4gSXQgSXMgSnVz
dCBBIFJlZ3VsYXIgQmFzZTY0IEVuY29kZWQgU3RyaW5nLi4uCg==
------END FAKE PUBLIC KEY-----`),
ServiceAccountID: 2,
SelfPermissions: nil,
ImpersonatePermissions: nil,
RedirectURI: "/whereto",
},
wantErr: false,
},
{
name: "register with impersonate permissions and get",
client: oauthserver.ExternalService{
Name: "The Best App Ever",
ClientID: "AnAlmostRandomClientID",
Secret: "ICannotKeepSecrets",
GrantTypes: "clients_credentials",
PublicPem: []byte(`test`),
ServiceAccountID: 2,
SelfPermissions: nil,
ImpersonatePermissions: []accesscontrol.Permission{
{Action: "dashboards:create", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "dashboards:*"},
{Action: "dashboards:write", Scope: "folders:*"},
{Action: "dashboards:write", Scope: "dashboards:*"},
},
RedirectURI: "/whereto",
},
wantErr: false,
},
{
name: "register with audiences and get",
client: oauthserver.ExternalService{
Name: "The Most Normal App Ever",
ClientID: "AnAlmostRandomClientIDAgain",
Secret: "ICanKeepSecretsEventually",
GrantTypes: "clients_credentials",
PublicPem: []byte(`test`),
ServiceAccountID: 2,
SelfPermissions: nil,
Audiences: "https://oauth.test/,https://sub.oauth.test/",
RedirectURI: "/whereto",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
err := s.RegisterExternalService(ctx, &tt.client)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// Compare results
compareClientToStored(t, s, &tt.client)
})
}
}
func TestStore_SaveExternalService(t *testing.T) {
client1 := oauthserver.ExternalService{
Name: "my-external-service",
ClientID: "ClientID",
Secret: "Secret",
GrantTypes: "client_credentials",
PublicPem: []byte("test"),
ServiceAccountID: 2,
ImpersonatePermissions: []accesscontrol.Permission{},
RedirectURI: "/whereto",
}
client1WithPerm := client1
client1WithPerm.ImpersonatePermissions = []accesscontrol.Permission{
{Action: "dashboards:read", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "dashboards:*"},
}
client1WithNewSecrets := client1
client1WithNewSecrets.ClientID = "NewClientID"
client1WithNewSecrets.Secret = "NewSecret"
client1WithNewSecrets.PublicPem = []byte("newtest")
client1WithAud := client1
client1WithAud.Audiences = "https://oauth.test/,https://sub.oauth.test/"
tests := []struct {
name string
runs []oauthserver.ExternalService
wantErr bool
}{
{
name: "error no name",
runs: []oauthserver.ExternalService{{}},
wantErr: true,
},
{
name: "simple register",
runs: []oauthserver.ExternalService{client1},
wantErr: false,
},
{
name: "no update",
runs: []oauthserver.ExternalService{client1, client1},
wantErr: false,
},
{
name: "add permissions",
runs: []oauthserver.ExternalService{client1, client1WithPerm},
wantErr: false,
},
{
name: "remove permissions",
runs: []oauthserver.ExternalService{client1WithPerm, client1},
wantErr: false,
},
{
name: "update id and secrets",
runs: []oauthserver.ExternalService{client1, client1WithNewSecrets},
wantErr: false,
},
{
name: "update audience",
runs: []oauthserver.ExternalService{client1, client1WithAud},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})}
for i := range tt.runs {
err := s.SaveExternalService(context.Background(), &tt.runs[i])
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
compareClientToStored(t, s, &tt.runs[i])
}
})
}
}
func TestStore_GetExternalServiceByName(t *testing.T) {
client1 := oauthserver.ExternalService{
Name: "my-external-service",
ClientID: "ClientID",
Secret: "Secret",
GrantTypes: "client_credentials",
PublicPem: []byte("test"),
ServiceAccountID: 2,
ImpersonatePermissions: []accesscontrol.Permission{},
RedirectURI: "/whereto",
}
client2 := oauthserver.ExternalService{
Name: "my-external-service-2",
ClientID: "ClientID2",
Secret: "Secret2",
GrantTypes: "client_credentials,urn:ietf:params:grant-type:jwt-bearer",
PublicPem: []byte("test2"),
ServiceAccountID: 3,
Audiences: "https://oauth.test/,https://sub.oauth.test/",
ImpersonatePermissions: []accesscontrol.Permission{
{Action: "dashboards:read", Scope: "folders:*"},
{Action: "dashboards:read", Scope: "dashboards:*"},
},
RedirectURI: "/whereto",
}
s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})}
require.NoError(t, s.SaveExternalService(context.Background(), &client1))
require.NoError(t, s.SaveExternalService(context.Background(), &client2))
tests := []struct {
name string
search string
want *oauthserver.ExternalService
wantErr bool
}{
{
name: "no name provided",
search: "",
want: nil,
wantErr: true,
},
{
name: "not found",
search: "unknown-external-service",
want: nil,
wantErr: true,
},
{
name: "search client 1 by name",
search: "my-external-service",
want: &client1,
wantErr: false,
},
{
name: "search client 2 by name",
search: "my-external-service-2",
want: &client2,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stored, err := s.GetExternalServiceByName(context.Background(), tt.search)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
compareClients(t, stored, tt.want)
})
}
}
func TestStore_GetExternalServicePublicKey(t *testing.T) {
clientID := "ClientID"
createClient := func(clientID string, publicPem string) *oauthserver.ExternalService {
return &oauthserver.ExternalService{
Name: "my-external-service",
ClientID: clientID,
Secret: "Secret",
GrantTypes: "client_credentials",
PublicPem: []byte(publicPem),
ServiceAccountID: 2,
ImpersonatePermissions: []accesscontrol.Permission{},
RedirectURI: "/whereto",
}
}
testCases := []struct {
name string
client *oauthserver.ExternalService
clientID string
want *jose.JSONWebKey
wantKeyType string
wantErr bool
}{
{
name: "should return an error when clientID is empty",
clientID: "",
client: createClient(clientID, ""),
want: nil,
wantErr: true,
},
{
name: "should return an error when the client was not found",
clientID: "random",
client: createClient(clientID, ""),
want: nil,
wantErr: true,
},
{
name: "should return an error when PublicPem is not valid",
clientID: clientID,
client: createClient(clientID, ""),
want: nil,
wantErr: true,
},
{
name: "should return the JSON Web Key ES256",
clientID: clientID,
client: createClient(clientID, `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+
mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw==
-----END PUBLIC KEY-----`),
wantKeyType: oauthserver.ES256,
wantErr: false,
},
{
name: "should return the JSON Web Key RS256",
clientID: clientID,
client: createClient(clientID, `-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAxkly/cHvsxd6EcShGUlFAB5lIMlIbGRocCVWbIM26f6pnGr+gCNv
s365DQdQ/jUjF8bSEQM+EtjGlv2Y7Jm7dQROpPzX/1M+53Us/Gl138UtAEgL5ZKe
SKN5J/f9Nx4wkgb99v2Bt0nz6xv+kSJwgR0o8zi8shDR5n7a5mTdlQe2NOixzWlT
vnpp6Tm+IE+XyXXcrCr01I9Rf+dKuYOPSJ1K3PDgFmmGvsLcjRCCK9EftfY0keU+
IP+sh8ewNxc6KcaLBXm3Tadb1c/HyuMi6FyYw7s9m8tyAvI1CMBAcXqLIEaRgNrc
vuO8AU0bVoUmYMKhozkcCYHudkeS08hEjQIDAQAB
-----END RSA PUBLIC KEY-----`),
wantKeyType: oauthserver.RS256,
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})}
require.NoError(t, s.SaveExternalService(context.Background(), tc.client))
webKey, err := s.GetExternalServicePublicKey(context.Background(), tc.clientID)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.wantKeyType, webKey.Algorithm)
})
}
}
func compareClientToStored(t *testing.T, s *store, wanted *oauthserver.ExternalService) {
ctx := context.Background()
stored, err := s.GetExternalService(ctx, wanted.ClientID)
require.NoError(t, err)
require.NotNil(t, stored)
compareClients(t, stored, wanted)
}
func compareClients(t *testing.T, stored *oauthserver.ExternalService, wanted *oauthserver.ExternalService) {
// Reset ID so we can compare
require.NotZero(t, stored.ID)
stored.ID = 0
// Compare permissions separately
wantedPerms := wanted.ImpersonatePermissions
storedPerms := stored.ImpersonatePermissions
wanted.ImpersonatePermissions = nil
stored.ImpersonatePermissions = nil
require.EqualValues(t, *wanted, *stored)
require.ElementsMatch(t, wantedPerms, storedPerms)
}