/
PermissionSubjectsMap.java
executable file
·299 lines (263 loc) · 11.8 KB
/
PermissionSubjectsMap.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
/*
* Copyright (c) 2017 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.model.enforcers.trie;
import static org.eclipse.ditto.model.base.common.ConditionChecker.checkNotNull;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Weighted N-to-N relation (en.wikipedia.org/wiki/Binary_relation) between permissions and authorization subjects.
* All uses of the words "relation" and "relate" on this page refers to binary relations in the mathematical sense as
* defined in the linked Wikipedia page.
*/
@NotThreadSafe
final class PermissionSubjectsMap extends AbstractMap<String, Map<String, Integer>> {
private final Map<String, Map<String, Integer>> data;
/**
* Constructs a new {@code PermissionSubjectsMap} object.
*/
PermissionSubjectsMap() {
data = new HashMap<>();
}
@Override
public Set<Entry<String, Map<String, Integer>>> entrySet() {
return data.entrySet();
}
@Override
public Map<String, Integer> put(final String key, final Map<String, Integer> value) {
return data.put(checkNotNull(key, "key"), checkNotNull(value, "value"));
}
/**
* Augment this relation by a total relation between a set of permissions and a set of authorization subject IDs.
* Every pair in the total relation has weight 0.
*
* @param permissions Left projection of the added total relation.
* @param subjectIds Right projection of the added total relation.
* @throws NullPointerException if any argument is {@code null}.
*/
void addTotalRelationOfWeightZero(final Iterable<String> permissions, final Collection<String> subjectIds) {
validatePermissions(permissions);
validateSubjectIds(subjectIds);
final Map<String, Integer> subjectsWithDefaultWeight = subjectIds.stream()
.collect(Collectors.toMap(Function.identity(), subject -> 0));
permissions.forEach(permission -> addPermissionSubjects(permission, subjectsWithDefaultWeight));
}
/**
* If <em>some</em> of the given permissions are related to some of the given subject IDs, then return the maximum
* weight of related permission-subject pairs among the given. Mathematically, intersect this relation with the
* Cartesian product of {@code permissions} and {@code subjectIds}, then return the maximum weight in the resulting
* relation.
*
* @param subjectIds The set of subject IDs to check.
* @param permissions The set of permissions to check.
* @return Either the maximum weight of given subject IDs related to the given permissions or
* {@code Optional.empty()}.
* @throws NullPointerException if any argument is {@code null}.
*/
Optional<Integer> getMaxWeightForAllPermissions(final Set<String> subjectIds,
final Collection<String> permissions) {
validateSubjectIds(subjectIds);
validatePermissions(permissions);
return permissions.stream()
.flatMap(permission -> {
final Map<String, Integer> permittedSubjects = getOrDefault(permission, Collections.emptyMap());
return intersect(subjectIds, permittedSubjects.keySet()).map(permittedSubjects::get);
})
.max(Comparator.naturalOrder());
}
/**
* If <em>all</em>of the given permissions are related to some of the given subject IDs, then return the maximum
* weight of the related subject IDs among the given subject IDs; otherwise return {@code Optional.empty()}.
*
* @param subjectIds The set of subject IDs to check.
* @param permissions The set of permissions to check.
* @return Either the maximum weight of given subject IDs related to the given permissions, or
* {@code Optional.empty()}.
* @throws NullPointerException if any argument is {@code null}.
*/
Optional<Integer> getMaxNonemptyWeightForAllPermissions(final Set<String> subjectIds,
final Collection<String> permissions) {
validateSubjectIds(subjectIds);
validatePermissions(permissions);
final List<Optional<Integer>> permissionWeights = permissions.stream()
.map(permission -> {
final Map<String, Integer> permittedSubjects = getOrDefault(permission, Collections.emptyMap());
return intersect(subjectIds, permittedSubjects.keySet())
.map(permittedSubjects::get)
.max(Comparator.naturalOrder());
})
.collect(Collectors.toList());
if (permissionWeights.stream().anyMatch(maybeWeight -> !maybeWeight.isPresent())) {
return Optional.empty();
} else {
return permissionWeights.stream().map(Optional::get).max(Comparator.naturalOrder());
}
}
/**
* Returns the set of subjects each of which is related to <em>some</em> permission among the given.
*
* @param permissions The set of permissions to check.
* @return The set of subjects related to some permission among the given.
* @throws NullPointerException if {@code permissions} is {@code null}.
*/
Map<String, Integer> getSubjectUnion(final Set<String> permissions) {
validatePermissions(permissions);
final Map<String, Integer> subjectUnion = new HashMap<>();
intersect(keySet(), permissions)
.flatMap(permission -> get(permission).entrySet().stream())
.forEach(entry -> subjectUnion.compute(entry.getKey(), (subject, weight) ->
weight == null ? entry.getValue() : Math.max(entry.getValue(), weight)));
return subjectUnion;
}
/**
* Returns the set of subjects each of which is related to <em>all</em> given permissions.
*
* @param permissions The set of permissions to check.
* @return The set of subjects each of which is related to all given permissions.
* @throws NullPointerException if {@code permissions} is {@code null}.
*/
Map<String, Integer> getSubjectIntersect(final Set<String> permissions) {
validatePermissions(permissions);
final Stream<Map<String, Integer>> subjectsOfPermissions = intersect(keySet(), permissions).map(this::get);
final Optional<Map<String, Integer>> reduceResult = subjectsOfPermissions.reduce((map1, map2) ->
intersect(map1.keySet(), map2.keySet())
.collect(Collectors.toMap(Function.identity(), key -> Math.max(map1.get(key), map2.get(key)))));
return reduceResult.orElse(Collections.emptyMap());
}
/**
* Returns a copy of this relation.
*
* @return The copy.
*/
PermissionSubjectsMap copy() {
final PermissionSubjectsMap copy = new PermissionSubjectsMap();
forEach((permission, subjectMap) -> copy.put(permission, new HashMap<>(subjectMap)));
return copy;
}
/**
* Returns a copy of this relation where the weight of each permission-subject pair is increased by 1.
*
* @return The copy with incremented weight.
*/
PermissionSubjectsMap copyWithIncrementedWeight() {
return copyWithWeightAdjustment(1);
}
/**
* Returns a copy of this relation where the weight of each permission-subject pair is decreased by 1.
*
* @return The copy with decremented weight.
*/
PermissionSubjectsMap copyWithDecrementedWeight() {
return copyWithWeightAdjustment(-1);
}
/**
* Removes all permission-subject pairs in the given relation {@code update}.
*
* @param update The relation to delete from this.
* @return This object after the mutation.
* @throws NullPointerException if {@code update} is {@code null}.
*/
PermissionSubjectsMap removeAllEntriesFrom(final PermissionSubjectsMap update) {
checkNotNull(update, "relation to be deleted");
update.forEach((permission, subjectMap) -> removePermissionSubjects(permission, subjectMap.keySet()));
return this;
}
private void removePermissionSubjects(final String permission, final Iterable<String> subjectIds) {
compute(permission, (p, subjectMap) -> {
if (subjectMap == null) {
return null;
} else {
subjectIds.forEach(subjectMap::remove);
return subjectMap;
}
});
}
/**
* Add all permission-subject pairs in the given relation to this relation such that the weight of each pair is
* the maximum weight of the pair in both relations. Mathematically, compute the union of this relation with the
* relation {@code other} such that pairs are assigned their maximum weight in both relations, then replace this
* object by the union.
*
* @param other The relation to add to this.
* @return This object after the mutation.
* @throws NullPointerException if {@code other} is {@code null}.
*/
PermissionSubjectsMap addAllEntriesFrom(final PermissionSubjectsMap other) {
checkNotNull(other, "relation to be added");
other.forEach(this::addPermissionSubjects);
return this;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
final PermissionSubjectsMap that = (PermissionSubjectsMap) o;
return Objects.equals(data, that.data);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), data);
}
private static void validateSubjectIds(final Collection<String> subjectIds) {
checkNotNull(subjectIds, "subject IDs to check");
}
private static void validatePermissions(final Object permissions) {
checkNotNull(permissions, "permissions to check");
}
private void addPermissionSubjects(final String permission, final Map<String, Integer> subjects) {
compute(permission, (p, subjectMap) -> {
if (subjectMap == null) {
return new HashMap<>(subjects);
} else {
subjects.forEach((subject, thatWeight) -> {
subjectMap.compute(subject, (s, thisWeight) ->
thisWeight == null ? thatWeight : Math.max(thisWeight, thatWeight));
});
return subjectMap;
}
});
}
private static <T> Stream<T> intersect(final Set<T> set1, final Set<T> set2) {
return set1.size() <= set2.size()
? set1.stream().filter(set2::contains)
: set2.stream().filter(set1::contains);
}
private PermissionSubjectsMap copyWithWeightAdjustment(final int adjustment) {
final PermissionSubjectsMap copy = new PermissionSubjectsMap();
forEach((permission, subjectMap) -> {
final Map<String, Integer> adjustedSubjectMap = subjectMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue() + adjustment));
copy.put(permission, adjustedSubjectMap);
});
return copy;
}
}