View Javadoc
1   /*
2    * Copyright 2022 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.acl.spring.data.mongodb;
18  
19  import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
20  import static org.springframework.util.ObjectUtils.isEmpty;
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import org.bremersee.acl.AccessEvaluation;
30  import org.bremersee.acl.Ace;
31  import org.bremersee.acl.Acl;
32  import org.bremersee.acl.AclUserContext;
33  import org.bremersee.acl.annotation.AclHolder;
34  import org.bremersee.acl.model.AccessControlEntryModifications;
35  import org.bremersee.acl.model.AccessControlListModifications;
36  import org.springframework.data.mongodb.core.query.Criteria;
37  import org.springframework.data.mongodb.core.query.Update;
38  import org.springframework.util.Assert;
39  
40  /**
41   * The acl criteria and update builder.
42   *
43   * @author Christian Bremer
44   */
45  public class AclCriteriaAndUpdateBuilder {
46  
47    private final String aclPath;
48  
49    /**
50     * Instantiates a new acl criteria and update builder.
51     *
52     * @param aclPath the acl path
53     */
54    public AclCriteriaAndUpdateBuilder(String aclPath) {
55      this.aclPath = Objects.isNull(aclPath) ? "" : aclPath;
56    }
57  
58    /**
59     * Instantiates a new acl criteria and update builder.
60     *
61     * @param entityClass the entity class
62     */
63    public AclCriteriaAndUpdateBuilder(Class<?> entityClass) {
64      Assert.notNull(entityClass, "Entity class must be present.");
65      this.aclPath = Optional
66          .ofNullable(findAnnotation(entityClass, AclHolder.class))
67          .map(AclHolder::path)
68          .orElseThrow(() -> new IllegalArgumentException(String
69              .format(
70                  "Entity class %s must be annotated with %s.",
71                  entityClass.getSimpleName(), AclHolder.class.getSimpleName())));
72    }
73  
74    /**
75     * Build update acl modification update.
76     *
77     * @param accessControlListModifications the access control list modifications
78     * @return the acl modification update
79     */
80    public AclModificationUpdate buildUpdate(
81        AccessControlListModifications accessControlListModifications) {
82  
83      Collection<AccessControlEntryModifications> mods = isEmpty(accessControlListModifications)
84          ? List.of()
85          : accessControlListModifications.getModificationsDistinct();
86  
87      Update addAndSetUpdate = new Update();
88      Update removeUpdate = new Update();
89      boolean isSomethingRemoved = false;
90  
91      for (AccessControlEntryModifications mod : mods) {
92  
93        // guest
94        addAndSetUpdate = addAndSetUpdate.set(
95            path(Acl.ENTRIES, mod.getPermission(), Ace.GUEST),
96            mod.isGuest());
97  
98        // users
99        if (!mod.getAddUsers().isEmpty()) {
100         addAndSetUpdate = addAndSetUpdate
101             .addToSet(path(Acl.ENTRIES, mod.getPermission(), Ace.USERS))
102             .each((Object[]) mod.getAddUsers().toArray(new String[0]));
103       }
104       if (!mod.getRemoveUsers().isEmpty()) {
105         isSomethingRemoved = true;
106         removeUpdate = removeUpdate.pullAll(
107             path(Acl.ENTRIES, mod.getPermission(), Ace.USERS),
108             mod.getRemoveUsers().toArray(new String[0]));
109       }
110 
111       // roles
112       if (!mod.getAddRoles().isEmpty()) {
113         addAndSetUpdate = addAndSetUpdate
114             .addToSet(path(Acl.ENTRIES, mod.getPermission(), Ace.ROLES))
115             .each((Object[]) mod.getAddRoles().toArray(new String[0]));
116       }
117       if (!mod.getRemoveRoles().isEmpty()) {
118         isSomethingRemoved = true;
119         removeUpdate = removeUpdate.pullAll(
120             path(Acl.ENTRIES, mod.getPermission(), Ace.ROLES),
121             mod.getRemoveRoles().toArray(new String[0]));
122       }
123 
124       if (!mod.getAddGroups().isEmpty()) {
125         addAndSetUpdate = addAndSetUpdate
126             .addToSet(path(Acl.ENTRIES, mod.getPermission(), Ace.GROUPS))
127             .each((Object[]) mod.getAddGroups().toArray(new String[0]));
128       }
129       if (!mod.getRemoveGroups().isEmpty()) {
130         isSomethingRemoved = true;
131         removeUpdate = removeUpdate.pullAll(
132             path(Acl.ENTRIES, mod.getPermission(), Ace.GROUPS),
133             mod.getRemoveGroups().toArray(new String[0]));
134       }
135     }
136     return AclModificationUpdate.builder()
137         .preparationUpdates(isSomethingRemoved ? List.of(addAndSetUpdate) : List.of())
138         .finalUpdate(isSomethingRemoved ? removeUpdate : addAndSetUpdate)
139         .build();
140   }
141 
142   /**
143    * Build update.
144    *
145    * @param acl the acl
146    * @return the update
147    */
148   public Update buildUpdate(Acl acl) {
149     return Update.update(path(), isEmpty(acl) ? Acl.builder().build() : acl);
150   }
151 
152   /**
153    * Build update.
154    *
155    * @param newOwner the new owner
156    * @return the update
157    */
158   public Update buildUpdate(String newOwner) {
159     return Update.update(path(Acl.OWNER), isEmpty(newOwner) ? "" : newOwner);
160   }
161 
162   /**
163    * Build update owner criteria.
164    *
165    * @param userContext the user context
166    * @return the criteria
167    */
168   public Criteria buildUpdateOwnerCriteria(AclUserContext userContext) {
169     Assert.notNull(userContext, "User context must be present.");
170     return Criteria.where(path(Acl.OWNER)).is(userContext.getName());
171   }
172 
173   /**
174    * Build permission criteria.
175    *
176    * @param userContext the user context
177    * @param accessEvaluation the access evaluation
178    * @param permissions the permissions
179    * @return the criteria
180    */
181   public Criteria buildPermissionCriteria(
182       AclUserContext userContext,
183       AccessEvaluation accessEvaluation,
184       Collection<String> permissions) {
185 
186     Assert.notNull(userContext, "User context must be present.");
187     Assert.notNull(accessEvaluation, "Access evaluation type must be present.");
188     Assert.notEmpty(permissions, "At least one permission must be present.");
189 
190     List<Criteria> permissionCriteriaList = Set.copyOf(permissions).stream()
191         .map(permission -> createAccessCriteria(userContext, permission))
192         .collect(Collectors.toList());
193     Criteria permissionCriteria = accessEvaluation.isAnyPermission()
194         ? new Criteria().orOperator(permissionCriteriaList)
195         : new Criteria().andOperator(permissionCriteriaList);
196     if (userContext.getName().isBlank()) {
197       return permissionCriteria;
198     }
199     Criteria ownerCriteria = Criteria.where(path(Acl.OWNER)).is(userContext.getName());
200     return new Criteria().orOperator(ownerCriteria, permissionCriteria);
201   }
202 
203   private Criteria createAccessCriteria(
204       AclUserContext userContext,
205       String permission) {
206 
207     List<Criteria> criteriaList = new ArrayList<>();
208     criteriaList.add(Criteria.where(path(Acl.ENTRIES, permission, Ace.GUEST)).is(true));
209     if (!userContext.getName().isBlank()) {
210       criteriaList.add(Criteria
211           .where(path(Acl.ENTRIES, permission, Ace.USERS))
212           .all(userContext.getName()));
213     }
214     criteriaList.addAll(userContext.getRoles().stream()
215         .filter(role -> !isEmpty(role))
216         .map(role -> Criteria.
217             where(path(Acl.ENTRIES, permission, Ace.ROLES))
218             .all(role))
219         .toList()
220     );
221     criteriaList.addAll(userContext.getGroups().stream()
222         .filter(group -> !isEmpty(group))
223         .map(group -> Criteria
224             .where(path(Acl.ENTRIES, permission, Ace.GROUPS))
225             .all(group))
226         .toList()
227     );
228     return new Criteria().orOperator(criteriaList);
229   }
230 
231   private String path(String... pathSegments) {
232     if (isEmpty(pathSegments)) {
233       return aclPath;
234     }
235     return aclPath + "." + String.join(".", pathSegments);
236   }
237 
238 }