AuthN: Move oauthserver to extsvcauth (#75972)
* AuthN: Move oauthserver to extsvcauth * Codeowners
This commit is contained in:
@@ -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/extsvcauth/oauthserver"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
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