Skip to content

Commit

Permalink
SAML/ OIDC Identity Provider AutoUpdate
Browse files Browse the repository at this point in the history
  • Loading branch information
cgeorgilakis-grnet committed Oct 14, 2024
1 parent dee2001 commit 9349c93
Show file tree
Hide file tree
Showing 36 changed files with 611 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ public class RealmRepresentation {

protected Boolean organizationsEnabled;
private List<OrganizationRepresentation> organizations;
protected Long autoUpdatedIdPsInterval;
protected Long autoUpdatedIdPsLastRefreshTime;

@Deprecated
protected Boolean social;
Expand Down Expand Up @@ -1450,4 +1452,20 @@ public void addOrganization(OrganizationRepresentation org) {
}
organizations.add(org);
}

public Long getAutoUpdatedIdPsInterval() {
return autoUpdatedIdPsInterval;
}

public void setAutoUpdatedIdPsInterval(Long autoUpdatedIdPsInterval) {
this.autoUpdatedIdPsInterval = autoUpdatedIdPsInterval;
}

public Long getAutoUpdatedIdPsLastRefreshTime() {
return autoUpdatedIdPsLastRefreshTime;
}

public void setAutoUpdatedIdPsLastRefreshTime(Long autoUpdatedIdPsLastRefreshTime) {
this.autoUpdatedIdPsLastRefreshTime = autoUpdatedIdPsLastRefreshTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ importKeys=Import keys
useMetadataDescriptorUrl=Use metadata descriptor URL
useMetadataDescriptorUrlHelp=If the switch is on, the certificates to validate signatures will be downloaded and cached from the given "Metadata descriptor URL". The "Reload keys" action can be used to refresh the certificates in the cache. If the switch is off, certificates from "Validating X509 certificates" option are used, they need to be manually updated when changed in the IDP.
metadataDescriptorUrl=Metadata descriptor URL
metadataDescriptorUrlHelp=External URL where Identity Provider publishes the metadata information needed by the client (certificates, keys, other URLs,...).
metadataDescriptorUrlHelp=External URL where Identity Provider publishes the metadata information needed by the client (certificates, keys, other URLs,...). This url is used for auto-updated IdPs and when use metadata descriptor URL is true.
reloadKeysSuccess=Keys successfully reloaded
reloadKeysError=Error reloading keys. {{error}}
reloadKeysSuccessButFalse=The reload was not executed, maybe the time between request was too short.
Expand Down Expand Up @@ -3264,3 +3264,8 @@ groupDuplicated=Group duplicated
duplicateAGroup=Duplicate group
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
autoUpdatedIdPsInterval= Autoupdated Identity Providers execution interval
autoUpdatedIdPsIntervalHelp= Every how much time autoupdated Identity Providers will be updated based on metadata url
autoUpdatedIdPsLastRefreshTime= Last execution time of autoupdated Identity Providers task
autoUpdate= Auto Update
autoUpdateHelp= When auto update is true, IdP metadata will be updated based on metadata descriptor URL
36 changes: 23 additions & 13 deletions js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
name: "config.validateSignature",
});

const autoUpdated = useWatch({
control,
name: "config.autoUpdate",
});

const useMetadataDescriptorUrl = useWatch({
control,
name: "config.useMetadataDescriptorUrl",
Expand All @@ -51,6 +56,24 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
return (
<div className="pf-v5-c-form pf-m-horizontal">
<FormProvider {...form}>
<DefaultSwitchControl
name="config.autoUpdate"
label={t("autoUpdate")}
isDisabled={readOnly}
stringify
/>
<TextControl
name="config.metadataDescriptorUrl"
label={t("metadataDescriptorUrl")}
labelIcon={t("metadataDescriptorUrlHelp")}
type="url"
readOnly={readOnly}
rules={{
required: {
value: useMetadataDescriptorUrl === "true" || autoUpdated === "true",message: t("required")

Check failure on line 73 in js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Replace `·useMetadataDescriptorUrl·===·"true"·||·autoUpdated·===·"true",message:·t("required")` with `⏎················useMetadataDescriptorUrl·===·"true"·||·autoUpdated·===·"true",⏎··············message:·t("required"),`
}

Check failure on line 74 in js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `,`
}}
/>
<TextControl
name="config.entityId"
label={t("serviceProviderEntityId")}
Expand Down Expand Up @@ -279,19 +302,6 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
/>
{validateSignature === "true" && (
<>
<TextControl
name="config.metadataDescriptorUrl"
label={t("metadataDescriptorUrl")}
labelIcon={t("metadataDescriptorUrlHelp")}
type="url"
readOnly={readOnly}
rules={{
required: {
value: useMetadataDescriptorUrl === "true",
message: t("required"),
},
}}
/>
<DefaultSwitchControl
name="config.useMetadataDescriptorUrl"
label={t("useMetadataDescriptorUrl")}
Expand Down
23 changes: 23 additions & 0 deletions js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,32 @@ const Fields = ({ readOnly }: DiscoverySettingsProps) => {
control,
name: "config.pkceEnabled",
});
const autoUpdated = useWatch({
control,
name: "config.autoUpdate",
});

return (
<div className="pf-v5-c-form pf-m-horizontal">
<DefaultSwitchControl

Check failure on line 44 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
name="config.autoUpdate"

Check failure on line 45 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
label={t("autoUpdate")}

Check failure on line 46 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
isDisabled={readOnly}

Check failure on line 47 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
stringify

Check failure on line 48 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
/>

Check failure on line 49 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
<TextControl

Check failure on line 50 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
name="config.metadataDescriptorUrl"

Check failure on line 51 in js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx

View workflow job for this annotation

GitHub Actions / Admin UI

Insert `·`
label={t("metadataOfDiscoveryEndpoint")}
labelIcon={t("discoveryEndpointHelp")}
type="url"
readOnly={readOnly}
rules={{
required: {
value: autoUpdated === "true",
message: t("required"),
}
}}
/>
<TextControl
name="config.authorizationUrl"
label={t("authorizationUrl")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const DiscoveryEndpointField = ({
Object.keys(result).map((k) => setValue(`config.${k}`, result[k]));
};

const discover = async (fromUrl: string) => {
const discover = async (fromUrl: string) => {
setDiscovering(true);
try {
const result = await adminClient.identityProviders.importFromUrl({
Expand Down
48 changes: 47 additions & 1 deletion js/apps/admin-ui/src/realm-settings/GeneralTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
StackItem,
} from "@patternfly/react-core";
import { useEffect, useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import { DefaultSwitchControl } from "../components/SwitchControl";
Expand All @@ -36,6 +36,7 @@ import {
} from "../util";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { UIRealmRepresentation } from "./RealmSettingsTabs";
import { TimeSelector } from "../components/time-selector/TimeSelector";

type RealmSettingsGeneralTabProps = {
realm: UIRealmRepresentation;
Expand Down Expand Up @@ -112,6 +113,16 @@ function RealmSettingsGeneralTabForm({
const isOrganizationsEnabled = isFeatureEnabled(Feature.Organizations);
const isOpenid4vciEnabled = isFeatureEnabled(Feature.OpenId4VCI);

const autoUpdatedIdPsInterval = useWatch({
control,
name: "autoUpdatedIdPsInterval",
});

const autoUpdatedIdPsLastRefreshTime = useWatch({
control,
name: "autoUpdatedIdPsLastRefreshTime",
});

const setupForm = () => {
convertToFormValues(realm, setValue);
setValue(
Expand Down Expand Up @@ -239,6 +250,41 @@ function RealmSettingsGeneralTabForm({
value: t(`unmanagedAttributePolicy.${policy}`),
}))}
/>
<FormGroup
label={t("autoUpdatedIdPsInterval")}
fieldId="autoUpdatedIdPsInterval"
labelIcon={
<HelpItem
helpText={t("autoUpdatedIdPsIntervalHelp")}
fieldLabelId="autoUpdatedIdPsInterval"
/>
}
>
<Controller
name="autoUpdatedIdPsInterval"
control={control}
render={({ field }) => (
<TimeSelector
units={["minute", "hour", "day"]}
value={field.value!}
onChange={field.onChange}
/>
)}
/>
</FormGroup>
{autoUpdatedIdPsInterval && !!autoUpdatedIdPsLastRefreshTime && (
<FormGroup
label={t("autoUpdatedIdPsLastRefreshTime")}
labelIcon={
<HelpItem
helpText={t("autoUpdatedIdPsLastRefreshTime")}
fieldLabelId="autoUpdatedIdPsLastRefreshTime"
/>
}
>
{new Date(autoUpdatedIdPsLastRefreshTime).toLocaleString()}
</FormGroup>
)}
<FormGroup
label={t("endpoints")}
labelIcon={
Expand Down
2 changes: 2 additions & 0 deletions js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default interface RealmRepresentation {
adminEventsEnabled?: boolean;
adminTheme?: string;
attributes?: Record<string, any>;
autoUpdatedIdPsInterval?: number;
autoUpdatedIdPsLastRefreshTime?: number;
// AuthenticationFlowRepresentation
authenticationFlows?: any[];
// AuthenticatorConfigRepresentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,29 @@ public void setOrganizationsEnabled(boolean organizationsEnabled) {
updated.setOrganizationsEnabled(organizationsEnabled);
}

@Override
public Long getAutoUpdatedIdPsInterval() {
if (isUpdated()) return updated.getAutoUpdatedIdPsInterval();
return cached.getAutoUpdatedIdPsInterval();
}

@Override
public void setAutoUpdatedIdPsInterval(Long autoUpdatedIdPsInterval) {
getDelegateForUpdate();
updated.setAutoUpdatedIdPsInterval(autoUpdatedIdPsInterval);
}

@Override
public Long getAutoUpdatedIdPsLastRefreshTime() {
return null;
}

@Override
public void setAutoUpdatedIdPsLastRefreshTime(Long autoUpdatedIdPsLastRefreshTime) {
getDelegateForUpdate();
updated.setAutoUpdatedIdPsLastRefreshTime(autoUpdatedIdPsLastRefreshTime);
}

private boolean featureAwareIsOrganizationsEnabled(boolean isOrganizationsEnabled) {
if (!Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) return false;
return isOrganizationsEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected boolean identityFederationEnabled;
protected boolean editUsernameAllowed;
protected boolean organizationsEnabled;
protected Long autoUpdatedIdPsInterval;
protected Long autoUpdatedIdPsLastRefreshTime;
//--- brute force settings
protected boolean bruteForceProtected;
protected boolean permanentLockout;
Expand Down Expand Up @@ -741,4 +743,12 @@ public Map<String, RequiredActionConfigModel> getRequiredActionProviderConfigsBy
public Map<String, RequiredActionConfigModel> getRequiredActionProviderConfigs() {
return requiredActionProviderConfigs;
}

public Long getAutoUpdatedIdPsInterval() {
return autoUpdatedIdPsInterval;
}

public Long getAutoUpdatedIdPsLastRefreshTime() {
return autoUpdatedIdPsLastRefreshTime;
}
}
20 changes: 20 additions & 0 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,26 @@ public void setOrganizationsEnabled(boolean organizationsEnabled) {
setAttribute(RealmAttributes.ORGANIZATIONS_ENABLED, organizationsEnabled);
}

@Override
public Long getAutoUpdatedIdPsInterval() {
return getAttribute(RealmAttributes.AUTO_UPDATED_IDPS_INTERVAL) == null ? null : Long.valueOf(getAttribute(RealmAttributes.AUTO_UPDATED_IDPS_INTERVAL));
}

@Override
public void setAutoUpdatedIdPsInterval(Long autoUpdatedIdPsInterval) {
setAttribute(RealmAttributes.AUTO_UPDATED_IDPS_INTERVAL, autoUpdatedIdPsInterval);
}

@Override
public Long getAutoUpdatedIdPsLastRefreshTime() {
return getAttribute(RealmAttributes.AUTO_UPDATED_IDPS_REFRESH_TIME) == null ? null : Long.valueOf(getAttribute(RealmAttributes.AUTO_UPDATED_IDPS_INTERVAL));
}

@Override
public void setAutoUpdatedIdPsLastRefreshTime(Long autoUpdatedIdPsLastRefreshTime) {
setAttribute(RealmAttributes.AUTO_UPDATED_IDPS_REFRESH_TIME, autoUpdatedIdPsLastRefreshTime);
}

@Override
public ClientModel getMasterAdminClient() {
String masterAdminClientId = realm.getMasterAdminClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ public interface RealmAttributes {
String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";

String ORGANIZATIONS_ENABLED = "organizationsEnabled";
String AUTO_UPDATED_IDPS_INTERVAL = "autoUpdatedIdPsInterval";
String AUTO_UPDATED_IDPS_REFRESH_TIME = "autoUpdatedIdPsLastRefreshTime";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.keycloak.services.scheduled;


import org.jboss.logging.Logger;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.timer.ScheduledTask;

import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

public class UpdateAutoUpdatedIdPsTask implements ScheduledTask {

protected static final Logger logger = Logger.getLogger(UpdateAutoUpdatedIdPsTask.class);
protected final String realmId;

public UpdateAutoUpdatedIdPsTask(String realmId) {
this.realmId = realmId;
}

@Override
public void run(KeycloakSession session) {
logger.info(" Updating autoupdated identity providers in realm= " + realmId);
RealmModel realm = session.realms().getRealm(realmId);
session.getContext().setRealm(realm);
session.identityProviders().getAllStream(Map.of(IdentityProviderModel.AUTO_UPDATE, "true"), null, null).forEach(idp -> autoUpdateIdP(idp, session));
realm.setAutoUpdatedIdPsLastRefreshTime(Instant.now().toEpochMilli());

}

private void autoUpdateIdP(IdentityProviderModel idp, KeycloakSession session) {
try {
String file = session.getProvider(HttpClientProvider.class).getString(idp.getConfig().get(IdentityProviderModel.METADATA_DESCRIPTOR_URL));
idp = getProviderFactoryById(idp.getProviderId(), session).parseConfig(session, file, idp);
session.identityProviders().update(idp);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private IdentityProviderFactory<?> getProviderFactoryById(String providerId, KeycloakSession session) {
return Stream.concat(session.getKeycloakSessionFactory().getProviderFactoriesStream(IdentityProvider.class),
session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class))
.filter(providerFactory -> Objects.equals(providerId, providerFactory.getId()))
.map(IdentityProviderFactory.class::cast)
.findFirst()
.orElse(null);
}

}
Loading

0 comments on commit 9349c93

Please sign in to comment.