From f84992f81fe6331714a567df9394a93cf2e57f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Wed, 6 May 2026 13:25:13 +0200 Subject: [PATCH 1/2] [SYNCOPE-1967] Support for CAS Attribute Release Consent --- .../AuthProfileDirectoryPanel.java | 80 ++++++- .../AuthProfileItemDirectoryPanel.java | 14 +- .../authprofiles/AuthProfilePanel.java | 5 +- .../AuthProfileWizardBuilder.java | 54 ++++- .../client/console/commons/AMConstants.java | 3 + .../syncope/client/console/pages/WA.java | 2 +- .../console/rest/AuthProfileRestClient.java | 40 ++++ .../AuthProfileDirectoryPanel.properties | 6 + ...AuthProfileDirectoryPanel_fr_CA.properties | 6 + .../AuthProfileDirectoryPanel_it.properties | 6 + .../AuthProfileDirectoryPanel_ja.properties | 6 + ...AuthProfileDirectoryPanel_pt_BR.properties | 6 + .../AuthProfileDirectoryPanel_ru.properties | 6 + ...rofileWizardBuilder$ConsentAttributes.html | 23 ++ .../client/enduser/pages/AuthProfile.java | 65 +++++ .../enduser/rest/AuthProfileRestClient.java | 41 ++++ .../client/enduser/pages/AuthProfile.html | 52 +++- .../enduser/pages/AuthProfile.properties | 4 + .../enduser/pages/AuthProfile_it.properties | 4 + .../enduser/pages/AuthProfile_ja.properties | 4 + .../pages/AuthProfile_pt_BR.properties | 4 + .../enduser/pages/AuthProfile_ru.properties | 4 + .../markup/html/form/AlertBehavior.java | 68 ++++++ .../markup/html/form/JsonEditorPanel.java | 4 - .../syncope/common/lib/to/AuthProfileTO.java | 62 ++++- .../common/lib/wa/WAConsentDecision.java | 226 ++++++++++++++++++ .../service/wa/ConsentDecisionService.java | 84 +++++++ .../syncope/core/logic/AMLogicContext.java | 11 + .../core/logic/wa/ConsentDecisionLogic.java | 115 +++++++++ .../core/rest/cxf/AMRESTCXFContext.java | 11 + .../wa/ConsentDecisionServiceImpl.java | 79 ++++++ .../api/entity/am/AuthProfile.java | 5 + .../jpa/entity/am/JPAAuthProfile.java | 19 ++ .../neo4j/entity/am/Neo4jAuthProfile.java | 18 ++ .../java/data/AuthProfileDataBinderImpl.java | 20 +- .../src/main/resources/wa-embedded.properties | 2 +- pom.xml | 6 +- .../reference-guide/concepts/authprofile.adoc | 28 +++ .../reference-guide/concepts/concepts.adoc | 2 + wa/starter/pom.xml | 4 + .../syncope/wa/starter/config/WAContext.java | 8 + .../starter/consent/WAConsentRepository.java | 124 ++++++++++ .../WAGoogleMfaAuthCredentialRepository.java | 62 +++-- wa/starter/src/main/resources/wa.properties | 2 +- .../test/resources/debug/wa-debug.properties | 2 +- 45 files changed, 1327 insertions(+), 70 deletions(-) create mode 100644 client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html create mode 100644 client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java create mode 100644 common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java create mode 100644 common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java create mode 100644 core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java create mode 100644 core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java create mode 100644 src/main/asciidoc/reference-guide/concepts/authprofile.adoc create mode 100644 wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java index be7597329ef..932b602757a 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java @@ -42,12 +42,14 @@ import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel; import org.apache.syncope.client.ui.commons.Constants; import org.apache.syncope.client.ui.commons.pages.BaseWebPage; +import org.apache.syncope.common.keymaster.client.api.ServiceOps; import org.apache.syncope.common.lib.to.AuthProfileTO; import org.apache.syncope.common.lib.types.AMEntitlement; import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount; import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; import org.apache.wicket.PageReference; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -66,14 +68,20 @@ public class AuthProfileDirectoryPanel private static final long serialVersionUID = 2018518567549153364L; - private String keyword; + private final ServiceOps serviceOps; private final BaseModal authProfileModal; + private String keyword; + public AuthProfileDirectoryPanel( - final String id, final AuthProfileRestClient restClient, final PageReference pageRef) { + final String id, + final ServiceOps serviceOps, + final AuthProfileRestClient restClient, + final PageReference pageRef) { super(id, restClient, pageRef); + this.serviceOps = serviceOps; authProfileModal = new BaseModal<>(Constants.OUTER) { @@ -162,6 +170,15 @@ protected boolean isCondition(final IModel rowModel) { return CollectionUtils.isNotEmpty(rowModel.getObject().getWebAuthnDeviceCredentials()); } }); + columns.add(new BooleanConditionColumn<>(new StringResourceModel("consentDecisions")) { + + private static final long serialVersionUID = -8236820422411536323L; + + @Override + protected boolean isCondition(final IModel rowModel) { + return CollectionUtils.isNotEmpty(rowModel.getObject().getConsentDecisions()); + } + }); return columns; } @@ -180,7 +197,7 @@ public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( authProfileModal, new AuthProfileItemDirectoryPanel( - "panel", restClient, authProfileModal, model.getObject(), pageRef) { + "panel", serviceOps, restClient, authProfileModal, model.getObject(), null, pageRef) { private static final long serialVersionUID = -5380664539000792237L; @@ -227,7 +244,7 @@ public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( authProfileModal, new AuthProfileItemDirectoryPanel( - "panel", restClient, authProfileModal, model.getObject(), pageRef) { + "panel", serviceOps, restClient, authProfileModal, model.getObject(), null, pageRef) { private static final long serialVersionUID = 7332357430197837993L; @@ -276,7 +293,7 @@ public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( authProfileModal, new AuthProfileItemDirectoryPanel( - "panel", restClient, authProfileModal, model.getObject(), pageRef) { + "panel", serviceOps, restClient, authProfileModal, model.getObject(), null, pageRef) { private static final long serialVersionUID = -670769282358547044L; @@ -325,7 +342,7 @@ public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( authProfileModal, new AuthProfileItemDirectoryPanel( - "panel", restClient, authProfileModal, model.getObject(), pageRef) { + "panel", serviceOps, restClient, authProfileModal, model.getObject(), null, pageRef) { private static final long serialVersionUID = 5788448799796630011L; @@ -376,7 +393,7 @@ public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( authProfileModal, new AuthProfileItemDirectoryPanel( - "panel", restClient, authProfileModal, model.getObject(), pageRef) { + "panel", serviceOps, restClient, authProfileModal, model.getObject(), null, pageRef) { private static final long serialVersionUID = 6820212423488933184L; @@ -415,6 +432,55 @@ protected List> getColumns() { } }, ActionLink.ActionType.HTML, AMEntitlement.AUTH_PROFILE_UPDATE); + panel.add(new ActionLink<>() { + + private static final long serialVersionUID = -3722207913631435501L; + + @Override + public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) { + model.setObject(restClient.read(model.getObject().getKey())); + target.add(authProfileModal.setContent(new ModalDirectoryPanel<>( + authProfileModal, + new AuthProfileItemDirectoryPanel("panel", serviceOps, + restClient, authProfileModal, model.getObject(), List.of("attributes"), pageRef) { + + private static final long serialVersionUID = -670769282358547044L; + + @Override + protected List getItems() { + return model.getObject().getConsentDecisions(); + } + + @Override + protected WAConsentDecision defaultItem() { + return new WAConsentDecision(); + } + + @Override + protected String sortProperty() { + return "id"; + } + + @Override + protected String paginatorRowsKey() { + return AMConstants.PREF_AUTHPROFILE_CONSENT_DECISION_PAGINATOR_ROWS; + } + + @Override + protected List> getColumns() { + List> columns = new ArrayList<>(); + columns.add(new PropertyColumn<>(new ResourceModel("id"), "id", "id")); + columns.add(new PropertyColumn<>(new ResourceModel("service"), "service", "service")); + columns.add(new DatePropertyColumn<>( + new ResourceModel("createdDate"), "createdDate", "createdDate")); + return columns; + } + }, pageRef))); + authProfileModal.header(new Model<>(getString("consentDecisions", model))); + authProfileModal.show(true); + } + }, ActionLink.ActionType.ASSIGN, AMEntitlement.AUTH_PROFILE_UPDATE); + panel.add(new ActionLink<>() { private static final long serialVersionUID = -3722207913631435501L; diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java index cfa57a04ea7..f795d7a483e 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java @@ -35,6 +35,7 @@ import org.apache.syncope.client.ui.commons.Constants; import org.apache.syncope.client.ui.commons.pages.BaseWebPage; import org.apache.syncope.client.ui.commons.wizards.AjaxWizard; +import org.apache.syncope.common.keymaster.client.api.ServiceOps; import org.apache.syncope.common.lib.BaseBean; import org.apache.syncope.common.lib.to.AuthProfileTO; import org.apache.syncope.common.lib.types.AMEntitlement; @@ -60,9 +61,11 @@ public abstract class AuthProfileItemDirectoryPanel public AuthProfileItemDirectoryPanel( final String id, + final ServiceOps serviceOps, final AuthProfileRestClient restClient, final BaseModal authProfileModal, final AuthProfileTO authProfile, + final List excluded, final PageReference pageRef) { super(id, restClient, pageRef, false); @@ -75,6 +78,8 @@ public AuthProfileItemDirectoryPanel( enableUtilityButton(); setFooterVisibility(false); + addNewItemPanelBuilder(new AuthProfileItemWizardBuilder(excluded, serviceOps, restClient, pageRef), false); + disableCheckBoxes(); initResultTable(); } @@ -179,8 +184,13 @@ protected class AuthProfileItemWizardBuilder extends AuthProfileWizardBuilder private static final long serialVersionUID = -7174537333960225216L; - protected AuthProfileItemWizardBuilder(final PageReference pageRef) { - super(defaultItem(), new StepModel<>(), pageRef); + protected AuthProfileItemWizardBuilder( + final List excluded, + final ServiceOps serviceOps, + final AuthProfileRestClient authProfileRestClient, + final PageReference pageRef) { + + super(defaultItem(), new StepModel<>(), excluded, serviceOps, authProfileRestClient, pageRef); } @Override diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java index b3f2607cfe4..749f33f7e96 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfilePanel.java @@ -22,6 +22,7 @@ import org.apache.syncope.client.console.commons.KeywordSearchEvent; import org.apache.syncope.client.console.rest.AuthProfileRestClient; import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel; +import org.apache.syncope.common.keymaster.client.api.ServiceOps; import org.apache.wicket.PageReference; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.form.AjaxButton; @@ -37,6 +38,7 @@ public class AuthProfilePanel extends Panel { public AuthProfilePanel( final String id, + final ServiceOps serviceOps, final AuthProfileRestClient authProfileRestClient, final PageReference pageRef) { @@ -66,6 +68,7 @@ protected void onSubmit(final AjaxRequestTarget target) { form.add(search); form.setDefaultButton(search); - add(new AuthProfileDirectoryPanel("authProfiles", authProfileRestClient, pageRef).setOutputMarkupId(true)); + add(new AuthProfileDirectoryPanel("authProfiles", serviceOps, authProfileRestClient, pageRef). + setOutputMarkupId(true)); } } diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java index 41a5ff99586..44c53dc93ee 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder.java @@ -18,10 +18,16 @@ */ package org.apache.syncope.client.console.authprofiles; +import java.util.List; import org.apache.commons.lang3.SerializationUtils; import org.apache.syncope.client.console.panels.BeanPanel; +import org.apache.syncope.client.console.rest.AuthProfileRestClient; +import org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel; import org.apache.syncope.client.console.wizards.BaseAjaxWizardBuilder; +import org.apache.syncope.common.keymaster.client.api.ServiceOps; +import org.apache.syncope.common.keymaster.client.api.model.NetworkService; import org.apache.syncope.common.lib.BaseBean; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.wicket.PageReference; import org.apache.wicket.extensions.wizard.WizardModel; import org.apache.wicket.extensions.wizard.WizardStep; @@ -33,14 +39,33 @@ public abstract class AuthProfileWizardBuilder extends BaseA protected final StepModel model; - public AuthProfileWizardBuilder(final T defaultItem, final StepModel model, final PageReference pageRef) { + protected final List excluded; + + protected final ServiceOps serviceOps; + + protected final AuthProfileRestClient authProfileRestClient; + + public AuthProfileWizardBuilder( + final T defaultItem, + final StepModel model, + final List excluded, + final ServiceOps serviceOps, + final AuthProfileRestClient authProfileRestClient, + final PageReference pageRef) { + super(defaultItem, pageRef); this.model = model; + this.excluded = excluded; + this.serviceOps = serviceOps; + this.authProfileRestClient = authProfileRestClient; } @Override protected WizardModel buildModelSteps(final T modelObject, final WizardModel wizardModel) { wizardModel.add(new Step(modelObject)); + if (modelObject instanceof WAConsentDecision consentDecision) { + wizardModel.add(new ConsentAttributes(consentDecision)); + } return wizardModel; } @@ -66,7 +91,32 @@ protected class Step extends WizardStep { Step(final T modelObject) { model.setObject(modelObject); model.setInitialModelObject(modelObject); - add(new BeanPanel<>("bean", model, pageRef).setRenderBodyOnly(true)); + add(new BeanPanel<>("bean", model, pageRef, excluded == null ? null : excluded.toArray(String[]::new)). + setRenderBodyOnly(true)); + } + } + + protected class ConsentAttributes extends WizardStep { + + private static final long serialVersionUID = -4865650799450548351L; + + ConsentAttributes(final WAConsentDecision consentDecision) { + String attributes = "{}"; + try { + attributes = authProfileRestClient.readConsentAttributes( + serviceOps.get(NetworkService.Type.WA), + consentDecision.getPrincipal(), + consentDecision.getId()); + } catch (Exception e) { + LOG.error("While attempting to fetch consent attributes for principal {} and id {}", + consentDecision.getPrincipal(), consentDecision.getId(), e); + } + add(new JsonEditorPanel(null, Model.of(attributes), true, pageRef)); + } + + @Override + public String getTitle() { + return getString("attributes"); } } } diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java index ae1e1347d9d..2143e27046e 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java @@ -52,6 +52,9 @@ public final class AMConstants { public static final String PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS = "authprofile.webAuthnDeviceCredentials.paginator.rows"; + public static final String PREF_AUTHPROFILE_CONSENT_DECISION_PAGINATOR_ROWS = + "authprofile.consentDecisions.paginator.rows"; + public static final String PREF_OIDC_CUSTOMSCOPES_PAGINATOR_ROWS = "oidc.customScopes.paginator.rows"; private AMConstants() { diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java b/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java index e37d5a2cb99..d106f3ca787 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/pages/WA.java @@ -264,7 +264,7 @@ public Panel getPanel(final String panelId) { @Override public Panel getPanel(final String panelId) { - return new AuthProfilePanel(panelId, authProfileRestClient, getPageReference()); + return new AuthProfilePanel(panelId, serviceOps, authProfileRestClient, getPageReference()); } }); } diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java index 99f8cb18ce8..6093a178a0f 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/AuthProfileRestClient.java @@ -18,7 +18,17 @@ */ package org.apache.syncope.client.console.rest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; import java.util.List; +import org.apache.commons.lang3.Strings; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.syncope.client.console.SyncopeWebApplication; +import org.apache.syncope.common.keymaster.client.api.model.NetworkService; import org.apache.syncope.common.lib.to.AuthProfileTO; import org.apache.syncope.common.rest.api.beans.AuthProfileQuery; import org.apache.syncope.common.rest.api.service.AuthProfileService; @@ -27,6 +37,8 @@ public class AuthProfileRestClient extends BaseRestClient { private static final long serialVersionUID = -7379778542101161274L; + protected static final JsonMapper MAPPER = JsonMapper.builder().findAndAddModules().build(); + public long count(final String keyword) { return getService(AuthProfileService.class). search(new AuthProfileQuery.Builder().page(1).size(0).keyword(keyword).build()). @@ -50,4 +62,32 @@ public void update(final AuthProfileTO authProfile) { public void delete(final String key) { getService(AuthProfileService.class).delete(key); } + + public String readConsentAttributes(final NetworkService service, final String principal, final long id) + throws IOException { + + Response response = WebClient.create( + Strings.CS.appendIfMissing(service.getAddress(), "/") + "actuator/attributeConsent/" + principal, + List.of(), + SyncopeWebApplication.get().getAnonymousUser(), + SyncopeWebApplication.get().getAnonymousKey(), + null).accept(MediaType.APPLICATION_JSON_TYPE).get(); + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + JsonNode nodes = MAPPER.readTree((InputStream) response.getEntity()); + for (JsonNode node : nodes) { + if (node.has("decision")) { + JsonNode decision = node.get("decision"); + if (decision.has("id") && id == decision.get("id").asLong()) { + if (node.has("attributes")) { + return node.get("attributes").toPrettyString(); + } + } + } + } + } else { + LOG.error("While contacting the /actuator/attributeConsent endpoint: HTTP {}", response.getStatus()); + } + + return "{}"; + } } diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties index 196e06fde41..d933a844d83 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.properties @@ -46,3 +46,9 @@ recordDate=Record Date mfaTrustedDevices=MFA Devices down.title=mfa devices down.class=fas fa-barcode +consentDecisions=Consent Decisions +service=Client Application +createdDate=Created Date +assign.title=consent decisions +assign.class=fa-solid fa-clipboard-list +attributes=Attributes diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties index db0318cea8b..a9e4e56144f 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_fr_CA.properties @@ -46,3 +46,9 @@ recordDate=Record Date mfaTrustedDevices=MFA Devices down.title=mfa devices down.class=fas fa-barcode +consentDecisions=Consent Decisions +service=Client Application +createdDate=Created Date +assign.title=consent decisions +assign.class=fa-solid fa-clipboard-list +attributes=Attributes diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties index e9378093ce7..555584bc7de 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_it.properties @@ -46,3 +46,9 @@ recordDate=Memorizzazione mfaTrustedDevices=Dispositivi MFA down.title=dispositivi mfa down.class=fas fa-barcode +consentDecisions=Consensi +service=Applicazione client +createdDate=Data creazione +assign.title=consensi +assign.class=fa-solid fa-clipboard-list +attributes=Attributi diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties index 4d3fd17e731..b0a43be7c29 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ja.properties @@ -46,3 +46,9 @@ recordDate=Record Date mfaTrustedDevices=MFA Devices down.title=mfa devices down.class=fas fa-barcode +consentDecisions=Consent Decisions +service=Client Application +createdDate=Created Date +assign.title=consent decisions +assign.class=fa-solid fa-clipboard-list +attributes=Attributes diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties index 9e1722ead79..fb3e964b47f 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_pt_BR.properties @@ -46,3 +46,9 @@ recordDate=Record Date mfaTrustedDevices=MFA Devices down.title=mfa devices down.class=fas fa-barcode +consentDecisions=Consent Decisions +service=Client Application +createdDate=Created Date +assign.title=consent decisions +assign.class=fa-solid fa-clipboard-list +attributes=Attributes diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties index 028bda75657..9934e89a03e 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel_ru.properties @@ -47,3 +47,9 @@ recordDate=Record Date mfaTrustedDevices=MFA Devices down.title=mfa devices down.class=fas fa-barcode +consentDecisions=Consent Decisions +service=Client Application +createdDate=Created Date +assign.title=consent decisions +assign.class=fa-solid fa-clipboard-list +attributes=Attributes diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html new file mode 100644 index 00000000000..aefa83b98b2 --- /dev/null +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html @@ -0,0 +1,23 @@ + + + +
+ + diff --git a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java index 50a584d4f83..8f752e7fa3b 100644 --- a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java +++ b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AuthProfile.java @@ -23,20 +23,26 @@ import org.apache.syncope.client.enduser.rest.AuthProfileRestClient; import org.apache.syncope.client.ui.commons.Constants; import org.apache.syncope.client.ui.commons.annotations.AMPage; +import org.apache.syncope.client.ui.commons.markup.html.form.AlertBehavior; import org.apache.syncope.client.ui.commons.markup.html.form.IndicatingOnConfirmAjaxLink; +import org.apache.syncope.common.keymaster.client.api.ServiceOps; +import org.apache.syncope.common.keymaster.client.api.model.NetworkService; import org.apache.syncope.common.lib.to.AuthProfileTO; import org.apache.syncope.common.lib.wa.GoogleMfaAuthAccount; import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.ajax.markup.html.navigation.paging.AjaxPagingNavigator; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.data.DataView; import org.apache.wicket.markup.repeater.data.ListDataProvider; +import org.apache.wicket.model.Model; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; @@ -49,6 +55,9 @@ public class AuthProfile extends BaseReauthPage { protected static final int ROWS_PER_PAGE = 5; + @SpringBean + protected ServiceOps serviceOps; + @SpringBean protected AuthProfileRestClient restClient; @@ -216,5 +225,61 @@ public void onClick(final AjaxRequestTarget target) { webAuthnDeviceCredentials.setItemsPerPage(ROWS_PER_PAGE); container.add(webAuthnDeviceCredentials.setOutputMarkupPlaceholderTag(true)); container.add(new AjaxPagingNavigator("webAuthnDeviceCredentialsNavigator", webAuthnDeviceCredentials)); + + DataView consentDecisions = new DataView<>( + "consentDecisions", new ListDataProvider<>( + authProfile == null ? List.of() : authProfile.getConsentDecisions())) { + + private static final long serialVersionUID = 6127875313385810666L; + + @Override + public void populateItem(final Item item) { + String attributes = "{}"; + try { + attributes = restClient.readConsentAttributes( + serviceOps.get(NetworkService.Type.WA), + item.getModelObject().getPrincipal(), + item.getModelObject().getId()); + } catch (Exception e) { + LOG.error("While attempting to fetch consent attributes for principal {} and id {}", + item.getModelObject().getPrincipal(), item.getModelObject().getId(), e); + } + + item.add(new Label("id", item.getModelObject().getId())); + item.add(new Label("service", item.getModelObject().getService())); + item.add(new Label("createdDate", item.getModelObject().getCreatedDate())); + AjaxLink consentAttributes = new AjaxLink<>("consentAttributes", Model.of()) { + + private static final long serialVersionUID = 2706290656177366584L; + + @Override + public void onClick(final AjaxRequestTarget target) { + // nothing to do + } + }; + item.add(consentAttributes.add(new AlertBehavior( + consentAttributes, + getString("attributes"), + "
' + JSON.stringify(JSON.parse('"
+                        + attributes.replace("'", "\'") + "'), null, 2) + '
"))); + item.add(new IndicatingOnConfirmAjaxLink<>( + "consentDecisionDelete", Constants.CONFIRM_DELETE, true) { + + private static final long serialVersionUID = 1632838687547839512L; + + @Override + public void onClick(final AjaxRequestTarget target) { + if (authProfile != null) { + authProfile.getConsentDecisions().remove(item.getModelObject()); + restClient.update(authProfile); + target.add(container); + } + } + }); + } + }; + consentDecisions.setItemsPerPage(ROWS_PER_PAGE); + container.add(consentDecisions.setOutputMarkupPlaceholderTag(true)); + container.add(new AjaxPagingNavigator("consentDecisionsNavigator", consentDecisions)); } } diff --git a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java index 2714954ff59..0f59438f1fc 100644 --- a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java +++ b/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java @@ -18,6 +18,17 @@ */ package org.apache.syncope.client.enduser.rest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.apache.commons.lang3.Strings; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.syncope.client.enduser.SyncopeWebApplication; +import org.apache.syncope.common.keymaster.client.api.model.NetworkService; import org.apache.syncope.common.lib.to.AuthProfileTO; import org.apache.syncope.common.rest.api.service.AuthProfileSelfService; @@ -25,6 +36,8 @@ public class AuthProfileRestClient extends BaseRestClient { private static final long serialVersionUID = 4139153766778113329L; + protected static final JsonMapper MAPPER = JsonMapper.builder().findAndAddModules().build(); + public AuthProfileTO read() { try { return getService(AuthProfileSelfService.class).read(); @@ -41,4 +54,32 @@ public void update(final AuthProfileTO authProfile) { public void delete() { getService(AuthProfileSelfService.class).delete(); } + + public String readConsentAttributes(final NetworkService service, final String principal, final long id) + throws IOException { + + Response response = WebClient.create( + Strings.CS.appendIfMissing(service.getAddress(), "/") + "actuator/attributeConsent/" + principal, + List.of(), + SyncopeWebApplication.get().getAnonymousUser(), + SyncopeWebApplication.get().getAnonymousKey(), + null).accept(MediaType.APPLICATION_JSON_TYPE).get(); + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + JsonNode nodes = MAPPER.readTree((InputStream) response.getEntity()); + for (JsonNode node : nodes) { + if (node.has("decision")) { + JsonNode decision = node.get("decision"); + if (decision.has("id") && id == decision.get("id").asLong()) { + if (node.has("attributes")) { + return node.get("attributes").toString(); + } + } + } + } + } else { + LOG.error("While contacting the /actuator/attributeConsent endpoint: HTTP {}", response.getStatus()); + } + + return "{}"; + } } diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html index 17b7ea9a842..16b6801ccae 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.html @@ -208,7 +208,8 @@

-
+
+
- + +
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + +
+
+
+
+
+
+
diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties index 5bf4119b60e..ec839853d23 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile.properties @@ -33,3 +33,7 @@ source=Source deviceFingerprint=Fingerprint recordDate=Record Date expirationDate=Expiration Date +consent.decisions.title=Consent Decisions +service=Application +createdDate=Created Date +attributes=Attributes diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties index bb47fe6606f..c1dca5b588f 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_it.properties @@ -33,3 +33,7 @@ source=Origine deviceFingerprint=Fingerprint recordDate=Data di registrazione expirationDate=Data di scadenza +consent.decisions.title=Consensi +service=Applicazione +createdDate=Data creazione +attributes=Attributi diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties index 5bf4119b60e..ec839853d23 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ja.properties @@ -33,3 +33,7 @@ source=Source deviceFingerprint=Fingerprint recordDate=Record Date expirationDate=Expiration Date +consent.decisions.title=Consent Decisions +service=Application +createdDate=Created Date +attributes=Attributes diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties index 5bf4119b60e..ec839853d23 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_pt_BR.properties @@ -33,3 +33,7 @@ source=Source deviceFingerprint=Fingerprint recordDate=Record Date expirationDate=Expiration Date +consent.decisions.title=Consent Decisions +service=Application +createdDate=Created Date +attributes=Attributes diff --git a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties index 5bf4119b60e..ec839853d23 100644 --- a/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties +++ b/client/am/enduser/src/main/resources/org/apache/syncope/client/enduser/pages/AuthProfile_ru.properties @@ -33,3 +33,7 @@ source=Source deviceFingerprint=Fingerprint recordDate=Record Date expirationDate=Expiration Date +consent.decisions.title=Consent Decisions +service=Application +createdDate=Created Date +attributes=Attributes diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java new file mode 100644 index 00000000000..16f29c7ecc1 --- /dev/null +++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AlertBehavior.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.client.ui.commons.markup.html.form; + +import static de.agilecoders.wicket.jquery.JQuery.$; + +import de.agilecoders.wicket.jquery.function.JavaScriptInlineFunction; +import java.util.ArrayList; +import org.apache.wicket.Component; +import org.apache.wicket.Session; +import org.apache.wicket.behavior.Behavior; +import org.apache.wicket.markup.head.IHeaderResponse; + +public class AlertBehavior extends Behavior { + + private static final long serialVersionUID = 2210125898183667592L; + + private final Component parent; + + private final String title; + + private final String body; + + public AlertBehavior(final Component parent, final String title, final String body) { + this.parent = parent; + this.title = title; + this.body = body; + } + + @Override + public void renderHead(final Component component, final IHeaderResponse response) { + super.renderHead(component, response); + + response.render($(parent).on("click", + new JavaScriptInlineFunction("" + + "bootbox.alert({" + + "size:'large', " + + "title:'" + title + "', " + + "message: '" + body + "', " + + "buttons: {" + + " ok: {" + + " className: 'btn-success'" + + " }" + + "}," + + "locale: '" + Session.get().getLocale().getLanguage() + "'," + + "callback: function() {" + + " return true;" + + "}" + + "});", new ArrayList<>() + )).asDomReadyScript()); + } +} diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java index 7efb694d90f..460ed128aac 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/JsonEditorPanel.java @@ -34,10 +34,6 @@ public class JsonEditorPanel extends AbstractModalPanel { private final boolean readOnly; - public JsonEditorPanel(final IModel content) { - this(null, content, false, null); - } - public JsonEditorPanel( final BaseModal modal, final IModel content, diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java index 92c06999a46..2063e2ed71c 100644 --- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java +++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/AuthProfileTO.java @@ -18,6 +18,7 @@ */ package org.apache.syncope.common.lib.to; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.ws.rs.PathParam; import java.util.ArrayList; import java.util.Collection; @@ -28,9 +29,10 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; -public class AuthProfileTO implements EntityTO { +public class AuthProfileTO implements NamedEntityTO { private static final long serialVersionUID = -6543425997956703057L; @@ -48,6 +50,23 @@ public AuthProfileTO.Builder owner(final String owner) { return this; } + public AuthProfileTO.Builder impersonationAccount(final ImpersonationAccount impersonationAccount) { + instance.getImpersonationAccounts().add(impersonationAccount); + return this; + } + + public AuthProfileTO.Builder impersonationAccounts(final ImpersonationAccount... impersonationAccounts) { + instance.getImpersonationAccounts().addAll(List.of(impersonationAccounts)); + return this; + } + + public AuthProfileTO.Builder impersonationAccounts( + final Collection impersonationAccounts) { + + instance.getImpersonationAccounts().addAll(impersonationAccounts); + return this; + } + public AuthProfileTO.Builder googleMfaAuthToken(final GoogleMfaAuthToken token) { instance.getGoogleMfaAuthTokens().add(token); return this; @@ -93,21 +112,36 @@ public AuthProfileTO.Builder mfaTrustedDevices(final Collection credentials) { + public AuthProfileTO.Builder webAuthnDeviceCredentials(final Collection credentials) { instance.getWebAuthnDeviceCredentials().addAll(credentials); return this; } + public AuthProfileTO.Builder consentDecision(final WAConsentDecision consentDecision) { + instance.getConsentDecisions().add(consentDecision); + return this; + } + + public AuthProfileTO.Builder consentDecisions(final WAConsentDecision... consentDecisions) { + instance.getConsentDecisions().addAll(List.of(consentDecisions)); + return this; + } + + public AuthProfileTO.Builder consentDecisions(final Collection consentDecisions) { + instance.getConsentDecisions().addAll(consentDecisions); + return this; + } + public AuthProfileTO build() { return instance; } @@ -127,6 +161,8 @@ public AuthProfileTO build() { private final List webAuthnDeviceCredentials = new ArrayList<>(); + private final List consentDecisions = new ArrayList<>(); + @Override public String getKey() { return key; @@ -146,6 +182,18 @@ public void setOwner(final String owner) { this.owner = owner; } + @JsonIgnore + @Override + public String getName() { + return getOwner(); + } + + @JsonIgnore + @Override + public void setName(final String name) { + throw new UnsupportedOperationException(); + } + public List getImpersonationAccounts() { return impersonationAccounts; } @@ -166,6 +214,10 @@ public List getWebAuthnDeviceCredentials() { return webAuthnDeviceCredentials; } + public List getConsentDecisions() { + return consentDecisions; + } + @Override public int hashCode() { return new HashCodeBuilder(). @@ -176,6 +228,7 @@ public int hashCode() { append(googleMfaAuthAccounts). append(mfaTrustedDevices). append(webAuthnDeviceCredentials). + append(consentDecisions). build(); } @@ -199,6 +252,7 @@ public boolean equals(final Object obj) { append(googleMfaAuthAccounts, other.googleMfaAuthAccounts). append(mfaTrustedDevices, other.mfaTrustedDevices). append(webAuthnDeviceCredentials, other.webAuthnDeviceCredentials). + append(consentDecisions, other.consentDecisions). build(); } } diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java new file mode 100644 index 00000000000..6f33c07b149 --- /dev/null +++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/WAConsentDecision.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.common.lib.wa; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.syncope.common.lib.BaseBean; + +public class WAConsentDecision implements BaseBean { + + private static final long serialVersionUID = -4763224069061622840L; + + public static class Builder { + + private final WAConsentDecision instance; + + public Builder(final long id, final String principal, final String service, final LocalDateTime createDate) { + instance = new WAConsentDecision(); + instance.setId(id); + instance.setPrincipal(principal); + instance.setService(service); + instance.setCreatedDate(createDate); + } + + public Builder options(final ReminderOptions options) { + instance.setOptions(options); + return this; + } + + public Builder reminder(final long reminder) { + instance.setReminder(reminder); + return this; + } + + public Builder reminderTimeUnit(final ChronoUnit reminderTimeUnit) { + instance.setReminderTimeUnit(reminderTimeUnit); + return this; + } + + public Builder attributes(final String attributes) { + instance.setAttributes(attributes); + return this; + } + + public WAConsentDecision build() { + return instance; + } + } + + public enum ReminderOptions { + /** + * Always ask for consent. + */ + ALWAYS(0), + /** + * Ask for consent when there is modification in one of the attribute names or if consent is expired. + */ + ATTRIBUTE_NAME(1), + /** + * Ask for consent when there is modification in one of the attribute names, the values contain inside the + * attributes or if consent is expired. + */ + ATTRIBUTE_VALUE(2); + + private final int value; + + ReminderOptions(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private long id; + + private String principal; + + private String service; + + private LocalDateTime createdDate; + + private ReminderOptions options = ReminderOptions.ATTRIBUTE_NAME; + + private long reminder = 14L; + + private ChronoUnit reminderTimeUnit = ChronoUnit.DAYS; + + private String attributes; + + public long getId() { + return id; + } + + public void setId(final long id) { + this.id = id; + } + + public String getPrincipal() { + return principal; + } + + public void setPrincipal(final String principal) { + this.principal = principal; + } + + public String getService() { + return service; + } + + public void setService(final String service) { + this.service = service; + } + + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(final LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + public ReminderOptions getOptions() { + return options; + } + + public void setOptions(final ReminderOptions options) { + this.options = options; + } + + public long getReminder() { + return reminder; + } + + public void setReminder(final long reminder) { + this.reminder = reminder; + } + + public ChronoUnit getReminderTimeUnit() { + return reminderTimeUnit; + } + + public void setReminderTimeUnit(final ChronoUnit reminderTimeUnit) { + this.reminderTimeUnit = reminderTimeUnit; + } + + public String getAttributes() { + return attributes; + } + + public void setAttributes(final String attributes) { + this.attributes = attributes; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(). + append(id). + append(principal). + append(service). + append(createdDate). + append(options). + append(reminder). + append(reminderTimeUnit). + append(attributes). + build(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + WAConsentDecision other = (WAConsentDecision) obj; + return new EqualsBuilder(). + append(id, other.id). + append(principal, other.principal). + append(service, other.service). + append(createdDate, other.createdDate). + append(options, other.options). + append(reminder, other.reminder). + append(reminderTimeUnit, other.reminderTimeUnit). + append(attributes, other.attributes). + build(); + } + + @Override + public String toString() { + return new ToStringBuilder(this). + append("id", id). + append("principal", principal). + append("service", service). + append("createdDate", createdDate). + append("options", options). + append("reminder", reminder). + append("reminderTimeUnit", reminderTimeUnit). + append("attributes", attributes). + build(); + } +} diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java new file mode 100644 index 00000000000..96c3b457161 --- /dev/null +++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ConsentDecisionService.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.common.rest.api.service.wa; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.apache.syncope.common.lib.to.PagedResult; +import org.apache.syncope.common.lib.wa.WAConsentDecision; +import org.apache.syncope.common.rest.api.service.JAXRSService; + +@Tag(name = "WA") +@SecurityRequirements({ + @SecurityRequirement(name = "BasicAuthentication"), + @SecurityRequirement(name = "Bearer") }) +@Path("wa/consentDecision") +public interface ConsentDecisionService extends JAXRSService { + + @DELETE + @Path("{owner}/{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + void delete(@NotNull @PathParam("owner") String owner, @NotNull @PathParam("id") long id); + + @DELETE + @Path("{owner}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + void delete(@NotNull @PathParam("owner") String owner); + + @DELETE + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + void deleteAll(); + + @PUT + @Path("{owner}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + void store(@NotNull @PathParam("owner") String owner, @NotNull WAConsentDecision consentDecision); + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{owner}/service") + WAConsentDecision read(@NotNull @PathParam("owner") String owner, @NotNull @QueryParam("service") String service); + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{owner}") + PagedResult read(@NotNull @PathParam("owner") String owner); + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + PagedResult list(); +} diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java index 7cbf0757423..f14808b37ab 100644 --- a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java +++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java @@ -20,6 +20,7 @@ import org.apache.syncope.common.keymaster.client.api.ServiceOps; import org.apache.syncope.core.logic.init.AMEntitlementLoader; +import org.apache.syncope.core.logic.wa.ConsentDecisionLogic; import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic; import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic; import org.apache.syncope.core.logic.wa.ImpersonationLogic; @@ -181,6 +182,16 @@ public ImpersonationLogic impersonationLogic( return new ImpersonationLogic(authProfileDataBinder, authProfileDAO, entityFactory); } + @ConditionalOnMissingBean + @Bean + public ConsentDecisionLogic consentDecisionLogic( + final AuthProfileDataBinder authProfileDataBinder, + final AuthProfileDAO authProfileDAO, + final EntityFactory entityFactory) { + + return new ConsentDecisionLogic(authProfileDataBinder, authProfileDAO, entityFactory); + } + @ConditionalOnMissingBean @Bean public MfaTrusStorageLogic mfaTrusStorageLogic( diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java new file mode 100644 index 00000000000..b891acb2054 --- /dev/null +++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.logic.wa; + +import java.util.List; +import java.util.function.Predicate; +import org.apache.syncope.common.lib.types.IdRepoEntitlement; +import org.apache.syncope.common.lib.wa.WAConsentDecision; +import org.apache.syncope.core.logic.AbstractAuthProfileLogic; +import org.apache.syncope.core.persistence.api.dao.AuthProfileDAO; +import org.apache.syncope.core.persistence.api.dao.NotFoundException; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.am.AuthProfile; +import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; + +public class ConsentDecisionLogic extends AbstractAuthProfileLogic { + + public ConsentDecisionLogic( + final AuthProfileDataBinder binder, + final AuthProfileDAO authProfileDAO, + final EntityFactory entityFactory) { + + super(binder, authProfileDAO, entityFactory); + } + + protected void removeAndSave(final AuthProfile profile, final Predicate criteria) { + List consentDecisions = profile.getConsentDecisions(); + if (consentDecisions.removeIf(criteria)) { + profile.setConsentDecisions(consentDecisions); + authProfileDAO.save(profile); + } + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public void delete(final String owner, final long id) { + authProfileDAO.findByOwner(owner). + ifPresent(profile -> removeAndSave(profile, consentDecision -> consentDecision.getId() == id)); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public void delete(final String owner) { + authProfileDAO.findByOwner(owner).ifPresent(profile -> { + profile.setConsentDecisions(List.of()); + authProfileDAO.save(profile); + }); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public void deleteAll() { + authProfileDAO.findAll(Pageable.unpaged()).forEach(profile -> { + profile.setConsentDecisions(List.of()); + authProfileDAO.save(profile); + }); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public void store(final String owner, final WAConsentDecision contentDecision) { + AuthProfile profile = authProfile(owner); + + List consentDecisions = profile.getConsentDecisions(); + consentDecisions.removeIf(cd -> cd.getId() == contentDecision.getId()); + consentDecisions.add(contentDecision); + profile.setConsentDecisions(consentDecisions); + authProfileDAO.save(profile); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + @Transactional(readOnly = true) + public WAConsentDecision read(final String owner, final String service) { + return authProfileDAO.findByOwner(owner). + stream(). + map(AuthProfile::getConsentDecisions). + flatMap(List::stream). + filter(consentDecision -> consentDecision.getService().equals(service)). + findFirst(). + orElseThrow(() -> new NotFoundException( + "Could not find consent decision for owner " + owner + " and service " + service)); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + @Transactional(readOnly = true) + public List list() { + return authProfileDAO.findAll(Pageable.unpaged()).stream(). + map(AuthProfile::getConsentDecisions). + flatMap(List::stream). + toList(); + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + @Transactional(readOnly = true) + public List read(final String owner) { + return authProfileDAO.findByOwner(owner). + map(AuthProfile::getConsentDecisions). + orElseGet(List::of); + } +} diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java index 157f8363067..99fa06d7125 100644 --- a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java +++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/AMRESTCXFContext.java @@ -27,6 +27,7 @@ import org.apache.syncope.common.rest.api.service.PasswordManagementService; import org.apache.syncope.common.rest.api.service.SAML2IdPEntityService; import org.apache.syncope.common.rest.api.service.SRARouteService; +import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService; import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthAccountService; import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService; import org.apache.syncope.common.rest.api.service.wa.ImpersonationService; @@ -43,6 +44,7 @@ import org.apache.syncope.core.logic.PasswordManagementLogic; import org.apache.syncope.core.logic.SAML2IdPEntityLogic; import org.apache.syncope.core.logic.SRARouteLogic; +import org.apache.syncope.core.logic.wa.ConsentDecisionLogic; import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic; import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic; import org.apache.syncope.core.logic.wa.ImpersonationLogic; @@ -59,6 +61,7 @@ import org.apache.syncope.core.rest.cxf.service.PasswordManagementServiceImpl; import org.apache.syncope.core.rest.cxf.service.SAML2IdPEntityServiceImpl; import org.apache.syncope.core.rest.cxf.service.SRARouteServiceImpl; +import org.apache.syncope.core.rest.cxf.service.wa.ConsentDecisionServiceImpl; import org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthAccountServiceImpl; import org.apache.syncope.core.rest.cxf.service.wa.GoogleMfaAuthTokenServiceImpl; import org.apache.syncope.core.rest.cxf.service.wa.ImpersonationServiceImpl; @@ -132,6 +135,14 @@ public ImpersonationService impersonationService(final ImpersonationLogic impers return new ImpersonationServiceImpl(impersonationLogic); } + @ConditionalOnMissingBean + @Bean + public ConsentDecisionService consentDecisionService( + final ConsentDecisionLogic consentDecisionLogic) { + + return new ConsentDecisionServiceImpl(consentDecisionLogic); + } + @ConditionalOnMissingBean @Bean public OIDCOpEntityService oidcOpService(final OIDCOpEntityLogic oidcOpLogic) { diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java new file mode 100644 index 00000000000..3ecffb5b39c --- /dev/null +++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ConsentDecisionServiceImpl.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.rest.cxf.service.wa; + +import java.util.List; +import org.apache.syncope.common.lib.to.PagedResult; +import org.apache.syncope.common.lib.wa.WAConsentDecision; +import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService; +import org.apache.syncope.core.logic.wa.ConsentDecisionLogic; +import org.apache.syncope.core.rest.cxf.service.AbstractService; + +public class ConsentDecisionServiceImpl extends AbstractService implements ConsentDecisionService { + + protected final ConsentDecisionLogic logic; + + public ConsentDecisionServiceImpl(final ConsentDecisionLogic logic) { + this.logic = logic; + } + + @Override + public void delete(final String owner, final long id) { + logic.delete(owner, id); + } + + @Override + public void delete(final String owner) { + logic.delete(owner); + } + + @Override + public void deleteAll() { + logic.deleteAll(); + } + + @Override + public void store(final String owner, final WAConsentDecision consentDecision) { + logic.store(owner, consentDecision); + } + + @Override + public WAConsentDecision read(final String owner, final String service) { + return logic.read(owner, service); + } + + private PagedResult build(final List read) { + PagedResult result = new PagedResult<>(); + result.setPage(1); + result.setSize(read.size()); + result.setTotalCount(read.size()); + result.getResult().addAll(read); + return result; + } + + @Override + public PagedResult read(final String owner) { + return build(logic.read(owner)); + } + + @Override + public PagedResult list() { + return build(logic.list()); + } +} diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java index bd922215110..332d0e22d82 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/AuthProfile.java @@ -23,6 +23,7 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; import org.apache.syncope.core.persistence.api.entity.Entity; @@ -51,4 +52,8 @@ public interface AuthProfile extends Entity { List getImpersonationAccounts(); void setImpersonationAccounts(List accounts); + + List getConsentDecisions(); + + void setConsentDecisions(List consentDecisions); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java index 303436b9c38..18147d6d029 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java @@ -31,6 +31,7 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; import org.apache.syncope.core.persistence.api.entity.am.AuthProfile; import org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity; @@ -65,6 +66,10 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr new TypeReference>() { }; + protected static final TypeReference> WA_CONSENT_DECISION_TYPEREF = + new TypeReference>() { + }; + @Column(nullable = false) private String owner; @@ -83,6 +88,9 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr @Lob private String webAuthnDeviceCredentials; + @Lob + private String waConsentDecisions; + @Override public String getOwner() { return owner; @@ -147,4 +155,15 @@ public List getWebAuthnDeviceCredentials() { public void setWebAuthnDeviceCredentials(final List credentials) { webAuthnDeviceCredentials = POJOHelper.serialize(credentials); } + + @Override + public List getConsentDecisions() { + return Optional.ofNullable(waConsentDecisions). + map(v -> POJOHelper.deserialize(v, WA_CONSENT_DECISION_TYPEREF)).orElseGet(() -> new ArrayList<>(0)); + } + + @Override + public void setConsentDecisions(final List consentDecisions) { + waConsentDecisions = POJOHelper.serialize(consentDecisions); + } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java index 5927b1b82b5..ac1c96bd8f0 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/am/Neo4jAuthProfile.java @@ -27,6 +27,7 @@ import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken; import org.apache.syncope.common.lib.wa.ImpersonationAccount; import org.apache.syncope.common.lib.wa.MfaTrustedDevice; +import org.apache.syncope.common.lib.wa.WAConsentDecision; import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential; import org.apache.syncope.core.persistence.api.entity.am.AuthProfile; import org.apache.syncope.core.persistence.neo4j.entity.AbstractGeneratedKeyNode; @@ -60,6 +61,10 @@ public class Neo4jAuthProfile extends AbstractGeneratedKeyNode implements AuthPr new TypeReference>() { }; + protected static final TypeReference> WA_CONSENT_DECISION_TYPEREF = + new TypeReference>() { + }; + @NotNull private String owner; @@ -73,6 +78,8 @@ public class Neo4jAuthProfile extends AbstractGeneratedKeyNode implements AuthPr private String webAuthnDeviceCredentials; + private String waConsentDecisions; + @Override public String getOwner() { return owner; @@ -137,4 +144,15 @@ public List getWebAuthnDeviceCredentials() { public void setWebAuthnDeviceCredentials(final List credentials) { webAuthnDeviceCredentials = POJOHelper.serialize(credentials); } + + @Override + public List getConsentDecisions() { + return Optional.ofNullable(waConsentDecisions). + map(v -> POJOHelper.deserialize(v, WA_CONSENT_DECISION_TYPEREF)).orElseGet(() -> new ArrayList<>(0)); + } + + @Override + public void setConsentDecisions(final List consentDecisions) { + waConsentDecisions = POJOHelper.serialize(consentDecisions); + } } diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java index 615e1b4a9ea..b9e1fdd74b3 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthProfileDataBinderImpl.java @@ -33,15 +33,16 @@ public AuthProfileDataBinderImpl(final EntityFactory entityFactory) { @Override public AuthProfileTO getAuthProfileTO(final AuthProfile authProfile) { - AuthProfileTO authProfileTO = new AuthProfileTO(); - authProfileTO.setKey(authProfile.getKey()); - authProfileTO.setOwner(authProfile.getOwner()); - authProfileTO.getImpersonationAccounts().addAll(authProfile.getImpersonationAccounts()); - authProfileTO.getGoogleMfaAuthTokens().addAll(authProfile.getGoogleMfaAuthTokens()); - authProfileTO.getGoogleMfaAuthAccounts().addAll(authProfile.getGoogleMfaAuthAccounts()); - authProfileTO.getMfaTrustedDevices().addAll(authProfile.getMfaTrustedDevices()); - authProfileTO.getWebAuthnDeviceCredentials().addAll(authProfile.getWebAuthnDeviceCredentials()); - return authProfileTO; + return new AuthProfileTO.Builder(). + key(authProfile.getKey()). + owner(authProfile.getOwner()). + impersonationAccounts(authProfile.getImpersonationAccounts()). + googleMfaAuthTokens(authProfile.getGoogleMfaAuthTokens()). + googleMfaAuthAccounts(authProfile.getGoogleMfaAuthAccounts()). + mfaTrustedDevices(authProfile.getMfaTrustedDevices()). + webAuthnDeviceCredentials(authProfile.getWebAuthnDeviceCredentials()). + consentDecisions(authProfile.getConsentDecisions()). + build(); } @Override @@ -58,6 +59,7 @@ public AuthProfile update(final AuthProfile authProfile, final AuthProfileTO aut authProfile.setGoogleMfaAuthAccounts(authProfileTO.getGoogleMfaAuthAccounts()); authProfile.setMfaTrustedDevices(authProfileTO.getMfaTrustedDevices()); authProfile.setWebAuthnDeviceCredentials(authProfileTO.getWebAuthnDeviceCredentials()); + authProfile.setConsentDecisions(authProfileTO.getConsentDecisions()); return authProfile; } } diff --git a/fit/wa-reference/src/main/resources/wa-embedded.properties b/fit/wa-reference/src/main/resources/wa-embedded.properties index 8f9abcecf5e..40d66584896 100644 --- a/fit/wa-reference/src/main/resources/wa-embedded.properties +++ b/fit/wa-reference/src/main/resources/wa-embedded.properties @@ -44,4 +44,4 @@ cas.tgc.crypto.encryption.key=mW6lMvsSo48eZ1Ntt74a-O9jjQQQ_OLUE24RVN2_A_sPX43mpB cas.webflow.crypto.signing.key=Md6kkPlXx5L18TD0mFELpQXWnDbMffj-uPutPckMnAPPuJQEbfcLLYBnOynYIEDgnEpd7sxUwGYd8_sVYFMcjw cas.webflow.crypto.encryption.key=FhLgLpaPL8GVNuqqo7gtiw -management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes +management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent diff --git a/pom.xml b/pom.xml index e0db99fc868..4cbe23e8efd 100644 --- a/pom.xml +++ b/pom.xml @@ -451,7 +451,7 @@ under the License. 4.0.0 - 9.3.3 + 9.3.4 3.6.0 3.8.0 @@ -484,7 +484,7 @@ under the License. 2.0.7 4.4.3 - 10.8.0 + 10.9.0 10.8.0 7.0.14 4.1.1 @@ -654,7 +654,7 @@ under the License. co.elastic.clients elasticsearch-java - 9.3.4 + ${elasticsearch.version} diff --git a/src/main/asciidoc/reference-guide/concepts/authprofile.adoc b/src/main/asciidoc/reference-guide/concepts/authprofile.adoc new file mode 100644 index 00000000000..d7e230e8410 --- /dev/null +++ b/src/main/asciidoc/reference-guide/concepts/authprofile.adoc @@ -0,0 +1,28 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +=== Auth Profile + +When users log into the <> integrated with <>, the following +information is tracked for <> and <>: + +* https://apereo.github.io/cas/7.3.x/authentication/Surrogate-Authentication.html[Surrogate Authentication^] +* https://apereo.github.io/cas/7.3.x/mfa/GoogleAuthenticator-Authentication.html[Google Authenticator Authentication^] +* https://apereo.github.io/cas/7.3.x/mfa/Multifactor-TrustedDevice-Authentication.html[Multifactor Authentication Trusted Devices^] +* https://apereo.github.io/cas/7.3.x/mfa/FIDO2-WebAuthn-Authentication.html[FIDO2 WebAuthn (Passkey) Multifactor Authentication^] +* https://apereo.github.io/cas/7.3.x/integration/Attribute-Release-Consent.html[Attribute Consent^] diff --git a/src/main/asciidoc/reference-guide/concepts/concepts.adoc b/src/main/asciidoc/reference-guide/concepts/concepts.adoc index b2e15b34220..449335f66b8 100644 --- a/src/main/asciidoc/reference-guide/concepts/concepts.adoc +++ b/src/main/asciidoc/reference-guide/concepts/concepts.adoc @@ -54,6 +54,8 @@ include::passwordmanagement.adoc[] include::clientapplications.adoc[] +include::authprofile.adoc[] + include::domains.adoc[] include::implementations.adoc[] diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml index 3140027232d..22d60f64f15 100644 --- a/wa/starter/pom.xml +++ b/wa/starter/pom.xml @@ -245,6 +245,10 @@ under the License. org.apereo.cas cas-server-support-consent-webflow + + org.apereo.cas + cas-server-support-consent-api + org.apereo.cas cas-server-support-aup-webflow diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java index bad09c3947e..462ac330082 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java @@ -42,6 +42,7 @@ import org.apache.syncope.wa.starter.actuate.SyncopeCoreHealthIndicator; import org.apache.syncope.wa.starter.actuate.SyncopeWAInfoContributor; import org.apache.syncope.wa.starter.audit.WAAuditTrailManager; +import org.apache.syncope.wa.starter.consent.WAConsentRepository; import org.apache.syncope.wa.starter.events.WAEventRepository; import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthCredentialRepository; import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthTokenRepository; @@ -77,6 +78,7 @@ import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; +import org.apereo.cas.consent.ConsentRepository; import org.apereo.cas.gauth.CasGoogleAuthenticator; import org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository; import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService; @@ -543,6 +545,12 @@ public WebAuthnCredentialRepository webAuthnCredentialRepository( return new WAWebAuthnCredentialRepository(casProperties, waRestClient); } + @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) + @Bean + public ConsentRepository consentRepository(final WARestClient waRestClient) { + return new WAConsentRepository(waRestClient); + } + @Bean public SurrogateAuthenticationService surrogateAuthenticationService(final WARestClient waRestClient) { return new WASurrogateAuthenticationService(waRestClient); diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java new file mode 100644 index 00000000000..0e891e76b26 --- /dev/null +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/consent/WAConsentRepository.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.wa.starter.consent; + +import java.util.Collection; +import org.apache.syncope.common.lib.wa.WAConsentDecision; +import org.apache.syncope.common.rest.api.service.wa.ConsentDecisionService; +import org.apache.syncope.wa.bootstrap.WARestClient; +import org.apereo.cas.authentication.Authentication; +import org.apereo.cas.authentication.principal.Service; +import org.apereo.cas.consent.ConsentDecision; +import org.apereo.cas.consent.ConsentReminderOptions; +import org.apereo.cas.consent.ConsentRepository; +import org.apereo.cas.services.RegisteredService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WAConsentRepository implements ConsentRepository { + + private static final long serialVersionUID = -3094119228321296264L; + + protected static final Logger LOG = LoggerFactory.getLogger(WAConsentRepository.class); + + protected static WAConsentDecision toWAConsentDecision(final ConsentDecision decision) { + return new WAConsentDecision.Builder( + decision.getId(), decision.getPrincipal(), decision.getService(), decision.getCreatedDate()). + options(WAConsentDecision.ReminderOptions.valueOf(decision.getOptions().name())). + reminder(decision.getReminder()). + reminderTimeUnit(decision.getReminderTimeUnit()). + attributes(decision.getAttributes()). + build(); + } + + protected static ConsentDecision toConsentDecision(final String tenant, final WAConsentDecision waConsentDecision) { + ConsentDecision consentDecision = new ConsentDecision(); + consentDecision.setId(waConsentDecision.getId()); + consentDecision.setPrincipal(waConsentDecision.getPrincipal()); + consentDecision.setService(waConsentDecision.getService()); + consentDecision.setCreatedDate(waConsentDecision.getCreatedDate()); + consentDecision.setOptions(ConsentReminderOptions.valueOf(waConsentDecision.getOptions().getValue())); + consentDecision.setReminder(waConsentDecision.getReminder()); + consentDecision.setReminderTimeUnit(waConsentDecision.getReminderTimeUnit()); + consentDecision.setTenant(tenant); + consentDecision.setAttributes(waConsentDecision.getAttributes()); + return consentDecision; + } + + protected final WARestClient waRestClient; + + public WAConsentRepository(final WARestClient waRestClient) { + this.waRestClient = waRestClient; + } + + @Override + public ConsentDecision findConsentDecision( + final Service service, + final RegisteredService registeredService, + final Authentication authentication) { + + try { + WAConsentDecision waContentDecision = waRestClient.getService(ConsentDecisionService.class). + read(authentication.getPrincipal().getId(), service.getId()); + return toConsentDecision(waRestClient.getSyncopeClient().getDomain(), waContentDecision); + } catch (Exception e) { + LOG.error("While attempting to find ConsentDecision for principal {} and service {}", + authentication.getPrincipal().getId(), service.getId(), e); + return null; + } + } + + @Override + public Collection findConsentDecisions(final String principal) { + return waRestClient.getService(ConsentDecisionService.class).read(principal).getResult().stream(). + map(wcd -> toConsentDecision(waRestClient.getSyncopeClient().getDomain(), wcd)). + toList(); + } + + @Override + public Collection findConsentDecisions() { + return waRestClient.getService(ConsentDecisionService.class).list().getResult().stream(). + map(wcd -> toConsentDecision(waRestClient.getSyncopeClient().getDomain(), wcd)). + toList(); + } + + @Override + public ConsentDecision storeConsentDecision(final ConsentDecision decision) throws Throwable { + waRestClient.getService(ConsentDecisionService.class). + store(decision.getPrincipal(), toWAConsentDecision(decision)); + return decision; + } + + @Override + public boolean deleteConsentDecision(final long id, final String principal) throws Throwable { + waRestClient.getService(ConsentDecisionService.class).delete(principal, id); + return true; + } + + @Override + public boolean deleteConsentDecisions(final String principal) throws Throwable { + waRestClient.getService(ConsentDecisionService.class).delete(principal); + return true; + } + + @Override + public void deleteAll() throws Throwable { + waRestClient.getService(ConsentDecisionService.class).deleteAll(); + } +} diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java index f94f7d1da28..563a76f8b24 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthCredentialRepository.java @@ -39,16 +39,7 @@ public class WAGoogleMfaAuthCredentialRepository extends BaseGoogleAuthenticator protected static final Logger LOG = LoggerFactory.getLogger(WAGoogleMfaAuthTokenRepository.class); - protected final WARestClient waRestClient; - - public WAGoogleMfaAuthCredentialRepository( - final WARestClient waRestClient, final CasGoogleAuthenticator googleAuthenticator) { - - super(CipherExecutor.noOpOfStringToString(), CipherExecutor.noOpOfNumberToNumber(), googleAuthenticator); - this.waRestClient = waRestClient; - } - - protected GoogleAuthenticatorAccount mapGoogleMfaAuthAccount(final GoogleMfaAuthAccount gmfaa) { + protected static GoogleAuthenticatorAccount mapGoogleMfaAuthAccount(final GoogleMfaAuthAccount gmfaa) { return GoogleAuthenticatorAccount.builder(). id(gmfaa.getId()). name(gmfaa.getName()). @@ -61,6 +52,28 @@ protected GoogleAuthenticatorAccount mapGoogleMfaAuthAccount(final GoogleMfaAuth build(); } + protected static GoogleMfaAuthAccount mapOneTimeTokenAccount(final OneTimeTokenAccount otta) { + return new GoogleMfaAuthAccount.Builder(). + id(otta.getId()). + name(otta.getName()). + username(otta.getUsername()). + secretKey(otta.getSecretKey()). + validationCode(otta.getValidationCode()). + scratchCodes(otta.getScratchCodes().stream().map(Number::intValue).toList()). + registrationDate(ZonedDateTime.now()). + source(otta.getSource()). + build(); + } + + protected final WARestClient waRestClient; + + public WAGoogleMfaAuthCredentialRepository( + final WARestClient waRestClient, final CasGoogleAuthenticator googleAuthenticator) { + + super(CipherExecutor.noOpOfStringToString(), CipherExecutor.noOpOfNumberToNumber(), googleAuthenticator); + this.waRestClient = waRestClient; + } + @Override public OneTimeTokenAccount get(final long id) { try { @@ -81,7 +94,7 @@ public OneTimeTokenAccount get(final String username, final long id) { return waRestClient.getService(GoogleMfaAuthAccountService.class).read(username). getResult().stream(). filter(account -> account.getId() == id). - map(this::mapGoogleMfaAuthAccount). + map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount). findFirst(). orElse(null); } catch (SyncopeClientException e) { @@ -99,7 +112,7 @@ public Collection get(final String username) { try { return waRestClient.getService(GoogleMfaAuthAccountService.class).read(username). getResult().stream(). - map(this::mapGoogleMfaAuthAccount). + map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount). toList(); } catch (SyncopeClientException e) { if (e.getType() == ClientExceptionType.NotFound) { @@ -115,34 +128,19 @@ public Collection get(final String username) { public Collection load() { return waRestClient.getService(GoogleMfaAuthAccountService.class).list(). getResult().stream(). - map(this::mapGoogleMfaAuthAccount). + map(WAGoogleMfaAuthCredentialRepository::mapGoogleMfaAuthAccount). toList(); } - protected GoogleMfaAuthAccount mapOneTimeTokenAccount(final OneTimeTokenAccount otta) { - return new GoogleMfaAuthAccount.Builder(). - id(otta.getId()). - name(otta.getName()). - username(otta.getUsername()). - secretKey(otta.getSecretKey()). - validationCode(otta.getValidationCode()). - scratchCodes(otta.getScratchCodes().stream().map(Number::intValue).toList()). - registrationDate(ZonedDateTime.now()). - source(otta.getSource()). - build(); - } - @Override - public OneTimeTokenAccount save(final OneTimeTokenAccount otta) { - GoogleMfaAuthAccount account = mapOneTimeTokenAccount(otta); - waRestClient.getService(GoogleMfaAuthAccountService.class).create(account); - return otta; + public OneTimeTokenAccount save(final OneTimeTokenAccount tokenAccount) { + waRestClient.getService(GoogleMfaAuthAccountService.class).create(mapOneTimeTokenAccount(tokenAccount)); + return tokenAccount; } @Override public OneTimeTokenAccount update(final OneTimeTokenAccount tokenAccount) { - GoogleMfaAuthAccount acct = mapOneTimeTokenAccount(tokenAccount); - waRestClient.getService(GoogleMfaAuthAccountService.class).update(acct); + waRestClient.getService(GoogleMfaAuthAccountService.class).update(mapOneTimeTokenAccount(tokenAccount)); return tokenAccount; } diff --git a/wa/starter/src/main/resources/wa.properties b/wa/starter/src/main/resources/wa.properties index 8ab439b6cd1..6adbeefd1e4 100644 --- a/wa/starter/src/main/resources/wa.properties +++ b/wa/starter/src/main/resources/wa.properties @@ -37,7 +37,7 @@ spring.web.resources.static-locations=classpath:/thymeleaf/static,classpath:/syn cas.monitor.endpoints.endpoint.defaults.access=AUTHENTICATED management.endpoints.access.default=UNRESTRICTED -management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes +management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent management.endpoint.health.show-details=ALWAYS management.endpoint.env.show-values=WHEN_AUTHORIZED spring.cloud.discovery.client.health-indicator.enabled=false diff --git a/wa/starter/src/test/resources/debug/wa-debug.properties b/wa/starter/src/test/resources/debug/wa-debug.properties index 22a486ef8fd..35bd946735d 100644 --- a/wa/starter/src/test/resources/debug/wa-debug.properties +++ b/wa/starter/src/test/resources/debug/wa-debug.properties @@ -21,7 +21,7 @@ keymaster.address=https://localhost:9443/syncope/rest/keymaster keymaster.username=${anonymousUser} keymaster.password=${anonymousKey} -management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes +management.endpoints.web.exposure.include=info,health,env,beans,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes,attributeConsent cas.server.name=http://localhost:8080 cas.server.prefix=${cas.server.name}/syncope-wa From aaf093a70538a99b93ef41e184f22a22d3ecefe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Wed, 6 May 2026 14:37:34 +0200 Subject: [PATCH 2/2] Finally fixing unwanted calls to JpaBeans#newDataSource --- .../wa/starter/SyncopeWAApplication.java | 7 ++- .../syncope/wa/starter/config/WAContext.java | 48 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java index 5a438833b50..c9ba3f06fc1 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java @@ -24,6 +24,7 @@ import org.apache.syncope.wa.bootstrap.WARestClient; import org.apache.syncope.wa.starter.config.WARefreshContextJob; import org.apereo.cas.config.CasGoogleAuthenticatorLdapAutoConfiguration; +import org.apereo.cas.config.CasJdbcPasswordManagementAutoConfiguration; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.metadata.CasConfigurationPropertiesValidator; import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator; @@ -57,9 +58,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication(exclude = { - /* - * List of Spring Boot classes that we want to disable and remove from auto-configuration. - */ HibernateJpaAutoConfiguration.class, JerseyAutoConfiguration.class, GroovyTemplateAutoConfiguration.class, @@ -73,7 +71,8 @@ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, CassandraAutoConfiguration.class, - CasGoogleAuthenticatorLdapAutoConfiguration.class + CasGoogleAuthenticatorLdapAutoConfiguration.class, + CasJdbcPasswordManagementAutoConfiguration.class }) @EnableConfigurationProperties({ WAProperties.class, CasConfigurationProperties.class }) @EnableAsync(proxyTargetClass = false) diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java index 462ac330082..261779890ce 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java @@ -78,6 +78,7 @@ import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; +import org.apereo.cas.configuration.support.JpaBeans; import org.apereo.cas.consent.ConsentRepository; import org.apereo.cas.gauth.CasGoogleAuthenticator; import org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository; @@ -108,6 +109,8 @@ import org.apereo.cas.webauthn.storage.WebAuthnCredentialRepository; import org.ldaptive.ConnectionFactory; import org.pac4j.core.client.Client; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -118,17 +121,20 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.transaction.support.TransactionOperations; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.client.RestTemplate; @Configuration(proxyBeanMethods = false) public class WAContext { + protected static final Logger LOG = LoggerFactory.getLogger(WAContext.class); + public static final String CUSTOM_GOOGLE_AUTHENTICATOR_ACCOUNT_REGISTRY = "customGoogleAuthenticatorAccountRegistry"; @@ -425,25 +431,39 @@ public PasswordManagementService ldapPasswordChangeService( public PasswordManagementService jdbcPasswordChangeService( final CasConfigurationProperties casProperties, final ConfigurableApplicationContext ctx, - @Qualifier("jdbcPasswordManagementDataSource") - final DataSource jdbcPasswordManagementDataSource, - @Qualifier("jdbcPasswordManagementTransactionTemplate") - final TransactionOperations jdbcPasswordManagementTransactionTemplate, @Qualifier("passwordManagementCipherExecutor") final CipherExecutor passwordManagementCipherExecutor, @Qualifier(PasswordHistoryService.BEAN_NAME) final PasswordHistoryService passwordHistoryService) { PasswordManagementProperties pm = casProperties.getAuthn().getPm(); - if (pm.getCore().isEnabled() && StringUtils.isNotBlank(pm.getJdbc().getUrl())) { - PasswordEncoder encoder = PasswordEncoderUtils.newPasswordEncoder( - pm.getJdbc().getPasswordEncoder(), ctx); - return new JdbcPasswordManagementService( - passwordManagementCipherExecutor, - casProperties, - jdbcPasswordManagementDataSource, - jdbcPasswordManagementTransactionTemplate, - passwordHistoryService, encoder); + if (pm.getCore().isEnabled()) { + try { + Class.forName(pm.getJdbc().getDriverClass()); + + DataSource jdbcPasswordManagementDataSource = JpaBeans.newDataSource(pm.getJdbc()); + DataSourceTransactionManager jdbcPasswordManagementTransactionManager = + new DataSourceTransactionManager(jdbcPasswordManagementDataSource); + TransactionTemplate jdbcPasswordManagementTransactionTemplate = + new TransactionTemplate(jdbcPasswordManagementTransactionManager); + jdbcPasswordManagementTransactionTemplate.setIsolationLevelName( + pm.getJdbc().getIsolationLevelName()); + jdbcPasswordManagementTransactionTemplate.setPropagationBehaviorName( + pm.getJdbc().getPropagationBehaviorName()); + + PasswordEncoder encoder = PasswordEncoderUtils.newPasswordEncoder( + pm.getJdbc().getPasswordEncoder(), ctx); + + return new JdbcPasswordManagementService( + passwordManagementCipherExecutor, + casProperties, + jdbcPasswordManagementDataSource, + jdbcPasswordManagementTransactionTemplate, + passwordHistoryService, + encoder); + } catch (ClassNotFoundException e) { + LOG.debug("{} is not available, disabling jdbcPasswordChangeService", pm.getJdbc().getDriverClass(), e); + } } return new NoOpPasswordManagementService(passwordManagementCipherExecutor, casProperties);