This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch 3_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/3_0_X by this push:
new 93d17132bf [SYNCOPE-1772] Adding MfaTrustedDevice to AuhtProfile (#502)
93d17132bf is described below
commit 93d17132bf9e3b52000aa346962e927e98c40786
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Fri Aug 4 20:36:34 2023 +0200
[SYNCOPE-1772] Adding MfaTrustedDevice to AuhtProfile (#502)
---
.../authprofiles/AuthProfileDirectoryPanel.java | 61 ++++++++
.../client/console/commons/AMConstants.java | 6 +-
.../AuthProfileDirectoryPanel.properties | 5 +
.../AuthProfileDirectoryPanel_fr_CA.properties | 5 +
.../AuthProfileDirectoryPanel_it.properties | 5 +
.../AuthProfileDirectoryPanel_ja.properties | 5 +
.../AuthProfileDirectoryPanel_pt_BR.properties | 5 +
.../AuthProfileDirectoryPanel_ru.properties | 5 +
.../apache/syncope/client/ui/commons/DateOps.java | 48 +++++-
.../syncope/client/console/panels/BeanPanel.java | 6 +-
.../client/console/rest/ReportRestClient.java | 2 +-
.../client/console/rest/TaskRestClient.java | 2 +-
.../repeater/data/table/DatePropertyColumn.java | 3 +
.../console/wizards/DelegationWizardBuilder.java | 4 +-
.../syncope/common/lib/to/AuthProfileTO.java | 26 ++++
.../syncope/common/lib/wa/MfaTrustedDevice.java | 136 +++++++++++++++++
.../rest/api/beans/MfaTrustedDeviceQuery.java | 128 ++++++++++++++++
.../api/service/wa/MfaTrustStorageService.java | 62 ++++++++
.../apache/syncope/core/logic/AMLogicContext.java | 11 ++
...strationLogic.java => MfaTrusStorageLogic.java} | 116 ++++++++-------
.../core/logic/wa/U2FRegistrationLogic.java | 5 +-
.../syncope/core/rest/cxf/AMRESTCXFContext.java | 9 ++
.../cxf/service/wa/MfaTrustStorageServiceImpl.java | 61 ++++++++
.../persistence/api/entity/am/AuthProfile.java | 5 +
.../persistence/jpa/entity/am/JPAAuthProfile.java | 23 ++-
.../java/data/AuthProfileDataBinderImpl.java | 2 +
.../console/panels/SCIMConfGeneralPanel.java | 4 +-
.../org/apache/syncope/fit/AbstractITCase.java | 4 +
.../syncope/fit/core/wa/MfaTrustStorageTCase.java | 92 ++++++++++++
pom.xml | 2 +-
wa/starter/pom.xml | 8 +
.../syncope/wa/starter/config/WAContext.java | 21 +++
.../WAMultifactorAuthenticationTrustStorage.java | 165 +++++++++++++++++++++
.../WAGoogleMfaAuthTokenRepositoryTest.java | 2 +-
34 files changed, 968 insertions(+), 76 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 80f2dfc9ae..f8f86b7034 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
@@ -45,6 +45,7 @@ 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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
import org.apache.wicket.PageReference;
@@ -157,6 +158,15 @@ public class AuthProfileDirectoryPanel
return
!rowModel.getObject().getU2FRegisteredDevices().isEmpty();
}
});
+ columns.add(new BooleanConditionColumn<>(new
StringResourceModel("mfaTrustedDevices")) {
+
+ private static final long serialVersionUID = -8236820422411536323L;
+
+ @Override
+ protected boolean isCondition(final IModel<AuthProfileTO>
rowModel) {
+ return !rowModel.getObject().getMfaTrustedDevices().isEmpty();
+ }
+ });
columns.add(new BooleanConditionColumn<>(new
StringResourceModel("webAuthnAccount")) {
private static final long serialVersionUID = -8236820422411536323L;
@@ -368,6 +378,57 @@ public class AuthProfileDirectoryPanel
}
}, ActionLink.ActionType.FO_EDIT, 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<MfaTrustedDevice>(
+ "panel", restClient, authProfileModal,
model.getObject(), pageRef) {
+
+ private static final long serialVersionUID =
5788448799796630011L;
+
+ @Override
+ protected List<MfaTrustedDevice> getItems() {
+ return model.getObject().getMfaTrustedDevices();
+ }
+
+ @Override
+ protected MfaTrustedDevice defaultItem() {
+ return new MfaTrustedDevice();
+ }
+
+ @Override
+ protected String sortProperty() {
+ return "id";
+ }
+
+ @Override
+ protected String paginatorRowsKey() {
+ return
AMConstants.PREF_AUTHPROFILE_MFA_TRUSTED_FDEVICES_PAGINATOR_ROWS;
+ }
+
+ @Override
+ protected List<IColumn<MfaTrustedDevice, String>>
getColumns() {
+ List<IColumn<MfaTrustedDevice, String>> columns = new
ArrayList<>();
+ columns.add(new PropertyColumn<>(new
ResourceModel("id"), "id", "id"));
+ columns.add(new PropertyColumn<>(new
ResourceModel("name"), "name", "name"));
+ columns.add(new DatePropertyColumn<>(
+ new ResourceModel("recordDate"), "recordDate",
"recordDate"));
+ columns.add(new DatePropertyColumn<>(
+ new ResourceModel("expirationDate"),
"expirationDate", "expirationDate"));
+ return columns;
+ }
+ }, pageRef)));
+ authProfileModal.header(new
Model<>(getString("mfaTrustedDevices", model)));
+ authProfileModal.show(true);
+ }
+ }, ActionLink.ActionType.DOWN, 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/commons/AMConstants.java
b/client/am/console/src/main/java/org/apache/syncope/client/console/commons/AMConstants.java
index c185b55d1a..08c9663fb5 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
@@ -48,7 +48,11 @@ public final class AMConstants {
public static final String
PREF_AUTHPROFILE_GOOGLEMFAAUTHACCOUNTS_PAGINATOR_ROWS =
"authprofile.googlemfaauthaccounts.paginator.rows";
- public static final String PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS =
"authprofile.u2fdevices.paginator.rows";
+ public static final String PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS =
+ "authprofile.u2fdevices.paginator.rows";
+
+ public static final String
PREF_AUTHPROFILE_MFA_TRUSTED_FDEVICES_PAGINATOR_ROWS =
+ "authprofile.mfaTrustedDevices.paginator.rows";
public static final String
PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS =
"authprofile.webAuthnDeviceCredentials.paginator.rows";
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 e095d91b2b..56627ae0a4 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
@@ -44,3 +44,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
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 8a42e53c73..495d278dbc 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
@@ -44,3 +44,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
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 0fd02d5f98..cc0d9a5ce7 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
@@ -44,3 +44,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=Dispositivi Credenziali WebAuthn
+expirationDate=Scadenza
+recordDate=Memorizzazione
+mfaTrustedDevices=Dispositivi MFA
+down.title=dispositivi mfa
+down.class=fas fa-barcode
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 d2fc5697f7..f3d1ac236c 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
@@ -44,3 +44,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
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 8d919ed952..e227e6f900 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
@@ -44,3 +44,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
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 b638d451d5..a60b6d6825 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
@@ -45,3 +45,8 @@ json=JSON
html.class=fas fa-at
html.title=webauthn
webAuthnDeviceCredentials=WebAuthn Device Credentials
+expirationDate=Expiration Date
+recordDate=Record Date
+mfaTrustedDevices=MFA Devices
+down.title=mfa devices
+down.class=fas fa-barcode
diff --git
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
index f68187b4cb..17216b409b 100644
---
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
+++
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/DateOps.java
@@ -20,7 +20,9 @@ package org.apache.syncope.client.ui.commons;
import java.io.Serializable;
import java.time.OffsetDateTime;
+import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
@@ -46,39 +48,71 @@ public final class DateOps {
public String format(final OffsetDateTime date) {
return Optional.ofNullable(date).map(v ->
fdf.format(convert(date))).orElse(StringUtils.EMPTY);
}
+
+ public String format(final ZonedDateTime date) {
+ return Optional.ofNullable(date).map(v ->
fdf.format(convert(date))).orElse(StringUtils.EMPTY);
+ }
}
- public static class WrappedDateModel implements IModel<Date>, Serializable
{
+ public static final class WrappedDateModel implements IModel<Date>,
Serializable {
private static final long serialVersionUID = 31027882183172L;
- private final IModel<OffsetDateTime> wrapped;
+ public static WrappedDateModel ofOffset(final IModel<OffsetDateTime>
offset) {
+ WrappedDateModel instance = new WrappedDateModel();
+ instance.offset = offset;
+ return instance;
+ }
+
+ public static WrappedDateModel ofZoned(final IModel<ZonedDateTime>
zoned) {
+ WrappedDateModel instance = new WrappedDateModel();
+ instance.zoned = zoned;
+ return instance;
+ }
+
+ private IModel<OffsetDateTime> offset;
- public WrappedDateModel(final IModel<OffsetDateTime> wrapped) {
- this.wrapped = wrapped;
+ private IModel<ZonedDateTime> zoned;
+
+ private WrappedDateModel() {
+ // private constructor for static utility class
}
@Override
public Date getObject() {
- return convert(wrapped.getObject());
+ return offset == null ? convert(zoned.getObject()) :
convert(offset.getObject());
}
@Override
public void setObject(final Date object) {
- wrapped.setObject(convert(object));
+ if (offset == null) {
+ zoned.setObject(toZonedDateTime(object));
+ } else {
+ offset.setObject(toOffsetDateTime(object));
+ }
}
}
public static final ZoneOffset DEFAULT_OFFSET =
OffsetDateTime.now().getOffset();
+ public static final ZoneId DEFAULT_ZONE = ZonedDateTime.now().getZone();
+
public static Date convert(final OffsetDateTime date) {
return Optional.ofNullable(date).map(v -> new
Date(v.toInstant().toEpochMilli())).orElse(null);
}
- public static OffsetDateTime convert(final Date date) {
+ public static Date convert(final ZonedDateTime date) {
+ return Optional.ofNullable(date).map(v -> new
Date(v.toInstant().toEpochMilli())).orElse(null);
+ }
+
+ public static OffsetDateTime toOffsetDateTime(final Date date) {
return Optional.ofNullable(date).map(v ->
v.toInstant().atOffset(DEFAULT_OFFSET)).orElse(null);
}
+ public static ZonedDateTime toZonedDateTime(final Date date) {
+ return Optional.ofNullable(date).map(v ->
ZonedDateTime.ofInstant(v.toInstant(), DEFAULT_ZONE)).orElse(null);
+ }
+
private DateOps() {
// private constructor for static utility class
}
diff --git
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
index 7ee7b5e73b..aef0071628 100644
---
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
+++
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/BeanPanel.java
@@ -27,6 +27,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.time.Duration;
import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -301,7 +302,10 @@ public class BeanPanel<T extends Serializable> extends
Panel {
panel = new AjaxDateTimeFieldPanel(id, fieldName, model,
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
} else if (OffsetDateTime.class.equals(type)) {
- panel = new AjaxDateTimeFieldPanel(id, fieldName, new
DateOps.WrappedDateModel(model),
+ panel = new AjaxDateTimeFieldPanel(id, fieldName,
DateOps.WrappedDateModel.ofOffset(model),
+
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
+ } else if (ZonedDateTime.class.equals(type)) {
+ panel = new AjaxDateTimeFieldPanel(id, fieldName,
DateOps.WrappedDateModel.ofZoned(model),
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
} else if (type.isEnum()) {
panel = new AjaxDropDownChoicePanel(id, fieldName, model).
diff --git
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
index cb2a03c886..3e8d480569 100644
---
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
+++
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
@@ -85,7 +85,7 @@ public class ReportRestClient extends BaseRestClient
implements ExecutionRestCli
@Override
public void startExecution(final String reportKey, final Date startAt) {
getService(ReportService.class).execute(new
ExecSpecs.Builder().key(reportKey).
- startAt(DateOps.convert(startAt)).build());
+ startAt(DateOps.toOffsetDateTime(startAt)).build());
}
@Override
diff --git
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
index e4b6dc67f3..1a9b50014b 100644
---
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
+++
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
@@ -203,7 +203,7 @@ public class TaskRestClient extends BaseRestClient
implements ExecutionRestClien
public void startExecution(final String taskKey, final Date startAt, final
boolean dryRun) {
getService(TaskService.class).execute(new
ExecSpecs.Builder().key(taskKey).
- startAt(DateOps.convert(startAt)).dryRun(dryRun).build());
+
startAt(DateOps.toOffsetDateTime(startAt)).dryRun(dryRun).build());
}
@Override
diff --git
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
index 83c1e68cbd..bdc0674ffe 100644
---
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
+++
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/DatePropertyColumn.java
@@ -19,6 +19,7 @@
package
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table;
import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
import java.util.Date;
import org.apache.syncope.client.console.SyncopeConsoleSession;
import
org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
@@ -49,6 +50,8 @@ public class DatePropertyColumn<T> extends PropertyColumn<T,
String> {
String convertedDate = "";
if (date.getObject() instanceof OffsetDateTime) {
convertedDate =
SyncopeConsoleSession.get().getDateFormat().format((OffsetDateTime)
date.getObject());
+ } else if (date.getObject() instanceof ZonedDateTime) {
+ convertedDate =
SyncopeConsoleSession.get().getDateFormat().format((ZonedDateTime)
date.getObject());
} else if (date.getObject() instanceof Date) {
convertedDate =
SyncopeConsoleSession.get().getDateFormat().format((Date) date.getObject());
}
diff --git
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
index c284bfba6f..675f2ecf42 100644
---
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
+++
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/DelegationWizardBuilder.java
@@ -151,14 +151,14 @@ public class DelegationWizardBuilder extends
BaseAjaxWizardBuilder<DelegationTO>
add(new AjaxDateTimeFieldPanel(
"start",
"start",
- new DateOps.WrappedDateModel(new
PropertyModel<>(modelObject, "start")),
+ DateOps.WrappedDateModel.ofOffset(new
PropertyModel<>(modelObject, "start")),
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT).
addRequiredLabel());
add(new AjaxDateTimeFieldPanel(
"end",
"end",
- new DateOps.WrappedDateModel(new
PropertyModel<>(modelObject, "end")),
+ DateOps.WrappedDateModel.ofOffset(new
PropertyModel<>(modelObject, "end")),
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT));
}
}
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 8904f04f3c..365699bd34 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
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
@@ -95,6 +96,21 @@ public class AuthProfileTO implements EntityTO {
return this;
}
+ public AuthProfileTO.Builder mfaTrustedDevice(final MfaTrustedDevice
device) {
+ instance.getMfaTrustedDevices().add(device);
+ return this;
+ }
+
+ public AuthProfileTO.Builder mfaTrustedDevices(final
MfaTrustedDevice... devices) {
+ instance.getMfaTrustedDevices().addAll(List.of(devices));
+ return this;
+ }
+
+ public AuthProfileTO.Builder mfaTrustedDevices(final
Collection<MfaTrustedDevice> devices) {
+ instance.getMfaTrustedDevices().addAll(devices);
+ return this;
+ }
+
public AuthProfileTO.Builder credential(final WebAuthnDeviceCredential
credential) {
instance.getWebAuthnDeviceCredentials().add(credential);
return this;
@@ -127,6 +143,8 @@ public class AuthProfileTO implements EntityTO {
private final List<U2FDevice> u2fRegisteredDevices = new ArrayList<>();
+ private final List<MfaTrustedDevice> mfaTrustedDevices = new ArrayList<>();
+
private final List<WebAuthnDeviceCredential> webAuthnDeviceCredentials =
new ArrayList<>();
@Override
@@ -172,6 +190,12 @@ public class AuthProfileTO implements EntityTO {
return u2fRegisteredDevices;
}
+ @JacksonXmlElementWrapper(localName = "mfaTrustedDevices")
+ @JacksonXmlProperty(localName = "mfaTrustedDevice")
+ public List<MfaTrustedDevice> getMfaTrustedDevices() {
+ return mfaTrustedDevices;
+ }
+
@JacksonXmlElementWrapper(localName = "credentials")
@JacksonXmlProperty(localName = "credential")
public List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials() {
@@ -187,6 +211,7 @@ public class AuthProfileTO implements EntityTO {
append(googleMfaAuthTokens).
append(googleMfaAuthAccounts).
append(u2fRegisteredDevices).
+ append(mfaTrustedDevices).
append(webAuthnDeviceCredentials).
build();
}
@@ -210,6 +235,7 @@ public class AuthProfileTO implements EntityTO {
append(googleMfaAuthTokens, other.googleMfaAuthTokens).
append(googleMfaAuthAccounts, other.googleMfaAuthAccounts).
append(u2fRegisteredDevices, other.u2fRegisteredDevices).
+ append(mfaTrustedDevices, other.mfaTrustedDevices).
append(webAuthnDeviceCredentials,
other.webAuthnDeviceCredentials).
build();
}
diff --git
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
new file mode 100644
index 0000000000..b53efdd2e0
--- /dev/null
+++
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/MfaTrustedDevice.java
@@ -0,0 +1,136 @@
+/*
+ * 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.ZonedDateTime;
+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 MfaTrustedDevice implements BaseBean {
+
+ private static final long serialVersionUID = 5120423450725182470L;
+
+ private long id;
+
+ private String name;
+
+ private String deviceFingerprint;
+
+ private String recordKey;
+
+ private ZonedDateTime recordDate;
+
+ private ZonedDateTime expirationDate;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(final long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getDeviceFingerprint() {
+ return deviceFingerprint;
+ }
+
+ public void setDeviceFingerprint(final String deviceFingerprint) {
+ this.deviceFingerprint = deviceFingerprint;
+ }
+
+ public ZonedDateTime getRecordDate() {
+ return recordDate;
+ }
+
+ public void setRecordDate(final ZonedDateTime recordDate) {
+ this.recordDate = recordDate;
+ }
+
+ public String getRecordKey() {
+ return recordKey;
+ }
+
+ public void setRecordKey(final String recordKey) {
+ this.recordKey = recordKey;
+ }
+
+ public ZonedDateTime getExpirationDate() {
+ return expirationDate;
+ }
+
+ public void setExpirationDate(final ZonedDateTime expirationDate) {
+ this.expirationDate = expirationDate;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder()
+ .append(id)
+ .append(name)
+ .append(deviceFingerprint)
+ .append(recordDate)
+ .append(recordKey)
+ .append(expirationDate)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (obj.getClass() != getClass()) {
+ return false;
+ }
+ MfaTrustedDevice other = (MfaTrustedDevice) obj;
+ return new EqualsBuilder()
+ .append(this.id, other.id)
+ .append(this.name, other.name)
+ .append(this.deviceFingerprint, other.deviceFingerprint)
+ .append(this.recordDate, other.recordDate)
+ .append(this.recordKey, other.recordKey)
+ .append(this.expirationDate, other.expirationDate)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("id", id)
+ .append("name", name)
+ .append("deviceFingerprint", deviceFingerprint)
+ .append("recordDate", recordDate)
+ .append("recordKey", recordKey)
+ .append("expirationDate", expirationDate)
+ .toString();
+ }
+}
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
new file mode 100644
index 0000000000..7a8bcf2042
--- /dev/null
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/MfaTrustedDeviceQuery.java
@@ -0,0 +1,128 @@
+/*
+ * 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.beans;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.OffsetDateTime;
+import javax.ws.rs.QueryParam;
+
+public class MfaTrustedDeviceQuery extends AbstractQuery {
+
+ private static final long serialVersionUID = -7381828286332101171L;
+
+ public static class Builder extends
AbstractQuery.Builder<MfaTrustedDeviceQuery, MfaTrustedDeviceQuery.Builder> {
+
+ @Override
+ protected MfaTrustedDeviceQuery newInstance() {
+ return new MfaTrustedDeviceQuery();
+ }
+
+ public MfaTrustedDeviceQuery.Builder id(final Long id) {
+ getInstance().setId(id);
+ return this;
+ }
+
+ public MfaTrustedDeviceQuery.Builder recordKey(final String recordKey)
{
+ getInstance().setRecordKey(recordKey);
+ return this;
+ }
+
+ public MfaTrustedDeviceQuery.Builder principal(final String principal)
{
+ getInstance().setPrincipal(principal);
+ return this;
+ }
+
+ public MfaTrustedDeviceQuery.Builder expirationDate(final
OffsetDateTime date) {
+ getInstance().setExpirationDate(date);
+ return this;
+ }
+
+ public MfaTrustedDeviceQuery.Builder recordDate(final OffsetDateTime
date) {
+ getInstance().setRecordDate(date);
+ return this;
+ }
+ }
+
+ private Long id;
+
+ private String recordKey;
+
+ private OffsetDateTime expirationDate;
+
+ private OffsetDateTime recordDate;
+
+ private String principal;
+
+ @Parameter(name = "id", in = ParameterIn.QUERY, schema =
+ @Schema(implementation = Long.class))
+ public Long getId() {
+ return id;
+ }
+
+ @QueryParam("id")
+ public void setId(final Long id) {
+ this.id = id;
+ }
+
+ @Parameter(name = "recordKey", in = ParameterIn.QUERY, schema =
+ @Schema(implementation = String.class))
+ public String getRecordKey() {
+ return recordKey;
+ }
+
+ @QueryParam("recordKey")
+ public void setRecordKey(final String recordKey) {
+ this.recordKey = recordKey;
+ }
+
+ @Parameter(name = "expirationDate", in = ParameterIn.QUERY, schema =
+ @Schema(implementation = OffsetDateTime.class))
+ public OffsetDateTime getExpirationDate() {
+ return expirationDate;
+ }
+
+ @QueryParam("expirationDate")
+ public void setExpirationDate(final OffsetDateTime expirationDate) {
+ this.expirationDate = expirationDate;
+ }
+
+ @Parameter(name = "recordDate", in = ParameterIn.QUERY, schema =
+ @Schema(implementation = OffsetDateTime.class))
+ public OffsetDateTime getRecordDate() {
+ return recordDate;
+ }
+
+ @QueryParam("recordDate")
+ public void setRecordDate(final OffsetDateTime recordDate) {
+ this.recordDate = recordDate;
+ }
+
+ @Parameter(name = "principal", in = ParameterIn.QUERY, schema =
+ @Schema(implementation = String.class))
+ public String getPrincipal() {
+ return principal;
+ }
+
+ @QueryParam("principal")
+ public void setPrincipal(final String principal) {
+ this.principal = principal;
+ }
+}
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
new file mode 100644
index 0000000000..872e7733d4
--- /dev/null
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/MfaTrustStorageService.java
@@ -0,0 +1,62 @@
+/*
+ * 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 javax.validation.constraints.NotNull;
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+@Tag(name = "WA")
+@SecurityRequirements({
+ @SecurityRequirement(name = "BasicAuthentication"),
+ @SecurityRequirement(name = "Bearer") })
+@Path("wa/mfaTrustedDevice")
+public interface MfaTrustStorageService extends JAXRSService {
+
+ @GET
+ @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ PagedResult<MfaTrustedDevice> search(@BeanParam MfaTrustedDeviceQuery
query);
+
+ @POST
+ @Path("{principal}")
+ @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ void create(@NotNull @PathParam("principal") String principal, @NotNull
MfaTrustedDevice device);
+
+ @DELETE
+ @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
+ void delete(@BeanParam MfaTrustedDeviceQuery query);
+}
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 5535b72538..8a365cc522 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
@@ -23,6 +23,7 @@ import org.apache.syncope.core.logic.init.AMEntitlementLoader;
import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
import org.apache.syncope.core.logic.wa.ImpersonationLogic;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
import org.apache.syncope.core.logic.wa.U2FRegistrationLogic;
import org.apache.syncope.core.logic.wa.WAClientAppLogic;
import org.apache.syncope.core.logic.wa.WAConfigLogic;
@@ -189,6 +190,16 @@ public class AMLogicContext {
return new U2FRegistrationLogic(entityFactory, authProfileDAO,
authProfileDataBinder);
}
+ @ConditionalOnMissingBean
+ @Bean
+ public MfaTrusStorageLogic mfaTrusStorageLogic(
+ final AuthProfileDAO authProfileDAO,
+ final AuthProfileDataBinder authProfileDataBinder,
+ final EntityFactory entityFactory) {
+
+ return new MfaTrusStorageLogic(entityFactory, authProfileDAO,
authProfileDataBinder);
+ }
+
@ConditionalOnMissingBean
@Bean
public WAClientAppLogic waClientAppLogic(
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
similarity index 66%
copy from
core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
copy to
core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
index c12d81b8ba..eb1a110c27 100644
---
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java
@@ -19,15 +19,17 @@
package org.apache.syncope.core.logic.wa;
import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
-import org.apache.syncope.common.lib.wa.U2FDevice;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
import org.apache.syncope.core.logic.AbstractAuthProfileLogic;
import org.apache.syncope.core.persistence.api.dao.AuthProfileDAO;
import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
@@ -36,11 +38,11 @@ import
org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
import org.springframework.security.access.prepost.PreAuthorize;
-public class U2FRegistrationLogic extends AbstractAuthProfileLogic {
+public class MfaTrusStorageLogic extends AbstractAuthProfileLogic {
protected final EntityFactory entityFactory;
- public U2FRegistrationLogic(
+ public MfaTrusStorageLogic(
final EntityFactory entityFactory,
final AuthProfileDAO authProfileDAO,
final AuthProfileDataBinder binder) {
@@ -50,60 +52,29 @@ public class U2FRegistrationLogic extends
AbstractAuthProfileLogic {
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
- public void create(final String owner, final U2FDevice device) {
- AuthProfile profile = authProfileDAO.findByOwner(owner).orElseGet(()
-> {
- AuthProfile authProfile =
entityFactory.newEntity(AuthProfile.class);
- authProfile.setOwner(owner);
- return authProfile;
- });
-
- List<U2FDevice> devices = profile.getU2FRegisteredDevices();
- devices.add(device);
- profile.setU2FRegisteredDevices(devices);
- authProfileDAO.save(profile);
- }
-
- @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
- public void delete(final Long id, final OffsetDateTime expirationDate) {
- List<AuthProfile> profiles = authProfileDAO.findAll(-1, -1);
- profiles.forEach(profile -> {
- List<U2FDevice> devices = profile.getU2FRegisteredDevices();
- if (devices != null) {
- if (id != null) {
- devices.removeIf(device -> device.getId() == id);
- } else if (expirationDate != null) {
- devices.removeIf(device ->
device.getIssueDate().compareTo(expirationDate) < 0);
- } else {
- devices = List.of();
- }
- profile.setU2FRegisteredDevices(devices);
- authProfileDAO.save(profile);
- }
- });
- }
-
- @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
- public Pair<Integer, List<U2FDevice>> search(
+ public Pair<Integer, List<MfaTrustedDevice>> search(
final Integer page,
- final Integer itemsPerPage, final Long id,
- final OffsetDateTime expirationDate,
+ final Integer itemsPerPage,
+ final String principal,
+ final Long id,
+ final OffsetDateTime recordDate,
final List<OrderByClause> orderByClauses) {
- List<Comparator<U2FDevice>> comparatorList = orderByClauses.
+ List<Comparator<MfaTrustedDevice>> comparatorList = orderByClauses.
stream().
map(orderByClause -> {
- Comparator<U2FDevice> comparator = null;
+ Comparator<MfaTrustedDevice> comparator = null;
if (orderByClause.getField().equals("id")) {
comparator = (o1, o2) -> new CompareToBuilder().
append(o1.getId(), o2.getId()).toComparison();
}
- if (orderByClause.getField().equals("issueDate")) {
+ if (orderByClause.getField().equals("expirationDate")) {
comparator = (o1, o2) -> new CompareToBuilder().
- append(o1.getIssueDate(),
o2.getIssueDate()).toComparison();
+ append(o1.getExpirationDate(),
o2.getExpirationDate()).toComparison();
}
- if (orderByClause.getField().equals("record")) {
+ if (orderByClause.getField().equals("recordDate")) {
comparator = (o1, o2) -> new CompareToBuilder().
- append(o1.getRecord(),
o2.getRecord()).toComparison();
+ append(o1.getRecordDate(),
o2.getRecordDate()).toComparison();
}
if (comparator != null) {
if (orderByClause.getDirection() ==
OrderByClause.Direction.DESC) {
@@ -116,30 +87,32 @@ public class U2FRegistrationLogic extends
AbstractAuthProfileLogic {
filter(Objects::nonNull).
collect(Collectors.toList());
- List<U2FDevice> devices = authProfileDAO.findAll(-1, -1).
- stream().
- map(AuthProfile::getU2FRegisteredDevices).
- filter(Objects::nonNull).
- flatMap(List::stream).
+ List<MfaTrustedDevice> devices = (principal == null
+ ? authProfileDAO.findAll(-1, -1).stream().
+
map(AuthProfile::getMfaTrustedDevices).filter(Objects::nonNull).flatMap(List::stream)
+ : authProfileDAO.findByOwner(principal).
+
map(AuthProfile::getMfaTrustedDevices).filter(Objects::nonNull).map(List::stream).
+ orElse(Stream.empty())).
filter(device -> {
EqualsBuilder builder = new EqualsBuilder();
+
builder.appendSuper(device.getExpirationDate().isAfter(ZonedDateTime.now()));
if (id != null) {
builder.append(id, (Long) device.getId());
}
- if (expirationDate != null) {
-
builder.appendSuper(device.getIssueDate().compareTo(expirationDate) >= 0);
+ if (recordDate != null) {
+
builder.appendSuper(device.getRecordDate().isAfter(recordDate.toZonedDateTime()));
}
- return true;
+ return builder.build();
}).
filter(Objects::nonNull).
collect(Collectors.toList());
- List<U2FDevice> result = devices.stream().
+ List<MfaTrustedDevice> result = devices.stream().
limit(itemsPerPage).
skip(itemsPerPage * (page <= 0 ? 0L : page.longValue() - 1L)).
sorted((o1, o2) -> {
int compare;
- for (Comparator<U2FDevice> comparator : comparatorList) {
+ for (Comparator<MfaTrustedDevice> comparator :
comparatorList) {
compare = comparator.compare(o1, o2);
if (compare != 0) {
return compare;
@@ -150,4 +123,37 @@ public class U2FRegistrationLogic extends
AbstractAuthProfileLogic {
.collect(Collectors.toList());
return Pair.of(devices.size(), result);
}
+
+ @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void create(final String owner, final MfaTrustedDevice device) {
+ AuthProfile profile = authProfileDAO.findByOwner(owner).orElseGet(()
-> {
+ AuthProfile authProfile =
entityFactory.newEntity(AuthProfile.class);
+ authProfile.setOwner(owner);
+ return authProfile;
+ });
+
+ List<MfaTrustedDevice> devices = profile.getMfaTrustedDevices();
+ devices.add(device);
+ profile.setMfaTrustedDevices(devices);
+ authProfileDAO.save(profile);
+ }
+
+ @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final OffsetDateTime expirationDate, final String
recordKey) {
+ List<AuthProfile> profiles = authProfileDAO.findAll(-1, -1);
+ profiles.forEach(profile -> {
+ List<MfaTrustedDevice> devices = profile.getMfaTrustedDevices();
+ if (devices != null) {
+ if (recordKey != null) {
+ devices.removeIf(device ->
recordKey.equals(device.getRecordKey()));
+ } else if (expirationDate != null) {
+ devices.removeIf(device ->
device.getExpirationDate().isBefore(expirationDate.toZonedDateTime()));
+ } else {
+ devices = List.of();
+ }
+ profile.setMfaTrustedDevices(devices);
+ authProfileDAO.save(profile);
+ }
+ });
+ }
}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
index c12d81b8ba..31f7828832 100644
---
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/U2FRegistrationLogic.java
@@ -85,7 +85,8 @@ public class U2FRegistrationLogic extends
AbstractAuthProfileLogic {
@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
public Pair<Integer, List<U2FDevice>> search(
final Integer page,
- final Integer itemsPerPage, final Long id,
+ final Integer itemsPerPage,
+ final Long id,
final OffsetDateTime expirationDate,
final List<OrderByClause> orderByClauses) {
@@ -129,7 +130,7 @@ public class U2FRegistrationLogic extends
AbstractAuthProfileLogic {
if (expirationDate != null) {
builder.appendSuper(device.getIssueDate().compareTo(expirationDate) >= 0);
}
- return true;
+ return builder.build();
}).
filter(Objects::nonNull).
collect(Collectors.toList());
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 06fd5faadf..81942ea9ec 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
@@ -29,6 +29,7 @@ import
org.apache.syncope.common.rest.api.service.SRARouteService;
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;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
import org.apache.syncope.common.rest.api.service.wa.WAClientAppService;
import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
@@ -44,6 +45,7 @@ import org.apache.syncope.core.logic.SRARouteLogic;
import org.apache.syncope.core.logic.wa.GoogleMfaAuthAccountLogic;
import org.apache.syncope.core.logic.wa.GoogleMfaAuthTokenLogic;
import org.apache.syncope.core.logic.wa.ImpersonationLogic;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
import org.apache.syncope.core.logic.wa.U2FRegistrationLogic;
import org.apache.syncope.core.logic.wa.WAClientAppLogic;
import org.apache.syncope.core.logic.wa.WAConfigLogic;
@@ -59,6 +61,7 @@ import
org.apache.syncope.core.rest.cxf.service.SRARouteServiceImpl;
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;
+import org.apache.syncope.core.rest.cxf.service.wa.MfaTrustStorageServiceImpl;
import org.apache.syncope.core.rest.cxf.service.wa.U2FRegistrationServiceImpl;
import org.apache.syncope.core.rest.cxf.service.wa.WAClientAppServiceImpl;
import org.apache.syncope.core.rest.cxf.service.wa.WAConfigServiceImpl;
@@ -146,6 +149,12 @@ public class AMRESTCXFContext {
return new U2FRegistrationServiceImpl(u2fRegistrationLogic);
}
+ @ConditionalOnMissingBean
+ @Bean
+ public MfaTrustStorageService mfaTrustStorageService(final
MfaTrusStorageLogic mfaTrusStorageLogic) {
+ return new MfaTrustStorageServiceImpl(mfaTrusStorageLogic);
+ }
+
@ConditionalOnMissingBean
@Bean
public WAClientAppService waClientAppService(final WAClientAppLogic
waClientAppLogic) {
diff --git
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
new file mode 100644
index 0000000000..92e732d1aa
--- /dev/null
+++
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/MfaTrustStorageServiceImpl.java
@@ -0,0 +1,61 @@
+/*
+ * 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.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
+import org.apache.syncope.core.logic.wa.MfaTrusStorageLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MfaTrustStorageServiceImpl extends AbstractService implements
MfaTrustStorageService {
+
+ protected final MfaTrusStorageLogic logic;
+
+ public MfaTrustStorageServiceImpl(final MfaTrusStorageLogic logic) {
+ this.logic = logic;
+ }
+
+ @Override
+ public PagedResult<MfaTrustedDevice> search(final MfaTrustedDeviceQuery
query) {
+ Pair<Integer, List<MfaTrustedDevice>> result = logic.search(
+ query.getPage(),
+ query.getSize(),
+ query.getPrincipal(),
+ query.getId(),
+ query.getRecordDate(),
+ getOrderByClauses(query.getOrderBy()));
+ return buildPagedResult(result.getRight(), query.getPage(),
query.getSize(), result.getLeft());
+ }
+
+ @Override
+ public void create(final String owner, final MfaTrustedDevice device) {
+ logic.create(owner, device);
+ }
+
+ @Override
+ public void delete(final MfaTrustedDeviceQuery query) {
+ logic.delete(query.getExpirationDate(), query.getRecordKey());
+ }
+}
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 8abfefc15d..2bf4554de6 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
@@ -22,6 +22,7 @@ import java.util.List;
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
import org.apache.syncope.core.persistence.api.entity.Entity;
@@ -44,6 +45,10 @@ public interface AuthProfile extends Entity {
void setGoogleMfaAuthAccounts(List<GoogleMfaAuthAccount> accounts);
+ List<MfaTrustedDevice> getMfaTrustedDevices();
+
+ void setMfaTrustedDevices(List<MfaTrustedDevice> records);
+
List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials();
void setWebAuthnDeviceCredentials(List<WebAuthnDeviceCredential>
credentials);
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 e8349e11ac..ab8b88aecd 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,6 +30,7 @@ import javax.persistence.UniqueConstraint;
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
import org.apache.syncope.core.persistence.api.entity.am.AuthProfile;
@@ -56,6 +57,10 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
protected static final TypeReference<List<U2FDevice>> U2F_TYPEREF = new
TypeReference<List<U2FDevice>>() {
};
+ protected static final TypeReference<List<MfaTrustedDevice>>
MFA_TRUSTED_DEVICE_TYPEREF =
+ new TypeReference<List<MfaTrustedDevice>>() {
+ };
+
protected static final TypeReference<List<ImpersonationAccount>>
IMPERSONATION_TYPEREF =
new TypeReference<List<ImpersonationAccount>>() {
};
@@ -79,6 +84,9 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
@Lob
private String u2fRegisteredDevices;
+ @Lob
+ private String mfaTrustedDevices;
+
@Lob
private String webAuthnDeviceCredentials;
@@ -121,8 +129,19 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
}
@Override
- public void setU2FRegisteredDevices(final List<U2FDevice> records) {
- u2fRegisteredDevices = POJOHelper.serialize(records);
+ public void setU2FRegisteredDevices(final List<U2FDevice> devices) {
+ u2fRegisteredDevices = POJOHelper.serialize(devices);
+ }
+
+ @Override
+ public List<MfaTrustedDevice> getMfaTrustedDevices() {
+ return Optional.ofNullable(mfaTrustedDevices).
+ map(v -> POJOHelper.deserialize(v,
MFA_TRUSTED_DEVICE_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
+ }
+
+ @Override
+ public void setMfaTrustedDevices(final List<MfaTrustedDevice> devices) {
+ mfaTrustedDevices = POJOHelper.serialize(devices);
}
@Override
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 79722e4ebe..584a020931 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
@@ -40,6 +40,7 @@ public class AuthProfileDataBinderImpl implements
AuthProfileDataBinder {
authProfileTO.getGoogleMfaAuthTokens().addAll(authProfile.getGoogleMfaAuthTokens());
authProfileTO.getGoogleMfaAuthAccounts().addAll(authProfile.getGoogleMfaAuthAccounts());
authProfileTO.getU2FRegisteredDevices().addAll(authProfile.getU2FRegisteredDevices());
+
authProfileTO.getMfaTrustedDevices().addAll(authProfile.getMfaTrustedDevices());
authProfileTO.getWebAuthnDeviceCredentials().addAll(authProfile.getWebAuthnDeviceCredentials());
return authProfileTO;
}
@@ -57,6 +58,7 @@ public class AuthProfileDataBinderImpl implements
AuthProfileDataBinder {
authProfile.setGoogleMfaAuthTokens(authProfileTO.getGoogleMfaAuthTokens());
authProfile.setGoogleMfaAuthAccounts(authProfileTO.getGoogleMfaAuthAccounts());
authProfile.setU2FRegisteredDevices(authProfileTO.getU2FRegisteredDevices());
+ authProfile.setMfaTrustedDevices(authProfileTO.getMfaTrustedDevices());
authProfile.setWebAuthnDeviceCredentials(authProfileTO.getWebAuthnDeviceCredentials());
return authProfile;
}
diff --git
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
index 00a76404d1..257d225501 100644
---
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
+++
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
@@ -50,7 +50,7 @@ public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
@Override
public void setObject(final Date object) {
-
scimGeneralConf.setCreationDate(DateOps.convert(object));
+
scimGeneralConf.setCreationDate(DateOps.toOffsetDateTime(object));
}
},
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
creationDatePanel.setEnabled(false);
@@ -67,7 +67,7 @@ public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
@Override
public void setObject(final Date object) {
-
scimGeneralConf.setLastChangeDate(DateOps.convert(object));
+
scimGeneralConf.setLastChangeDate(DateOps.toOffsetDateTime(object));
}
},
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
lastChangeDatePanel.setEnabled(false);
diff --git
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 14ebc41dc2..970603a60f 100644
---
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -159,6 +159,7 @@ import
org.apache.syncope.common.rest.api.service.UserWorkflowTaskService;
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;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
import org.apache.syncope.common.rest.api.service.wa.U2FRegistrationService;
import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
import
org.apache.syncope.common.rest.api.service.wa.WebAuthnRegistrationService;
@@ -385,6 +386,8 @@ public abstract class AbstractITCase {
protected static U2FRegistrationService U2F_REGISTRATION_SERVICE;
+ protected static MfaTrustStorageService MFA_TRUST_STORAGE_SERVICE;
+
protected static WebAuthnRegistrationService WEBAUTHN_REGISTRATION_SERVICE;
protected static ImpersonationService IMPERSONATION_SERVICE;
@@ -418,6 +421,7 @@ public abstract class AbstractITCase {
GOOGLE_MFA_AUTH_TOKEN_SERVICE =
ANONYMOUS_CLIENT.getService(GoogleMfaAuthTokenService.class);
GOOGLE_MFA_AUTH_ACCOUNT_SERVICE =
ANONYMOUS_CLIENT.getService(GoogleMfaAuthAccountService.class);
U2F_REGISTRATION_SERVICE =
ANONYMOUS_CLIENT.getService(U2FRegistrationService.class);
+ MFA_TRUST_STORAGE_SERVICE =
ANONYMOUS_CLIENT.getService(MfaTrustStorageService.class);
WEBAUTHN_REGISTRATION_SERVICE =
ANONYMOUS_CLIENT.getService(WebAuthnRegistrationService.class);
IMPERSONATION_SERVICE =
ANONYMOUS_CLIENT.getService(ImpersonationService.class);
diff --git
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
new file mode 100644
index 0000000000..1a1e488ce9
--- /dev/null
+++
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/MfaTrustStorageTCase.java
@@ -0,0 +1,92 @@
+/*
+ * 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.fit.core.wa;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.UUID;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class MfaTrustStorageTCase extends AbstractITCase {
+
+ private static MfaTrustedDevice createDeviceRegistration() {
+ MfaTrustedDevice device = new MfaTrustedDevice();
+ device.setId(System.currentTimeMillis());
+ device.setDeviceFingerprint(UUID.randomUUID().toString());
+ device.setName(UUID.randomUUID().toString());
+ device.setRecordKey(UUID.randomUUID().toString());
+ device.setRecordDate(ZonedDateTime.now());
+ device.setExpirationDate(ZonedDateTime.now().plusDays(30));
+ return device;
+ }
+
+ @BeforeEach
+ public void setup() {
+ MFA_TRUST_STORAGE_SERVICE.delete(new
MfaTrustedDeviceQuery.Builder().build());
+ }
+
+ @Test
+ public void create() {
+ assertDoesNotThrow(() -> MFA_TRUST_STORAGE_SERVICE.create(
+ UUID.randomUUID().toString(), createDeviceRegistration()));
+ }
+
+ @Test
+ public void count() {
+ String owner = UUID.randomUUID().toString();
+ MfaTrustedDevice device = createDeviceRegistration();
+ MFA_TRUST_STORAGE_SERVICE.create(owner, device);
+
+ List<MfaTrustedDevice> devices = MFA_TRUST_STORAGE_SERVICE.search(new
MfaTrustedDeviceQuery.Builder().
+ principal(owner).build()).getResult();
+ assertEquals(1, devices.size());
+
+ MFA_TRUST_STORAGE_SERVICE.delete(new
MfaTrustedDeviceQuery.Builder().recordKey(device.getRecordKey()).build());
+
+ devices = MFA_TRUST_STORAGE_SERVICE.search(new
MfaTrustedDeviceQuery.Builder().build()).getResult();
+ assertTrue(devices.isEmpty());
+ }
+
+ @Test
+ public void delete() {
+ MfaTrustedDevice device = createDeviceRegistration();
+ String owner = UUID.randomUUID().toString();
+ MFA_TRUST_STORAGE_SERVICE.create(owner, device);
+
+ MFA_TRUST_STORAGE_SERVICE.delete(new
MfaTrustedDeviceQuery.Builder().recordKey(device.getRecordKey()).build());
+ assertTrue(MFA_TRUST_STORAGE_SERVICE.search(
+ new
MfaTrustedDeviceQuery.Builder().id(device.getId()).build()).getResult().isEmpty());
+
+ OffsetDateTime date = OffsetDateTime.now().plusDays(1);
+
+ MFA_TRUST_STORAGE_SERVICE.delete(new
MfaTrustedDeviceQuery.Builder().expirationDate(date).build());
+
+ assertTrue(MFA_TRUST_STORAGE_SERVICE.search(
+ new
MfaTrustedDeviceQuery.Builder().id(device.getId()).build()).getResult().isEmpty());
+ }
+}
diff --git a/pom.xml b/pom.xml
index 85743eb133..64fccb14b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1486,7 +1486,7 @@ under the License.
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
- <version>5.0.0</version>
+ <version>6.0.0</version>
<executions>
<execution>
<goals>
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 494494a7be..02a490dd2d 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -294,6 +294,14 @@ under the License.
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-otp-mfa-core</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-trusted-mfa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-trusted-mfa-core</artifactId>
+ </dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-oidc-services</artifactId>
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 122597d506..9451b94c4d 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
@@ -26,6 +26,7 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
+import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -57,6 +58,7 @@ import
org.apache.syncope.wa.starter.mapping.RemoteEndpointAccessMapper;
import org.apache.syncope.wa.starter.mapping.SAML2SPClientAppTOMapper;
import org.apache.syncope.wa.starter.mapping.TicketExpirationMapper;
import org.apache.syncope.wa.starter.mapping.TimeBasedAccessMapper;
+import
org.apache.syncope.wa.starter.mfa.WAMultifactorAuthenticationTrustStorage;
import org.apache.syncope.wa.starter.oidc.WAOIDCJWKSGeneratorService;
import org.apache.syncope.wa.starter.pac4j.saml.WASAML2ClientCustomizer;
import org.apache.syncope.wa.starter.saml.idp.WASamlIdPCasEventListener;
@@ -88,6 +90,8 @@ import
org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerat
import
org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
import
org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecordKeyGenerator;
+import
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustStorage;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.util.LdapUtils;
import org.apereo.cas.util.crypto.CipherExecutor;
@@ -325,6 +329,23 @@ public class WAContext {
return new WAGoogleMfaAuthCredentialRepository(waRestClient,
googleAuthenticatorInstance);
}
+ @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
+ @Bean(name = MultifactorAuthenticationTrustStorage.BEAN_NAME)
+ public MultifactorAuthenticationTrustStorage mfaTrustStorage(
+ final CasConfigurationProperties casProperties,
+ @Qualifier("mfaTrustRecordKeyGenerator")
+ final MultifactorAuthenticationTrustRecordKeyGenerator
keyGenerationStrategy,
+ @Qualifier("mfaTrustCipherExecutor")
+ final CipherExecutor<Serializable, String> mfaTrustCipherExecutor,
+ final WARestClient waRestClient) {
+
+ return new WAMultifactorAuthenticationTrustStorage(
+ casProperties.getAuthn().getMfa().getTrusted(),
+ mfaTrustCipherExecutor,
+ keyGenerationStrategy,
+ waRestClient);
+ }
+
@Bean
public OidcJsonWebKeystoreGeneratorService
oidcJsonWebKeystoreGeneratorService(
final CasConfigurationProperties casProperties,
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
new file mode 100644
index 0000000000..084b6e084b
--- /dev/null
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mfa/WAMultifactorAuthenticationTrustStorage.java
@@ -0,0 +1,165 @@
+/*
+ * 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.mfa;
+
+import java.io.Serializable;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.wa.MfaTrustedDevice;
+import org.apache.syncope.common.rest.api.beans.MfaTrustedDeviceQuery;
+import org.apache.syncope.common.rest.api.service.wa.MfaTrustStorageService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apache.syncope.wa.starter.services.WAServiceRegistry;
+import
org.apereo.cas.configuration.model.support.mfa.trusteddevice.TrustedDevicesMultifactorProperties;
+import
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecord;
+import
org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecordKeyGenerator;
+import
org.apereo.cas.trusted.authentication.storage.BaseMultifactorAuthenticationTrustStorage;
+import org.apereo.cas.util.crypto.CipherExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WAMultifactorAuthenticationTrustStorage extends
BaseMultifactorAuthenticationTrustStorage {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(WAServiceRegistry.class);
+
+ protected static final int PAGE_SIZE = 500;
+
+ protected final WARestClient waRestClient;
+
+ public WAMultifactorAuthenticationTrustStorage(
+ final TrustedDevicesMultifactorProperties
trustedDevicesMultifactorProperties,
+ final CipherExecutor<Serializable, String> cipherExecutor,
+ final MultifactorAuthenticationTrustRecordKeyGenerator
keyGenerationStrategy,
+ final WARestClient waRestClient) {
+
+ super(trustedDevicesMultifactorProperties, cipherExecutor,
keyGenerationStrategy);
+ this.waRestClient = waRestClient;
+ }
+
+ @Override
+ protected MultifactorAuthenticationTrustRecord saveInternal(final
MultifactorAuthenticationTrustRecord record) {
+ MfaTrustedDevice device = new MfaTrustedDevice();
+ device.setRecordKey(record.getRecordKey());
+ device.setId(record.getId());
+ device.setName(record.getName());
+ device.setDeviceFingerprint(record.getDeviceFingerprint());
+ Optional.ofNullable(record.getExpirationDate()).
+ ifPresent(date ->
device.setExpirationDate(date.toInstant().atZone(ZoneId.systemDefault())));
+ device.setRecordDate(record.getRecordDate());
+
+ LOG.trace("Saving multifactor authentication trust record [{}]",
device);
+
+
waRestClient.getService(MfaTrustStorageService.class).create(record.getPrincipal(),
device);
+
+ return record;
+ }
+
+ @Override
+ public void remove(final ZonedDateTime expirationDate) {
+ waRestClient.getService(MfaTrustStorageService.class).delete(
+ new
MfaTrustedDeviceQuery.Builder().expirationDate(expirationDate.toOffsetDateTime()).build());
+ }
+
+ @Override
+ public void remove(final String recordKey) {
+ waRestClient.getService(MfaTrustStorageService.class).delete(
+ new
MfaTrustedDeviceQuery.Builder().recordKey(recordKey).build());
+ }
+
+ protected MultifactorAuthenticationTrustRecord translate(final
MfaTrustedDevice device) {
+ MultifactorAuthenticationTrustRecord record = new
MultifactorAuthenticationTrustRecord();
+ record.setRecordKey(device.getRecordKey());
+ record.setId(device.getId());
+ record.setName(device.getName());
+ record.setDeviceFingerprint(device.getDeviceFingerprint());
+ Optional.ofNullable(device.getExpirationDate()).
+ ifPresent(date ->
record.setExpirationDate(Date.from(date.toInstant())));
+ record.setRecordDate(device.getRecordDate());
+ return record;
+ }
+
+ @Override
+ public Set<? extends MultifactorAuthenticationTrustRecord> getAll() {
+ if (!waRestClient.isReady()) {
+ LOG.debug("Syncope client is not yet ready to fetch MFA trusted
device records");
+ return Set.of();
+ }
+
+ int count = waRestClient.getService(MfaTrustStorageService.class).
+ search(new
MfaTrustedDeviceQuery.Builder().page(1).size(0).build()).getTotalCount();
+
+ Set<MultifactorAuthenticationTrustRecord> result = new HashSet<>();
+
+ for (int page = 1; page <= (count / PAGE_SIZE) + 1; page++) {
+ waRestClient.getService(MfaTrustStorageService.class).
+ search(new
MfaTrustedDeviceQuery.Builder().page(page).size(PAGE_SIZE).
+ orderBy("expirationDate").build()).
+ getResult().stream().
+ map(this::translate).
+ forEach(result::add);
+ }
+
+ return result;
+ }
+
+ @Override
+ public Set<? extends MultifactorAuthenticationTrustRecord> get(final
ZonedDateTime onOrAfterDate) {
+ if (!waRestClient.isReady()) {
+ LOG.debug("Syncope client is not yet ready to fetch MFA trusted
device records");
+ return Set.of();
+ }
+
+ return waRestClient.getService(MfaTrustStorageService.class).
+ search(new
MfaTrustedDeviceQuery.Builder().recordDate(onOrAfterDate.toOffsetDateTime()).build()).
+ getResult().stream().
+ map(this::translate).
+ collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<? extends MultifactorAuthenticationTrustRecord> get(final
String principal) {
+ if (!waRestClient.isReady()) {
+ LOG.debug("Syncope client is not yet ready to fetch MFA trusted
device records");
+ return Set.of();
+ }
+
+ return waRestClient.getService(MfaTrustStorageService.class).
+ search(new
MfaTrustedDeviceQuery.Builder().principal(principal).build()).getResult().stream().
+ map(this::translate).
+ collect(Collectors.toSet());
+ }
+
+ @Override
+ public MultifactorAuthenticationTrustRecord get(final long id) {
+ if (!waRestClient.isReady()) {
+ LOG.debug("Syncope client is not yet ready to fetch MFA trusted
device records");
+ return null;
+ }
+
+ return waRestClient.getService(MfaTrustStorageService.class).
+ search(new
MfaTrustedDeviceQuery.Builder().id(id).build()).getResult().stream().findFirst().
+ map(this::translate).
+ orElse(null);
+ }
+}
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
similarity index 96%
rename from
wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
rename to
wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
index b3089233da..e21bda8114 100644
---
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/token/WAGoogleMfaAuthTokenRepositoryTest.java
+++
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/gauth/WAGoogleMfaAuthTokenRepositoryTest.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.syncope.wa.starter.gauth.token;
+package org.apache.syncope.wa.starter.gauth;
import static org.junit.jupiter.api.Assertions.assertEquals;