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()));
+
+    }
+}

Reply via email to