mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-27 11:09:04 +01:00
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
co-authored by
Elias Schneider
parent
7c55bdf115
commit
1934efa84c
@@ -176,8 +176,8 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
Description: "This is an example description for Nextcloud",
|
||||
LaunchURL: new("https://nextcloud.local"),
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://nextcloud.localhost/auth/callback"},
|
||||
LogoutCallbackURLs: model.UrlList{"http://nextcloud.localhost/auth/logout/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://nextcloud.localhost/auth/callback"},
|
||||
LogoutCallbackURLs: datatype.StringList{"http://nextcloud.localhost/auth/logout/callback"},
|
||||
ImageType: new("png"),
|
||||
CreatedByID: new(users[0].ID),
|
||||
},
|
||||
@@ -187,7 +187,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "Immich",
|
||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||
CallbackURLs: model.UrlList{"http://immich.localhost/auth/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://immich.localhost/auth/callback"},
|
||||
CreatedByID: new(users[1].ID),
|
||||
IsGroupRestricted: true,
|
||||
AllowedUserGroups: []model.UserGroup{
|
||||
@@ -200,8 +200,8 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "Tailscale",
|
||||
Secret: "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a", // n4VfQeXlTzA6yKpWbR9uJcMdSx2qH0Lo
|
||||
CallbackURLs: model.UrlList{"http://tailscale.localhost/auth/callback"},
|
||||
LogoutCallbackURLs: model.UrlList{"http://tailscale.localhost/auth/logout/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://tailscale.localhost/auth/callback"},
|
||||
LogoutCallbackURLs: datatype.StringList{"http://tailscale.localhost/auth/logout/callback"},
|
||||
IsGroupRestricted: true,
|
||||
CreatedByID: new(users[0].ID),
|
||||
AllowedUserGroups: []model.UserGroup{
|
||||
@@ -214,7 +214,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "Federated",
|
||||
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
|
||||
CallbackURLs: model.UrlList{"http://federated.localhost/auth/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://federated.localhost/auth/callback"},
|
||||
CreatedByID: new(users[1].ID),
|
||||
AllowedUserGroups: []model.UserGroup{},
|
||||
Credentials: model.OidcClientCredentials{
|
||||
@@ -234,7 +234,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "SCIM Client",
|
||||
Secret: "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK", // nQbiuMRG7FpdK2EnDd5MBivWQeKFXohn
|
||||
CallbackURLs: model.UrlList{"http://scimclient.localhost/auth/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://scimclient.localhost/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
IsGroupRestricted: true,
|
||||
AllowedUserGroups: []model.UserGroup{
|
||||
@@ -248,7 +248,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "PAR Test Client",
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://par-client.localhost/auth/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://par-client.localhost/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
},
|
||||
{
|
||||
@@ -257,7 +257,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
||||
},
|
||||
Name: "Skip Consent Client",
|
||||
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
|
||||
CallbackURLs: model.UrlList{"http://skip-consent.localhost/auth/callback"},
|
||||
CallbackURLs: datatype.StringList{"http://skip-consent.localhost/auth/callback"},
|
||||
CreatedByID: new(users[0].ID),
|
||||
// Trusted client that bypasses the consent screen by default
|
||||
SkipConsent: true,
|
||||
|
||||
@@ -37,10 +37,11 @@ const (
|
||||
)
|
||||
|
||||
type OidcService struct {
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
previewBuilder oidcClientPreviewBuilder
|
||||
scimService *ScimService
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
previewBuilder oidcClientPreviewBuilder
|
||||
metadataRefresher metadataRefresher
|
||||
scimService *ScimService
|
||||
|
||||
httpClient *http.Client
|
||||
fileStorage storage.FileStorage
|
||||
@@ -50,21 +51,27 @@ type oidcClientPreviewBuilder interface {
|
||||
BuildClientPreview(ctx context.Context, client model.OidcClient, userID string, scopes []string, authenticationMethod string) (*oidc.ClientPreview, error)
|
||||
}
|
||||
|
||||
type metadataRefresher interface {
|
||||
RefreshClientMetadata(ctx context.Context, clientID string) (model.OidcClient, error)
|
||||
}
|
||||
|
||||
func NewOidcService(
|
||||
db *gorm.DB,
|
||||
jwtService *JwtService,
|
||||
previewBuilder oidcClientPreviewBuilder,
|
||||
metadataRefresher metadataRefresher,
|
||||
scimService *ScimService,
|
||||
httpClient *http.Client,
|
||||
fileStorage storage.FileStorage,
|
||||
) (s *OidcService, err error) {
|
||||
s = &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
previewBuilder: previewBuilder,
|
||||
scimService: scimService,
|
||||
httpClient: httpClient,
|
||||
fileStorage: fileStorage,
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
previewBuilder: previewBuilder,
|
||||
metadataRefresher: metadataRefresher,
|
||||
scimService: scimService,
|
||||
httpClient: httpClient,
|
||||
fileStorage: fileStorage,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -74,6 +81,22 @@ func (s *OidcService) GetClient(ctx context.Context, clientID string) (model.Oid
|
||||
return s.getClientInternal(ctx, clientID, s.db, false)
|
||||
}
|
||||
|
||||
// RefreshClientMetadata forces a re-fetch of the OAuth Client ID Metadata Document
|
||||
// for a CIMD client, bypassing the cache TTL, and returns the refreshed client.
|
||||
func (s *OidcService) RefreshClientMetadata(ctx context.Context, clientID string) (model.OidcClient, error) {
|
||||
if s.metadataRefresher == nil {
|
||||
return model.OidcClient{}, &common.ValidationError{Message: "client ID metadata documents are not enabled"}
|
||||
}
|
||||
client, err := s.metadataRefresher.RefreshClientMetadata(ctx, clientID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.OidcClient{}, err
|
||||
}
|
||||
return model.OidcClient{}, &common.ValidationError{Message: err.Error()}
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) getClientInternal(ctx context.Context, clientID string, tx *gorm.DB, forUpdate bool) (model.OidcClient, error) {
|
||||
var client model.OidcClient
|
||||
q := tx.
|
||||
@@ -161,10 +184,7 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
var client model.OidcClient
|
||||
err := tx.WithContext(ctx).
|
||||
Preload("CreatedBy").
|
||||
First(&client, "id = ?", clientID).Error
|
||||
client, err := s.getClientInternal(ctx, clientID, tx, true)
|
||||
if err != nil {
|
||||
return model.OidcClient{}, err
|
||||
}
|
||||
@@ -179,7 +199,22 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).Save(&client).Error
|
||||
// Metadata refresh owns all other CIMD columns, so an admin update must never write back a stale metadata snapshot
|
||||
if client.IsMetadataDocument() {
|
||||
err = tx.WithContext(ctx).
|
||||
Model(&client).
|
||||
Select(
|
||||
"Description",
|
||||
"RequiresReauthentication",
|
||||
"RequiresPushedAuthorizationRequests",
|
||||
"SkipConsent",
|
||||
"LaunchURL",
|
||||
"IsGroupRestricted",
|
||||
).
|
||||
Updates(&client).Error
|
||||
} else {
|
||||
err = tx.WithContext(ctx).Save(&client).Error
|
||||
}
|
||||
if err != nil {
|
||||
return model.OidcClient{}, err
|
||||
}
|
||||
@@ -208,25 +243,32 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
|
||||
}
|
||||
|
||||
func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
|
||||
// Base fields
|
||||
client.Name = input.Name
|
||||
// Update fields that remain locally managed for every client type
|
||||
client.Description = input.Description
|
||||
client.CallbackURLs = input.CallbackURLs
|
||||
client.LogoutCallbackURLs = input.LogoutCallbackURLs
|
||||
client.IsPublic = input.IsPublic
|
||||
// PKCE is required for public clients
|
||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||
// Reset any pkce support prompt if previously flagged
|
||||
if !input.PkceEnabled {
|
||||
client.PkceSupported = false
|
||||
}
|
||||
client.RequiresReauthentication = input.RequiresReauthentication
|
||||
client.RequiresPushedAuthorizationRequests = input.RequiresPushedAuthorizationRequests
|
||||
client.SkipConsent = input.SkipConsent
|
||||
client.LaunchURL = input.LaunchURL
|
||||
client.IsGroupRestricted = input.IsGroupRestricted
|
||||
|
||||
// Credentials
|
||||
// Preserve fields that are sourced from the client metadata document
|
||||
if client.IsMetadataDocument() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update registration fields for manually configured clients
|
||||
client.Name = input.Name
|
||||
client.CallbackURLs = input.CallbackURLs
|
||||
client.LogoutCallbackURLs = input.LogoutCallbackURLs
|
||||
client.IsPublic = input.IsPublic
|
||||
// PKCE is required for public clients
|
||||
client.PkceEnabled = input.IsPublic || input.PkceEnabled
|
||||
// Reset any PKCE support prompt if previously flagged
|
||||
if !input.PkceEnabled {
|
||||
client.PkceSupported = false
|
||||
}
|
||||
|
||||
// Replace the federated credentials with the submitted configuration
|
||||
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
|
||||
for i, fi := range input.Credentials.FederatedIdentities {
|
||||
client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
|
||||
@@ -255,11 +297,11 @@ func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
|
||||
// Delete images if present
|
||||
// Note that storage operations must be done outside of a transaction
|
||||
if client.ImageType != nil && *client.ImageType != "" {
|
||||
old := path.Join("oidc-client-images", client.ID+"."+*client.ImageType)
|
||||
old := oidcClientImagePath(client.ID, "", *client.ImageType)
|
||||
_ = s.fileStorage.Delete(ctx, old)
|
||||
}
|
||||
if client.DarkImageType != nil && *client.DarkImageType != "" {
|
||||
old := path.Join("oidc-client-images", client.ID+"-dark."+*client.DarkImageType)
|
||||
old := oidcClientImagePath(client.ID, "-dark", *client.DarkImageType)
|
||||
_ = s.fileStorage.Delete(ctx, old)
|
||||
}
|
||||
|
||||
@@ -281,6 +323,10 @@ func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string, i
|
||||
return "", err
|
||||
}
|
||||
|
||||
if client.IsPublic {
|
||||
return "", &common.ValidationError{Message: "cannot create a secret for a public client"}
|
||||
}
|
||||
|
||||
clientSecret := input.Secret
|
||||
if clientSecret == "" {
|
||||
clientSecret, err = utils.GenerateRandomAlphanumericString(32)
|
||||
@@ -339,7 +385,7 @@ func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light
|
||||
if mimeType == "" {
|
||||
return nil, 0, "", fmt.Errorf("unsupported image type '%s'", ext)
|
||||
}
|
||||
key := path.Join("oidc-client-images", client.ID+suffix+"."+ext)
|
||||
key := oidcClientImagePath(client.ID, suffix, ext)
|
||||
reader, size, err := s.fileStorage.Open(ctx, key)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
@@ -359,7 +405,7 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil
|
||||
darkSuffix = "-dark"
|
||||
}
|
||||
|
||||
imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+fileType)
|
||||
imagePath := oidcClientImagePath(clientID, darkSuffix, fileType)
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -441,7 +487,7 @@ func (s *OidcService) deleteClientLogoInternal(ctx context.Context, clientID str
|
||||
}
|
||||
|
||||
// All storage operations must be performed outside of a database transaction
|
||||
imagePath := path.Join("oidc-client-images", client.ID+imagePathSuffix+"."+oldImageType)
|
||||
imagePath := oidcClientImagePath(client.ID, imagePathSuffix, oldImageType)
|
||||
err = s.fileStorage.Delete(ctx, imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -625,6 +671,7 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
|
||||
LaunchURL: client.LaunchURL,
|
||||
HasLogo: client.HasLogo(),
|
||||
HasDarkLogo: client.HasDarkLogo(),
|
||||
ClientType: string(client.ClientType),
|
||||
},
|
||||
LastUsedAt: lastUsedAt,
|
||||
}
|
||||
@@ -761,7 +808,7 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, clie
|
||||
return err
|
||||
}
|
||||
|
||||
imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+ext)
|
||||
imagePath := oidcClientImagePath(clientID, darkSuffix, ext)
|
||||
err = s.fileStorage.Save(ctx, imagePath, strippedReader)
|
||||
if errors.Is(err, utils.ErrSizeExceeded) {
|
||||
return errLogoTooLarge
|
||||
@@ -823,13 +870,21 @@ func (s *OidcService) updateClientLogoType(ctx context.Context, clientID string,
|
||||
|
||||
// Storage operations must be executed outside of a transaction
|
||||
if currentType != nil && *currentType != ext {
|
||||
old := path.Join("oidc-client-images", client.ID+darkSuffix+"."+*currentType)
|
||||
old := oidcClientImagePath(client.ID, darkSuffix, *currentType)
|
||||
_ = s.fileStorage.Delete(ctx, old)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func oidcClientImagePath(clientID string, suffix string, extension string) string {
|
||||
storageID := clientID
|
||||
if !dto.ValidateClientID(clientID) {
|
||||
storageID = "cimd-" + utils.CreateSha256Hash(clientID)
|
||||
}
|
||||
return path.Join("oidc-client-images", storageID+suffix+"."+extension)
|
||||
}
|
||||
|
||||
func (s *OidcService) GetClientScimServiceProvider(ctx context.Context, clientID string) (model.ScimServiceProvider, error) {
|
||||
var provider model.ScimServiceProvider
|
||||
err := s.db.
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
@@ -37,7 +38,7 @@ func TestOidcService_updateClientLogoType(t *testing.T) {
|
||||
// Create a test client
|
||||
client := model.OidcClient{
|
||||
Name: "Test Client",
|
||||
CallbackURLs: model.UrlList{"https://example.com/callback"},
|
||||
CallbackURLs: datatype.StringList{"https://example.com/callback"},
|
||||
}
|
||||
err = db.Create(&client).Error
|
||||
require.NoError(t, err)
|
||||
@@ -152,6 +153,18 @@ func TestOidcService_updateClientLogoType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOidcClientImagePath(t *testing.T) {
|
||||
const metadataClientID = "https://app.example.com/oauth/client"
|
||||
|
||||
assert.Equal(t, "oidc-client-images/client-id.png", oidcClientImagePath("client-id", "", "png"))
|
||||
assert.Equal(
|
||||
t,
|
||||
"oidc-client-images/cimd-"+utils.CreateSha256Hash(metadataClientID)+"-dark.webp",
|
||||
oidcClientImagePath(metadataClientID, "-dark", "webp"),
|
||||
)
|
||||
assert.NotContains(t, oidcClientImagePath(metadataClientID, "", "png"), "app.example.com")
|
||||
}
|
||||
|
||||
func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
const publicLogoHost = "https://8.8.8.8"
|
||||
|
||||
@@ -165,7 +178,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
// Create a test client
|
||||
client := model.OidcClient{
|
||||
Name: "Test Client",
|
||||
CallbackURLs: model.UrlList{"https://example.com/callback"},
|
||||
CallbackURLs: datatype.StringList{"https://example.com/callback"},
|
||||
}
|
||||
err = db.Create(&client).Error
|
||||
require.NoError(t, err)
|
||||
@@ -457,7 +470,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
func TestOidcService_CreateClient_withDescription(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
description := "A test client description"
|
||||
@@ -482,7 +495,7 @@ func TestOidcService_CreateClient_withDescription(t *testing.T) {
|
||||
func TestOidcService_CreateClient_withoutDescription(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
input := dto.OidcClientCreateDto{
|
||||
@@ -504,7 +517,7 @@ func TestOidcService_CreateClient_withoutDescription(t *testing.T) {
|
||||
func TestOidcService_CreateClientSecret_withCustomSecret(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := model.OidcClient{Name: "Test Client"}
|
||||
@@ -527,13 +540,13 @@ func TestOidcService_CreateClientSecret_withCustomSecret(t *testing.T) {
|
||||
func TestOidcService_UpdateClient_description(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a client without a description
|
||||
client := model.OidcClient{
|
||||
Name: "Test Client",
|
||||
CallbackURLs: model.UrlList{"https://example.com/callback"},
|
||||
CallbackURLs: datatype.StringList{"https://example.com/callback"},
|
||||
}
|
||||
err = db.Create(&client).Error
|
||||
require.NoError(t, err)
|
||||
@@ -565,9 +578,104 @@ func TestOidcService_UpdateClient_description(t *testing.T) {
|
||||
assert.Empty(t, fetched.Description)
|
||||
}
|
||||
|
||||
func TestOidcService_UpdateClient_CIMDPreservesMetadataFields(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := model.OidcClient{
|
||||
Name: "Metadata Client",
|
||||
CallbackURLs: datatype.StringList{"https://metadata.example.com/callback"},
|
||||
LogoutCallbackURLs: datatype.StringList{"https://metadata.example.com/logout"},
|
||||
IsPublic: true,
|
||||
PkceEnabled: true,
|
||||
Credentials: model.OidcClientCredentials{
|
||||
FederatedIdentities: []model.OidcClientFederatedIdentity{{
|
||||
Issuer: "https://metadata.example.com/client.json",
|
||||
Subject: "https://metadata.example.com/client.json",
|
||||
JWKS: "https://metadata.example.com/jwks.json",
|
||||
}},
|
||||
},
|
||||
ClientType: model.OidcClientTypeCIMD,
|
||||
}
|
||||
require.NoError(t, db.Create(&client).Error)
|
||||
|
||||
launchURL := "https://app.example.com"
|
||||
input := dto.OidcClientUpdateDto{
|
||||
Name: "Overridden Client",
|
||||
Description: "Locally managed description",
|
||||
CallbackURLs: []string{"https://override.example.com/callback"},
|
||||
LogoutCallbackURLs: []string{"https://override.example.com/logout"},
|
||||
IsPublic: false,
|
||||
PkceEnabled: false,
|
||||
RequiresReauthentication: true,
|
||||
RequiresPushedAuthorizationRequests: true,
|
||||
SkipConsent: true,
|
||||
LaunchURL: &launchURL,
|
||||
IsGroupRestricted: true,
|
||||
Credentials: dto.OidcClientCredentialsDto{
|
||||
FederatedIdentities: []dto.OidcClientFederatedIdentityDto{{
|
||||
Issuer: "https://override.example.com",
|
||||
JWKS: "https://override.example.com/jwks.json",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = s.UpdateClient(t.Context(), client.ID, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fetched model.OidcClient
|
||||
require.NoError(t, db.First(&fetched, "id = ?", client.ID).Error)
|
||||
assert.Equal(t, client.Name, fetched.Name)
|
||||
assert.Equal(t, client.CallbackURLs, fetched.CallbackURLs)
|
||||
assert.Equal(t, client.LogoutCallbackURLs, fetched.LogoutCallbackURLs)
|
||||
assert.Equal(t, client.IsPublic, fetched.IsPublic)
|
||||
assert.Equal(t, client.PkceEnabled, fetched.PkceEnabled)
|
||||
assert.Equal(t, client.Credentials, fetched.Credentials)
|
||||
assert.Equal(t, input.Description, fetched.Description)
|
||||
assert.Equal(t, input.RequiresReauthentication, fetched.RequiresReauthentication)
|
||||
assert.Equal(t, input.RequiresPushedAuthorizationRequests, fetched.RequiresPushedAuthorizationRequests)
|
||||
assert.Equal(t, input.SkipConsent, fetched.SkipConsent)
|
||||
assert.Equal(t, input.LaunchURL, fetched.LaunchURL)
|
||||
assert.Equal(t, input.IsGroupRestricted, fetched.IsGroupRestricted)
|
||||
}
|
||||
|
||||
func TestOidcService_UpdateClient_CIMDDoesNotOverwriteConcurrentMetadataRefresh(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := model.OidcClient{
|
||||
Name: "Original metadata name",
|
||||
CallbackURLs: datatype.StringList{"https://metadata.example.com/callback"},
|
||||
ClientType: model.OidcClientTypeCIMD,
|
||||
}
|
||||
require.NoError(t, db.Create(&client).Error)
|
||||
|
||||
// Simulate metadata refresh changing a document-owned column after the admin request read its snapshot
|
||||
require.NoError(t, db.Exec(`
|
||||
CREATE TRIGGER refresh_metadata_before_admin_update
|
||||
BEFORE UPDATE OF description ON oidc_clients
|
||||
BEGIN
|
||||
UPDATE oidc_clients SET name = 'Refreshed metadata name' WHERE id = OLD.id;
|
||||
END;
|
||||
`).Error)
|
||||
|
||||
input := dto.OidcClientUpdateDto{Description: "Locally managed description"}
|
||||
_, err = s.UpdateClient(t.Context(), client.ID, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fetched model.OidcClient
|
||||
require.NoError(t, db.First(&fetched, "id = ?", client.ID).Error)
|
||||
assert.Equal(t, "Refreshed metadata name", fetched.Name)
|
||||
assert.Equal(t, input.Description, fetched.Description)
|
||||
}
|
||||
|
||||
func TestOidcService_ListAccessibleOidcClients_requiresExplicitGroupPermission(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
allowedGroup := model.UserGroup{Name: "allowed", FriendlyName: "Allowed"}
|
||||
@@ -581,10 +689,10 @@ func TestOidcService_ListAccessibleOidcClients_requiresExplicitGroupPermission(t
|
||||
require.NoError(t, db.Create(&userWithoutGroup).Error)
|
||||
|
||||
clients := []model.OidcClient{
|
||||
{Name: "Unrestricted", CallbackURLs: model.UrlList{"https://unrestricted.example.com/callback"}},
|
||||
{Name: "Restricted without groups", CallbackURLs: model.UrlList{"https://empty.example.com/callback"}, IsGroupRestricted: true},
|
||||
{Name: "Restricted to user group", CallbackURLs: model.UrlList{"https://allowed.example.com/callback"}, IsGroupRestricted: true, AllowedUserGroups: []model.UserGroup{allowedGroup}},
|
||||
{Name: "Restricted to other group", CallbackURLs: model.UrlList{"https://other.example.com/callback"}, IsGroupRestricted: true, AllowedUserGroups: []model.UserGroup{otherGroup}},
|
||||
{Name: "Unrestricted", CallbackURLs: datatype.StringList{"https://unrestricted.example.com/callback"}},
|
||||
{Name: "Restricted without groups", CallbackURLs: datatype.StringList{"https://empty.example.com/callback"}, IsGroupRestricted: true},
|
||||
{Name: "Restricted to user group", CallbackURLs: datatype.StringList{"https://allowed.example.com/callback"}, IsGroupRestricted: true, AllowedUserGroups: []model.UserGroup{allowedGroup}},
|
||||
{Name: "Restricted to other group", CallbackURLs: datatype.StringList{"https://other.example.com/callback"}, IsGroupRestricted: true, AllowedUserGroups: []model.UserGroup{otherGroup}},
|
||||
}
|
||||
for i := range clients {
|
||||
require.NoError(t, db.Create(&clients[i]).Error)
|
||||
|
||||
Reference in New Issue
Block a user