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>
This commit is contained in:
co-authored by
Mihály Gyöngyösi
parent
73681a251e
commit
edf1775d49
@@ -0,0 +1,219 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/oauthserver"
|
||||
"github.com/grafana/grafana/pkg/services/oauthserver/utils"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
db db.DB
|
||||
}
|
||||
|
||||
func NewStore(db db.DB) oauthserver.Store {
|
||||
return &store{db: db}
|
||||
}
|
||||
|
||||
func createImpersonatePermissions(sess *db.Session, client *oauthserver.ExternalService) error {
|
||||
if len(client.ImpersonatePermissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
insertPermQuery := make([]interface{}, 1, len(client.ImpersonatePermissions)*3+1)
|
||||
insertPermStmt := `INSERT INTO oauth_impersonate_permission (client_id, action, scope) VALUES `
|
||||
for _, perm := range client.ImpersonatePermissions {
|
||||
insertPermStmt += "(?, ?, ?),"
|
||||
insertPermQuery = append(insertPermQuery, client.ClientID, perm.Action, perm.Scope)
|
||||
}
|
||||
insertPermQuery[0] = insertPermStmt[:len(insertPermStmt)-1]
|
||||
_, err := sess.Exec(insertPermQuery...)
|
||||
return err
|
||||
}
|
||||
|
||||
func registerExternalService(sess *db.Session, client *oauthserver.ExternalService) error {
|
||||
insertQuery := []interface{}{
|
||||
`INSERT INTO oauth_client (name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
client.Name,
|
||||
client.ClientID,
|
||||
client.Secret,
|
||||
client.GrantTypes,
|
||||
client.Audiences,
|
||||
client.ServiceAccountID,
|
||||
client.PublicPem,
|
||||
client.RedirectURI,
|
||||
}
|
||||
if _, err := sess.Exec(insertQuery...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createImpersonatePermissions(sess, client)
|
||||
}
|
||||
|
||||
func (s *store) RegisterExternalService(ctx context.Context, client *oauthserver.ExternalService) error {
|
||||
return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
return registerExternalService(sess, client)
|
||||
})
|
||||
}
|
||||
|
||||
func recreateImpersonatePermissions(sess *db.Session, client *oauthserver.ExternalService, prevClientID string) error {
|
||||
deletePermQuery := `DELETE FROM oauth_impersonate_permission WHERE client_id = ?`
|
||||
if _, errDelPerm := sess.Exec(deletePermQuery, prevClientID); errDelPerm != nil {
|
||||
return errDelPerm
|
||||
}
|
||||
|
||||
if len(client.ImpersonatePermissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return createImpersonatePermissions(sess, client)
|
||||
}
|
||||
|
||||
func updateExternalService(sess *db.Session, client *oauthserver.ExternalService, prevClientID string) error {
|
||||
updateQuery := []interface{}{
|
||||
`UPDATE oauth_client SET client_id = ?, secret = ?, grant_types = ?, audiences = ?, service_account_id = ?, public_pem = ?, redirect_uri = ? WHERE name = ?`,
|
||||
client.ClientID,
|
||||
client.Secret,
|
||||
client.GrantTypes,
|
||||
client.Audiences,
|
||||
client.ServiceAccountID,
|
||||
client.PublicPem,
|
||||
client.RedirectURI,
|
||||
client.Name,
|
||||
}
|
||||
if _, err := sess.Exec(updateQuery...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return recreateImpersonatePermissions(sess, client, prevClientID)
|
||||
}
|
||||
|
||||
func (s *store) SaveExternalService(ctx context.Context, client *oauthserver.ExternalService) error {
|
||||
if client.Name == "" {
|
||||
return oauthserver.ErrClientRequiredName
|
||||
}
|
||||
return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
previous, errFetchExtSvc := getExternalServiceByName(sess, client.Name)
|
||||
if errFetchExtSvc != nil {
|
||||
var srcError errutil.Error
|
||||
if errors.As(errFetchExtSvc, &srcError) {
|
||||
if srcError.MessageID != oauthserver.ErrClientNotFoundMessageID {
|
||||
return errFetchExtSvc
|
||||
}
|
||||
}
|
||||
}
|
||||
if previous == nil {
|
||||
return registerExternalService(sess, client)
|
||||
}
|
||||
return updateExternalService(sess, client, previous.ClientID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) GetExternalService(ctx context.Context, id string) (*oauthserver.ExternalService, error) {
|
||||
res := &oauthserver.ExternalService{}
|
||||
if id == "" {
|
||||
return nil, oauthserver.ErrClientRequiredID
|
||||
}
|
||||
|
||||
err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
getClientQuery := `SELECT
|
||||
id, name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri
|
||||
FROM oauth_client
|
||||
WHERE client_id = ?`
|
||||
found, err := sess.SQL(getClientQuery, id).Get(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return oauthserver.ErrClientNotFound(id)
|
||||
}
|
||||
|
||||
impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?`
|
||||
return sess.SQL(impersonatePermQuery, id).Find(&res.ImpersonatePermissions)
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// GetPublicKey returns public key, issued by 'issuer', and assigned for subject. Public key is used to check
|
||||
// signature of jwt assertion in authorization grants.
|
||||
func (s *store) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) {
|
||||
res := &oauthserver.ExternalService{}
|
||||
if clientID == "" {
|
||||
return nil, oauthserver.ErrClientRequiredID
|
||||
}
|
||||
|
||||
if err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
getKeyQuery := `SELECT public_pem FROM oauth_client WHERE client_id = ?`
|
||||
found, err := sess.SQL(getKeyQuery, clientID).Get(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return oauthserver.ErrClientNotFound(clientID)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, errParseKey := utils.ParsePublicKeyPem(res.PublicPem)
|
||||
if errParseKey != nil {
|
||||
return nil, errParseKey
|
||||
}
|
||||
|
||||
var alg string
|
||||
switch key.(type) {
|
||||
case *rsa.PublicKey:
|
||||
alg = oauthserver.RS256
|
||||
case *ecdsa.PublicKey:
|
||||
alg = oauthserver.ES256
|
||||
}
|
||||
|
||||
return &jose.JSONWebKey{
|
||||
Algorithm: alg,
|
||||
Key: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *store) GetExternalServiceByName(ctx context.Context, name string) (*oauthserver.ExternalService, error) {
|
||||
res := &oauthserver.ExternalService{}
|
||||
if name == "" {
|
||||
return nil, oauthserver.ErrClientRequiredName
|
||||
}
|
||||
|
||||
err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
var errGetByName error
|
||||
res, errGetByName = getExternalServiceByName(sess, name)
|
||||
return errGetByName
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.ExternalService, error) {
|
||||
res := &oauthserver.ExternalService{}
|
||||
getClientQuery := `SELECT
|
||||
id, name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri
|
||||
FROM oauth_client
|
||||
WHERE name = ?`
|
||||
found, err := sess.SQL(getClientQuery, name).Get(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, oauthserver.ErrClientNotFound(name)
|
||||
}
|
||||
|
||||
impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?`
|
||||
errPerm := sess.SQL(impersonatePermQuery, res.ClientID).Find(&res.ImpersonatePermissions)
|
||||
|
||||
return res, errPerm
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user