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