syncope-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ilgro...@apache.org
Subject [1/2] syncope git commit: [SYNCOPE-1821] Implementation on Core completed; still missing console + doc
Date Thu, 15 Mar 2018 13:10:36 GMT
Repository: syncope
Updated Branches:
  refs/heads/master 5724a50ca -> 425f9b9ed


http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
index 30a1f47..728c360 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
@@ -42,6 +42,7 @@ import org.apache.syncope.core.persistence.api.entity.Role;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPADynRoleMembership;
 import org.apache.syncope.core.persistence.jpa.validation.entity.RoleCheck;
 import org.apache.syncope.core.persistence.api.entity.DynRealm;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
 
 @Entity
 @Table(name = JPARole.TABLE)
@@ -83,6 +84,14 @@ public class JPARole extends AbstractProvidedKeyEntity implements Role {
     @Lob
     private String consoleLayoutInfo;
 
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(joinColumns =
+            @JoinColumn(name = "role_id"),
+            inverseJoinColumns =
+            @JoinColumn(name = "privilege_id"))
+    @Valid
+    private Set<JPAPrivilege> privileges = new HashSet<>();
+
     @Override
     public Set<String> getEntitlements() {
         return entitlements;
@@ -131,4 +140,15 @@ public class JPARole extends AbstractProvidedKeyEntity implements Role {
         this.consoleLayoutInfo = consoleLayoutInfo;
     }
 
+    @Override
+    public boolean add(final Privilege privilege) {
+        checkType(privilege, JPAPrivilege.class);
+        return privileges.add((JPAPrivilege) privilege);
+    }
+
+    @Override
+    public Set<? extends Privilege> getPrivileges() {
+        return privileges;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/persistence-jpa/src/main/resources/views.xml
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/resources/views.xml b/core/persistence-jpa/src/main/resources/views.xml
index 88b4bb8..37fd431 100644
--- a/core/persistence-jpa/src/main/resources/views.xml
+++ b/core/persistence-jpa/src/main/resources/views.xml
@@ -111,6 +111,20 @@ under the License.
     SELECT ss.user_id AS any_id, ss.role_id AS role_id
     FROM SyncopeUser_SyncopeRole ss
   </entry>
+  <entry key="user_search_priv">
+    CREATE VIEW user_search_priv AS
+
+    SELECT ss.user_id AS any_id, sp.privilege_id AS privilege_id
+    FROM SyncopeUser_SyncopeRole ss, SyncopeRole_Privilege sp
+    WHERE ss.role_id = sp.role_id
+  </entry>
+  <entry key="user_search_dynpriv">
+    CREATE VIEW user_search_dynpriv AS
+
+    SELECT any_id, privilege_id
+    FROM DynRoleMembers drm, SyncopeRole_Privilege rp
+    WHERE drm.role_id = rp.role_id
+  </entry>
   <entry key="user_search_resource">
     CREATE VIEW user_search_resource AS
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
index 480b959..bac5f53 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
@@ -46,6 +46,7 @@ import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
 import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
 import org.apache.syncope.core.persistence.api.dao.search.AssignableCond;
 import org.apache.syncope.core.persistence.api.dao.search.MemberCond;
+import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond;
 import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
 import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
@@ -110,6 +111,10 @@ public class AnySearchTest extends AbstractTest {
         roleCond.setRole("Other");
         assertTrue(searchDAO.matches(user, SearchCond.getLeafCond(roleCond)));
 
+        PrivilegeCond privilegeCond = new PrivilegeCond();
+        privilegeCond.setPrivilege("postMighty");
+        assertTrue(searchDAO.matches(user, SearchCond.getLeafCond(privilegeCond)));
+
         user = userDAO.find("c9b2dec2-00a7-4855-97c0-d854842b4b24");
         assertNotNull(user);
 
@@ -303,6 +308,16 @@ public class AnySearchTest extends AbstractTest {
     }
 
     @Test
+    public void searchByPrivilege() {
+        PrivilegeCond privilegeCond = new PrivilegeCond();
+        privilegeCond.setPrivilege("postMighty");
+
+        List<User> users = searchDAO.search(SearchCond.getLeafCond(privilegeCond), AnyTypeKind.USER);
+        assertNotNull(users);
+        assertEquals(1, users.size());
+    }
+
+    @Test
     public void searchByIsNull() {
         AttributeCond coolLeafCond = new AttributeCond(AttributeCond.Type.ISNULL);
         coolLeafCond.setSchema("cool");

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ApplicationTest.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ApplicationTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ApplicationTest.java
new file mode 100644
index 0000000..ab5da92
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ApplicationTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.persistence.jpa.inner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.List;
+import java.util.UUID;
+import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
+import org.apache.syncope.core.persistence.api.entity.Application;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class ApplicationTest extends AbstractTest {
+
+    @Autowired
+    private ApplicationDAO applicationDAO;
+
+    @Test
+    public void findAll() {
+        List<Application> applications = applicationDAO.findAll();
+        assertFalse(applications.isEmpty());
+        assertEquals(1, applications.size());
+    }
+
+    @Test
+    public void find() {
+        Application mightyApp = applicationDAO.find("mightyApp");
+        assertNotNull(mightyApp);
+        assertEquals(2, mightyApp.getPrivileges().size());
+
+        Privilege getMighty = applicationDAO.findPrivilege("getMighty");
+        assertNotNull(getMighty);
+        assertEquals(getMighty, mightyApp.getPrivilege("getMighty").get());
+
+    }
+
+    @Test
+    public void crud() {
+        // 1. create application
+        Application application = entityFactory.newEntity(Application.class);
+        application.setKey(UUID.randomUUID().toString());
+
+        String privilege1Key = UUID.randomUUID().toString();
+        Privilege privilege = entityFactory.newEntity(Privilege.class);
+        privilege.setKey(privilege1Key);
+        privilege.setSpecMimeType("application/xml");
+        privilege.setSpec("<one/>".getBytes());
+        application.add(privilege);
+
+        String privilege2Key = UUID.randomUUID().toString();
+        privilege = entityFactory.newEntity(Privilege.class);
+        privilege.setKey(privilege2Key);
+        privilege.setSpecMimeType("application/xml");
+        privilege.setSpec("<one><two/></one>".getBytes());
+        application.add(privilege);
+
+        String privilege3Key = UUID.randomUUID().toString();
+        privilege = entityFactory.newEntity(Privilege.class);
+        privilege.setKey(privilege3Key);
+        privilege.setSpecMimeType("application/xml");
+        privilege.setSpec("<one><two><three/></two></one>".getBytes());
+        application.add(privilege);
+
+        application = applicationDAO.save(application);
+        assertNotNull(application);
+        assertNull(application.getDescription());
+        assertEquals(3, application.getPrivileges().size());
+
+        // 2. update application
+        application.setDescription("A description");
+
+        Privilege priv3 = applicationDAO.findPrivilege(privilege3Key);
+        priv3.setApplication(null);
+        application.getPrivileges().remove(priv3);
+        assertEquals(2, application.getPrivileges().size());
+
+        applicationDAO.save(application);
+
+        applicationDAO.flush();
+
+        application = applicationDAO.find(application.getKey());
+        assertNotNull(application);
+        assertNotNull(application.getDescription());
+        assertEquals(2, application.getPrivileges().size());
+
+        // 3. delete application
+        applicationDAO.delete(application);
+
+        applicationDAO.flush();
+
+        assertNull(applicationDAO.find(application.getKey()));
+        assertNull(applicationDAO.findPrivilege(privilege1Key));
+        assertNull(applicationDAO.findPrivilege(privilege2Key));
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index a7e4478..f68e9aa 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -235,6 +235,16 @@ under the License.
                  left_anyObject_id="fc6dbc3a-6c07-4965-8781-921e7401a4a5"
                  right_anyObject_id="8559d14d-58c2-46eb-a2d4-a7d35161e8f8"/>
   
+  <Application id="mightyApp" description="A cool application"/>
+  <Privilege id="postMighty" description="Ability to POST"
+             spec="7B20226D6574686F64223A2022504F5354222C202275726C223A20222F612F622F6322207D"
+             specmimetype="application/json"
+             application_id="mightyApp"/>
+  <Privilege id="getMighty" description="Ability to GET"
+             spec="7B20226D6574686F64223A2022474554222C202275726C223A20222F612F622F6322207D"
+             specmimetype="application/json"
+             application_id="mightyApp"/>
+
   <SyncopeRole id="User reviewer"/>
   <SyncopeRole_entitlements entitlement="USER_READ" role_id="User reviewer"/>
   <SyncopeRole_entitlements entitlement="USER_LIST" role_id="User reviewer"/>
@@ -263,6 +273,7 @@ under the License.
   <SyncopeRole_entitlements entitlement="GROUP_READ" role_id="Other"/>
   <SyncopeRole_entitlements entitlement="WORKFLOW_FORM_CLAIM" role_id="Other"/>
   <SyncopeRole_Realm role_id="Other" realm_id="722f3d84-9c2b-4525-8f6e-e4b82c55a36c"/>
+  <SyncopeRole_Privilege role_id="Other" privilege_id="postMighty"/>
   
   <SyncopeRole id="Search for realm evenTwo"/>
   <SyncopeRole_entitlements entitlement="USER_READ" role_id="Search for realm evenTwo"/>

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ApplicationDataBinder.java
----------------------------------------------------------------------
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ApplicationDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ApplicationDataBinder.java
new file mode 100644
index 0000000..c8b7795
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ApplicationDataBinder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.ApplicationTO;
+import org.apache.syncope.common.lib.to.PrivilegeTO;
+import org.apache.syncope.core.persistence.api.entity.Application;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
+
+public interface ApplicationDataBinder {
+
+    Application create(ApplicationTO applicationTO);
+
+    Application update(Application application, ApplicationTO applicationTO);
+
+    PrivilegeTO getPrivilegeTO(Privilege privilege);
+
+    ApplicationTO getApplicationTO(Application application);
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ApplicationDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ApplicationDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ApplicationDataBinderImpl.java
new file mode 100644
index 0000000..635ba57
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ApplicationDataBinderImpl.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.provisioning.java.data;
+
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.ApplicationTO;
+import org.apache.syncope.common.lib.to.PrivilegeTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
+import org.apache.syncope.core.persistence.api.entity.Application;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
+import org.apache.syncope.core.provisioning.api.data.ApplicationDataBinder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ApplicationDataBinderImpl implements ApplicationDataBinder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ApplicationDataBinder.class);
+
+    @Autowired
+    private ApplicationDAO applicationDAO;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Override
+    public Application create(final ApplicationTO applicationTO) {
+        return update(entityFactory.newEntity(Application.class), applicationTO);
+    }
+
+    @Override
+    public Application update(final Application toBeUpdated, final ApplicationTO applicationTO) {
+        toBeUpdated.setKey(applicationTO.getKey());
+        Application application = applicationDAO.save(toBeUpdated);
+
+        application.setDescription(applicationTO.getDescription());
+
+        // 1. add or update all (valid) privileges from TO
+        applicationTO.getPrivileges().forEach(privilegeTO -> {
+            if (privilegeTO == null) {
+                LOG.error("Null {}", PrivilegeTO.class.getSimpleName());
+            } else {
+                Privilege privilege = applicationDAO.findPrivilege(privilegeTO.getKey());
+                if (privilege == null) {
+                    privilege = entityFactory.newEntity(Privilege.class);
+                    privilege.setKey(privilegeTO.getKey());
+                    privilege.setApplication(application);
+
+                    application.add(privilege);
+                } else if (!application.equals(privilege.getApplication())) {
+                    SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPrivilege);
+                    sce.getElements().add(
+                            "Privilege " + privilege.getKey() + " already owned by " + privilege.getApplication());
+                    throw sce;
+                }
+
+                privilege.setDescription(privilegeTO.getDescription());
+                privilege.setSpecMimeType(privilegeTO.getSpecMimeType());
+                privilege.setSpec(Base64.getDecoder().decode(privilegeTO.getSpec()));
+            }
+        });
+
+        // 2. remove all privileges not contained in the TO
+        for (Iterator<? extends Privilege> itor = application.getPrivileges().iterator(); itor.hasNext();) {
+            Privilege privilege = itor.next();
+            if (!applicationTO.getPrivileges().stream().
+                    anyMatch(privilegeTO -> privilege.getKey().equals(privilegeTO.getKey()))) {
+
+                privilege.setApplication(null);
+                itor.remove();
+            }
+        }
+
+        return application;
+    }
+
+    @Override
+    public PrivilegeTO getPrivilegeTO(final Privilege privilege) {
+        PrivilegeTO privilegeTO = new PrivilegeTO();
+        privilegeTO.setKey(privilege.getKey());
+        privilegeTO.setDescription(privilege.getDescription());
+        privilegeTO.setApplication(privilege.getApplication().getKey());
+        privilegeTO.setSpecMimeType(privilege.getSpecMimeType());
+        privilegeTO.setSpec(Base64.getEncoder().encodeToString(privilege.getSpec()));
+        return privilegeTO;
+    }
+
+    @Override
+    public ApplicationTO getApplicationTO(final Application application) {
+        ApplicationTO applicationTO = new ApplicationTO();
+
+        applicationTO.setKey(application.getKey());
+        applicationTO.setDescription(application.getDescription());
+        applicationTO.getPrivileges().addAll(
+                application.getPrivileges().stream().map(privilege -> getPrivilegeTO(privilege)).
+                        collect(Collectors.toList()));
+
+        return applicationTO;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
index f15011e..25717d3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
@@ -22,13 +22,16 @@ import java.util.stream.Collectors;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.RoleTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
 import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
 import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.DynRealm;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.Role;
 import org.apache.syncope.core.persistence.api.entity.user.DynRoleMembership;
@@ -53,6 +56,9 @@ public class RoleDataBinderImpl implements RoleDataBinder {
     private RoleDAO roleDAO;
 
     @Autowired
+    private ApplicationDAO applicationDAO;
+
+    @Autowired
     private EntityFactory entityFactory;
 
     private void setDynMembership(final Role role, final String dynMembershipFIQL) {
@@ -123,6 +129,16 @@ public class RoleDataBinderImpl implements RoleDataBinder {
             setDynMembership(role, roleTO.getDynMembershipCond());
         }
 
+        role.getPrivileges().clear();
+        for (String key : roleTO.getPrivileges()) {
+            Privilege privilege = applicationDAO.findPrivilege(key);
+            if (privilege == null) {
+                LOG.debug("Invalid privilege {}, ignoring", key);
+            } else {
+                role.add(privilege);
+            }
+        }
+
         return role;
     }
 
@@ -134,15 +150,18 @@ public class RoleDataBinderImpl implements RoleDataBinder {
         roleTO.getEntitlements().addAll(role.getEntitlements());
 
         roleTO.getRealms().addAll(role.getRealms().stream().
-                map(r -> r.getFullPath()).collect(Collectors.toList()));
+                map(Realm::getFullPath).collect(Collectors.toList()));
 
         roleTO.getDynRealms().addAll(role.getDynRealms().stream().
-                map(r -> r.getKey()).collect(Collectors.toList()));
+                map(Entity::getKey).collect(Collectors.toList()));
 
         if (role.getDynMembership() != null) {
             roleTO.setDynMembershipCond(role.getDynMembership().getFIQLCond());
         }
 
+        roleTO.getPrivileges().addAll(role.getPrivileges().stream().
+                map(Entity::getKey).collect(Collectors.toList()));
+
         return roleTO;
     }
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
index c082555..6cffa6b 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
@@ -603,7 +603,15 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
             userTO.getDynRealms().addAll(userDAO.findDynRealms(user.getKey()));
 
             // roles
-            userTO.getRoles().addAll(user.getRoles().stream().map(r -> r.getKey()).collect(Collectors.toList()));
+            userTO.getRoles().addAll(user.getRoles().stream().map(Entity::getKey).collect(Collectors.toList()));
+
+            // dynamic roles
+            userTO.getDynRoles().addAll(
+                    userDAO.findDynRoles(user.getKey()).stream().map(Entity::getKey).collect(Collectors.toList()));
+
+            // privileges
+            userTO.getPrivileges().addAll(userDAO.findAllRoles(user).stream().
+                    flatMap(role -> role.getPrivileges().stream()).map(Entity::getKey).collect(Collectors.toSet()));
 
             // relationships
             userTO.getRelationships().addAll(
@@ -622,9 +630,6 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
                     }).collect(Collectors.toList()));
 
             // dynamic memberships
-            userTO.getDynRoles().addAll(
-                    userDAO.findDynRoles(user.getKey()).stream().map(Entity::getKey).collect(Collectors.toList()));
-
             userTO.getDynMemberships().addAll(
                     userDAO.findDynGroups(user.getKey()).stream().map(group -> {
                         return new MembershipTO.Builder().

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ApplicationServiceImpl.java
----------------------------------------------------------------------
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ApplicationServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ApplicationServiceImpl.java
new file mode 100644
index 0000000..ed944da
--- /dev/null
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ApplicationServiceImpl.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import java.net.URI;
+import java.util.List;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.to.ApplicationTO;
+import org.apache.syncope.common.lib.to.PrivilegeTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.ApplicationService;
+import org.apache.syncope.core.logic.ApplicationLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ApplicationServiceImpl extends AbstractServiceImpl implements ApplicationService {
+
+    @Autowired
+    private ApplicationLogic logic;
+
+    @Override
+    public List<ApplicationTO> list() {
+        return logic.list();
+    }
+
+    @Override
+    public ApplicationTO read(final String key) {
+        return logic.read(key);
+    }
+
+    @Override
+    public PrivilegeTO readPrivilege(final String key) {
+        return logic.readPrivilege(key);
+    }
+
+    @Override
+    public Response create(final ApplicationTO applicationTO) {
+        ApplicationTO created = logic.create(applicationTO);
+        URI location = uriInfo.getAbsolutePathBuilder().path(created.getKey()).build();
+        return Response.created(location).
+                header(RESTHeaders.RESOURCE_KEY, created.getKey()).
+                build();
+    }
+
+    @Override
+    public void update(final ApplicationTO applicationTO) {
+        logic.update(applicationTO);
+    }
+
+    @Override
+    public void delete(final String key) {
+        logic.delete(key);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
----------------------------------------------------------------------
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
index 82d9cbc..4a08852 100644
--- a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
@@ -59,9 +59,9 @@ public class UserSelfServiceImpl extends AbstractServiceImpl implements UserSelf
     public Response read() {
         Pair<String, UserTO> self = logic.selfRead();
         return Response.ok().
-                header(RESTHeaders.RESOURCE_KEY, self.getValue().getKey()).
-                header(RESTHeaders.OWNED_ENTITLEMENTS, self.getKey()).
-                entity(self.getValue()).
+                header(RESTHeaders.RESOURCE_KEY, self.getRight().getKey()).
+                header(RESTHeaders.OWNED_ENTITLEMENTS, self.getLeft()).
+                entity(self.getRight()).
                 build();
     }
 
@@ -74,7 +74,7 @@ public class UserSelfServiceImpl extends AbstractServiceImpl implements UserSelf
     @Override
     public Response update(final UserTO user) {
         Pair<String, UserTO> self = logic.selfRead();
-        return update(AnyOperations.diff(user, self.getValue(), false));
+        return update(AnyOperations.diff(user, self.getRight(), false));
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
----------------------------------------------------------------------
diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
index e51b53f..c3f296f 100644
--- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
+++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
@@ -28,6 +28,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
@@ -147,6 +148,10 @@ public class ElasticsearchUtils {
                     map(r -> r.getKey()).collect(Collectors.toList());
             builder = builder.field("roles", roles);
 
+            Set<Object> privileges = userDAO.findAllRoles(user).stream().
+                    flatMap(role -> role.getPrivileges().stream()).map(Entity::getKey).collect(Collectors.toSet());
+            builder = builder.field("privileges", privileges);
+
             List<Object> memberships = new ArrayList<>(userDAO.findAllGroupKeys(user));
             builder = builder.field("memberships", memberships);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
----------------------------------------------------------------------
diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
index 2a60c6a..de070f1 100644
--- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
+++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java
@@ -39,6 +39,7 @@ import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond;
 import org.apache.syncope.core.persistence.api.dao.search.MemberCond;
 import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond;
 import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
 import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond;
 import org.apache.syncope.core.persistence.api.dao.search.ResourceCond;
@@ -211,6 +212,8 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                     builder = getQueryBuilder(cond.getAssignableCond());
                 } else if (cond.getRoleCond() != null && AnyTypeKind.USER == kind) {
                     builder = getQueryBuilder(cond.getRoleCond());
+                } else if (cond.getPrivilegeCond() != null && AnyTypeKind.USER == kind) {
+                    builder = getQueryBuilder(cond.getPrivilegeCond());
                 } else if (cond.getDynRealmCond() != null) {
                     builder = getQueryBuilder(cond.getDynRealmCond());
                 } else if (cond.getMemberCond() != null && AnyTypeKind.GROUP == kind) {
@@ -306,6 +309,10 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
         return QueryBuilders.termQuery("roles", cond.getRole());
     }
 
+    private QueryBuilder getQueryBuilder(final PrivilegeCond cond) {
+        return QueryBuilders.termQuery("privileges", cond.getPrivilege());
+    }
+
     private QueryBuilder getQueryBuilder(final DynRealmCond cond) {
         return QueryBuilders.termQuery("dynRealms", cond.getDynRealm());
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/pom.xml
----------------------------------------------------------------------
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index 5284b9e..05555a9 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -199,6 +199,30 @@ under the License.
       <artifactId>bcpkix-jdk15on</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.seleniumhq.selenium</groupId>
+      <artifactId>selenium-java</artifactId>
+      <scope>test</scope>
+      <version>2.44.0</version>
+    </dependency>
+    <dependency>
+      <groupId>com.opera</groupId>
+      <artifactId>operadriver</artifactId>
+      <scope>test</scope>
+      <version>1.5</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.seleniumhq.selenium</groupId>
+          <artifactId>selenium-remote-driver</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+      <version>4.11</version>
+    </dependency>
   </dependencies>
 
   <build>

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
----------------------------------------------------------------------
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 08d626b..7143545 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
@@ -69,6 +69,7 @@ import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.service.AnyObjectService;
 import org.apache.syncope.common.rest.api.service.AnyTypeClassService;
 import org.apache.syncope.common.rest.api.service.AnyTypeService;
+import org.apache.syncope.common.rest.api.service.ApplicationService;
 import org.apache.syncope.common.rest.api.service.CamelRouteService;
 import org.apache.syncope.common.rest.api.service.ConfigurationService;
 import org.apache.syncope.common.rest.api.service.ConnectorHistoryService;
@@ -184,6 +185,8 @@ public abstract class AbstractITCase {
 
     protected static DomainService domainService;
 
+    protected static ApplicationService applicationService;
+
     protected static AnyTypeClassService anyTypeClassService;
 
     protected static AnyTypeService anyTypeService;
@@ -280,6 +283,7 @@ public abstract class AbstractITCase {
 
         syncopeService = adminClient.getService(SyncopeService.class);
         domainService = adminClient.getService(DomainService.class);
+        applicationService = adminClient.getService(ApplicationService.class);
         anyTypeClassService = adminClient.getService(AnyTypeClassService.class);
         anyTypeService = adminClient.getService(AnyTypeService.class);
         relationshipTypeService = adminClient.getService(RelationshipTypeService.class);

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ApplicationITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ApplicationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ApplicationITCase.java
new file mode 100644
index 0000000..5cc79b0
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ApplicationITCase.java
@@ -0,0 +1,132 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Base64;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.ApplicationTO;
+import org.apache.syncope.common.lib.to.EntityTO;
+import org.apache.syncope.common.lib.to.PrivilegeTO;
+import org.apache.syncope.common.lib.to.RoleTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.rest.api.service.ApplicationService;
+import org.apache.syncope.common.rest.api.service.RoleService;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+
+public class ApplicationITCase extends AbstractITCase {
+
+    @Test
+    public void read() {
+        ApplicationTO mightyApp = applicationService.read("mightyApp");
+        assertNotNull(mightyApp);
+        assertEquals(2, mightyApp.getPrivileges().size());
+        assertTrue(mightyApp.getPrivileges().stream().anyMatch(privilege -> "postMighty".equals(privilege.getKey())));
+
+        PrivilegeTO getMighty = applicationService.readPrivilege("getMighty");
+        assertNotNull(getMighty);
+        assertEquals("mightyApp", getMighty.getApplication());
+
+        RoleTO role = roleService.read("Other");
+        assertFalse(role.getPrivileges().isEmpty());
+        assertEquals(1, role.getPrivileges().size());
+        assertTrue(role.getPrivileges().stream().anyMatch(privilege -> "postMighty".equals(privilege)));
+    }
+
+    @Test
+    public void crud() {
+        // 1. create application
+        ApplicationTO application = new ApplicationTO();
+        application.setKey(UUID.randomUUID().toString());
+
+        PrivilegeTO privilegeTO = new PrivilegeTO();
+        privilegeTO.setKey(UUID.randomUUID().toString());
+        privilegeTO.setSpecMimeType("application/xml");
+        privilegeTO.setSpec(Base64.getEncoder().encodeToString("<one/>".getBytes()));
+        application.getPrivileges().add(privilegeTO);
+
+        privilegeTO = new PrivilegeTO();
+        privilegeTO.setKey(UUID.randomUUID().toString());
+        privilegeTO.setSpecMimeType("application/xml");
+        privilegeTO.setSpec(Base64.getEncoder().encodeToString("<one><two/></one>".getBytes()));
+        application.getPrivileges().add(privilegeTO);
+
+        privilegeTO = new PrivilegeTO();
+        privilegeTO.setKey(UUID.randomUUID().toString());
+        privilegeTO.setSpecMimeType("application/xml");
+        privilegeTO.setSpec(Base64.getEncoder().encodeToString("<one><two><three/></two></one>".getBytes()));
+        application.getPrivileges().add(privilegeTO);
+
+        Response response = applicationService.create(application);
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+        application = getObject(response.getLocation(), ApplicationService.class, ApplicationTO.class);
+        assertNotNull(application);
+        assertNull(application.getDescription());
+        assertEquals(3, application.getPrivileges().size());
+
+        // 2. update application
+        application.setDescription("A description");
+        application.getPrivileges().remove(1);
+
+        applicationService.update(application);
+
+        application = applicationService.read(application.getKey());
+        assertNotNull(application);
+        assertNotNull(application.getDescription());
+        assertEquals(2, application.getPrivileges().size());
+
+        // 3. assign application's privileges to a new role
+        RoleTO role = new RoleTO();
+        role.setKey("privileged");
+        role.getPrivileges().addAll(
+                application.getPrivileges().stream().map(EntityTO::getKey).collect(Collectors.toList()));
+
+        response = roleService.create(role);
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+        role = getObject(response.getLocation(), RoleService.class, RoleTO.class);
+        assertNotNull(role);
+        assertEquals(2, role.getPrivileges().size());
+
+        // 4. delete application => delete privileges
+        applicationService.delete(application.getKey());
+
+        try {
+            applicationService.read(application.getKey());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.NotFound, e.getType());
+        }
+
+        role = roleService.read(role.getKey());
+        assertNotNull(role);
+        assertTrue(role.getPrivileges().isEmpty());
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
index 6951a60..1c3eba7 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthenticationITCase.java
@@ -104,21 +104,21 @@ public class AuthenticationITCase extends AbstractITCase {
         // 2. as anonymous
         Pair<Map<String, Set<String>>, UserTO> self = clientFactory.create(
                 new AnonymousAuthenticationHandler(ANONYMOUS_UNAME, ANONYMOUS_KEY)).self();
-        assertEquals(1, self.getKey().size());
-        assertTrue(self.getKey().keySet().contains(StandardEntitlement.ANONYMOUS));
-        assertEquals(ANONYMOUS_UNAME, self.getValue().getUsername());
+        assertEquals(1, self.getLeft().size());
+        assertTrue(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
+        assertEquals(ANONYMOUS_UNAME, self.getRight().getUsername());
 
         // 3. as admin
         self = adminClient.self();
-        assertEquals(syncopeService.platform().getEntitlements().size(), self.getKey().size());
-        assertFalse(self.getKey().keySet().contains(StandardEntitlement.ANONYMOUS));
-        assertEquals(ADMIN_UNAME, self.getValue().getUsername());
+        assertEquals(syncopeService.platform().getEntitlements().size(), self.getLeft().size());
+        assertFalse(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
+        assertEquals(ADMIN_UNAME, self.getRight().getUsername());
 
         // 4. as user
         self = clientFactory.create("bellini", ADMIN_PWD).self();
-        assertFalse(self.getKey().isEmpty());
-        assertFalse(self.getKey().keySet().contains(StandardEntitlement.ANONYMOUS));
-        assertEquals("bellini", self.getValue().getUsername());
+        assertFalse(self.getLeft().isEmpty());
+        assertFalse(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
+        assertEquals("bellini", self.getRight().getUsername());
     }
 
     @Test
@@ -404,7 +404,7 @@ public class AuthenticationITCase extends AbstractITCase {
         assertEquals("active", userTO.getStatus());
 
         SyncopeClient goodPwdClient = clientFactory.create(userTO.getUsername(), "password123");
-        assertEquals(0, goodPwdClient.self().getValue().getFailedLogins().intValue());
+        assertEquals(0, goodPwdClient.self().getRight().getFailedLogins().intValue());
     }
 
     @Test
@@ -507,8 +507,8 @@ public class AuthenticationITCase extends AbstractITCase {
         Pair<Map<String, Set<String>>, UserTO> self =
                 clientFactory.create(userTO.getUsername(), "password123").self();
         assertNotNull(self);
-        assertNotNull(self.getKey());
-        assertNotNull(self.getValue());
+        assertNotNull(self.getLeft());
+        assertNotNull(self.getRight());
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RoleITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RoleITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RoleITCase.java
index 730c7fa..b349d60 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RoleITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RoleITCase.java
@@ -29,6 +29,7 @@ import javax.ws.rs.core.Response;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.RoleTO;
+import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
 import org.apache.syncope.common.rest.api.service.RoleService;
@@ -122,20 +123,26 @@ public class RoleITCase extends AbstractITCase {
 
     @Test
     public void dynMembership() {
-        assertTrue(userService.read("c9b2dec2-00a7-4855-97c0-d854842b4b24").getDynRoles().isEmpty());
+        UserTO bellini = userService.read("bellini");
+        assertTrue(bellini.getDynRoles().isEmpty());
+        assertTrue(bellini.getPrivileges().isEmpty());
 
         RoleTO role = getSampleRoleTO("dynMembership");
+        role.getPrivileges().add("getMighty");
         role.setDynMembershipCond("cool==true");
         Response response = roleService.create(role);
         role = getObject(response.getLocation(), RoleService.class, RoleTO.class);
         assertNotNull(role);
 
-        assertTrue(userService.read(
-                "c9b2dec2-00a7-4855-97c0-d854842b4b24").getDynRoles().contains(role.getKey()));
+        bellini = userService.read("bellini");
+        assertTrue(bellini.getDynRoles().contains(role.getKey()));
+        assertTrue(bellini.getPrivileges().contains("getMighty"));
 
         role.setDynMembershipCond("cool==false");
         roleService.update(role);
 
-        assertTrue(userService.read("c9b2dec2-00a7-4855-97c0-d854842b4b24").getDynMemberships().isEmpty());
+        bellini = userService.read("bellini");
+        assertTrue(bellini.getDynMemberships().isEmpty());
+        assertTrue(bellini.getPrivileges().isEmpty());
     }
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index e159778..bc743ab 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -171,6 +171,19 @@ public class SearchITCase extends AbstractITCase {
     }
 
     @Test
+    public void searchByPrivilege() {
+        PagedResult<UserTO> matchingUsers = userService.search(
+                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
+                        fiql(SyncopeClient.getUserSearchConditionBuilder().withPrivileges("postMighty").query()).
+                        build());
+        assertNotNull(matchingUsers);
+        assertFalse(matchingUsers.getResult().isEmpty());
+
+        assertTrue(matchingUsers.getResult().stream().
+                anyMatch(user -> "1417acbe-cbf6-4277-9372-e75e04f97000".equals(user.getKey())));
+    }
+
+    @Test
     public void searchByDynRole() {
         RoleTO role = RoleITCase.getSampleRoleTO("dynMembership");
         role.setDynMembershipCond("cool==true");

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
index f962361..490859b 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
@@ -122,6 +122,13 @@ public class UserITCase extends AbstractITCase {
     }
 
     @Test
+    public void readPrivileges() {
+        Set<String> privileges = userService.read("rossini").getPrivileges();
+        assertNotNull(privileges);
+        assertEquals(1, privileges.size());
+    }
+
+    @Test
     public void createUserWithNoPropagation() {
         // create a new user
         UserTO userTO = getUniqueSampleTO("xxx@xxx.xxx");

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
index 565b043..541f905 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
@@ -141,7 +141,7 @@ public class UserSelfITCase extends AbstractITCase {
         }
 
         Pair<Map<String, Set<String>>, UserTO> self = clientFactory.create("rossini", ADMIN_PWD).self();
-        assertEquals("rossini", self.getValue().getUsername());
+        assertEquals("rossini", self.getRight().getUsername());
     }
 
     @Test
@@ -152,7 +152,7 @@ public class UserSelfITCase extends AbstractITCase {
         assertNotNull(userId);
 
         Pair<Map<String, Set<String>>, UserTO> self = clientFactory.create(userId, ADMIN_PWD).self();
-        assertEquals(rossini.getUsername(), self.getValue().getUsername());
+        assertEquals(rossini.getUsername(), self.getRight().getUsername());
     }
 
     @Test
@@ -248,7 +248,7 @@ public class UserSelfITCase extends AbstractITCase {
 
     @Test
     public void issueSYNCOPE373() {
-        UserTO userTO = adminClient.self().getValue();
+        UserTO userTO = adminClient.self().getRight();
         assertEquals(ADMIN_UNAME, userTO.getUsername());
     }
 
@@ -272,7 +272,7 @@ public class UserSelfITCase extends AbstractITCase {
 
         // 2. verify that new user is able to authenticate
         SyncopeClient authClient = clientFactory.create(user.getUsername(), "password123");
-        UserTO read = authClient.self().getValue();
+        UserTO read = authClient.self().getRight();
         assertNotNull(read);
 
         // 3. request password reset (as anonymous) providing the expected security answer
@@ -301,7 +301,7 @@ public class UserSelfITCase extends AbstractITCase {
 
         // 6. verify that password was reset and token removed
         authClient = clientFactory.create(user.getUsername(), "newPassword123");
-        read = authClient.self().getValue();
+        read = authClient.self().getRight();
         assertNotNull(read);
         assertNull(read.getToken());
 
@@ -323,7 +323,7 @@ public class UserSelfITCase extends AbstractITCase {
 
         // 2. verify that new user is able to authenticate
         SyncopeClient authClient = clientFactory.create(user.getUsername(), "password123");
-        UserTO read = authClient.self().getValue();
+        UserTO read = authClient.self().getRight();
         assertNotNull(read);
 
         // 3. request password reset (as anonymous) with no security answer
@@ -346,7 +346,7 @@ public class UserSelfITCase extends AbstractITCase {
 
         // 6. verify that password was reset and token removed
         authClient = clientFactory.create(user.getUsername(), "newPassword123");
-        read = authClient.self().getValue();
+        read = authClient.self().getRight();
         assertNotNull(read);
         assertNull(read.getToken());
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/src/main/asciidoc/reference-guide/concepts/concepts.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/concepts/concepts.adoc b/src/main/asciidoc/reference-guide/concepts/concepts.adoc
index b5ac17d..a4c1fd8 100644
--- a/src/main/asciidoc/reference-guide/concepts/concepts.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/concepts.adoc
@@ -28,6 +28,8 @@ include::realms.adoc[]
 
 include::entitlements.adoc[]
 
+include::privileges.adoc[]
+
 include::roles.adoc[]
 
 include::provisioning/provisioning.adoc[]

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/src/main/asciidoc/reference-guide/concepts/entitlements.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/concepts/entitlements.adoc b/src/main/asciidoc/reference-guide/concepts/entitlements.adoc
index 89b9b3f..7bfa9e6 100644
--- a/src/main/asciidoc/reference-guide/concepts/entitlements.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/entitlements.adoc
@@ -18,7 +18,7 @@
 //
 === Entitlements
 
-Entitlements are basically strings describing the right to perform an operation.
+Entitlements are basically strings describing the right to perform an operation on Syncope.
 
 The components in the <<logic,logic layer>> are annotated with
 http://projects.spring.io/spring-security/[Spring Security^] to implement declarative security; in the following
@@ -60,4 +60,6 @@ ifeval::["{snapshotOrRelease}" == "snapshot"]
 https://github.com/apache/syncope/blob/master/ext/camel/common-lib/src/main/java/org/apache/syncope/common/lib/types/CamelEntitlement.java[enlarge the initial list^]
 endif::[]
 : this is because entitlements are the pillars of the internal security model and are not meant for external usage.
+
+If you need to model the rights that Users own on external applications, look at <<privileges,privileges>>, instead.
 ====

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/src/main/asciidoc/reference-guide/concepts/privileges.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/concepts/privileges.adoc b/src/main/asciidoc/reference-guide/concepts/privileges.adoc
new file mode 100644
index 0000000..77fbfa5
--- /dev/null
+++ b/src/main/asciidoc/reference-guide/concepts/privileges.adoc
@@ -0,0 +1,21 @@
+//
+// 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.
+//
+=== Privileges
+
+Privileges model the rights that Users own on external applications.

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/src/main/asciidoc/reference-guide/concepts/roles.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/concepts/roles.adoc b/src/main/asciidoc/reference-guide/concepts/roles.adoc
index f45c0a1..f1fe2f5 100644
--- a/src/main/asciidoc/reference-guide/concepts/roles.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/roles.adoc
@@ -21,6 +21,8 @@
 Roles map a set of <<entitlements,entitlements>> to a set of <<realms,realms>> and / or
 <<dynamic-realms, dynamic realms>>.
 
+In addition, Roles can be used to assign <<privileges,privileges>> to Users.
+
 [TIP]
 .Static and Dynamic Memberships
 ====

http://git-wip-us.apache.org/repos/asf/syncope/blob/425f9b9e/src/main/asciidoc/reference-guide/workingwithapachesyncope/restfulservices.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/workingwithapachesyncope/restfulservices.adoc b/src/main/asciidoc/reference-guide/workingwithapachesyncope/restfulservices.adoc
index 426338a..e12dfc7 100644
--- a/src/main/asciidoc/reference-guide/workingwithapachesyncope/restfulservices.adoc
+++ b/src/main/asciidoc/reference-guide/workingwithapachesyncope/restfulservices.adoc
@@ -56,7 +56,7 @@ based on owned <<entitlements,entitlements>>.
 
 [NOTE]
 Users can examine their own entitlements looking at the `<<x-syncope-entitlements,X-Syncope-Entitlements>>`
-header value.
+header value, and their own privileges looking at the `<<x-syncope-privileges,X-Syncope-Privileges>>` header value.
 
 [TIP]
 ====
@@ -196,6 +196,11 @@ Groups and Any Objects operations.
 When invoking the REST endpoint `/users/self` in `GET`, the `X-Syncope-Entitlements` response header will list all
 the <<entitlements,entitlements>> owned by the requesting user.
 
+===== X-Syncope-Privileges
+
+When invoking the REST endpoint `/users/self` in `GET`, the `X-Syncope-Privileges` response header will list all
+the <<privileges,privileges>> owned by the requesting user.
+
 ==== Bulk Operations
 
 Some REST endpoints feature the _bulk mode_, e.g. the capability to perform a given operation onto several items at the


Mime
View raw message