View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.fit.core;
20  
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertNotEquals;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.junit.jupiter.api.Assertions.fail;
26  
27  import com.fasterxml.jackson.databind.node.ArrayNode;
28  import java.nio.charset.StandardCharsets;
29  import javax.ws.rs.core.GenericType;
30  import javax.ws.rs.core.MediaType;
31  import javax.ws.rs.core.Response;
32  import org.apache.commons.io.IOUtils;
33  import org.apache.syncope.client.lib.SyncopeClient;
34  import org.apache.syncope.common.lib.SyncopeClientException;
35  import org.apache.syncope.common.lib.SyncopeConstants;
36  import org.apache.syncope.common.lib.request.AttrPatch;
37  import org.apache.syncope.common.lib.request.GroupCR;
38  import org.apache.syncope.common.lib.request.GroupUR;
39  import org.apache.syncope.common.lib.request.StringPatchItem;
40  import org.apache.syncope.common.lib.request.UserCR;
41  import org.apache.syncope.common.lib.request.UserUR;
42  import org.apache.syncope.common.lib.to.DynRealmTO;
43  import org.apache.syncope.common.lib.to.GroupTO;
44  import org.apache.syncope.common.lib.to.PagedResult;
45  import org.apache.syncope.common.lib.to.ProvisioningResult;
46  import org.apache.syncope.common.lib.to.RoleTO;
47  import org.apache.syncope.common.lib.to.UserTO;
48  import org.apache.syncope.common.lib.types.AnyTypeKind;
49  import org.apache.syncope.common.lib.types.ClientExceptionType;
50  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
51  import org.apache.syncope.common.lib.types.PatchOperation;
52  import org.apache.syncope.common.rest.api.beans.AnyQuery;
53  import org.apache.syncope.common.rest.api.service.DynRealmService;
54  import org.apache.syncope.common.rest.api.service.GroupService;
55  import org.apache.syncope.common.rest.api.service.UserService;
56  import org.apache.syncope.fit.AbstractITCase;
57  import org.eclipse.jetty.client.HttpClient;
58  import org.eclipse.jetty.client.api.ContentResponse;
59  import org.eclipse.jetty.client.util.InputStreamContentProvider;
60  import org.eclipse.jetty.http.HttpHeader;
61  import org.eclipse.jetty.http.HttpMethod;
62  import org.eclipse.jetty.http.HttpStatus;
63  import org.junit.jupiter.api.Test;
64  
65  public class DynRealmITCase extends AbstractITCase {
66  
67      @Test
68      public void misc() {
69          DynRealmTO dynRealm = null;
70          try {
71              dynRealm = new DynRealmTO();
72              dynRealm.setKey("/name" + getUUIDString());
73              dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "cool==true");
74  
75              // invalid key (starts with /)
76              try {
77                  DYN_REALM_SERVICE.create(dynRealm);
78                  fail("This should not happen");
79              } catch (SyncopeClientException e) {
80                  assertEquals(ClientExceptionType.InvalidDynRealm, e.getType());
81              }
82              dynRealm.setKey("name" + getUUIDString());
83  
84              Response response = DYN_REALM_SERVICE.create(dynRealm);
85              dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
86              assertNotNull(dynRealm);
87  
88              PagedResult<UserTO> matching = USER_SERVICE.search(new AnyQuery.Builder().fiql("cool==true").build());
89              assertNotNull(matching);
90              assertNotEquals(0, matching.getSize());
91  
92              UserTO user = matching.getResult().get(0);
93  
94              assertTrue(user.getDynRealms().contains(dynRealm.getKey()));
95          } finally {
96              if (dynRealm != null) {
97                  DYN_REALM_SERVICE.delete(dynRealm.getKey());
98              }
99          }
100     }
101 
102     @Test
103     public void delegatedAdmin() {
104         DynRealmTO dynRealm = null;
105         RoleTO role = null;
106         try {
107             // 1. create dynamic realm for all users and groups having resource-ldap assigned
108             dynRealm = new DynRealmTO();
109             dynRealm.setKey("LDAPLovers" + getUUIDString());
110             dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "$resources==resource-ldap");
111             dynRealm.getDynMembershipConds().put(AnyTypeKind.GROUP.name(), "$resources==resource-ldap");
112 
113             Response response = DYN_REALM_SERVICE.create(dynRealm);
114             dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
115             assertNotNull(dynRealm);
116 
117             // 2. create role for such dynamic realm
118             role = new RoleTO();
119             role.setKey("Administer LDAP" + getUUIDString());
120             role.getEntitlements().add(IdRepoEntitlement.USER_SEARCH);
121             role.getEntitlements().add(IdRepoEntitlement.USER_READ);
122             role.getEntitlements().add(IdRepoEntitlement.USER_UPDATE);
123             role.getEntitlements().add(IdRepoEntitlement.GROUP_READ);
124             role.getEntitlements().add(IdRepoEntitlement.GROUP_UPDATE);
125             role.getDynRealms().add(dynRealm.getKey());
126 
127             role = createRole(role);
128             assertNotNull(role);
129 
130             // 3. create new user and assign the new role
131             UserCR dynRealmAdmin = UserITCase.getUniqueSample("dynRealmAdmin@apache.org");
132             dynRealmAdmin.setPassword("password123");
133             dynRealmAdmin.getRoles().add(role.getKey());
134             assertNotNull(createUser(dynRealmAdmin).getEntity());
135 
136             // 4. create new user and group, assign resource-ldap
137             UserCR userCR = UserITCase.getUniqueSample("dynRealmUser@apache.org");
138             userCR.setRealm("/even/two");
139             userCR.getResources().clear();
140             userCR.getResources().add(RESOURCE_NAME_LDAP);
141             UserTO user = createUser(userCR).getEntity();
142             assertNotNull(user);
143             final String userKey = user.getKey();
144 
145             GroupCR groupCR = GroupITCase.getSample("dynRealmGroup");
146             groupCR.setRealm("/odd");
147             groupCR.getResources().clear();
148             groupCR.getResources().add(RESOURCE_NAME_LDAP);
149             GroupTO group = createGroup(groupCR).getEntity();
150             assertNotNull(group);
151             final String groupKey = group.getKey();
152 
153             if (IS_EXT_SEARCH_ENABLED) {
154                 try {
155                     Thread.sleep(2000);
156                 } catch (InterruptedException ex) {
157                     // ignore
158                 }
159             }
160 
161             // 5. verify that the new user and group are found when searching by dynamic realm
162             PagedResult<UserTO> matchingUsers = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
163                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
164             assertTrue(matchingUsers.getResult().stream().anyMatch(object -> object.getKey().equals(userKey)));
165 
166             PagedResult<GroupTO> matchingGroups = GROUP_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
167                     SyncopeClient.getGroupSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
168             assertTrue(matchingGroups.getResult().stream().anyMatch(object -> object.getKey().equals(groupKey)));
169 
170             // 6. prepare to act as delegated admin
171             SyncopeClient delegatedClient = CLIENT_FACTORY.create(dynRealmAdmin.getUsername(), "password123");
172             UserService delegatedUserService = delegatedClient.getService(UserService.class);
173             GroupService delegatedGroupService = delegatedClient.getService(GroupService.class);
174 
175             // 7. verify delegated administration
176             // USER_READ
177             assertNotNull(delegatedUserService.read(userKey));
178 
179             // GROUP_READ
180             assertNotNull(delegatedGroupService.read(groupKey));
181 
182             // USER_SEARCH
183             matchingUsers = delegatedUserService.search(new AnyQuery.Builder().realm("/").build());
184             assertTrue(matchingUsers.getResult().stream().anyMatch(object -> object.getKey().equals(userKey)));
185 
186             // USER_UPDATE
187             UserUR userUR = new UserUR();
188             userUR.setKey(userKey);
189             userUR.getResources().add(new StringPatchItem.Builder().
190                     value(RESOURCE_NAME_LDAP).operation(PatchOperation.DELETE).build());
191             // this will fail because unassigning resource-ldap would result in removing the user from the dynamic realm
192             try {
193                 delegatedUserService.update(userUR);
194                 fail("This should not happen");
195             } catch (SyncopeClientException e) {
196                 assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
197             }
198             // this will succeed instead
199             userUR.getResources().clear();
200             userUR.getResources().add(new StringPatchItem.Builder().value(RESOURCE_NAME_NOPROPAGATION).build());
201             user = delegatedUserService.update(userUR).
202                     readEntity(new GenericType<ProvisioningResult<UserTO>>() {
203                     }).getEntity();
204             assertNotNull(user);
205             assertTrue(user.getResources().contains(RESOURCE_NAME_NOPROPAGATION));
206 
207             // GROUP_UPDATE
208             GroupUR groupUR = new GroupUR();
209             groupUR.setKey(groupKey);
210             groupUR.getPlainAttrs().add(new AttrPatch.Builder(attr("icon", "modified")).build());
211             group = delegatedGroupService.update(groupUR).readEntity(new GenericType<ProvisioningResult<GroupTO>>() {
212             }).getEntity();
213             assertNotNull(group);
214             assertEquals("modified", group.getPlainAttr("icon").get().getValues().get(0));
215         } finally {
216             if (role != null) {
217                 ROLE_SERVICE.delete(role.getKey());
218             }
219             if (dynRealm != null) {
220                 DYN_REALM_SERVICE.delete(dynRealm.getKey());
221             }
222         }
223     }
224 
225     private static ArrayNode fetchDynRealmsFromElasticsearch(final String userKey) throws Exception {
226         String body =
227                 '{'
228                 + "    \"query\": {"
229                 + "        \"match\": {\"_id\": \"" + userKey + "\"}"
230                 + "    }"
231                 + '}';
232 
233         HttpClient httpClient = new HttpClient();
234         httpClient.start();
235         ContentResponse response = httpClient.newRequest("http://localhost:9200/master_user/_search").
236                 method(HttpMethod.GET).
237                 header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON).
238                 content(new InputStreamContentProvider(IOUtils.toInputStream(body, StandardCharsets.UTF_8))).
239                 send();
240         assertEquals(HttpStatus.OK_200, response.getStatus());
241 
242         return (ArrayNode) JSON_MAPPER.readTree(response.getContent()).
243                 get("hits").get("hits").get(0).get("_source").get("dynRealms");
244     }
245 
246     @Test
247     public void issueSYNCOPE1480() throws Exception {
248         String ctype = getUUIDString();
249 
250         DynRealmTO dynRealm = null;
251         try {
252             // 1. create new dyn realm matching a very specific attribute value
253             dynRealm = new DynRealmTO();
254             dynRealm.setKey("name" + getUUIDString());
255             dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "ctype==" + ctype);
256             DYN_REALM_SERVICE.create(dynRealm);
257 
258             Response response = DYN_REALM_SERVICE.create(dynRealm);
259             dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
260             assertNotNull(dynRealm);
261 
262             // 2. no dyn realm members
263             PagedResult<UserTO> matching = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
264                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
265             assertEquals(0, matching.getSize());
266 
267             // 3. create user with that attribute value
268             UserCR userCR = UserITCase.getUniqueSample("syncope1480@syncope.apache.org");
269             userCR.getPlainAttr("ctype").get().getValues().set(0, ctype);
270             UserTO user = createUser(userCR).getEntity();
271             assertNotNull(user.getKey());
272 
273             // 4a. check that Elasticsearch index was updated correctly
274             if (IS_EXT_SEARCH_ENABLED) {
275                 try {
276                     Thread.sleep(2000);
277                 } catch (InterruptedException ex) {
278                     // ignore
279                 }
280 
281                 ArrayNode dynRealms = fetchDynRealmsFromElasticsearch(user.getKey());
282                 assertEquals(1, dynRealms.size());
283                 assertEquals(dynRealm.getKey(), dynRealms.get(0).asText());
284             }
285 
286             // 4b. now there is 1 realm member
287             matching = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).fiql(
288                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
289             assertEquals(1, matching.getSize());
290 
291             // 5. change dyn realm condition
292             dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "ctype==ANY");
293             DYN_REALM_SERVICE.update(dynRealm);
294 
295             // 6a. check that Elasticsearch index was updated correctly
296             if (IS_EXT_SEARCH_ENABLED) {
297                 try {
298                     Thread.sleep(2000);
299                 } catch (InterruptedException ex) {
300                     // ignore
301                 }
302 
303                 ArrayNode dynRealms = fetchDynRealmsFromElasticsearch(user.getKey());
304                 assertTrue(dynRealms.isEmpty());
305             }
306 
307             // 6b. no more dyn realm members
308             matching = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).fiql(
309                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
310             assertEquals(0, matching.getSize());
311         } finally {
312             if (dynRealm != null) {
313                 DYN_REALM_SERVICE.delete(dynRealm.getKey());
314             }
315         }
316     }
317 }