1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.authorization.xacml;
23
24 import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY_EPISODE;
25 import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY_SERIES;
26 import static org.opencastproject.security.util.SecurityUtil.getEpisodeRoleId;
27 import static org.opencastproject.util.data.Tuple.tuple;
28
29 import org.opencastproject.mediapackage.Attachment;
30 import org.opencastproject.mediapackage.MediaPackage;
31 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
32 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
33 import org.opencastproject.mediapackage.MediaPackageException;
34 import org.opencastproject.mediapackage.MediaPackageSerializer;
35 import org.opencastproject.security.api.AccessControlEntry;
36 import org.opencastproject.security.api.AccessControlList;
37 import org.opencastproject.security.api.AclScope;
38 import org.opencastproject.security.api.AuthorizationService;
39 import org.opencastproject.security.api.Role;
40 import org.opencastproject.security.api.SecurityService;
41 import org.opencastproject.security.api.User;
42 import org.opencastproject.util.MimeTypes;
43 import org.opencastproject.util.NotFoundException;
44 import org.opencastproject.util.data.Tuple;
45 import org.opencastproject.workspace.api.Workspace;
46
47 import org.apache.commons.io.IOUtils;
48 import org.apache.commons.lang3.StringUtils;
49 import org.osgi.service.component.ComponentContext;
50 import org.osgi.service.component.annotations.Activate;
51 import org.osgi.service.component.annotations.Component;
52 import org.osgi.service.component.annotations.Modified;
53 import org.osgi.service.component.annotations.Reference;
54 import org.osgi.service.component.annotations.ReferenceCardinality;
55 import org.osgi.service.component.annotations.ReferencePolicy;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.net.URI;
62 import java.net.URISyntaxException;
63 import java.util.Arrays;
64 import java.util.Optional;
65
66 import javax.xml.bind.JAXBException;
67
68
69
70
71 @Component(
72 property = {
73 "service.description=Provides translation between access control entries and xacml documents"
74 },
75 service = { AuthorizationService.class }
76 )
77 public class XACMLAuthorizationService implements AuthorizationService {
78
79
80 private static final Logger logger = LoggerFactory.getLogger(XACMLAuthorizationService.class);
81
82
83 private static final String XACML_FILENAME = "xacml.xml";
84
85
86 protected Workspace workspace;
87
88
89 protected SecurityService securityService;
90
91
92 private MediaPackageSerializer serializer;
93
94 private static final String CONFIG_MERGE_MODE = "merge.mode";
95
96
97 private static MergeMode mergeMode = MergeMode.OVERRIDE;
98
99 enum MergeMode {
100 OVERRIDE, ROLES, ACTIONS
101 }
102
103 @Activate
104 @Modified
105 public void activate(ComponentContext cc) {
106 var properties = cc.getProperties();
107
108 if (properties == null) {
109 mergeMode = MergeMode.OVERRIDE;
110 logger.debug("Merge mode set to {}", mergeMode);
111 logger.debug("Using episode ID roles is deactivated");
112 return;
113 }
114 final String mode = StringUtils.defaultIfBlank((String) properties.get(CONFIG_MERGE_MODE),
115 MergeMode.OVERRIDE.toString());
116 try {
117 mergeMode = MergeMode.valueOf(mode.toUpperCase());
118 } catch (IllegalArgumentException e) {
119 logger.warn("Invalid value set for ACL merge mode, defaulting to {}", MergeMode.OVERRIDE);
120 mergeMode = MergeMode.OVERRIDE;
121 }
122 logger.debug("Merge mode set to {}", mergeMode);
123 }
124
125 @Reference(
126 cardinality = ReferenceCardinality.OPTIONAL,
127 policy = ReferencePolicy.DYNAMIC,
128 unbind = "unsetMediaPackageSerializer",
129 target = "(service.pid=org.opencastproject.mediapackage.ChainingMediaPackageSerializer)"
130 )
131 public void setMediaPackageSerializer(MediaPackageSerializer serializer) {
132 this.serializer = serializer;
133 }
134
135 protected void unsetMediaPackageSerializer(MediaPackageSerializer serializer) {
136 if (this.serializer == serializer) {
137 this.serializer = null;
138 }
139 }
140
141 @Override
142 public Tuple<AccessControlList, AclScope> getActiveAcl(final MediaPackage mp) {
143 logger.debug("getActiveACl for media package {}", mp.getIdentifier());
144 return getAcl(mp, AclScope.Episode);
145 }
146
147 @Override
148 public Tuple<AccessControlList, AclScope> getAcl(final MediaPackage mp, final AclScope scope) {
149 Optional<AccessControlList> episode = Optional.empty();
150 Optional<AccessControlList> series = Optional.empty();
151
152
153
154 if (AclScope.Episode.equals(scope) || AclScope.Merged.equals(scope)) {
155 episode = getAclByFlavor(mp, XACML_POLICY_EPISODE);
156 }
157 if (Arrays.asList(AclScope.Episode, AclScope.Series, AclScope.Merged).contains(scope)) {
158 series = getAclByFlavor(mp, XACML_POLICY_SERIES);
159 }
160
161 if (episode.isPresent() && series.isPresent()) {
162 logger.debug("Found event and series ACL for media package {}", mp.getIdentifier());
163 switch (mergeMode) {
164 case ACTIONS:
165 logger.debug("Merging ACLs based on individual actions");
166 return tuple(series.get().mergeActions(episode.get()), AclScope.Merged);
167 case ROLES:
168 logger.debug("Merging ACLs based on roles");
169 return tuple(series.get().merge(episode.get()), AclScope.Merged);
170 default:
171 logger.debug("Episode ACL overrides series ACL");
172 return tuple(episode.get(), AclScope.Merged);
173 }
174 }
175 if (episode.isPresent()) {
176 logger.debug("Found event ACL for media package {}", mp.getIdentifier());
177 return tuple(episode.get(), AclScope.Episode);
178 }
179 if (series.isPresent()) {
180 logger.debug("Found series ACL for media package {}", mp.getIdentifier());
181 return tuple(series.get(), AclScope.Series);
182 }
183
184 logger.debug("Falling back to global default ACL");
185 return tuple(new AccessControlList(), AclScope.Global);
186 }
187
188 private Optional<AccessControlList> getAclByFlavor(MediaPackage mp, MediaPackageElementFlavor xacmlPolicyFlavor) {
189 Optional<AccessControlList> acl = Optional.empty();
190 for (Attachment xacml : mp.getAttachments(xacmlPolicyFlavor)) {
191 URI uri = xacml.getURI();
192 try {
193 if (serializer != null) {
194 uri = serializer.decodeURI(uri);
195 }
196 } catch (URISyntaxException e) {
197 logger.warn("URI {} syntax error, skip decoding", uri);
198 }
199 acl = loadAcl(uri);
200 }
201 return acl;
202 }
203
204 @Override
205 public Tuple<MediaPackage, Attachment> setAcl(
206 final MediaPackage mp,
207 final AclScope scope,
208 final AccessControlList acl
209 ) throws MediaPackageException {
210
211 String xacmlContent;
212 try {
213 xacmlContent = XACMLUtils.getXacml(mp, acl);
214 } catch (JAXBException e) {
215 throw new MediaPackageException("Unable to generate xacml for media package " + mp.getIdentifier());
216 }
217
218
219 Attachment attachment = removeFromMediaPackageAndWorkspace(mp, toFlavor(scope)).getB();
220
221
222 final String elementId = toElementId(scope);
223 URI uri;
224 try (InputStream in = IOUtils.toInputStream(xacmlContent, "UTF-8")) {
225 uri = workspace.put(mp.getIdentifier().toString(), elementId, XACML_FILENAME, in);
226 } catch (IOException e) {
227 throw new MediaPackageException("Error storing xacml for media package " + mp.getIdentifier());
228 }
229
230 if (attachment == null) {
231 attachment = (Attachment) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
232 .elementFromURI(uri, Attachment.TYPE, toFlavor(scope));
233 }
234 attachment.setURI(uri);
235 attachment.setIdentifier(elementId);
236 attachment.setMimeType(MimeTypes.XML);
237
238 attachment.setChecksum(null);
239 mp.add(attachment);
240
241 logger.debug("Saved XACML as {}", uri);
242
243
244 return tuple(mp, attachment);
245 }
246
247 @Override
248 public MediaPackage removeAcl(MediaPackage mp, AclScope scope) {
249 return removeFromMediaPackageAndWorkspace(mp, toFlavor(scope)).getA();
250 }
251
252
253 private static MediaPackageElementFlavor toFlavor(AclScope scope) {
254 switch (scope) {
255 case Episode:
256 return XACML_POLICY_EPISODE;
257 case Series:
258 return XACML_POLICY_SERIES;
259 default:
260 throw new IllegalArgumentException("No flavors match the given ACL scope");
261 }
262 }
263
264
265 private static String toElementId(AclScope scope) {
266 switch (scope) {
267 case Episode:
268 return "security-policy-episode";
269 case Series:
270 return "security-policy-series";
271 default:
272 throw new IllegalArgumentException("No element id matches the given ACL scope");
273 }
274 }
275
276
277
278
279
280
281 private Tuple<MediaPackage, Attachment> removeFromMediaPackageAndWorkspace(MediaPackage mp,
282 MediaPackageElementFlavor flavor) {
283 Attachment attachment = null;
284 for (Attachment a : mp.getAttachments(flavor)) {
285 attachment = (Attachment) a.clone();
286 try {
287 workspace.delete(a.getURI());
288 } catch (Exception e) {
289 logger.warn("Unable to delete XACML file:", e);
290 }
291 mp.remove(a);
292 }
293 return Tuple.tuple(mp, attachment);
294 }
295
296
297 private Optional<AccessControlList> loadAcl(final URI uri) {
298 logger.debug("Load Acl from {}", uri);
299 try (InputStream is = workspace.read(uri)) {
300 AccessControlList acl = XACMLUtils.parseXacml(is);
301 return Optional.of(acl);
302 } catch (NotFoundException e) {
303 logger.debug("URI {} not found", uri);
304 } catch (Exception e) {
305 logger.warn("Unable to load or parse Acl from URI {}", uri, e);
306 }
307 return Optional.empty();
308 }
309
310 public boolean hasPermission(final MediaPackage mp, final String action) {
311 AccessControlList acl = getActiveAcl(mp).getA();
312
313
314 final User user = securityService.getUser();
315 var episodeRole = getEpisodeRoleId(mp.getIdentifier().toString(), action);
316 logger.debug("Checking for role: {}", episodeRole);
317 var allowed = user.getRoles().stream().map(Role::getName).anyMatch(r -> r.equals(episodeRole));
318
319 return allowed || hasPermission(acl, action);
320 }
321
322 @Override
323 public boolean hasPermission(AccessControlList acl, final String action) {
324 final User user = securityService.getUser();
325 var allowed = false;
326
327
328 for (AccessControlEntry entry: acl.getEntries()) {
329
330 if (!entry.getAction().equals(action)) {
331 continue;
332 }
333 for (Role role : user.getRoles()) {
334 if (entry.getRole().equals(role.getName())) {
335
336
337 if (!entry.isAllow()) {
338 logger.debug("Access explicitly denied for role({}), action({})", role.getName(), action);
339 return false;
340 }
341 allowed = true;
342 }
343 }
344 }
345 logger.debug("XACML file allowed access");
346 return allowed;
347 }
348
349
350
351
352
353
354
355 @Reference
356 public void setWorkspace(Workspace workspace) {
357 this.workspace = workspace;
358 }
359
360
361
362
363
364
365
366 @Reference
367 public void setSecurityService(SecurityService securityService) {
368 this.securityService = securityService;
369 }
370
371 }