mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-15 19:53:58 +03:00
refactor: pass transaction to resolveResource
This commit is contained in:
@@ -34,8 +34,8 @@ func (m *Module) ClientAPIScopes(ctx context.Context, tx *gorm.DB, clientID stri
|
||||
}
|
||||
|
||||
// AllowedScopesForAudience implements the OIDC module's APIAccessProvider interface
|
||||
func (m *Module) AllowedScopesForAudience(ctx context.Context, clientID, audience string, subjectType oidc.SubjectType) (scopes []string, apiExists bool, err error) {
|
||||
return m.service.AllowedScopesForAudience(ctx, clientID, audience, subjectType)
|
||||
func (m *Module) AllowedScopesForAudience(ctx context.Context, tx *gorm.DB, clientID, audience string, subjectType oidc.SubjectType) (scopes []string, apiExists bool, err error) {
|
||||
return m.service.AllowedScopesForAudience(ctx, tx, clientID, audience, subjectType)
|
||||
}
|
||||
|
||||
// DescribePermissions implements the OIDC module's APIAccessProvider interface
|
||||
|
||||
@@ -379,9 +379,13 @@ func (s *Service) ClientAPIScopesAndAudiences(ctx context.Context, tx *gorm.DB,
|
||||
}
|
||||
|
||||
// AllowedScopesForAudience returns the permission keys the client is allowed for the API identified by the given audience and subject type, plus whether such an API exists
|
||||
func (s *Service) AllowedScopesForAudience(ctx context.Context, clientID, audience string, subjectType oidc.SubjectType) (scopes []string, apiExists bool, err error) {
|
||||
func (s *Service) AllowedScopesForAudience(ctx context.Context, tx *gorm.DB, clientID, audience string, subjectType oidc.SubjectType) (scopes []string, apiExists bool, err error) {
|
||||
if tx == nil {
|
||||
tx = s.db
|
||||
}
|
||||
|
||||
var api API
|
||||
err = s.db.WithContext(ctx).
|
||||
err = tx.WithContext(ctx).
|
||||
Select("id").
|
||||
Where("audience = ?", audience).
|
||||
First(&api).
|
||||
@@ -393,7 +397,7 @@ func (s *Service) AllowedScopesForAudience(ctx context.Context, clientID, audien
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
err = s.db.WithContext(ctx).
|
||||
err = tx.WithContext(ctx).
|
||||
Table("api_permissions").
|
||||
Select("api_permissions.key").
|
||||
Joins("JOIN oidc_clients_allowed_api_permissions g ON g.api_permission_id = api_permissions.id AND g.oidc_client_id = ? AND g.subject_type = ?", clientID, subjectType).
|
||||
|
||||
@@ -148,12 +148,12 @@ func TestAllowedScopesForAudienceFiltersBySubjectType(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
userScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), "client-1", "https://api.orders.example.com", oidc.SubjectTypeUser)
|
||||
userScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), nil, "client-1", "https://api.orders.example.com", oidc.SubjectTypeUser)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
assert.ElementsMatch(t, []string{"read:orders"}, userScopes)
|
||||
|
||||
clientScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), "client-1", "https://api.orders.example.com", oidc.SubjectTypeClient)
|
||||
clientScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), nil, "client-1", "https://api.orders.example.com", oidc.SubjectTypeClient)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
assert.ElementsMatch(t, []string{"write:orders"}, clientScopes)
|
||||
|
||||
@@ -37,7 +37,7 @@ type APIAccessProvider interface {
|
||||
// ClientAPIScopes returns the custom-API permission keys and the distinct API audiences a client is allowed to request across all subject types
|
||||
ClientAPIScopes(ctx context.Context, tx *gorm.DB, clientID string) (scopes []string, audiences []string, err error)
|
||||
// AllowedScopesForAudience returns the permission keys the client is allowed for the API identified by the given audience and subject type, and whether such an API exists
|
||||
AllowedScopesForAudience(ctx context.Context, clientID, audience string, subjectType SubjectType) (scopes []string, apiExists bool, err error)
|
||||
AllowedScopesForAudience(ctx context.Context, tx *gorm.DB, clientID, audience string, subjectType SubjectType) (scopes []string, apiExists bool, err error)
|
||||
// DescribePermissions returns the display information for the given permission keys of the API identified by audience
|
||||
// Unknown keys are omitted
|
||||
DescribePermissions(ctx context.Context, audience string, keys []string) ([]PermissionInfo, error)
|
||||
@@ -46,7 +46,7 @@ type APIAccessProvider interface {
|
||||
// resolveResource maps an RFC 8707 resource, which may be empty, to the audience to stamp on the issued token and the subset of requestedScopes that may be granted
|
||||
// An empty resource is a plain login token bound to the requesting client and yields only identity scopes
|
||||
// The subject type selects which of the client's grants apply: user-delegated flows only see user grants, the client credentials grant only sees client grants
|
||||
func resolveResource(ctx context.Context, provider APIAccessProvider, clientID, resource string, requestedScopes []string, subjectType SubjectType) (audience string, grantedScopes []string, err error) {
|
||||
func resolveResource(ctx context.Context, tx *gorm.DB, provider APIAccessProvider, clientID, resource string, requestedScopes []string, subjectType SubjectType) (audience string, grantedScopes []string, err error) {
|
||||
grantable := make(map[string]struct{}, len(standardScopes))
|
||||
for _, scope := range standardScopes {
|
||||
grantable[scope] = struct{}{}
|
||||
@@ -60,7 +60,7 @@ func resolveResource(ctx context.Context, provider APIAccessProvider, clientID,
|
||||
return "", nil, fosite.ErrInvalidTarget.WithHintf("The requested resource '%s' is invalid, missing, unknown, or malformed.", resource)
|
||||
}
|
||||
|
||||
allowedScopes, apiExists, err := provider.AllowedScopesForAudience(ctx, clientID, resource, subjectType)
|
||||
allowedScopes, apiExists, err := provider.AllowedScopesForAudience(ctx, tx, clientID, resource, subjectType)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (f fakeAPIAccess) ClientAPIScopes(_ context.Context, _ *gorm.DB, _ string)
|
||||
return scopes, audiences, nil
|
||||
}
|
||||
|
||||
func (f fakeAPIAccess) AllowedScopesForAudience(_ context.Context, _ string, audience string, subjectType SubjectType) ([]string, bool, error) {
|
||||
func (f fakeAPIAccess) AllowedScopesForAudience(_ context.Context, _ *gorm.DB, _ string, audience string, subjectType SubjectType) ([]string, bool, error) {
|
||||
bySubject, exists := f.allowed[audience]
|
||||
if !exists {
|
||||
return nil, false, nil
|
||||
@@ -69,7 +69,7 @@ func (f fakeAPIAccess) DescribePermissions(_ context.Context, audience string, k
|
||||
|
||||
func TestResolveResourceDefaultIsLoginToken(t *testing.T) {
|
||||
// With no resource the token is a plain login token audienced to the requesting client
|
||||
audience, granted, err := resolveResource(t.Context(), nil, "client-1", "", []string{"openid", "profile"}, SubjectTypeUser)
|
||||
audience, granted, err := resolveResource(t.Context(), nil, nil, "client-1", "", []string{"openid", "profile"}, SubjectTypeUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "client-1", audience)
|
||||
assert.Equal(t, []string{"openid", "profile"}, granted)
|
||||
@@ -77,7 +77,7 @@ func TestResolveResourceDefaultIsLoginToken(t *testing.T) {
|
||||
|
||||
func TestResolveResourceRejectsCustomScopeWithoutResource(t *testing.T) {
|
||||
// Requesting a custom scope without targeting its API must be rejected, not dropped.
|
||||
_, _, err := resolveResource(t.Context(), nil, "client-1", "", []string{"openid", "read:orders"}, SubjectTypeUser)
|
||||
_, _, err := resolveResource(t.Context(), nil, nil, "client-1", "", []string{"openid", "read:orders"}, SubjectTypeUser)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestResolveResourceCustomAPIGrantsValidScopes(t *testing.T) {
|
||||
"https://api.orders.example.com": {"read:orders", "write:orders"},
|
||||
})
|
||||
|
||||
audience, granted, err := resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"openid", "read:orders"}, SubjectTypeUser)
|
||||
audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"openid", "read:orders"}, SubjectTypeUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://api.orders.example.com", audience)
|
||||
// openid stays (identity, for the ID token); read:orders is allowed for this API.
|
||||
@@ -98,13 +98,13 @@ func TestResolveResourceRejectsScopeFromAnotherAPI(t *testing.T) {
|
||||
"https://api.orders.example.com": {"read:orders", "write:orders"},
|
||||
})
|
||||
// write:billing belongs to a different API than the one targeted -> rejected.
|
||||
_, _, err := resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"openid", "write:billing"}, SubjectTypeUser)
|
||||
_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"openid", "write:billing"}, SubjectTypeUser)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestResolveResourceUnknownIsRejected(t *testing.T) {
|
||||
provider := userAccess(map[string][]string{})
|
||||
_, _, err := resolveResource(t.Context(), provider, "client-1", "https://api.unknown.example.com", []string{"read"}, SubjectTypeUser)
|
||||
_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.unknown.example.com", []string{"read"}, SubjectTypeUser)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestResolveResourceUnauthorizedClientIsRejected(t *testing.T) {
|
||||
provider := userAccess(map[string][]string{
|
||||
"https://api.orders.example.com": {},
|
||||
})
|
||||
_, _, err := resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
|
||||
_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -129,23 +129,23 @@ func TestResolveResourceSubjectTypesAreIndependent(t *testing.T) {
|
||||
}}
|
||||
|
||||
// The user-delegated grant works for user flows...
|
||||
audience, granted, err := resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
|
||||
audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://api.orders.example.com", audience)
|
||||
assert.ElementsMatch(t, []string{"read:orders"}, granted)
|
||||
|
||||
// ...but not for the client itself.
|
||||
_, _, err = resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeClient)
|
||||
_, _, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeClient)
|
||||
require.Error(t, err)
|
||||
|
||||
// The client grant works machine-to-machine...
|
||||
audience, granted, err = resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeClient)
|
||||
audience, granted, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeClient)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://api.orders.example.com", audience)
|
||||
assert.ElementsMatch(t, []string{"write:orders"}, granted)
|
||||
|
||||
// ...but users cannot be asked to delegate it.
|
||||
_, _, err = resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeUser)
|
||||
_, _, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeUser)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestResolveResourceClientWithoutClientGrantsIsDenied(t *testing.T) {
|
||||
},
|
||||
}}
|
||||
|
||||
_, _, err := resolveResource(t.Context(), provider, "client-1", "https://api.orders.example.com", nil, SubjectTypeClient)
|
||||
_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", nil, SubjectTypeClient)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ type authorizationService struct {
|
||||
// resolveGrant resolves the RFC 8707 resource of a request into the token audience, the scopes that may actually be granted, and the audience-qualified keys used to record and check consent
|
||||
// It always resolves against the client's user-delegated grants because every flow that passes through here acts on behalf of a user
|
||||
func (s *authorizationService) resolveGrant(ctx context.Context, clientID, resource string, requestedScopes []string) (audience string, grantedScopes []string, consentKeys []string, err error) {
|
||||
audience, grantedScopes, err = resolveResource(ctx, s.apiAccess, clientID, resource, requestedScopes, SubjectTypeUser)
|
||||
audience, grantedScopes, err = resolveResource(ctx, dbFromContext(ctx, s.db), s.apiAccess, clientID, resource, requestedScopes, SubjectTypeUser)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (h *tokenHandler) token(c *gin.Context) {
|
||||
h.provider.WriteAccessError(ctx, c.Writer, accessRequest, err)
|
||||
return
|
||||
}
|
||||
audience, grantedScopes, err := resolveResource(ctx, h.apiAccess, client.GetID(), resource, accessRequest.GetRequestedScopes(), SubjectTypeClient)
|
||||
audience, grantedScopes, err := resolveResource(ctx, nil, h.apiAccess, client.GetID(), resource, accessRequest.GetRequestedScopes(), SubjectTypeClient)
|
||||
if err != nil {
|
||||
h.provider.WriteAccessError(ctx, c.Writer, accessRequest, err)
|
||||
return
|
||||
@@ -127,7 +127,7 @@ func (h *tokenHandler) validateRefreshAPIGrant(ctx context.Context, client Clien
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err = resolveResource(ctx, h.apiAccess, client.GetID(), resource, accessRequest.GetGrantedScopes(), SubjectTypeUser)
|
||||
_, _, err = resolveResource(ctx, nil, h.apiAccess, client.GetID(), resource, accessRequest.GetGrantedScopes(), SubjectTypeUser)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user