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.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 @@ protected boolean isCondition(final IModel rowModel) { return !rowModel.getObject().getU2FRegisteredDevices().isEmpty(); } }); + columns.add(new BooleanConditionColumn<>(new StringResourceModel("mfaTrustedDevices")) { + + private static final long serialVersionUID = -8236820422411536323L; + + @Override + protected boolean isCondition(final IModel rowModel) { + return !rowModel.getObject().getMfaTrustedDevices().isEmpty(); + } + }); columns.add(new BooleanConditionColumn<>(new StringResourceModel("webAuthnAccount")) { private static final long serialVersionUID = -8236820422411536323L; @@ -368,6 +378,57 @@ protected List> getColumns() { } }, 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( + "panel", restClient, authProfileModal, model.getObject(), pageRef) { + + private static final long serialVersionUID = 5788448799796630011L; + + @Override + protected List 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> getColumns() { + List> 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 @@ 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 String format(final Date date) { 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, Serializable { + public static final class WrappedDateModel implements IModel, Serializable { private static final long serialVersionUID = 31027882183172L; - private final IModel wrapped; + public static WrappedDateModel ofOffset(final IModel offset) { + WrappedDateModel instance = new WrappedDateModel(); + instance.offset = offset; + return instance; + } + + public static WrappedDateModel ofZoned(final IModel zoned) { + WrappedDateModel instance = new WrappedDateModel(); + instance.zoned = zoned; + return instance; + } + + private IModel offset; - public WrappedDateModel(final IModel wrapped) { - this.wrapped = wrapped; + private IModel 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.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 @@ private Triple> buildSinglePanel( 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 4345deac6d..7d7b6449e6 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 void delete(final String reportKey) { @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 void startExecution(final String taskKey, final Date startAt) { 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 void populateItem(final Item> item, final String compon 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 @@ private static class StartEnd extends WizardStep { 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 7cd493db5c..f06cde8cc8 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.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 AuthProfileTO.Builder u2fRegisteredDevices(final Collection de 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 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 AuthProfileTO build() { private final List u2fRegisteredDevices = new ArrayList<>(); + private final List mfaTrustedDevices = new ArrayList<>(); + private final List webAuthnDeviceCredentials = new ArrayList<>(); @Override @@ -172,6 +190,12 @@ public List getU2FRegisteredDevices() { return u2fRegisteredDevices; } + @JacksonXmlElementWrapper(localName = "mfaTrustedDevices") + @JacksonXmlProperty(localName = "mfaTrustedDevice") + public List getMfaTrustedDevices() { + return mfaTrustedDevices; + } + @JacksonXmlElementWrapper(localName = "credentials") @JacksonXmlProperty(localName = "credential") public List getWebAuthnDeviceCredentials() { @@ -187,6 +211,7 @@ public int hashCode() { append(googleMfaAuthTokens). append(googleMfaAuthAccounts). append(u2fRegisteredDevices). + append(mfaTrustedDevices). append(webAuthnDeviceCredentials). build(); } @@ -210,6 +235,7 @@ public boolean equals(final Object obj) { 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..69d4ae7a18 --- /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 jakarta.ws.rs.QueryParam; +import java.time.OffsetDateTime; + +public class MfaTrustedDeviceQuery extends AbstractQuery { + + private static final long serialVersionUID = -7381828286332101171L; + + public static class Builder extends AbstractQuery.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..1c7c362d0a --- /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 jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.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 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.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 U2FRegistrationLogic u2fRegistrationLogic( 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/MfaTrusStorageLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java new file mode 100644 index 0000000000..eb1a110c27 --- /dev/null +++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/MfaTrusStorageLogic.java @@ -0,0 +1,159 @@ +/* + * 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.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.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; +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.security.access.prepost.PreAuthorize; + +public class MfaTrusStorageLogic extends AbstractAuthProfileLogic { + + protected final EntityFactory entityFactory; + + public MfaTrusStorageLogic( + final EntityFactory entityFactory, + final AuthProfileDAO authProfileDAO, + final AuthProfileDataBinder binder) { + + super(authProfileDAO, binder); + this.entityFactory = entityFactory; + } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public Pair> search( + final Integer page, + final Integer itemsPerPage, + final String principal, + final Long id, + final OffsetDateTime recordDate, + final List orderByClauses) { + + List> comparatorList = orderByClauses. + stream(). + map(orderByClause -> { + Comparator comparator = null; + if (orderByClause.getField().equals("id")) { + comparator = (o1, o2) -> new CompareToBuilder(). + append(o1.getId(), o2.getId()).toComparison(); + } + if (orderByClause.getField().equals("expirationDate")) { + comparator = (o1, o2) -> new CompareToBuilder(). + append(o1.getExpirationDate(), o2.getExpirationDate()).toComparison(); + } + if (orderByClause.getField().equals("recordDate")) { + comparator = (o1, o2) -> new CompareToBuilder(). + append(o1.getRecordDate(), o2.getRecordDate()).toComparison(); + } + if (comparator != null) { + if (orderByClause.getDirection() == OrderByClause.Direction.DESC) { + return comparator.reversed(); + } + return comparator; + } + return null; + }). + filter(Objects::nonNull). + collect(Collectors.toList()); + + List 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 (recordDate != null) { + builder.appendSuper(device.getRecordDate().isAfter(recordDate.toZonedDateTime())); + } + return builder.build(); + }). + filter(Objects::nonNull). + collect(Collectors.toList()); + + List result = devices.stream(). + limit(itemsPerPage). + skip(itemsPerPage * (page <= 0 ? 0L : page.longValue() - 1L)). + sorted((o1, o2) -> { + int compare; + for (Comparator comparator : comparatorList) { + compare = comparator.compare(o1, o2); + if (compare != 0) { + return compare; + } + } + return 0; + }) + .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 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 profiles = authProfileDAO.findAll(-1, -1); + profiles.forEach(profile -> { + List 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 void delete(final Long id, final OffsetDateTime expirationDate) { @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") public Pair> search( final Integer page, - final Integer itemsPerPage, final Long id, + final Integer itemsPerPage, + final Long id, final OffsetDateTime expirationDate, final List orderByClauses) { @@ -129,7 +130,7 @@ public Pair> search( 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.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.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.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 U2FRegistrationService u2fRegistrationService(final U2FRegistrationLogic 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 search(final MfaTrustedDeviceQuery query) { + Pair> 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 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 accounts); + List getMfaTrustedDevices(); + + void setMfaTrustedDevices(List records); + List getWebAuthnDeviceCredentials(); void setWebAuthnDeviceCredentials(List 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 98761e7559..5e15eab2c2 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 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> U2F_TYPEREF = new TypeReference>() { }; + protected static final TypeReference> MFA_TRUSTED_DEVICE_TYPEREF = + new TypeReference>() { + }; + protected static final TypeReference> IMPERSONATION_TYPEREF = new TypeReference>() { }; @@ -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 List getU2FRegisteredDevices() { } @Override - public void setU2FRegisteredDevices(final List records) { - u2fRegisteredDevices = POJOHelper.serialize(records); + public void setU2FRegisteredDevices(final List devices) { + u2fRegisteredDevices = POJOHelper.serialize(devices); + } + + @Override + public List getMfaTrustedDevices() { + return Optional.ofNullable(mfaTrustedDevices). + map(v -> POJOHelper.deserialize(v, MFA_TRUSTED_DEVICE_TYPEREF)).orElseGet(() -> new ArrayList<>(0)); + } + + @Override + public void setMfaTrustedDevices(final List 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 AuthProfileTO getAuthProfileTO(final AuthProfile authProfile) { 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 AuthProfile update(final AuthProfile authProfile, final AuthProfileTO aut 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 Date getObject() { @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 Date getObject() { @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 2e54a6ee31..01eda292e0 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.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 void initialize(final ConfigurableApplicationContext ctx) { 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 static void anonymousSetup() throws IOException { 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 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 554752dcc8..393ffb28a8 100644 --- a/pom.xml +++ b/pom.xml @@ -1356,7 +1356,7 @@ under the License. io.github.git-commit-id git-commit-id-maven-plugin - 5.0.0 + 6.0.0 @@ -1607,9 +1607,9 @@ under the License. us.hebi.sass sass-cli-maven-plugin - 1.0.2 + 1.0.3 - 1.57.1 + 1.62.0 ${sass.skip} diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml index c86776e873..ec9753feb2 100644 --- a/wa/starter/pom.xml +++ b/wa/starter/pom.xml @@ -329,6 +329,14 @@ under the License. org.apereo.cas cas-server-support-otp-mfa-core + + org.apereo.cas + cas-server-support-trusted-mfa + + + org.apereo.cas + cas-server-support-trusted-mfa-core + org.apereo.cas cas-server-support-oidc-services 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.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.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.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 OneTimeTokenCredentialRepository googleAuthenticatorAccountRegistry( 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 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 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 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 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 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 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;