This is an automated email from the ASF dual-hosted git repository.
mmoayyed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push:
new 7fc736f SYNCOPE-1625 - Support delegation/impersonation for WA (#251)
7fc736f is described below
commit 7fc736f2b1d81e49bb42455477af47b37ed9f23b
Author: Misagh Moayyed <[email protected]>
AuthorDate: Fri Apr 2 15:54:46 2021 +0430
SYNCOPE-1625 - Support delegation/impersonation for WA (#251)
---
.../syncope/common/lib/types/AMEntitlement.java | 8 ++
.../common/lib/wa/ImpersonationAccount.java | 104 ++++++++++++++++++
.../rest/api/service/AuthProfileService.java | 2 +-
.../ImpersonationService.java} | 65 ++++++-----
.../syncope/core/logic/wa/ImpersonationLogic.java | 108 +++++++++++++++++++
.../cxf/service/wa/ImpersonationServiceImpl.java | 68 ++++++++++++
.../persistence/api/entity/auth/AuthProfile.java | 5 +
.../jpa/entity/auth/JPAAuthProfile.java | 17 +++
.../persistence/jpa/inner/AuthProfileTest.java | 29 +++++
.../org/apache/syncope/fit/AbstractITCase.java | 4 +
.../syncope/fit/core/wa/ImpersonationITCase.java | 64 +++++++++++
pom.xml | 11 +-
wa/starter/pom.xml | 9 +-
.../wa/starter/config/SyncopeWAConfiguration.java | 8 ++
.../SyncopeWASurrogateAuthenticationService.java | 71 ++++++++++++
.../apache/syncope/wa/starter/AbstractTest.java | 1 -
.../wa/starter/SyncopeCoreTestingServer.java | 120 ++++++++++++++++-----
...yncopeWASurrogateAuthenticationServiceTest.java | 69 ++++++++++++
18 files changed, 707 insertions(+), 56 deletions(-)
diff --git
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
index 103400a..1a8a7e0 100644
---
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
+++
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
@@ -128,6 +128,14 @@ public final class AMEntitlement {
public static final String WEBAUTHN_LIST_DEVICE = "WEBAUTHN_LIST_DEVICE";
+ public static final String IMPERSONATION_CREATE_ACCOUNT =
"IMPERSONATION_CREATE_ACCOUNT";
+
+ public static final String IMPERSONATION_UPDATE_ACCOUNT =
"IMPERSONATION_UPDATE_ACCOUNT";
+
+ public static final String IMPERSONATION_DELETE_ACCOUNT =
"IMPERSONATION_DELETE_ACCOUNT";
+
+ public static final String IMPERSONATION_READ_ACCOUNT =
"IMPERSONATION_READ_ACCOUNT";
+
private static final Set<String> VALUES;
static {
diff --git
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/ImpersonationAccount.java
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/ImpersonationAccount.java
new file mode 100644
index 0000000..153efeb
--- /dev/null
+++
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/wa/ImpersonationAccount.java
@@ -0,0 +1,104 @@
+/*
+ * 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 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 ImpersonationAccount implements BaseBean {
+
+ private static final long serialVersionUID = 2285073386484048953L;
+
+ private String owner;
+
+ private String key;
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(final String owner) {
+ this.owner = owner;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder()
+ .appendSuper(super.hashCode())
+ .append(key)
+ .append(owner)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (obj.getClass() != getClass()) {
+ return false;
+ }
+ ImpersonationAccount rhs = (ImpersonationAccount) obj;
+ return new EqualsBuilder()
+ .appendSuper(super.equals(obj))
+ .append(this.key, rhs.key)
+ .append(this.owner, rhs.owner)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("key", key)
+ .append("owner", owner)
+ .toString();
+ }
+
+ public static class Builder {
+
+ private final ImpersonationAccount instance = new
ImpersonationAccount();
+
+ public ImpersonationAccount.Builder key(final String key) {
+ instance.setKey(key);
+ return this;
+ }
+
+ public ImpersonationAccount.Builder owner(final String owner) {
+ instance.setOwner(owner);
+ return this;
+ }
+
+ public ImpersonationAccount build() {
+ return instance;
+ }
+ }
+}
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
index 4f6a89d..af565f9 100644
---
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
@@ -31,7 +31,7 @@ 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 org.apache.syncope.common.lib.to.AuthProfileTO;
-import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.RESTHeaders;
/**
* REST operations for Auth profiles.
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ImpersonationService.java
similarity index 56%
copy from
common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
copy to
common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ImpersonationService.java
index 4f6a89d..8ecac7c 100644
---
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthProfileService.java
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/ImpersonationService.java
@@ -16,58 +16,67 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.syncope.common.rest.api.service;
+
+package org.apache.syncope.common.rest.api.service.wa;
+
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+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 org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
import java.util.List;
-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 org.apache.syncope.common.lib.to.AuthProfileTO;
-import org.apache.syncope.common.rest.api.RESTHeaders;
-/**
- * REST operations for Auth profiles.
- */
-@Tag(name = "AuthProfiles")
+@Tag(name = "WA")
@SecurityRequirements({
@SecurityRequirement(name = "BasicAuthentication"),
- @SecurityRequirement(name = "Bearer") })
-@Path("authProfiles")
-public interface AuthProfileService extends JAXRSService {
-
+ @SecurityRequirement(name = "Bearer")})
+@Path("wa/impersonation")
+public interface ImpersonationService extends JAXRSService {
@GET
- @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- List<AuthProfileTO> list();
+ @Path("accounts/{owner}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ List<ImpersonationAccount> findByOwner(@NotNull @PathParam("owner") String
owner);
@GET
- @Path("{key}")
- @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- AuthProfileTO read(@NotNull @PathParam("key") String key);
+ @Path("authz/{owner}")
+ @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML})
+ ImpersonationAccount find(@NotNull @PathParam("owner") String owner,
+ @NotNull @QueryParam("id") String id);
- @GET
- @Path("owners/{owner}")
+ @POST
@Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- AuthProfileTO readByOwner(@NotNull @PathParam("owner") String owner);
+ Response create(@NotNull ImpersonationAccount account);
@DELETE
- @Path("{key}")
@Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- void delete(@NotNull @PathParam("key") String key);
+ Response delete(@NotNull ImpersonationAccount account);
- @DELETE
- @Path("owners/{owner}")
+ @ApiResponses(
+ @ApiResponse(responseCode = "204", description = "Operation was
successful"))
+ @PUT
@Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- void deleteByOwner(@NotNull @PathParam("owner") String owner);
+ void update(@NotNull ImpersonationAccount account);
+
}
diff --git
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ImpersonationLogic.java
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ImpersonationLogic.java
new file mode 100644
index 0000000..77839a2
--- /dev/null
+++
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/ImpersonationLogic.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.core.logic.AbstractAuthProfileLogic;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class ImpersonationLogic extends AbstractAuthProfileLogic {
+
+ @Autowired
+ private EntityFactory entityFactory;
+
+ @PreAuthorize("hasRole('" + AMEntitlement.IMPERSONATION_READ_ACCOUNT + "')"
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public List<ImpersonationAccount> findByOwner(final String owner) {
+ return
authProfileDAO.findByOwner(owner).map(AuthProfile::getImpersonationAccounts).orElse(List.of());
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.IMPERSONATION_READ_ACCOUNT + "')"
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public ImpersonationAccount find(final String owner, final String id) {
+ return authProfileDAO.findByOwner(owner)
+ .map(AuthProfile::getImpersonationAccounts)
+ .stream()
+ .flatMap(List::stream)
+ .filter(acct -> acct.getKey().equalsIgnoreCase(id))
+ .findFirst()
+ .orElseThrow(() -> {
+ SyncopeClientException sce =
SyncopeClientException.build(ClientExceptionType.DelegatedAdministration);
+ sce.getElements().add(owner + " is not authorized to
impersonate " + id);
+ throw sce;
+ });
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.IMPERSONATION_CREATE_ACCOUNT +
"')"
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public String create(final ImpersonationAccount account) {
+ AuthProfile profile =
authProfileDAO.findByOwner(account.getOwner()).orElseGet(() -> {
+ AuthProfile authProfile =
entityFactory.newEntity(AuthProfile.class);
+ authProfile.setOwner(account.getOwner());
+ return authProfile;
+ });
+
+ if (profile.getImpersonationAccounts()
+ .stream()
+ .noneMatch(acct ->
acct.getKey().equalsIgnoreCase(account.getKey()))) {
+ final List<ImpersonationAccount> accounts = new
ArrayList<>(profile.getImpersonationAccounts());
+ accounts.add(account);
+ profile.setImpersonationAccounts(accounts);
+ }
+ return authProfileDAO.save(profile).getKey();
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.IMPERSONATION_UPDATE_ACCOUNT +
"') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void update(final ImpersonationAccount account) {
+ authProfileDAO.findByOwner(account.getOwner()).ifPresent(profile -> {
+ List<ImpersonationAccount> accounts =
profile.getImpersonationAccounts();
+ if (accounts.removeIf(acct ->
acct.getKey().equals(account.getKey()))) {
+ accounts.add(account);
+ profile.setImpersonationAccounts(accounts);
+ authProfileDAO.save(profile);
+ }
+ });
+ }
+
+ @PreAuthorize("hasRole('" + AMEntitlement.IMPERSONATION_DELETE_ACCOUNT +
"') "
+ + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+ public void delete(final ImpersonationAccount account) {
+ authProfileDAO.findByOwner(account.getOwner()).ifPresent(profile -> {
+ List<ImpersonationAccount> accounts =
profile.getImpersonationAccounts();
+ if (accounts.removeIf(acct ->
acct.getKey().equalsIgnoreCase(account.getKey()))) {
+ profile.setImpersonationAccounts(accounts);
+ authProfileDAO.save(profile);
+ }
+ });
+ }
+}
diff --git
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ImpersonationServiceImpl.java
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ImpersonationServiceImpl.java
new file mode 100644
index 0000000..17b7e88
--- /dev/null
+++
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/ImpersonationServiceImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.syncope.core.rest.cxf.service.wa;
+
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
+import org.apache.syncope.core.logic.wa.ImpersonationLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+import java.util.List;
+
+@Service
+public class ImpersonationServiceImpl extends AbstractServiceImpl implements
ImpersonationService {
+
+ @Autowired
+ private ImpersonationLogic logic;
+
+ @Override
+ public List<ImpersonationAccount> findByOwner(final String owner) {
+ return logic.findByOwner(owner);
+ }
+
+ @Override
+ public ImpersonationAccount find(final String owner,
+ final String id) {
+ return logic.find(owner, id);
+ }
+
+ @Override
+ public Response create(final ImpersonationAccount account) {
+ logic.create(account);
+ URI location = uriInfo.getAbsolutePathBuilder().build();
+ return Response.created(location).build();
+ }
+
+ @Override
+ public Response delete(final ImpersonationAccount account) {
+ logic.delete(account);
+ return Response.noContent().build();
+ }
+
+ @Override
+ public void update(final ImpersonationAccount account) {
+ logic.update(account);
+ }
+}
diff --git
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
index 3316176..7b97e0e 100644
---
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
+++
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/AuthProfile.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.api.entity.auth;
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnAccount;
import org.apache.syncope.core.persistence.api.entity.Entity;
@@ -46,4 +47,8 @@ public interface AuthProfile extends Entity {
WebAuthnAccount getWebAuthnAccount();
void setWebAuthnAccount(WebAuthnAccount accounts);
+
+ List<ImpersonationAccount> getImpersonationAccounts();
+
+ void setImpersonationAccounts(List<ImpersonationAccount> accounts);
}
diff --git
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
index 8956bcd..2ff0c58 100644
---
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAAuthProfile.java
@@ -28,6 +28,7 @@ import javax.persistence.Table;
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnAccount;
import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
@@ -50,6 +51,9 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
private String googleMfaAuthAccounts;
@Lob
+ private String impersonatedAccounts;
+
+ @Lob
private String googleMfaAuthTokens;
@Lob
@@ -108,6 +112,19 @@ public class JPAAuthProfile extends
AbstractGeneratedKeyEntity implements AuthPr
}
@Override
+ public List<ImpersonationAccount> getImpersonationAccounts() {
+ return impersonatedAccounts == null
+ ? new ArrayList<>(0)
+ : POJOHelper.deserialize(impersonatedAccounts, new
TypeReference<List<ImpersonationAccount>>() {
+ });
+ }
+
+ @Override
+ public void setImpersonationAccounts(final List<ImpersonationAccount>
accounts) {
+ this.impersonatedAccounts = POJOHelper.serialize(accounts);
+ }
+
+ @Override
public WebAuthnAccount getWebAuthnAccount() {
return webAuthnAccount == null
? null
diff --git
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
index 56adb12..df2c1dd 100644
---
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
+++
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthProfileTest.java
@@ -25,8 +25,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Date;
import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
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.U2FDevice;
import org.apache.syncope.common.lib.wa.WebAuthnAccount;
import org.apache.syncope.common.lib.wa.WebAuthnDeviceCredential;
@@ -156,6 +160,31 @@ public class AuthProfileTest extends AbstractTest {
assertEquals(secret,
authProfile.getGoogleMfaAuthAccounts().get(0).getSecretKey());
}
+ @Test
+ public void impersonationAccounts() {
+ String id = SecureRandomUtils.generateRandomUUID().toString();
+
+ createAuthProfileWithAccount(id);
+
+ Optional<AuthProfile> result = authProfileDAO.findByOwner(id);
+ assertTrue(result.isPresent());
+
+ AuthProfile authProfile = result.get();
+ result =
Optional.ofNullable(authProfileDAO.find(authProfile.getKey()));
+ assertTrue(result.isPresent());
+
+ List<ImpersonationAccount> accounts = IntStream.range(1, 10).
+ mapToObj(i -> new ImpersonationAccount.Builder()
+ .owner("impersonator")
+ .key("impersonatee" + i)
+ .build()).
+ collect(Collectors.toList());
+
+ authProfile.setImpersonationAccounts(accounts);
+ authProfile = authProfileDAO.save(authProfile);
+ assertEquals(accounts.size(),
authProfile.getImpersonationAccounts().size());
+ }
+
private AuthProfile createAuthProfileWithToken(final String owner, final
Integer otp) {
AuthProfile profile = entityFactory.newEntity(AuthProfile.class);
profile.setOwner(owner);
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 1958fea..250a978 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
@@ -144,6 +144,7 @@ import
org.apache.syncope.common.rest.api.service.SAML2SPKeystoreService;
import org.apache.syncope.common.rest.api.service.SAML2SPMetadataService;
import org.apache.syncope.common.rest.api.service.SRARouteService;
import org.apache.syncope.common.rest.api.service.UserWorkflowTaskService;
+import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
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;
@@ -347,6 +348,8 @@ public abstract class AbstractITCase {
protected static WebAuthnRegistrationService webAuthnRegistrationService;
+ protected static ImpersonationService impersonationService;
+
@BeforeAll
public static void securitySetup() {
try (InputStream propStream =
AbstractITCase.class.getResourceAsStream("/security.properties")) {
@@ -428,6 +431,7 @@ public abstract class AbstractITCase {
u2FRegistrationService =
adminClient.getService(U2FRegistrationService.class);
waConfigService = adminClient.getService(WAConfigService.class);
webAuthnRegistrationService =
adminClient.getService(WebAuthnRegistrationService.class);
+ impersonationService =
adminClient.getService(ImpersonationService.class);
}
@Autowired
diff --git
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/ImpersonationITCase.java
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/ImpersonationITCase.java
new file mode 100644
index 0000000..71e1706
--- /dev/null
+++
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/wa/ImpersonationITCase.java
@@ -0,0 +1,64 @@
+/*
+ * 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 org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class ImpersonationITCase extends AbstractITCase {
+ @Test
+ public void createAndFind() {
+ ImpersonationAccount account = new ImpersonationAccount.Builder()
+ .owner(getUUIDString())
+ .key(getUUIDString())
+ .build();
+
+ Response response = impersonationService.create(account);
+ assertNotNull(response);
+
+
assertFalse(impersonationService.findByOwner(account.getOwner()).isEmpty());
+ account = impersonationService.find(account.getOwner(),
account.getKey());
+ assertNotNull(account);
+
+ impersonationService.update(account);
+ account = impersonationService.find(account.getOwner(),
account.getKey());
+ assertNotNull(account);
+
+ response = impersonationService.delete(account);
+ assertNotNull(response);
+
+ try {
+ impersonationService.find(account.getOwner(), account.getKey());
+ fail("Should not happen");
+ } catch (final SyncopeClientException e) {
+ assertEquals(ClientExceptionType.DelegatedAdministration,
e.getType());
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 13f658e..f708aa0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1728,7 +1728,16 @@ under the License.
<artifactId>cas-server-webapp-config</artifactId>
<version>${cas.version}</version>
</dependency>
-
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-surrogate-webflow</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-surrogate-api</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 44865499..65fd2a9 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -270,7 +270,14 @@ under the License.
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-swagger</artifactId>
</dependency>
-
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-surrogate-webflow</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-surrogate-api</artifactId>
+ </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
index 26bb332..41a333d 100644
---
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
@@ -53,9 +53,11 @@ import
org.apache.syncope.wa.starter.pac4j.saml.SyncopeWASAML2ClientCustomizer;
import
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataGenerator;
import
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataLocator;
import org.apache.syncope.wa.starter.services.SyncopeWAServiceRegistry;
+import
org.apache.syncope.wa.starter.surrogate.SyncopeWASurrogateAuthenticationService;
import org.apache.syncope.wa.starter.u2f.SyncopeWAU2FDeviceRepository;
import org.apereo.cas.adaptors.u2f.storage.U2FDeviceRepository;
import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
+import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
import org.apereo.cas.configuration.CasConfigurationProperties;
import
org.apereo.cas.configuration.model.support.mfa.u2f.U2FMultifactorAuthenticationProperties;
import org.apereo.cas.oidc.jwks.OidcJsonWebKeystoreGeneratorService;
@@ -304,6 +306,12 @@ public class SyncopeWAConfiguration {
}
@Bean
+ @Autowired
+ public SurrogateAuthenticationService surrogateAuthenticationService(final
WARestClient restClient) {
+ return new SyncopeWASurrogateAuthenticationService(restClient);
+ }
+
+ @Bean
public KeymasterStart keymasterStart() {
return new KeymasterStart(NetworkService.Type.WA);
}
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationService.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationService.java
new file mode 100644
index 0000000..d45da0a
--- /dev/null
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationService.java
@@ -0,0 +1,71 @@
+/*
+ * 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.surrogate;
+
+import org.apereo.cas.authentication.principal.Principal;
+import org.apereo.cas.authentication.principal.Service;
+import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
+
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class SyncopeWASurrogateAuthenticationService implements
SurrogateAuthenticationService {
+ private static final Logger LOG =
LoggerFactory.getLogger(SyncopeWASurrogateAuthenticationService.class);
+
+ private final WARestClient waRestClient;
+
+ public SyncopeWASurrogateAuthenticationService(final WARestClient
waRestClient) {
+ this.waRestClient = waRestClient;
+ }
+
+ @Override
+ public boolean canAuthenticateAs(final String surrogate, final Principal
principal,
+ final Optional<Service> service) {
+ try {
+ LOG.debug("Checking impersonation attempt by {} for {}",
principal, surrogate);
+ return getImpersonationService().find(principal.getId(),
surrogate) != null;
+ } catch (final Exception e) {
+ LOG.info("Could not authorize account {} for owner {}", surrogate,
principal.getId());
+ }
+ return false;
+ }
+
+ @Override
+ public Collection<String> getEligibleAccountsForSurrogateToProxy(final
String username) {
+ return getImpersonationService().findByOwner(username).
+ stream().
+ map(ImpersonationAccount::getKey).
+ collect(Collectors.toList());
+ }
+
+ private ImpersonationService getImpersonationService() {
+ if (!WARestClient.isReady()) {
+ throw new RuntimeException("Syncope core is not yet ready");
+ }
+ return
waRestClient.getSyncopeClient().getService(ImpersonationService.class);
+ }
+}
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/AbstractTest.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/AbstractTest.java
index 0c80241..79284d7 100644
--- a/wa/starter/src/test/java/org/apache/syncope/wa/starter/AbstractTest.java
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/AbstractTest.java
@@ -25,7 +25,6 @@ import
org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.server.LocalServerPort;
-import org.springframework.boot.web.servlet.server.Session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.ContextConfiguration;
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
index 3e691b4..c77e4c4 100644
---
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
+++
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/SyncopeCoreTestingServer.java
@@ -19,29 +19,39 @@
package org.apache.syncope.wa.starter;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.stream.Collectors;
-import javax.ws.rs.NotFoundException;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.syncope.common.keymaster.client.api.ServiceOps;
import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
+import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.PagedResult;
import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.wa.GoogleMfaAuthToken;
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
import org.apache.syncope.common.lib.wa.WAClientApp;
+import org.apache.syncope.common.rest.api.RESTHeaders;
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.WAClientAppService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefreshedEvent> {
public static final List<WAClientApp> APPS = new ArrayList<>();
@@ -58,13 +68,16 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
// 1. start (mocked) Core as embedded CXF
JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
sf.setAddress(ADDRESS);
- sf.setResourceClasses(WAClientAppService.class,
GoogleMfaAuthTokenService.class);
+ sf.setResourceClasses(WAClientAppService.class,
GoogleMfaAuthTokenService.class, ImpersonationService.class);
+ sf.setResourceProvider(
+ WAClientAppService.class,
+ new SingletonResourceProvider(new
StubWAClientAppService(), true));
sf.setResourceProvider(
- WAClientAppService.class,
- new SingletonResourceProvider(new
StubWAClientAppService(), true));
+ GoogleMfaAuthTokenService.class,
+ new SingletonResourceProvider(new
StubGoogleMfaAuthTokenService(), true));
sf.setResourceProvider(
- GoogleMfaAuthTokenService.class,
- new SingletonResourceProvider(new
StubGoogleMfaAuthTokenService(), true));
+ ImpersonationService.class,
+ new SingletonResourceProvider(new
StubImpersonationService(), true));
sf.setProviders(List.of(new JacksonJsonProvider()));
sf.create();
@@ -77,6 +90,65 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
}
}
+ public static class StubImpersonationService implements
ImpersonationService {
+ private final Map<String, List<ImpersonationAccount>> accounts = new
HashMap<>();
+
+ @Override
+ public List<ImpersonationAccount> findByOwner(final String owner) {
+ return accounts.containsKey(owner) ? accounts.get(owner) :
List.of();
+ }
+
+ @Override
+ public ImpersonationAccount find(final String owner, final String id) {
+ SyncopeClientException exception =
SyncopeClientException.build(ClientExceptionType.DelegatedAdministration);
+ if (accounts.containsKey(owner)) {
+ return accounts.get(owner).
+ stream().
+ filter(acct -> acct.getKey().equalsIgnoreCase(id)).
+ findFirst().
+ orElseThrow(() -> exception);
+ }
+ throw exception;
+ }
+
+ @Override
+ public Response create(final ImpersonationAccount account) {
+ try {
+ if (accounts.containsKey(account.getOwner())
+ && accounts.get(account.getOwner()).
+ stream().
+ noneMatch(acct ->
acct.getKey().equalsIgnoreCase(account.getOwner()))) {
+ accounts.get(account.getOwner()).add(account);
+ } else {
+ List<ImpersonationAccount> list = new ArrayList<>();
+ list.add(account);
+ accounts.put(account.getOwner(), list);
+ }
+ return Response.created(new URI("wa/impersonation")).
+ header(RESTHeaders.RESOURCE_KEY, account.getKey()).
+ build();
+ } catch (final Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public Response delete(final ImpersonationAccount account) {
+ if (accounts.containsKey(account.getOwner())) {
+ accounts.get(account.getOwner()).removeIf(acct ->
acct.getKey().equalsIgnoreCase(account.getKey()));
+ }
+ return Response.noContent().build();
+ }
+
+ @Override
+ public void update(final ImpersonationAccount account) {
+ List<ImpersonationAccount> impersonatedAccounts =
accounts.get(account.getOwner());
+ if (impersonatedAccounts.removeIf(acct ->
acct.getKey().equals(account.getKey()))) {
+ impersonatedAccounts.add(account);
+ }
+ }
+ }
+
public static class StubGoogleMfaAuthTokenService implements
GoogleMfaAuthTokenService {
private final Map<String, GoogleMfaAuthToken> tokens = new HashMap<>();
@@ -93,7 +165,7 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
@Override
public void delete(final String owner, final int otp) {
tokens.entrySet().
- removeIf(e -> e.getValue().getOtp() == otp &&
e.getKey().equalsIgnoreCase(owner));
+ removeIf(e -> e.getValue().getOtp() == otp &&
e.getKey().equalsIgnoreCase(owner));
}
@Override
@@ -115,17 +187,17 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
@Override
public GoogleMfaAuthToken readFor(final String owner, final int otp) {
return tokens.entrySet().stream()
- .filter(to -> to.getValue().getOtp() == otp &&
to.getKey().equalsIgnoreCase(owner))
- .findFirst().get().getValue();
+ .filter(to -> to.getValue().getOtp() == otp &&
to.getKey().equalsIgnoreCase(owner))
+ .findFirst().get().getValue();
}
@Override
public PagedResult<GoogleMfaAuthToken> readFor(final String user) {
PagedResult<GoogleMfaAuthToken> result = new PagedResult<>();
result.getResult().addAll(tokens.entrySet().stream().
- filter(to -> to.getKey().equalsIgnoreCase(user)).
- map(Map.Entry::getValue).
- collect(Collectors.toList()));
+ filter(to -> to.getKey().equalsIgnoreCase(user)).
+ map(Map.Entry::getValue).
+ collect(Collectors.toList()));
result.setSize(result.getResult().size());
result.setTotalCount(result.getSize());
return result;
@@ -134,8 +206,8 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
@Override
public GoogleMfaAuthToken read(final String key) {
return tokens.entrySet().stream()
- .filter(to -> to.getKey().equalsIgnoreCase(key))
- .findFirst().get().getValue();
+ .filter(to -> to.getKey().equalsIgnoreCase(key))
+ .findFirst().get().getValue();
}
@Override
@@ -158,13 +230,13 @@ public class SyncopeCoreTestingServer implements
ApplicationListener<ContextRefr
@Override
public WAClientApp read(final Long clientAppId, final ClientAppType
type) {
return APPS.stream().filter(app -> Objects.equals(clientAppId,
app.getClientAppTO().getClientAppId())).
- findFirst().orElseThrow(() -> new
NotFoundException("ClientApp with clientId " + clientAppId));
+ findFirst().orElseThrow(() -> new NotFoundException("ClientApp
with clientId " + clientAppId));
}
@Override
public WAClientApp read(final String name, final ClientAppType type) {
return APPS.stream().filter(app -> Objects.equals(name,
app.getClientAppTO().getName())).
- findFirst().orElseThrow(() -> new
NotFoundException("ClientApp with name " + name));
+ findFirst().orElseThrow(() -> new NotFoundException("ClientApp
with name " + name));
}
}
}
diff --git
a/wa/starter/src/test/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationServiceTest.java
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationServiceTest.java
new file mode 100644
index 0000000..1a55b02
--- /dev/null
+++
b/wa/starter/src/test/java/org/apache/syncope/wa/starter/surrogate/SyncopeWASurrogateAuthenticationServiceTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.surrogate;
+
+import org.apereo.cas.authentication.principal.Principal;
+import org.apereo.cas.authentication.principal.PrincipalFactoryUtils;
+import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
+
+import org.apache.syncope.common.lib.wa.ImpersonationAccount;
+import org.apache.syncope.common.rest.api.service.wa.ImpersonationService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apache.syncope.wa.starter.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.ws.rs.core.Response;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SyncopeWASurrogateAuthenticationServiceTest extends AbstractTest {
+ @Autowired
+ private WARestClient wARestClient;
+
+ @Autowired
+ private SurrogateAuthenticationService surrogateService;
+
+ @Test
+ public void verifyImpersonation() {
+ ImpersonationAccount account = new ImpersonationAccount.Builder().
+ owner("syncope-principal").
+ key("impersonatee").
+ build();
+
+ ImpersonationService impersonationService = wARestClient.
+ getSyncopeClient().
+ getService(ImpersonationService.class);
+
+ Response response = impersonationService.create(account);
+ assertNotNull(response);
+
+
assertFalse(surrogateService.getEligibleAccountsForSurrogateToProxy(account.getOwner()).isEmpty());
+
+ Principal principal =
PrincipalFactoryUtils.newPrincipalFactory().createPrincipal(account.getOwner());
+ assertFalse(surrogateService.canAuthenticateAs("unknown", principal,
Optional.empty()));
+ assertTrue(surrogateService.canAuthenticateAs(account.getKey(),
principal, Optional.empty()));
+
+ }
+}