This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push:
new 09e7e9d593 [SYNCOPE-1967] Support for CAS Attribute Release Consent
(#1378)
09e7e9d593 is described below
commit 09e7e9d5933c56917dc3d1ad38c4b1d226d5a7a2
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Wed May 6 16:06:54 2026 +0200
[SYNCOPE-1967] Support for CAS Attribute Release Consent (#1378)
---
.../authprofiles/AuthProfileDirectoryPanel.java | 80 +++++++-
.../AuthProfileItemDirectoryPanel.java | 14 +-
.../console/authprofiles/AuthProfilePanel.java | 5 +-
.../authprofiles/AuthProfileWizardBuilder.java | 54 ++++-
.../client/console/commons/AMConstants.java | 3 +
.../apache/syncope/client/console/pages/WA.java | 2 +-
.../client/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 +
...AuthProfileWizardBuilder$ConsentAttributes.html | 23 +++
.../syncope/client/enduser/pages/AuthProfile.java | 65 ++++++
.../client/enduser/rest/AuthProfileRestClient.java | 41 ++++
.../syncope/client/enduser/pages/AuthProfile.html | 52 ++++-
.../client/enduser/pages/AuthProfile.properties | 4 +
.../client/enduser/pages/AuthProfile_it.properties | 4 +
.../client/enduser/pages/AuthProfile_ja.properties | 4 +
.../enduser/pages/AuthProfile_pt_BR.properties | 4 +
.../client/enduser/pages/AuthProfile_ru.properties | 4 +
.../ui/commons/markup/html/form/AlertBehavior.java | 68 +++++++
.../wicket/markup/html/form/JsonEditorPanel.java | 4 -
.../syncope/common/lib/to/AuthProfileTO.java | 62 +++++-
.../syncope/common/lib/wa/WAConsentDecision.java | 226 +++++++++++++++++++++
.../api/service/wa/ConsentDecisionService.java | 84 ++++++++
.../apache/syncope/core/logic/AMLogicContext.java | 11 +
.../core/logic/wa/ConsentDecisionLogic.java | 112 ++++++++++
.../syncope/core/rest/cxf/AMRESTCXFContext.java | 11 +
.../cxf/service/wa/ConsentDecisionServiceImpl.java | 79 +++++++
.../persistence/api/entity/am/AuthProfile.java | 5 +
.../converters/WAConsentDecisionListConverter.java | 33 ++-
.../persistence/jpa/entity/am/JPAAuthProfile.java | 17 ++
.../neo4j/entity/am/Neo4jAuthProfile.java | 24 +++
.../java/data/AuthProfileDataBinderImpl.java | 21 +-
.../src/main/resources/wa-embedded.properties | 2 +-
.../reference-guide/concepts/authprofile.adoc | 28 +++
.../reference-guide/concepts/concepts.adoc | 2 +
wa/starter/pom.xml | 4 +
.../syncope/wa/starter/SyncopeWAApplication.java | 4 +-
.../syncope/wa/starter/config/WAContext.java | 56 +++--
.../wa/starter/consent/WAConsentRepository.java | 124 +++++++++++
.../gauth/WAGoogleMfaAuthCredentialRepository.java | 62 +++---
wa/starter/src/main/resources/wa.properties | 2 +-
.../src/test/resources/debug/wa-debug.properties | 2 +-
46 files changed, 1377 insertions(+), 101 deletions(-)
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 be7597329e..932b602757 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.ActionLink;
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<AuthProfileTO> 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 @@ public class AuthProfileDirectoryPanel
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<AuthProfileTO>
rowModel) {
+ return
CollectionUtils.isNotEmpty(rowModel.getObject().getConsentDecisions());
+ }
+ });
return columns;
}
@@ -180,7 +197,7 @@ public class AuthProfileDirectoryPanel
target.add(authProfileModal.setContent(new
ModalDirectoryPanel<>(
authProfileModal,
new
AuthProfileItemDirectoryPanel<ImpersonationAccount>(
- "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 class AuthProfileDirectoryPanel
target.add(authProfileModal.setContent(new
ModalDirectoryPanel<>(
authProfileModal,
new AuthProfileItemDirectoryPanel<GoogleMfaAuthToken>(
- "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 class AuthProfileDirectoryPanel
target.add(authProfileModal.setContent(new
ModalDirectoryPanel<>(
authProfileModal,
new
AuthProfileItemDirectoryPanel<GoogleMfaAuthAccount>(
- "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 class AuthProfileDirectoryPanel
target.add(authProfileModal.setContent(new
ModalDirectoryPanel<>(
authProfileModal,
new AuthProfileItemDirectoryPanel<MfaTrustedDevice>(
- "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 class AuthProfileDirectoryPanel
target.add(authProfileModal.setContent(new
ModalDirectoryPanel<>(
authProfileModal,
new
AuthProfileItemDirectoryPanel<WebAuthnDeviceCredential>(
- "panel", restClient, authProfileModal,
model.getObject(), pageRef) {
+ "panel", serviceOps, restClient,
authProfileModal, model.getObject(), null, pageRef) {
private static final long serialVersionUID =
6820212423488933184L;
@@ -415,6 +432,55 @@ public class AuthProfileDirectoryPanel
}
}, 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<WAConsentDecision>("panel", serviceOps,
+ restClient, authProfileModal,
model.getObject(), List.of("attributes"), pageRef) {
+
+ private static final long serialVersionUID =
-670769282358547044L;
+
+ @Override
+ protected List<WAConsentDecision> 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<IColumn<WAConsentDecision, String>>
getColumns() {
+ List<IColumn<WAConsentDecision, String>> 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 cfa57a04ea..f795d7a483 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.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.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<I
extends BaseBean>
public AuthProfileItemDirectoryPanel(
final String id,
+ final ServiceOps serviceOps,
final AuthProfileRestClient restClient,
final BaseModal<AuthProfileTO> authProfileModal,
final AuthProfileTO authProfile,
+ final List<String> excluded,
final PageReference pageRef) {
super(id, restClient, pageRef, false);
@@ -75,6 +78,8 @@ public abstract class AuthProfileItemDirectoryPanel<I extends
BaseBean>
enableUtilityButton();
setFooterVisibility(false);
+ addNewItemPanelBuilder(new AuthProfileItemWizardBuilder(excluded,
serviceOps, restClient, pageRef), false);
+
disableCheckBoxes();
initResultTable();
}
@@ -179,8 +184,13 @@ public abstract class AuthProfileItemDirectoryPanel<I
extends BaseBean>
private static final long serialVersionUID = -7174537333960225216L;
- protected AuthProfileItemWizardBuilder(final PageReference pageRef) {
- super(defaultItem(), new StepModel<>(), pageRef);
+ protected AuthProfileItemWizardBuilder(
+ final List<String> 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 b3f2607cfe..749f33f7e9 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.commons.lang3.StringUtils;
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 @@ public class AuthProfilePanel extends Panel {
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 41a5ff9958..44c53dc93e 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<T extends
BaseBean> extends BaseA
protected final StepModel<T> model;
- public AuthProfileWizardBuilder(final T defaultItem, final StepModel<T>
model, final PageReference pageRef) {
+ protected final List<String> excluded;
+
+ protected final ServiceOps serviceOps;
+
+ protected final AuthProfileRestClient authProfileRestClient;
+
+ public AuthProfileWizardBuilder(
+ final T defaultItem,
+ final StepModel<T> model,
+ final List<String> 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 @@ public abstract class AuthProfileWizardBuilder<T extends
BaseBean> extends BaseA
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 ae1e1347d9..2143e27046 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 df2a52a8e9..b16398bc17 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
@@ -265,7 +265,7 @@ public class WA extends BasePage {
@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 99f8cb18ce..6093a178a0 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 class AuthProfileRestClient extends BaseRestClient {
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 196e06fde4..d933a844d8 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 db0318cea8..a9e4e56144 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 e9378093ce..555584bc7d 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 4d3fd17e73..b0a43be7c2 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 9e1722ead7..fb3e964b47 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 028bda7565..9934e89a03 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 0000000000..aefa83b98b
--- /dev/null
+++
b/client/am/console/src/main/resources/org/apache/syncope/client/console/authprofiles/AuthProfileWizardBuilder$ConsentAttributes.html
@@ -0,0 +1,23 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org">
+ <wicket:panel>
+ <div wicket:id="content"/>
+ </wicket:panel>
+</html>
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 50a584d4f8..8f752e7fa3 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 java.util.stream.Collectors;
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 class AuthProfile extends BaseReauthPage {
webAuthnDeviceCredentials.setItemsPerPage(ROWS_PER_PAGE);
container.add(webAuthnDeviceCredentials.setOutputMarkupPlaceholderTag(true));
container.add(new
AjaxPagingNavigator("webAuthnDeviceCredentialsNavigator",
webAuthnDeviceCredentials));
+
+ DataView<WAConsentDecision> consentDecisions = new DataView<>(
+ "consentDecisions", new ListDataProvider<>(
+ authProfile == null ? List.of() :
authProfile.getConsentDecisions())) {
+
+ private static final long serialVersionUID = 6127875313385810666L;
+
+ @Override
+ public void populateItem(final Item<WAConsentDecision> 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<String> 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"),
+ "<pre>' + JSON.stringify(JSON.parse('"
+ + attributes.replace("'", "\'") + "'), null, 2) +
'</pre>")));
+ 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 2714954ff5..0f59438f1f 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 class AuthProfileRestClient extends BaseRestClient {
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 17b7ea9a84..16b6801cca 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 @@ under the License.
</div>
</div>
</div>
- </div> <div class="box-body">
+ </div>
+ <div class="box-body">
<div class="box-header formcard">
<header class="card-container bg-danger">
<label class="form-label card-header-style">
@@ -247,7 +248,54 @@ under the License.
</div>
</div>
</div>
- </div>
+ </div>
+ <div class="box-body">
+ <div class="box-header formcard">
+ <header class="card-container bg-danger">
+ <label class="form-label card-header-style">
+ <wicket:message key="consent.decisions.title"/>
+ </label>
+ </header>
+ <div class="card-container-body">
+ <div>
+ <div class="col-xs-12">
+ <div class="form-group">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th><wicket:message key="id"/></th>
+ <th><wicket:message key="service"/></th>
+ <th><wicket:message key="createdDate"/></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr wicket:id="consentDecisions">
+ <td><span wicket:id="id"/></td>
+ <td><span wicket:id="service"/></td>
+ <td><span wicket:id="createdDate"/></td>
+ <td style="width: 20px;">
+ <a href="#" wicket:id="consentAttributes"><i
class="fas fa-search-plus"></i></a>
+ </td>
+ <td style="width: 20px;">
+ <a href="#"
wicket:id="consentDecisionDelete"><i class="fas fa-trash"></i></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table class="paginator">
+ <tfoot>
+ <tr>
+ <td wicket:id="consentDecisionsNavigator"></td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</div>
</div>
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 5bf4119b60..ec839853d2 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 bb47fe6606..c1dca5b588 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 5bf4119b60..ec839853d2 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 5bf4119b60..ec839853d2 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 5bf4119b60..ec839853d2 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 0000000000..16f29c7ecc
--- /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 7efb694d90..460ed128aa 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<String> {
private final boolean readOnly;
- public JsonEditorPanel(final IModel<String> content) {
- this(null, content, false, null);
- }
-
public JsonEditorPanel(
final BaseModal<String> modal,
final IModel<String> 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 92c06999a4..2063e2ed71 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.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;
-public class AuthProfileTO implements EntityTO {
+public class AuthProfileTO implements NamedEntityTO {
private static final long serialVersionUID = -6543425997956703057L;
@@ -48,6 +50,23 @@ public class AuthProfileTO implements EntityTO {
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<ImpersonationAccount> 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 class AuthProfileTO implements EntityTO {
return this;
}
- public AuthProfileTO.Builder credential(final WebAuthnDeviceCredential
credential) {
+ public AuthProfileTO.Builder webAuthnDeviceCredential(final
WebAuthnDeviceCredential credential) {
instance.getWebAuthnDeviceCredentials().add(credential);
return this;
}
- public AuthProfileTO.Builder credentials(final
WebAuthnDeviceCredential... credentials) {
+ public AuthProfileTO.Builder webAuthnDeviceCredentials(final
WebAuthnDeviceCredential... credentials) {
instance.getWebAuthnDeviceCredentials().addAll(List.of(credentials));
return this;
}
- public AuthProfileTO.Builder credentials(final
Collection<WebAuthnDeviceCredential> credentials) {
+ public AuthProfileTO.Builder webAuthnDeviceCredentials(final
Collection<WebAuthnDeviceCredential> 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<WAConsentDecision> consentDecisions) {
+ instance.getConsentDecisions().addAll(consentDecisions);
+ return this;
+ }
+
public AuthProfileTO build() {
return instance;
}
@@ -127,6 +161,8 @@ public class AuthProfileTO implements EntityTO {
private final List<WebAuthnDeviceCredential> webAuthnDeviceCredentials =
new ArrayList<>();
+ private final List<WAConsentDecision> consentDecisions = new ArrayList<>();
+
@Override
public String getKey() {
return key;
@@ -146,6 +182,18 @@ public class AuthProfileTO implements EntityTO {
this.owner = owner;
}
+ @JsonIgnore
+ @Override
+ public String getName() {
+ return getOwner();
+ }
+
+ @JsonIgnore
+ @Override
+ public void setName(final String name) {
+ throw new UnsupportedOperationException();
+ }
+
public List<ImpersonationAccount> getImpersonationAccounts() {
return impersonationAccounts;
}
@@ -166,6 +214,10 @@ public class AuthProfileTO implements EntityTO {
return webAuthnDeviceCredentials;
}
+ public List<WAConsentDecision> getConsentDecisions() {
+ return consentDecisions;
+ }
+
@Override
public int hashCode() {
return new HashCodeBuilder().
@@ -176,6 +228,7 @@ public class AuthProfileTO implements EntityTO {
append(googleMfaAuthAccounts).
append(mfaTrustedDevices).
append(webAuthnDeviceCredentials).
+ append(consentDecisions).
build();
}
@@ -199,6 +252,7 @@ public class AuthProfileTO implements EntityTO {
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 0000000000..6f33c07b14
--- /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 0000000000..96c3b45716
--- /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<WAConsentDecision> read(@NotNull @PathParam("owner") String
owner);
+
+ @GET
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ PagedResult<WAConsentDecision> 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 7cbf075742..f14808b37a 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 @@ package org.apache.syncope.core.logic;
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 class AMLogicContext {
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 0000000000..082c1a17c7
--- /dev/null
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ConsentDecisionLogic.java
@@ -0,0 +1,112 @@
+/*
+ * 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<WAConsentDecision> criteria) {
+ if (profile.getConsentDecisions().removeIf(criteria)) {
+ 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.getConsentDecisions().clear();
+ authProfileDAO.save(profile);
+ });
+ }
+
+ @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void deleteAll() {
+ authProfileDAO.findAll(Pageable.unpaged()).forEach(profile -> {
+ profile.getConsentDecisions().clear();
+ authProfileDAO.save(profile);
+ });
+ }
+
+ @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void store(final String owner, final WAConsentDecision
contentDecision) {
+ AuthProfile profile = authProfile(owner);
+
+ profile.getConsentDecisions().removeIf(cd -> cd.getId() ==
contentDecision.getId());
+ profile.add(contentDecision);
+
+ 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<WAConsentDecision> list() {
+ return authProfileDAO.findAll(Pageable.unpaged()).stream().
+ map(AuthProfile::getConsentDecisions).
+ flatMap(List::stream).
+ toList();
+ }
+
+ @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ @Transactional(readOnly = true)
+ public List<WAConsentDecision> 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 157f836306..99fa06d712 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.OIDCOpEntityService;
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.OIDCOpEntityLogic;
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.OIDCOpEntityServiceImpl;
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 class AMRESTCXFContext {
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 0000000000..3ecffb5b39
--- /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<WAConsentDecision> build(final List<WAConsentDecision>
read) {
+ PagedResult<WAConsentDecision> result = new PagedResult<>();
+ result.setPage(1);
+ result.setSize(read.size());
+ result.setTotalCount(read.size());
+ result.getResult().addAll(read);
+ return result;
+ }
+
+ @Override
+ public PagedResult<WAConsentDecision> read(final String owner) {
+ return build(logic.read(owner));
+ }
+
+ @Override
+ public PagedResult<WAConsentDecision> 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 f79e3ab8ea..c19eed5e7e 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.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.syncope.core.persistence.api.entity.Entity;
@@ -51,4 +52,8 @@ public interface AuthProfile extends Entity {
boolean add(ImpersonationAccount impersonationAccount);
List<ImpersonationAccount> getImpersonationAccounts();
+
+ boolean add(WAConsentDecision consentDecision);
+
+ List<WAConsentDecision> getConsentDecisions();
}
diff --git
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
similarity index 50%
copy from
client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
copy to
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
index 2714954ff5..f13aad72a3 100644
---
a/client/am/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AuthProfileRestClient.java
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/converters/WAConsentDecisionListConverter.java
@@ -16,29 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.syncope.client.enduser.rest;
+package org.apache.syncope.core.persistence.jpa.converters;
-import org.apache.syncope.common.lib.to.AuthProfileTO;
-import org.apache.syncope.common.rest.api.service.AuthProfileSelfService;
+import jakarta.persistence.Converter;
+import java.util.List;
+import org.apache.syncope.common.lib.wa.WAConsentDecision;
+import tools.jackson.core.type.TypeReference;
-public class AuthProfileRestClient extends BaseRestClient {
+@Converter
+public class WAConsentDecisionListConverter extends
SerializableListConverter<WAConsentDecision> {
- private static final long serialVersionUID = 4139153766778113329L;
+ protected static final TypeReference<List<WAConsentDecision>> TYPEREF =
+ new TypeReference<List<WAConsentDecision>>() {
+ };
- public AuthProfileTO read() {
- try {
- return getService(AuthProfileSelfService.class).read();
- } catch (Exception e) {
- LOG.debug("While attempting to read the auth profile", e);
- return null;
- }
- }
-
- public void update(final AuthProfileTO authProfile) {
- getService(AuthProfileSelfService.class).update(authProfile);
- }
-
- public void delete() {
- getService(AuthProfileSelfService.class).delete();
+ @Override
+ protected TypeReference<List<WAConsentDecision>> typeRef() {
+ return TYPEREF;
}
}
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 993b485f22..b869723272 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
@@ -30,12 +30,14 @@ 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.syncope.core.persistence.api.entity.am.AuthProfile;
import
org.apache.syncope.core.persistence.jpa.converters.GoogleMfaAuthAccountListConverter;
import
org.apache.syncope.core.persistence.jpa.converters.GoogleMfaAuthTokenListConverter;
import
org.apache.syncope.core.persistence.jpa.converters.ImpersonationAccountListConverter;
import
org.apache.syncope.core.persistence.jpa.converters.MfaTrustedDeviceListConverter;
+import
org.apache.syncope.core.persistence.jpa.converters.WAConsentDecisionListConverter;
import
org.apache.syncope.core.persistence.jpa.converters.WebAuthnDeviceCredentialListConverter;
import
org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
@@ -71,6 +73,10 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
@Lob
private List<WebAuthnDeviceCredential> webAuthnDeviceCredentials = new
ArrayList<>();
+ @Convert(converter = WAConsentDecisionListConverter.class)
+ @Lob
+ private List<WAConsentDecision> waConsentDecisions = new ArrayList<>();
+
@Override
public String getOwner() {
return owner;
@@ -135,4 +141,15 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
public List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials() {
return webAuthnDeviceCredentials;
}
+
+ @Override
+ public boolean add(final WAConsentDecision consentDecision) {
+ return !waConsentDecisions.contains(consentDecision)
+ && waConsentDecisions.add(consentDecision);
+ }
+
+ @Override
+ public List<WAConsentDecision> getConsentDecisions() {
+ return waConsentDecisions;
+ }
}
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 8e85783728..3acc813781 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
@@ -26,6 +26,7 @@ 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.syncope.core.persistence.api.entity.am.AuthProfile;
import
org.apache.syncope.core.persistence.neo4j.entity.AbstractGeneratedKeyNode;
@@ -62,6 +63,10 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
new TypeReference<List<WebAuthnDeviceCredential>>() {
};
+ protected static final TypeReference<List<WAConsentDecision>>
WA_CONSENT_DECISION_TYPEREF =
+ new TypeReference<List<WAConsentDecision>>() {
+ };
+
@NotNull
private String owner;
@@ -90,6 +95,11 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
@Transient
private List<WebAuthnDeviceCredential> webAuthnDeviceCredentialsList = new
ArrayList<>();
+ private String waConsentDecisions;
+
+ @Transient
+ private List<WAConsentDecision> waConsentDecisionsList = new ArrayList<>();
+
@Override
public String getOwner() {
return owner;
@@ -150,6 +160,16 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
return webAuthnDeviceCredentialsList;
}
+ @Override
+ public boolean add(final WAConsentDecision consentDecision) {
+ return waConsentDecisionsList.contains(consentDecision);
+ }
+
+ @Override
+ public List<WAConsentDecision> getConsentDecisions() {
+ return waConsentDecisionsList;
+ }
+
protected void json2list(final boolean clearFirst) {
if (clearFirst) {
getGoogleMfaAuthTokens().clear();
@@ -157,6 +177,7 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
getMfaTrustedDevices().clear();
getImpersonationAccounts().clear();
getWebAuthnDeviceCredentials().clear();
+ getConsentDecisions().clear();
}
Optional.ofNullable(googleMfaAuthTokens).ifPresent(v ->
getGoogleMfaAuthTokens().
addAll(POJOHelper.deserialize(v, GOOGLE_MFA_TOKENS_TYPEREF)));
@@ -168,6 +189,8 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
addAll(POJOHelper.deserialize(v, IMPERSONATION_TYPEREF)));
Optional.ofNullable(webAuthnDeviceCredentials).ifPresent(v ->
getWebAuthnDeviceCredentials().
addAll(POJOHelper.deserialize(v, WEBAUTHN_TYPEREF)));
+ Optional.ofNullable(waConsentDecisions).ifPresent(v ->
getConsentDecisions().
+ addAll(POJOHelper.deserialize(v,
WA_CONSENT_DECISION_TYPEREF)));
}
@PostLoad
@@ -185,5 +208,6 @@ public class Neo4jAuthProfile extends
AbstractGeneratedKeyNode implements AuthPr
mfaTrustedDevices = POJOHelper.serialize(getMfaTrustedDevices());
impersonationAccounts =
POJOHelper.serialize(getImpersonationAccounts());
webAuthnDeviceCredentials =
POJOHelper.serialize(getWebAuthnDeviceCredentials());
+ waConsentDecisions = POJOHelper.serialize(getConsentDecisions());
}
}
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 38cf317e30..00b7b48036 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 class AuthProfileDataBinderImpl implements
AuthProfileDataBinder {
@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
@@ -63,6 +64,8 @@ public class AuthProfileDataBinderImpl implements
AuthProfileDataBinder {
authProfileTO.getMfaTrustedDevices().forEach(authProfile::add);
authProfile.getWebAuthnDeviceCredentials().clear();
authProfileTO.getWebAuthnDeviceCredentials().forEach(authProfile::add);
+ authProfile.getConsentDecisions().clear();
+ authProfileTO.getConsentDecisions().forEach(authProfile::add);
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 8f9abcecf5..40d6658489 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/src/main/asciidoc/reference-guide/concepts/authprofile.adoc
b/src/main/asciidoc/reference-guide/concepts/authprofile.adoc
new file mode 100644
index 0000000000..d7e230e841
--- /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 <<client-applications,web applications>> integrated
with <<web-access,WA>>, the following
+information is tracked for <<admin-console,administration>> and
<<enduser-application,self-management>>:
+
+*
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 b2e15b3422..449335f66b 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 70c6244bec..79c47989b4 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -254,6 +254,10 @@ under the License.
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-consent-webflow</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-consent-api</artifactId>
+ </dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-aup-webflow</artifactId>
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 9f7232767b..809ba33071 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.WAProperties;
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;
@@ -52,7 +53,8 @@ import
org.springframework.transaction.annotation.EnableTransactionManagement;
DataSourceHealthContributorAutoConfiguration.class,
DataJdbcRepositoriesAutoConfiguration.class,
JmxAutoConfiguration.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 6aba695972..4452c96ea8 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.bootstrap.mapping.AttrReleaseMapper;
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,8 @@ import
org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
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;
import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService;
@@ -106,6 +109,8 @@ import
org.apereo.cas.util.spring.CasApplicationReadyListener;
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;
@@ -116,16 +121,20 @@ import
org.springframework.context.ConfigurableApplicationContext;
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";
@@ -422,24 +431,39 @@ public class WAContext {
public PasswordManagementService jdbcPasswordChangeService(
final CasConfigurationProperties casProperties,
final ConfigurableApplicationContext ctx,
- @Qualifier("jdbcPasswordManagementDataSource")
- final DataSource jdbcPasswordManagementDataSource,
- @Qualifier("jdbcPasswordManagementTransactionTemplate")
- final TransactionOperations
jdbcPasswordManagementTransactionTemplate,
@Qualifier("passwordManagementCipherExecutor")
final CipherExecutor<Serializable, String>
passwordManagementCipherExecutor,
@Qualifier(PasswordHistoryService.BEAN_NAME)
final PasswordHistoryService passwordHistoryService) {
PasswordManagementProperties pm = casProperties.getAuthn().getPm();
- if (pm.getCore().isEnabled() &&
StringUtils.isNotBlank(pm.getJdbc().getUrl())) {
- return new JdbcPasswordManagementService(
- passwordManagementCipherExecutor,
- casProperties,
- jdbcPasswordManagementDataSource,
- jdbcPasswordManagementTransactionTemplate,
- passwordHistoryService,
-
PasswordEncoderUtils.newPasswordEncoder(pm.getJdbc().getPasswordEncoder(),
ctx));
+ 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);
@@ -541,6 +565,12 @@ public class WAContext {
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 0000000000..0e891e76b2
--- /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<? extends ConsentDecision> findConsentDecisions(final
String principal) {
+ return
waRestClient.getService(ConsentDecisionService.class).read(principal).getResult().stream().
+ map(wcd ->
toConsentDecision(waRestClient.getSyncopeClient().getDomain(), wcd)).
+ toList();
+ }
+
+ @Override
+ public Collection<? extends ConsentDecision> 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 f94f7d1da2..563a76f8b2 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 @@ public class WAGoogleMfaAuthCredentialRepository extends
BaseGoogleAuthenticator
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 class WAGoogleMfaAuthCredentialRepository extends
BaseGoogleAuthenticator
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 class WAGoogleMfaAuthCredentialRepository extends
BaseGoogleAuthenticator
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 class WAGoogleMfaAuthCredentialRepository extends
BaseGoogleAuthenticator
public Collection<? extends OneTimeTokenAccount> 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 8ab439b6cd..6adbeefd1e 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 22a486ef8f..35bd946735 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