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.core.persistence.jpa.dao;
20  
21  import java.beans.PropertyDescriptor;
22  import java.lang.annotation.Annotation;
23  import java.lang.reflect.Method;
24  import java.time.OffsetDateTime;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.regex.Pattern;
30  import java.util.stream.Collectors;
31  import javax.persistence.Entity;
32  import javax.validation.ValidationException;
33  import javax.validation.constraints.Max;
34  import javax.validation.constraints.Min;
35  import org.apache.commons.lang3.BooleanUtils;
36  import org.apache.commons.lang3.ClassUtils;
37  import org.apache.syncope.common.lib.SyncopeConstants;
38  import org.apache.syncope.common.lib.types.AnyTypeKind;
39  import org.apache.syncope.common.lib.types.AttrSchemaType;
40  import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager;
41  import org.apache.syncope.core.persistence.api.dao.AnyMatchDAO;
42  import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
43  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
44  import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
45  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
46  import org.apache.syncope.core.persistence.api.dao.UserDAO;
47  import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
48  import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
49  import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
50  import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond;
51  import org.apache.syncope.core.persistence.api.dao.search.MemberCond;
52  import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
53  import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
54  import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond;
55  import org.apache.syncope.core.persistence.api.dao.search.ResourceCond;
56  import org.apache.syncope.core.persistence.api.dao.search.RoleCond;
57  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
58  import org.apache.syncope.core.persistence.api.entity.Any;
59  import org.apache.syncope.core.persistence.api.entity.AnyUtils;
60  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
61  import org.apache.syncope.core.persistence.api.entity.GroupableRelatable;
62  import org.apache.syncope.core.persistence.api.entity.PlainAttr;
63  import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
64  import org.apache.syncope.core.persistence.api.entity.PlainSchema;
65  import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
66  import org.apache.syncope.core.persistence.api.entity.group.Group;
67  import org.apache.syncope.core.persistence.api.entity.user.User;
68  import org.apache.syncope.core.persistence.jpa.entity.JPAPlainSchema;
69  import org.springframework.beans.BeanUtils;
70  import org.springframework.transaction.annotation.Transactional;
71  
72  public class JPAAnyMatchDAO extends AbstractDAO<Any<?>> implements AnyMatchDAO {
73  
74      protected final UserDAO userDAO;
75  
76      protected final GroupDAO groupDAO;
77  
78      protected final AnyObjectDAO anyObjectDAO;
79  
80      protected final RealmDAO realmDAO;
81  
82      protected final PlainSchemaDAO plainSchemaDAO;
83  
84      protected final AnyUtilsFactory anyUtilsFactory;
85  
86      protected final PlainAttrValidationManager validator;
87  
88      public JPAAnyMatchDAO(
89              final UserDAO userDAO,
90              final GroupDAO groupDAO,
91              final AnyObjectDAO anyObjectDAO,
92              final RealmDAO realmDAO,
93              final PlainSchemaDAO plainSchemaDAO,
94              final AnyUtilsFactory anyUtilsFactory,
95              final PlainAttrValidationManager validator) {
96  
97          this.userDAO = userDAO;
98          this.groupDAO = groupDAO;
99          this.anyObjectDAO = anyObjectDAO;
100         this.realmDAO = realmDAO;
101         this.plainSchemaDAO = plainSchemaDAO;
102         this.anyUtilsFactory = anyUtilsFactory;
103         this.validator = validator;
104     }
105 
106     /**
107      * Verify if any matches the given search condition.
108      *
109      * @param any to be checked
110      * @param cond to be verified
111      * @param <T> any
112      * @return true if any matches cond
113      */
114     @Transactional(readOnly = true)
115     @Override
116     public <T extends Any<?>> boolean matches(final T any, final SearchCond cond) {
117         boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
118         switch (cond.getType()) {
119             case LEAF:
120             case NOT_LEAF:
121                 Boolean match = cond.getLeaf(AnyTypeCond.class).
122                         filter(leaf -> AnyTypeKind.ANY_OBJECT == any.getType().getKind()).
123                         map(leaf -> matches(any, leaf, not)).
124                         orElse(null);
125 
126                 if (match == null) {
127                     match = cond.getLeaf(RelationshipTypeCond.class).
128                             filter(leaf -> any instanceof GroupableRelatable).
129                             map(leaf -> matches((GroupableRelatable) any, leaf, not)).
130                             orElse(null);
131                 }
132 
133                 if (match == null) {
134                     match = cond.getLeaf(RelationshipCond.class).
135                             filter(leaf -> any instanceof GroupableRelatable).
136                             map(leaf -> matches((GroupableRelatable) any, leaf, not)).
137                             orElse(null);
138                 }
139 
140                 if (match == null) {
141                     match = cond.getLeaf(MembershipCond.class).
142                             filter(leaf -> any instanceof GroupableRelatable).
143                             map(leaf -> matches((GroupableRelatable) any, leaf, not)).
144                             orElse(null);
145                 }
146 
147                 if (match == null) {
148                     match = cond.getLeaf(RoleCond.class).
149                             filter(leaf -> any instanceof User).
150                             map(leaf -> matches((User) any, leaf, not)).
151                             orElse(null);
152                 }
153 
154                 if (match == null) {
155                     match = cond.getLeaf(DynRealmCond.class).
156                             map(leaf -> matches(any, leaf, not)).
157                             orElse(null);
158                 }
159 
160                 if (match == null) {
161                     match = cond.getLeaf(MemberCond.class).
162                             filter(leaf -> any instanceof Group).
163                             map(leaf -> matches((Group) any, leaf, not)).
164                             orElse(null);
165                 }
166 
167                 if (match == null) {
168                     match = cond.getLeaf(ResourceCond.class).
169                             map(leaf -> matches(any, leaf, not)).
170                             orElse(null);
171                 }
172 
173                 if (match == null) {
174                     match = cond.getLeaf(AnyCond.class).
175                             map(value -> matches(any, value, not)).
176                             orElseGet(() -> cond.getLeaf(AttrCond.class).
177                             map(leaf -> matches(any, leaf, not)).
178                             orElse(null));
179                 }
180 
181                 if (match == null) {
182                     match = cond.getLeaf(AttrCond.class).
183                             map(leaf -> matches(any, leaf, not)).
184                             orElse(null);
185                 }
186 
187                 return BooleanUtils.toBoolean(match);
188 
189             case AND:
190                 return matches(any, cond.getLeft()) && matches(any, cond.getRight());
191 
192             case OR:
193                 return matches(any, cond.getLeft()) || matches(any, cond.getRight());
194 
195             default:
196         }
197 
198         return false;
199     }
200 
201     protected boolean matches(final Any<?> any, final AnyTypeCond cond, final boolean not) {
202         boolean equals = any.getType().getKey().equals(cond.getAnyTypeKey());
203         return not ? !equals : equals;
204     }
205 
206     protected boolean matches(
207             final GroupableRelatable<?, ?, ?, ?, ?> any, final RelationshipTypeCond cond, final boolean not) {
208 
209         boolean found = any.getRelationships().stream().
210                 anyMatch(rel -> rel.getType().getKey().equals(cond.getRelationshipTypeKey()));
211         return not ? !found : found;
212     }
213 
214     protected boolean matches(
215             final GroupableRelatable<?, ?, ?, ?, ?> any, final RelationshipCond cond, final boolean not) {
216 
217         Set<String> candidates = SyncopeConstants.UUID_PATTERN.matcher(cond.getAnyObject()).matches()
218                 ? Optional.ofNullable(cond.getAnyObject()).map(Set::of).orElse(Set.of())
219                 : anyObjectDAO.findByName(cond.getAnyObject()).stream().
220                         map(AnyObject::getKey).collect(Collectors.toSet());
221 
222         boolean found = any.getRelationships().stream().
223                 map(r -> r.getRightEnd().getKey()).
224                 filter(candidates::contains).
225                 count() > 0;
226 
227         return not ? !found : found;
228     }
229 
230     protected boolean matches(
231             final GroupableRelatable<?, ?, ?, ?, ?> any, final MembershipCond cond, final boolean not) {
232 
233         final String group = SyncopeConstants.UUID_PATTERN.matcher(cond.getGroup()).matches()
234                 ? cond.getGroup()
235                 : groupDAO.findKey(cond.getGroup());
236 
237         boolean found = any.getMembership(group).isPresent()
238                 || (any instanceof User
239                         ? userDAO.findDynGroups(any.getKey())
240                         : anyObjectDAO.findDynGroups(any.getKey())).stream().
241                         anyMatch(item -> item.getKey().equals(group));
242         return not ? !found : found;
243     }
244 
245     protected boolean matches(final User user, final RoleCond cond, final boolean not) {
246         boolean found = userDAO.findAllRoles(user).stream().anyMatch(role -> role.getKey().equals(cond.getRole()));
247         return not ? !found : found;
248     }
249 
250     protected boolean matches(final Any<?> any, final DynRealmCond cond, final boolean not) {
251         boolean found = anyUtilsFactory.getInstance(any).dao().findDynRealms(any.getKey()).stream().
252                 anyMatch(dynRealm -> dynRealm.equals(cond.getDynRealm()));
253         return not ? !found : found;
254     }
255 
256     protected boolean matches(final Group group, final MemberCond cond, final boolean not) {
257         boolean found = false;
258 
259         GroupableRelatable<?, ?, ?, ?, ?> any = userDAO.find(cond.getMember());
260         if (any == null) {
261             any = anyObjectDAO.find(cond.getMember());
262             if (any != null) {
263                 found = groupDAO.findAMemberships(group).stream().
264                         anyMatch(memb -> memb.getLeftEnd().getKey().equals(cond.getMember()))
265                         || groupDAO.findADynMembers(group).contains(cond.getMember());
266             }
267         } else {
268             found = groupDAO.findUMemberships(group).stream().
269                     anyMatch(memb -> memb.getLeftEnd().getKey().equals(cond.getMember()))
270                     || groupDAO.findUDynMembers(group).contains(cond.getMember());
271         }
272 
273         return not ? !found : found;
274     }
275 
276     protected boolean matches(final Any<?> any, final ResourceCond cond, final boolean not) {
277         boolean found = anyUtilsFactory.getInstance(any).getAllResources(any).stream().
278                 anyMatch(resource -> resource.getKey().equals(cond.getResource()));
279         return not ? !found : found;
280     }
281 
282     @SuppressWarnings({ "unchecked", "rawtypes" })
283     protected boolean matches(
284             final List<? extends PlainAttrValue> anyAttrValues,
285             final PlainAttrValue attrValue,
286             final PlainSchema schema,
287             final AttrCond cond) {
288 
289         return anyAttrValues.stream().anyMatch(item -> {
290             switch (cond.getType()) {
291                 case EQ:
292                     return attrValue.getValue().equals(item.getValue());
293 
294                 case IEQ:
295                     if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) {
296                         return attrValue.getStringValue().equalsIgnoreCase(item.getStringValue());
297                     } else {
298                         LOG.error("IEQ is only compatible with string or enum schemas");
299                         return false;
300                     }
301 
302                 case LIKE:
303                 case ILIKE:
304                     if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) {
305                         StringBuilder output = new StringBuilder();
306                         for (char c : cond.getExpression().toLowerCase().toCharArray()) {
307                             if (c == '%') {
308                                 output.append(".*");
309                             } else if (Character.isLetter(c)) {
310                                 output.append('[').
311                                         append(c).
312                                         append(Character.toUpperCase(c)).
313                                         append(']');
314                             } else {
315                                 output.append(c);
316                             }
317                         }
318                         return (cond.getType() == AttrCond.Type.LIKE
319                                 ? Pattern.compile(output.toString())
320                                 : Pattern.compile(output.toString(), Pattern.CASE_INSENSITIVE)).
321                                 matcher(item.getStringValue()).matches();
322                     } else {
323                         LOG.error("LIKE is only compatible with string or enum schemas");
324                         return false;
325                     }
326 
327                 case GT:
328                     return item.<Comparable>getValue().compareTo(attrValue.getValue()) > 0;
329 
330                 case GE:
331                     return item.<Comparable>getValue().compareTo(attrValue.getValue()) >= 0;
332 
333                 case LT:
334                     return item.<Comparable>getValue().compareTo(attrValue.getValue()) < 0;
335 
336                 case LE:
337                     return item.<Comparable>getValue().compareTo(attrValue.getValue()) <= 0;
338 
339                 default:
340                     return false;
341             }
342         });
343     }
344 
345     protected boolean matches(final Any<?> any, final AttrCond cond, final boolean not) {
346         PlainSchema schema = plainSchemaDAO.find(cond.getSchema());
347         if (schema == null) {
348             LOG.warn("Ignoring invalid schema '{}'", cond.getSchema());
349             return false;
350         }
351 
352         @SuppressWarnings("unchecked")
353         Optional<PlainAttr<?>> attr = (Optional<PlainAttr<?>>) any.getPlainAttr(cond.getSchema());
354 
355         boolean found;
356         switch (cond.getType()) {
357             case ISNULL:
358                 found = attr.isEmpty();
359                 break;
360 
361             case ISNOTNULL:
362                 found = attr.isPresent();
363                 break;
364 
365             default:
366                 PlainAttrValue attrValue = anyUtilsFactory.getInstance(any).newPlainAttrValue();
367                 try {
368                     if (cond.getType() != AttrCond.Type.LIKE
369                             && cond.getType() != AttrCond.Type.ILIKE
370                             && cond.getType() != AttrCond.Type.ISNULL
371                             && cond.getType() != AttrCond.Type.ISNOTNULL) {
372 
373                         validator.validate(schema, cond.getExpression(), attrValue);
374                     }
375                 } catch (ValidationException e) {
376                     LOG.error("Could not validate expression '" + cond.getExpression() + '\'', e);
377                     return false;
378                 }
379 
380                 found = attr.map(a -> matches(a.getValues(), attrValue, schema, cond)).orElse(false);
381         }
382         return not ? !found : found;
383     }
384 
385     protected boolean matches(final Any<?> any, final AnyCond cond, final boolean not) {
386         // Keeps track of difference between entity's getKey() and JPA @Id fields
387         if ("key".equals(cond.getSchema())) {
388             cond.setSchema("id");
389         }
390 
391         PropertyDescriptor pd;
392         Object anyAttrValue;
393         try {
394             pd = BeanUtils.getPropertyDescriptor(any.getClass(), cond.getSchema());
395             if (pd == null) {
396                 LOG.warn("Ignoring invalid schema '{}'", cond.getSchema());
397                 return false;
398             }
399 
400             anyAttrValue = pd.getReadMethod().invoke(any);
401         } catch (Exception e) {
402             LOG.error("While accessing {}.{}", any, cond.getSchema(), e);
403             return false;
404         }
405 
406         boolean found;
407         switch (cond.getType()) {
408             case ISNULL:
409                 found = anyAttrValue == null;
410                 break;
411 
412             case ISNOTNULL:
413                 found = anyAttrValue != null;
414                 break;
415 
416             default:
417                 PlainSchema schema = new JPAPlainSchema();
418                 schema.setKey(pd.getName());
419                 for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) {
420                     if (pd.getPropertyType().isAssignableFrom(attrSchemaType.getType())) {
421                         schema.setType(attrSchemaType);
422                     }
423                 }
424 
425                 // Deal with any Integer fields logically mapping to boolean values
426                 boolean foundBooleanMin = false;
427                 boolean foundBooleanMax = false;
428                 if (Integer.class.equals(pd.getPropertyType())) {
429                     for (Annotation annotation : pd.getPropertyType().getAnnotations()) {
430                         if (Min.class.equals(annotation.annotationType())) {
431                             foundBooleanMin = ((Min) annotation).value() == 0;
432                         } else if (Max.class.equals(annotation.annotationType())) {
433                             foundBooleanMax = ((Max) annotation).value() == 1;
434                         }
435                     }
436                 }
437                 if (foundBooleanMin && foundBooleanMax) {
438                     schema.setType(AttrSchemaType.Boolean);
439                 }
440 
441                 // Deal with any fields representing relationships to other entities
442                 if (pd.getPropertyType().getAnnotation(Entity.class) != null) {
443                     Method relMethod = null;
444                     try {
445                         relMethod = ClassUtils.getPublicMethod(pd.getPropertyType(), "getKey", new Class<?>[0]);
446                     } catch (Exception e) {
447                         LOG.error("Could not find {}#getKey", pd.getPropertyType(), e);
448                     }
449 
450                     if (relMethod != null && String.class.isAssignableFrom(relMethod.getReturnType())) {
451                         cond.setSchema(cond.getSchema() + "_id");
452                         schema.setType(AttrSchemaType.String);
453                     }
454                 }
455 
456                 AnyUtils anyUtils = anyUtilsFactory.getInstance(any);
457 
458                 PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
459                 if (cond.getType() != AttrCond.Type.LIKE
460                         && cond.getType() != AttrCond.Type.ILIKE
461                         && cond.getType() != AttrCond.Type.ISNULL
462                         && cond.getType() != AttrCond.Type.ISNOTNULL) {
463 
464                     try {
465                         validator.validate(schema, cond.getExpression(), attrValue);
466                     } catch (ValidationException e) {
467                         LOG.error("Could not validate expression '" + cond.getExpression() + '\'', e);
468                         return false;
469                     }
470                 }
471 
472                 List<PlainAttrValue> anyAttrValues = new ArrayList<>();
473                 anyAttrValues.add(anyUtils.newPlainAttrValue());
474                 if (anyAttrValue instanceof String) {
475                     anyAttrValues.get(0).setStringValue((String) anyAttrValue);
476                 } else if (anyAttrValue instanceof Long) {
477                     anyAttrValues.get(0).setLongValue((Long) anyAttrValue);
478                 } else if (anyAttrValue instanceof Double) {
479                     anyAttrValues.get(0).setDoubleValue((Double) anyAttrValue);
480                 } else if (anyAttrValue instanceof Boolean) {
481                     anyAttrValues.get(0).setBooleanValue((Boolean) anyAttrValue);
482                 } else if (anyAttrValue instanceof OffsetDateTime) {
483                     anyAttrValues.get(0).setDateValue((OffsetDateTime) anyAttrValue);
484                 } else if (anyAttrValue instanceof byte[]) {
485                     anyAttrValues.get(0).setBinaryValue((byte[]) anyAttrValue);
486                 }
487 
488                 found = matches(anyAttrValues, attrValue, schema, cond);
489         }
490         return not ? !found : found;
491     }
492 }