/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.authorization.xacml;
import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY;
import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY_EPISODE;
import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY_SERIES;
import static org.opencastproject.util.IoSupport.withFile;
import static org.opencastproject.util.data.Collections.list;
import static org.opencastproject.util.data.Collections.mkString;
import static org.opencastproject.util.data.Monadics.caseA;
import static org.opencastproject.util.data.Monadics.caseN;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Option.none;
import static org.opencastproject.util.data.Option.some;
import static org.opencastproject.util.data.Prelude.unexhaustiveMatch;
import static org.opencastproject.util.data.Tuple.tuple;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.security.api.AccessControlEntry;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AclScope;
import org.opencastproject.security.api.AuthorizationService;
import org.opencastproject.security.api.Role;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.data.Collections;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Function0;
import org.opencastproject.util.data.Function2;
import org.opencastproject.util.data.Monadics;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Option.Match;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.data.functions.Options;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.jboss.security.xacml.core.JBossPDP;
import org.jboss.security.xacml.core.model.context.AttributeType;
import org.jboss.security.xacml.core.model.context.RequestType;
import org.jboss.security.xacml.core.model.context.SubjectType;
import org.jboss.security.xacml.factories.RequestAttributeFactory;
import org.jboss.security.xacml.factories.RequestResponseContextFactory;
import org.jboss.security.xacml.interfaces.PolicyDecisionPoint;
import org.jboss.security.xacml.interfaces.RequestContext;
import org.jboss.security.xacml.interfaces.XACMLConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.xml.bind.JAXBException;
/**
* A XACML implementation of the {@link AuthorizationService}.
*/
public class XACMLAuthorizationService implements AuthorizationService {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(XACMLAuthorizationService.class);
/** The default filename for XACML attachments */
public static final String XACML_FILENAME = "xacml.xml";
public static final String READ_PERMISSION = "read";
/** The workspace */
protected Workspace workspace;
/** The security service */
protected SecurityService securityService;
/** The series service */
protected SeriesService seriesService;
@Override
public Tuple<AccessControlList, AclScope> getActiveAcl(final MediaPackage mp) {
// tuple up with episode flavor
final Function<AccessControlList, Tuple<AccessControlList, AclScope>> isEpisodeAcl = tupleA(AclScope.Episode);
// tuple up with series flavor
final Function<AccessControlList, Tuple<AccessControlList, AclScope>> isSeriesAcl = tupleA(AclScope.Series);
return withContextClassLoader(new Function0<Tuple<AccessControlList, AclScope>>() {
@Override
public Tuple<AccessControlList, AclScope> apply() {
// has an episode ACL?
return getAcl(mp, list(XACML_POLICY_EPISODE)).map(isEpisodeAcl)
// has a series ACL?
.orElse(new Function0<Option<Tuple<AccessControlList, AclScope>>>() {
@Override
public Option<Tuple<AccessControlList, AclScope>> apply() {
return getAcl(mp, list(XACML_POLICY_SERIES, XACML_POLICY)).map(isSeriesAcl);
}
})
// no -> return an empty series ACL
.getOrElse(getDefaultAcl(mp));
}
});
}
private Function0<Tuple<AccessControlList, AclScope>> getDefaultAcl(final MediaPackage mp) {
return new Function0<Tuple<AccessControlList, AclScope>>() {
@Override
public Tuple<AccessControlList, AclScope> apply() {
logger.debug("No XACML attachment found in {}", mp);
if (StringUtils.isNotBlank(mp.getSeries())) {
logger.info("Falling back to using default acl from series {} for mediapackage {}", mp.getSeries(),
mp.getIdentifier());
try {
return tuple(seriesService.getSeriesAccessControl(mp.getSeries()), AclScope.Series);
} catch (Exception e) {
logger.warn("Unable to get default acl from series '{}': {}", mp.getSeries(), e.getMessage());
}
}
logger.trace("Falling back to using default public acl for mediapackage '{}'", mp);
// TODO: We need a configuration option for open vs. closed by default
// Right now, rights management is based on series. Here we make sure that
// objects not belonging to a series are world readable
AccessControlList accessControlList = new AccessControlList();
List<AccessControlEntry> acl = accessControlList.getEntries();
String anonymousRole = securityService.getOrganization().getAnonymousRole();
acl.add(new AccessControlEntry(anonymousRole, READ_PERMISSION, true));
return tuple(accessControlList, AclScope.Series);
}
};
}
private static <A, B> Function<A, Tuple<A, B>> tupleA(final B b) {
return new Function<A, Tuple<A, B>>() {
@Override
public Tuple<A, B> apply(A a) {
return tuple(a, b);
}
};
}
@Override
@SuppressWarnings("unchecked")
public Option<AccessControlList> getAcl(final MediaPackage mp, final AclScope scope) {
return withContextClassLoader(new Function0<Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply() {
switch (scope) {
case Episode:
return getAcl(mp, list(XACML_POLICY_EPISODE)).orElse(new Function0<Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply() {
return getAcl(mp, list(XACML_POLICY_SERIES)).fold(
new Match<AccessControlList, Option<AccessControlList>>() {
@Override
public Option<AccessControlList> some(AccessControlList a) {
return Option.<AccessControlList> none();
}
@Override
public Option<AccessControlList> none() {
return getAcl(mp, list(XACML_POLICY));
}
});
}
});
case Series:
return getAcl(mp, list(XACML_POLICY_SERIES, XACML_POLICY));
default:
throw new NotImplementedException("AclScope " + scope + " has not been implemented yet!");
}
}
});
}
@Override
public Tuple<MediaPackage, Attachment> setAcl(final MediaPackage mp, final AclScope scope, final AccessControlList acl) {
return withContextClassLoader(new Function0.X<Tuple<MediaPackage, Attachment>>() {
@Override
public Tuple<MediaPackage, Attachment> xapply() throws Exception {
// Get XACML representation of these role + action tuples
String xacmlContent = null;
try {
xacmlContent = XACMLUtils.getXacml(mp, acl);
} catch (JAXBException e) {
throw new MediaPackageException("Unable to generate xacml for mediapackage " + mp.getIdentifier());
}
// Remove the old xacml file(s)
Attachment attachment = removeFromMediaPackageAndWorkspace(mp, toFlavors(scope)).getB();
// add attachment
final String elementId = toElementId(scope);
URI uri;
InputStream in = null;
try {
in = IOUtils.toInputStream(xacmlContent);
uri = workspace.put(mp.getIdentifier().toString(), elementId, XACML_FILENAME, in);
} catch (IOException e) {
throw new MediaPackageException("Error storing xacml for mediapackage " + mp.getIdentifier());
} finally {
IOUtils.closeQuietly(in);
}
if (attachment == null) {
attachment = (Attachment) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
.elementFromURI(uri, Attachment.TYPE, toFlavor(scope));
}
attachment.setURI(uri);
attachment.setIdentifier(elementId);
attachment.setMimeType(MimeTypes.XML);
// setting the URI to a new source so the checksum will most like be invalid
attachment.setChecksum(null);
mp.add(attachment);
logger.info("Saved XACML under {}", uri);
// return augmented mediapackage
return tuple(mp, attachment);
}
});
}
@Override
public List<Attachment> getAclAttachments(MediaPackage mp, Option<AclScope> scope) {
final List<MediaPackageElementFlavor> flavors = scope.map(toFlavorsF).getOrElse(
Collections.<MediaPackageElementFlavor> nil());
return getAttachments(mp, flavors);
}
@Override
public MediaPackage removeAcl(MediaPackage mp, AclScope scope) {
return removeFromMediaPackageAndWorkspace(mp, toFlavors(scope)).getA();
}
/** Apply function f within the context of the class loader of XACMLAuthorizationService. */
private static <A> A withContextClassLoader(Function0<A> f) {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(XACMLAuthorizationService.class.getClassLoader());
return f.apply();
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
}
/** Return an attachment of a given set of flavors only if there is exactly one. */
private static Option<Attachment> getSingleAttachment(MediaPackage mp, List<MediaPackageElementFlavor> flavors) {
final List<Attachment> as = getAttachments(mp, flavors);
if (as.size() == 0) {
logger.debug("No XACML attachment of type {} found in {}", mkString(flavors, ","), mp);
return none();
} else if (as.size() == 1) {
return some(as.get(0));
} else { // > 1
logger.warn("More than one XACML attachment of type {} is attached to {}", mkString(flavors, ","), mp);
return none();
}
}
private Function0<Option<Attachment>> getSingleAttachmentF(final MediaPackage mp,
final List<MediaPackageElementFlavor> flavors) {
return new Function0<Option<Attachment>>() {
@Override
public Option<Attachment> apply() {
return getSingleAttachment(mp, flavors);
}
};
}
/** Return all attachments of the given flavors. */
private static List<Attachment> getAttachments(MediaPackage mp, final List<MediaPackageElementFlavor> flavors) {
return mlist(mp.getAttachments()).filter(new Function<Attachment, Boolean>() {
@Override
public Boolean apply(Attachment a) {
return flavors.contains(a.getFlavor());
}
}).value();
}
/** Return the XACML attachment or none if the media package does not contain any XACMLs. */
private Option<Attachment> getXacmlAttachment(MediaPackage mp) {
return getSingleAttachment(mp, list(XACML_POLICY_EPISODE)).orElse(
getSingleAttachmentF(mp, list(XACML_POLICY_SERIES, XACML_POLICY)));
}
/**
* Get <em>all</em> flavors associated with a scope. This method has to exist as long as the deprecated
* {@link org.opencastproject.mediapackage.MediaPackageElements#XACML_POLICY} flavor exists.
*/
private static List<MediaPackageElementFlavor> toFlavors(AclScope scope) {
switch (scope) {
case Episode:
return list(XACML_POLICY_EPISODE);
case Series:
return list(XACML_POLICY_SERIES, XACML_POLICY);
default:
return unexhaustiveMatch();
}
}
/** Functional version of {@link #toFlavors(org.opencastproject.security.api.AclScope)}. */
private static Function<AclScope, List<MediaPackageElementFlavor>> toFlavorsF = new Function<AclScope, List<MediaPackageElementFlavor>>() {
@Override
public List<MediaPackageElementFlavor> apply(AclScope scope) {
return toFlavors(scope);
}
};
/** Get the flavor associated with a scope. */
private static MediaPackageElementFlavor toFlavor(AclScope scope) {
switch (scope) {
case Episode:
return XACML_POLICY_EPISODE;
case Series:
return XACML_POLICY_SERIES;
default:
return unexhaustiveMatch();
}
}
/** Get the element id associated with a scope. */
private static String toElementId(AclScope scope) {
switch (scope) {
case Episode:
return "security-policy-episode";
case Series:
return "security-policy-series";
default:
return unexhaustiveMatch();
}
}
private Function0<Option<AccessControlList>> getAclF(final MediaPackage mp,
final List<MediaPackageElementFlavor> flavors) {
return new Function0<Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply() {
return getAcl(mp, flavors);
}
};
}
/** Get the ACL of the given flavor from a media package. */
private Option<AccessControlList> getAcl(final MediaPackage mp, final List<MediaPackageElementFlavor> flavors) {
return mlist(getAttachments(mp, flavors)).match(
Monadics.<Attachment, Option<AccessControlList>> caseNil(Options.<AccessControlList> never2()),
caseA(new Function<Attachment, Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply(Attachment a) {
return loadAcl(a.getURI());
}
}), caseN(new Function<List<Attachment>, Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply(List<Attachment> as) {
// try to find the source policy. Some may be copies sent to distribution channels.
return mlist(as)
.filter(unreferencedAttachments)
.match(Monadics
.<Attachment, Option<AccessControlList>> caseNil(new Function0<Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply() {
logger.warn(
"Multiple XACML policies of type {} are attached to {}, and none seem to be authoritative.",
flavors, mp);
return none();
}
}), caseA(new Function<Attachment, Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply(Attachment a) {
return loadAcl(a.getURI());
}
}), caseN(new Function<List<Attachment>, Option<AccessControlList>>() {
@Override
public Option<AccessControlList> apply(List<Attachment> as) {
logger.warn("More than one non-referenced XACML policy of type {} is attached to {}.",
flavors, mp);
return none();
}
}));
}
}));
}
private static final Function<Attachment, Boolean> unreferencedAttachments = new Function<Attachment, Boolean>() {
@Override
public Boolean apply(Attachment a) {
return a.getReference() == null;
}
};
/**
* Get a file from the workspace.
*
* @param uri
* The file uri
* @return return the file if exists otherwise <code>null</code>
*/
private File fromWorkspace(URI uri) {
try {
return workspace.get(uri);
} catch (NotFoundException e) {
logger.warn("XACML policy file not found '{}'.", uri);
return null;
} catch (IOException e) {
logger.error("Unable to access XACML policy file. {}", uri, e);
return null;
}
}
/**
* Remove all attachments of the given flavors from media package and workspace.
*
* @return the a tuple with the mutated (!) media package as A and the deleted Attachment as B
*/
private Tuple<MediaPackage, Attachment> removeFromMediaPackageAndWorkspace(MediaPackage mp,
List<MediaPackageElementFlavor> flavors) {
Attachment attachment = null;
for (Attachment a : getAttachments(mp, flavors)) {
attachment = (Attachment) a.clone();
try {
workspace.delete(a.getURI());
} catch (Exception e) {
logger.warn("Unable to delete XACML file: {}", e);
}
mp.remove(a);
}
return Tuple.tuple(mp, attachment);
}
/** Load an ACL from the given URI. */
private Option<AccessControlList> loadAcl(final URI uri) {
final File file = fromWorkspace(uri);
if (file == null)
return none();
return withFile(file, new Function2<InputStream, File, AccessControlList>() {
@Override
public AccessControlList apply(InputStream in, File aclFile) {
try {
return XACMLUtils.parseXacml(in);
} catch (JAXBException e) {
FileUtils.deleteQuietly(file);
throw new Error("Unable to unmarshall XACML document from " + file + ":" + e);
}
}
});
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.security.api.AuthorizationService#hasPolicy(org.opencastproject.mediapackage.MediaPackage)
*/
@Override
public boolean hasPolicy(MediaPackage mp) {
return getXacmlAttachment(mp).isSome();
}
@Override
public boolean hasPermission(final MediaPackage mp, final String action) {
return withContextClassLoader(new Function0<Boolean>() {
@Override
public Boolean apply() {
return getXacmlAttachment(mp).map(new Function<Attachment, Boolean>() {
@Override
public Boolean apply(Attachment attachment) {
final File xacmlPolicyFile = fromWorkspace(attachment.getURI());
if (xacmlPolicyFile == null) {
logger.warn("Unable to read XACML file from {}! Prevent access permissions.", attachment);
return false;
}
final RequestContext requestCtx = RequestResponseContextFactory.createRequestCtx();
final User user = securityService.getUser();
// Create a subject type
SubjectType subject = new SubjectType();
subject.getAttribute().add(
RequestAttributeFactory.createStringAttributeType(XACMLUtils.SUBJECT_IDENTIFIER, XACMLUtils.ISSUER,
user.getUsername()));
for (Role role : user.getRoles()) {
AttributeType attSubjectID = RequestAttributeFactory.createStringAttributeType(
XACMLUtils.SUBJECT_ROLE_IDENTIFIER, XACMLUtils.ISSUER, role.getName());
subject.getAttribute().add(attSubjectID);
}
// Create a resource type
URI uri = null;
try {
uri = new URI(mp.getIdentifier().toString());
} catch (URISyntaxException e) {
logger.warn("Unable to represent mediapackage identifier '{}' as a URI", mp.getIdentifier().toString());
}
org.jboss.security.xacml.core.model.context.ResourceType resourceType = new org.jboss.security.xacml.core.model.context.ResourceType();
resourceType.getAttribute().add(
RequestAttributeFactory.createAnyURIAttributeType(XACMLUtils.RESOURCE_IDENTIFIER,
XACMLUtils.ISSUER, uri));
// Create an action type
org.jboss.security.xacml.core.model.context.ActionType actionType = new org.jboss.security.xacml.core.model.context.ActionType();
actionType.getAttribute().add(
RequestAttributeFactory.createStringAttributeType(XACMLUtils.ACTION_IDENTIFIER, XACMLUtils.ISSUER,
action));
// Create a Request Type
RequestType requestType = new RequestType();
requestType.getSubject().add(subject);
requestType.getResource().add(resourceType);
requestType.setAction(actionType);
try {
requestCtx.setRequest(requestType);
} catch (IOException e) {
logger.warn("Unable to set the xacml request type", e);
return false;
}
PolicyDecisionPoint pdp = getPolicyDecisionPoint(xacmlPolicyFile);
return pdp.evaluate(requestCtx).getDecision() == XACMLConstants.DECISION_PERMIT;
}
}).getOrElse(true);
}
});
}
private PolicyDecisionPoint getPolicyDecisionPoint(File xacmlFile) {
// Build a JBoss PDP configuration. This is a custom jboss format, so we're just hacking it together here
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
sb.append("<ns:jbosspdp xmlns:ns=\"urn:jboss:xacml:2.0\">");
sb.append("<ns:Policies><ns:Policy><ns:Location>");
sb.append(xacmlFile.toURI().toString());
sb.append("</ns:Location></ns:Policy></ns:Policies><ns:Locators>");
sb.append("<ns:Locator Name=\"org.jboss.security.xacml.locators.JBossPolicyLocator\">");
sb.append("</ns:Locator></ns:Locators></ns:jbosspdp>");
InputStream is = null;
try {
is = IOUtils.toInputStream(sb.toString(), "UTF-8");
return new JBossPDP(is);
} catch (IOException e) {
// Only happens if 'UTF-8' is an invalid encoding, which it isn't
throw new IllegalStateException("Unable to transform a string into a stream");
} finally {
IOUtils.closeQuietly(is);
}
}
/**
* Sets the workspace to use for retrieving XACML policies
*
* @param workspace
* the workspace to set
*/
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* Declarative services callback to set the security service.
*
* @param securityService
* the security service
*/
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/**
* Declarative services callback to set the series service.
*
* @param seriesService
* the series service
*/
protected void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
}