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.net.URI;
29  import java.net.http.HttpClient;
30  import java.net.http.HttpRequest;
31  import java.net.http.HttpRequest.BodyPublishers;
32  import java.net.http.HttpResponse;
33  import java.net.http.HttpResponse.BodyHandlers;
34  import javax.ws.rs.core.GenericType;
35  import javax.ws.rs.core.HttpHeaders;
36  import javax.ws.rs.core.MediaType;
37  import javax.ws.rs.core.Response;
38  import org.apache.syncope.client.lib.SyncopeClient;
39  import org.apache.syncope.common.lib.Attr;
40  import org.apache.syncope.common.lib.SyncopeClientException;
41  import org.apache.syncope.common.lib.SyncopeConstants;
42  import org.apache.syncope.common.lib.request.AttrPatch;
43  import org.apache.syncope.common.lib.request.GroupCR;
44  import org.apache.syncope.common.lib.request.GroupUR;
45  import org.apache.syncope.common.lib.request.StringPatchItem;
46  import org.apache.syncope.common.lib.request.UserCR;
47  import org.apache.syncope.common.lib.request.UserUR;
48  import org.apache.syncope.common.lib.to.DynRealmTO;
49  import org.apache.syncope.common.lib.to.GroupTO;
50  import org.apache.syncope.common.lib.to.PagedResult;
51  import org.apache.syncope.common.lib.to.ProvisioningResult;
52  import org.apache.syncope.common.lib.to.RoleTO;
53  import org.apache.syncope.common.lib.to.UserTO;
54  import org.apache.syncope.common.lib.types.AnyTypeKind;
55  import org.apache.syncope.common.lib.types.ClientExceptionType;
56  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
57  import org.apache.syncope.common.lib.types.PatchOperation;
58  import org.apache.syncope.common.rest.api.beans.AnyQuery;
59  import org.apache.syncope.common.rest.api.service.DynRealmService;
60  import org.apache.syncope.common.rest.api.service.GroupService;
61  import org.apache.syncope.common.rest.api.service.UserService;
62  import org.apache.syncope.fit.AbstractITCase;
63  import org.junit.jupiter.api.Test;
64  
65  public class DynRealmITCase extends AbstractITCase {
66  
67      private static ArrayNode fetchDynRealmsFromElasticsearch(final String userKey) throws Exception {
68          String body =
69                  '{'
70                  + "    \"query\": {"
71                  + "        \"match\": {\"_id\": \"" + userKey + "\"}"
72                  + "    }"
73                  + '}';
74  
75          HttpClient client = HttpClient.newHttpClient();
76          HttpResponse<String> response = client.send(
77                  HttpRequest.newBuilder(URI.create("http://localhost:9200/master_user/_search")).
78                          header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).
79                          method("GET", BodyPublishers.ofString(body)).
80                          build(),
81                  BodyHandlers.ofString());
82          assertEquals(Response.Status.OK.getStatusCode(), response.statusCode());
83  
84          return (ArrayNode) JSON_MAPPER.readTree(response.body()).
85                  get("hits").get("hits").get(0).get("_source").get("dynRealms");
86      }
87  
88      @Test
89      public void misc() {
90          DynRealmTO dynRealm = null;
91          try {
92              dynRealm = new DynRealmTO();
93              dynRealm.setKey("/name" + getUUIDString());
94              dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "cool==true");
95  
96              // invalid key (starts with /)
97              try {
98                  DYN_REALM_SERVICE.create(dynRealm);
99                  fail("This should not happen");
100             } catch (SyncopeClientException e) {
101                 assertEquals(ClientExceptionType.InvalidDynRealm, e.getType());
102             }
103             dynRealm.setKey("name" + getUUIDString());
104 
105             Response response = DYN_REALM_SERVICE.create(dynRealm);
106             dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
107             assertNotNull(dynRealm);
108 
109             PagedResult<UserTO> matching = USER_SERVICE.search(new AnyQuery.Builder().fiql("cool==true").build());
110             assertNotNull(matching);
111             assertNotEquals(0, matching.getSize());
112 
113             UserTO user = matching.getResult().get(0);
114 
115             assertTrue(user.getDynRealms().contains(dynRealm.getKey()));
116         } finally {
117             if (dynRealm != null) {
118                 DYN_REALM_SERVICE.delete(dynRealm.getKey());
119             }
120         }
121     }
122 
123     @Test
124     public void delegatedAdmin() {
125         DynRealmTO dynRealm = null;
126         RoleTO role = null;
127         try {
128             // 1. create dynamic realm for all users and groups having resource-ldap assigned
129             dynRealm = new DynRealmTO();
130             dynRealm.setKey("LDAPLovers" + getUUIDString());
131             dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "$resources==resource-ldap");
132             dynRealm.getDynMembershipConds().put(AnyTypeKind.GROUP.name(), "$resources==resource-ldap");
133 
134             Response response = DYN_REALM_SERVICE.create(dynRealm);
135             dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
136             assertNotNull(dynRealm);
137 
138             // 2. create role for such dynamic realm
139             role = new RoleTO();
140             role.setKey("Administer LDAP" + getUUIDString());
141             role.getEntitlements().add(IdRepoEntitlement.USER_SEARCH);
142             role.getEntitlements().add(IdRepoEntitlement.USER_READ);
143             role.getEntitlements().add(IdRepoEntitlement.USER_UPDATE);
144             role.getEntitlements().add(IdRepoEntitlement.GROUP_READ);
145             role.getEntitlements().add(IdRepoEntitlement.GROUP_UPDATE);
146             role.getDynRealms().add(dynRealm.getKey());
147 
148             role = createRole(role);
149             assertNotNull(role);
150 
151             // 3. create new user and assign the new role
152             UserCR dynRealmAdmin = UserITCase.getUniqueSample("dynRealmAdmin@apache.org");
153             dynRealmAdmin.setPassword("password123");
154             dynRealmAdmin.getRoles().add(role.getKey());
155             assertNotNull(createUser(dynRealmAdmin).getEntity());
156 
157             // 4. create new user and group, assign resource-ldap
158             UserCR userCR = UserITCase.getUniqueSample("dynRealmUser@apache.org");
159             userCR.setRealm("/even/two");
160             userCR.getResources().clear();
161             userCR.getResources().add(RESOURCE_NAME_LDAP);
162             UserTO user = createUser(userCR).getEntity();
163             assertNotNull(user);
164             final String userKey = user.getKey();
165 
166             GroupCR groupCR = GroupITCase.getSample("dynRealmGroup");
167             groupCR.setRealm("/odd");
168             groupCR.getResources().clear();
169             groupCR.getResources().add(RESOURCE_NAME_LDAP);
170             GroupTO group = createGroup(groupCR).getEntity();
171             assertNotNull(group);
172             final String groupKey = group.getKey();
173 
174             if (IS_EXT_SEARCH_ENABLED) {
175                 try {
176                     Thread.sleep(2000);
177                 } catch (InterruptedException ex) {
178                     // ignore
179                 }
180             }
181 
182             // 5. verify that the new user and group are found when searching by dynamic realm
183             PagedResult<UserTO> matchingUsers = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
184                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
185             assertTrue(matchingUsers.getResult().stream().anyMatch(object -> object.getKey().equals(userKey)));
186 
187             PagedResult<GroupTO> matchingGroups = GROUP_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
188                     SyncopeClient.getGroupSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
189             assertTrue(matchingGroups.getResult().stream().anyMatch(object -> object.getKey().equals(groupKey)));
190 
191             // 6. prepare to act as delegated admin
192             SyncopeClient delegatedClient = CLIENT_FACTORY.create(dynRealmAdmin.getUsername(), "password123");
193             UserService delegatedUserService = delegatedClient.getService(UserService.class);
194             GroupService delegatedGroupService = delegatedClient.getService(GroupService.class);
195 
196             // 7. verify delegated administration
197             // USER_READ
198             assertNotNull(delegatedUserService.read(userKey));
199 
200             // GROUP_READ
201             assertNotNull(delegatedGroupService.read(groupKey));
202 
203             // USER_SEARCH
204             matchingUsers = delegatedUserService.search(new AnyQuery.Builder().realm("/").build());
205             assertTrue(matchingUsers.getResult().stream().anyMatch(object -> object.getKey().equals(userKey)));
206 
207             // USER_UPDATE
208             UserUR userUR = new UserUR();
209             userUR.setKey(userKey);
210             userUR.getResources().add(new StringPatchItem.Builder().
211                     value(RESOURCE_NAME_LDAP).operation(PatchOperation.DELETE).build());
212             // this will fail because unassigning resource-ldap would result in removing the user from the dynamic realm
213             try {
214                 delegatedUserService.update(userUR);
215                 fail("This should not happen");
216             } catch (SyncopeClientException e) {
217                 assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
218             }
219             // this will succeed instead
220             userUR.getResources().clear();
221             userUR.getResources().add(new StringPatchItem.Builder().value(RESOURCE_NAME_NOPROPAGATION).build());
222             user = delegatedUserService.update(userUR).
223                     readEntity(new GenericType<ProvisioningResult<UserTO>>() {
224                     }).getEntity();
225             assertNotNull(user);
226             assertTrue(user.getResources().contains(RESOURCE_NAME_NOPROPAGATION));
227 
228             // GROUP_UPDATE
229             GroupUR groupUR = new GroupUR();
230             groupUR.setKey(groupKey);
231             groupUR.getPlainAttrs().add(new AttrPatch.Builder(attr("icon", "modified")).build());
232             group = delegatedGroupService.update(groupUR).readEntity(new GenericType<ProvisioningResult<GroupTO>>() {
233             }).getEntity();
234             assertNotNull(group);
235             assertEquals("modified", group.getPlainAttr("icon").get().getValues().get(0));
236         } finally {
237             if (role != null) {
238                 ROLE_SERVICE.delete(role.getKey());
239             }
240             if (dynRealm != null) {
241                 DYN_REALM_SERVICE.delete(dynRealm.getKey());
242             }
243         }
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 
257             Response response = DYN_REALM_SERVICE.create(dynRealm);
258             dynRealm = getObject(response.getLocation(), DynRealmService.class, DynRealmTO.class);
259             assertNotNull(dynRealm);
260 
261             // 2. no dyn realm members
262             PagedResult<UserTO> matching = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
263                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
264             assertEquals(0, matching.getSize());
265 
266             // 3. create user with that attribute value
267             UserCR userCR = UserITCase.getUniqueSample("syncope1480@syncope.apache.org");
268             userCR.getPlainAttr("ctype").get().getValues().set(0, ctype);
269             UserTO user = createUser(userCR).getEntity();
270             assertNotNull(user.getKey());
271 
272             // 4a. check that Elasticsearch index was updated correctly
273             if (IS_EXT_SEARCH_ENABLED) {
274                 try {
275                     Thread.sleep(2000);
276                 } catch (InterruptedException ex) {
277                     // ignore
278                 }
279 
280                 ArrayNode dynRealms = fetchDynRealmsFromElasticsearch(user.getKey());
281                 assertEquals(1, dynRealms.size());
282                 assertEquals(dynRealm.getKey(), dynRealms.get(0).asText());
283             }
284 
285             // 4b. now there is 1 realm member
286             matching = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).fiql(
287                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
288             assertEquals(1, matching.getSize());
289 
290             // 5. change dyn realm condition
291             dynRealm.getDynMembershipConds().put(AnyTypeKind.USER.name(), "ctype==ANY");
292             DYN_REALM_SERVICE.update(dynRealm);
293 
294             // 6a. check that Elasticsearch index was updated correctly
295             if (IS_EXT_SEARCH_ENABLED) {
296                 try {
297                     Thread.sleep(2000);
298                 } catch (InterruptedException ex) {
299                     // ignore
300                 }
301 
302                 ArrayNode dynRealms = fetchDynRealmsFromElasticsearch(user.getKey());
303                 assertTrue(dynRealms.isEmpty());
304             }
305 
306             // 6b. no more dyn realm members
307             matching = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).fiql(
308                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(dynRealm.getKey()).query()).build());
309             assertEquals(0, matching.getSize());
310         } finally {
311             if (dynRealm != null) {
312                 DYN_REALM_SERVICE.delete(dynRealm.getKey());
313             }
314         }
315     }
316 
317     @Test
318     public void issueSYNCOPE1806() {
319         DynRealmTO realm1 = null;
320         DynRealmTO realm2 = null;
321         try {
322             // 1. create two dyn realms with same condition
323             realm1 = new DynRealmTO();
324             realm1.setKey("realm1");
325             realm1.getDynMembershipConds().put(AnyTypeKind.USER.name(), "cool==true");
326             realm1 = getObject(DYN_REALM_SERVICE.create(realm1).getLocation(), DynRealmService.class, DynRealmTO.class);
327             assertNotNull(realm1);
328 
329             realm2 = new DynRealmTO();
330             realm2.setKey("realm2");
331             realm2.getDynMembershipConds().put(AnyTypeKind.USER.name(), "cool==true");
332             realm2 = getObject(DYN_REALM_SERVICE.create(realm2).getLocation(), DynRealmService.class, DynRealmTO.class);
333             assertNotNull(realm2);
334 
335             // 2. verify that dynamic members are the same
336             if (IS_EXT_SEARCH_ENABLED) {
337                 try {
338                     Thread.sleep(2000);
339                 } catch (InterruptedException ex) {
340                     // ignore
341                 }
342             }
343 
344             PagedResult<UserTO> matching1 = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
345                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(realm1.getKey()).query()).build());
346             PagedResult<UserTO> matching2 = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
347                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(realm2.getKey()).query()).build());
348 
349             assertEquals(matching1, matching2);
350             assertEquals(1, matching1.getResult().size());
351             assertTrue(matching1.getResult().stream().
352                     anyMatch(u -> "c9b2dec2-00a7-4855-97c0-d854842b4b24".equals(u.getKey())));
353 
354             // 3. update an user to let them become part of both dyn realms
355             UserUR userUR = new UserUR();
356             userUR.setKey("823074dc-d280-436d-a7dd-07399fae48ec");
357             userUR.getPlainAttrs().add(new AttrPatch.Builder(new Attr.Builder("cool").value("true").build()).build());
358             updateUser(userUR);
359 
360             // 4. verify that dynamic members are still the same
361             if (IS_EXT_SEARCH_ENABLED) {
362                 try {
363                     Thread.sleep(2000);
364                 } catch (InterruptedException ex) {
365                     // ignore
366                 }
367             }
368 
369             matching1 = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
370                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(realm1.getKey()).query()).build());
371             matching2 = USER_SERVICE.search(new AnyQuery.Builder().realm("/").fiql(
372                     SyncopeClient.getUserSearchConditionBuilder().inDynRealms(realm2.getKey()).query()).build());
373             assertEquals(matching1, matching2);
374             assertEquals(2, matching1.getResult().size());
375             assertTrue(matching1.getResult().stream().
376                     anyMatch(u -> "c9b2dec2-00a7-4855-97c0-d854842b4b24".equals(u.getKey())));
377             assertTrue(matching1.getResult().stream().
378                     anyMatch(u -> "823074dc-d280-436d-a7dd-07399fae48ec".equals(u.getKey())));
379         } finally {
380             if (realm1 != null) {
381                 DYN_REALM_SERVICE.delete(realm1.getKey());
382             }
383             if (realm2 != null) {
384                 DYN_REALM_SERVICE.delete(realm2.getKey());
385             }
386             UserUR userUR = new UserUR();
387             userUR.setKey("823074dc-d280-436d-a7dd-07399fae48ec");
388             userUR.getPlainAttrs().add(new AttrPatch.Builder(new Attr.Builder("cool").build()).
389                     operation(PatchOperation.DELETE).build());
390             updateUser(userUR);
391         }
392     }
393 }