Files
grafana/pkg/services/oauthserver/store/database.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

220 lines
6.4 KiB
Go

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
}