Compare commits

..

1 Commits

Author SHA1 Message Date
Elias Schneider
f61c784988 feat: restrict oidc clients by user groups per default 2025-12-23 13:51:35 +01:00
10 changed files with 90 additions and 15 deletions

View File

@@ -18,6 +18,7 @@ type OidcClientDto struct {
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"`
Credentials OidcClientCredentialsDto `json:"credentials"`
IsGroupRestricted bool `json:"isGroupRestricted"`
}
type OidcClientWithAllowedUserGroupsDto struct {
@@ -43,6 +44,7 @@ type OidcClientUpdateDto struct {
HasDarkLogo bool `json:"hasDarkLogo"`
LogoURL *string `json:"logoUrl"`
DarkLogoURL *string `json:"darkLogoUrl"`
IsGroupRestricted bool `json:"isGroupRestricted"`
}
type OidcClientCreateDto struct {

View File

@@ -58,6 +58,7 @@ type OidcClient struct {
RequiresReauthentication bool `sortable:"true" filterable:"true"`
Credentials OidcClientCredentials
LaunchURL *string
IsGroupRestricted bool
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
CreatedByID *string

View File

@@ -226,7 +226,7 @@ func (s *OidcService) hasAuthorizedClientInternal(ctx context.Context, clientID,
// IsUserGroupAllowedToAuthorize checks if the user group of the user is allowed to authorize the client
func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client model.OidcClient) bool {
if len(client.AllowedUserGroups) == 0 {
if !client.IsGroupRestricted {
return true
}
@@ -816,6 +816,7 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.PkceEnabled = input.IsPublic || input.PkceEnabled
client.RequiresReauthentication = input.RequiresReauthentication
client.LaunchURL = input.LaunchURL
client.IsGroupRestricted = input.IsGroupRestricted
// Credentials
client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))

View File

@@ -0,0 +1,7 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_clients DROP COLUMN is_group_restricted;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,13 @@
PRAGMA foreign_keys= OFF;
BEGIN;
ALTER TABLE oidc_clients
ADD COLUMN is_group_restricted BOOLEAN NOT NULL DEFAULT 0;
UPDATE oidc_clients
SET is_group_restricted = (SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END
FROM oidc_client_user_groups
WHERE oidc_client_user_groups.oidc_client_id = oidc_clients.id);
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -301,13 +301,16 @@
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
"generate": "Generate",
"new_client_secret_created_successfully": "New client secret created successfully",
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
"oidc_client_name": "OIDC Client {name}",
"client_id": "Client ID",
"client_secret": "Client secret",
"show_more_details": "Show more details",
"allowed_user_groups": "Allowed User Groups",
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
"allowed_user_groups_description": "Select user groups to restrict signing in to this client to only users in these groups.",
"allowed_user_groups_status_unrestricted_description": "No user group restrictions are applied. Any user can sign in to this client.",
"unrestrict": "Unrestrict",
"restrict": "Restrict",
"user_groups_restriction_updated_successfully": "User groups restriction updated successfully",
"favicon": "Favicon",
"light_mode_logo": "Light Mode Logo",
"dark_mode_logo": "Dark Mode Logo",

View File

@@ -12,6 +12,8 @@
title,
description,
defaultExpanded = false,
forcedExpanded,
button,
icon,
children
}: {
@@ -19,7 +21,9 @@
title: string;
description?: string;
defaultExpanded?: boolean;
forcedExpanded?: boolean;
icon?: typeof IconType;
button?: Snippet;
children: Snippet;
} = $props();
@@ -47,6 +51,12 @@
}
loadExpandedState();
});
$effect(() => {
if (forcedExpanded !== undefined) {
expanded = forcedExpanded;
}
});
</script>
<Card.Root>
@@ -63,11 +73,18 @@
<Card.Description>{description}</Card.Description>
{/if}
</div>
{#if button}
{@render button()}
{:else}
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
<LucideChevronDown
class={cn('size-5 transition-transform duration-200', expanded && 'rotate-180 transform')}
class={cn(
'size-5 transition-transform duration-200',
expanded && 'rotate-180 transform'
)}
/>
</Button>
{/if}
</div>
</Card.Header>
{#if expanded}

View File

@@ -28,6 +28,7 @@ export type OidcClient = OidcClientMetaData & {
requiresReauthentication: boolean;
credentials?: OidcClientCredentials;
launchURL?: string;
isGroupRestricted: boolean;
};
export type OidcClientWithAllowedUserGroups = OidcClient & {

View File

@@ -80,6 +80,16 @@
return success;
}
async function toggleGroupRestriction() {
client.isGroupRestricted = !client.isGroupRestricted;
await oidcService
.updateClient(client.id, client)
.then(() => {
toast.success(m.user_groups_restriction_updated_successfully());
})
.catch(axiosErrorToast);
}
async function createClientSecret() {
openConfirmDialog({
title: m.create_new_client_secret(),
@@ -104,7 +114,7 @@
await oidcService
.updateAllowedUserGroups(client.id, allowedGroups)
.then(() => {
toast.success(m.allowed_user_groups_updated_successfully());
toast.success(m.user_groups_restriction_updated_successfully());
})
.catch((e) => {
axiosErrorToast(e);
@@ -120,6 +130,14 @@
<title>{m.oidc_client_name({ name: client.name })}</title>
</svelte:head>
{#snippet UnrestrictButton()}
<Button
onclick={toggleGroupRestriction}
variant={client.isGroupRestricted ? 'secondary' : 'default'}
>{client.isGroupRestricted ? m.unrestrict() : m.restrict()}</Button
>
{/snippet}
<div>
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
><LucideChevronLeft class="size-5" /> {m.back()}</button
@@ -193,12 +211,23 @@
<CollapsibleCard
id="allowed-user-groups"
title={m.allowed_user_groups()}
description={m.add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups()}
button={!client.isGroupRestricted ? UnrestrictButton : undefined}
forcedExpanded={client.isGroupRestricted ? undefined : false}
description={client.isGroupRestricted
? m.allowed_user_groups_description()
: m.allowed_user_groups_status_unrestricted_description()}
>
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
<div class="mt-5 flex justify-end">
{#if client.isGroupRestricted}
<UserGroupSelection
bind:selectedGroupIds={client.allowedUserGroupIds}
selectionDisabled={!client.isGroupRestricted}
/>
<div class="mt-5 flex justify-end gap-3">
<Button onclick={toggleGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
<Button onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button>
</div>
{/if}
</CollapsibleCard>
<Card.Root>
<Card.Header>

View File

@@ -102,7 +102,8 @@
logo: $inputs.logoUrl?.value ? undefined : logo,
logoUrl: $inputs.logoUrl?.value,
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo,
darkLogoUrl: $inputs.darkLogoUrl?.value
darkLogoUrl: $inputs.darkLogoUrl?.value,
isGroupRestricted: existingClient?.isGroupRestricted ?? true
});
const hasLogo = logo != null || !!$inputs.logoUrl?.value;