/** * 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.assetmanager.impl; import static com.entwinemedia.fn.Prelude.chuck; import static com.entwinemedia.fn.Stream.$; import static java.lang.String.format; import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE; import org.opencastproject.assetmanager.api.Asset; import org.opencastproject.assetmanager.api.AssetManager; import org.opencastproject.assetmanager.api.Availability; import org.opencastproject.assetmanager.api.Property; import org.opencastproject.assetmanager.api.PropertyId; import org.opencastproject.assetmanager.api.Snapshot; import org.opencastproject.assetmanager.api.Value; import org.opencastproject.assetmanager.api.Version; import org.opencastproject.assetmanager.api.query.ADeleteQuery; import org.opencastproject.assetmanager.api.query.AQueryBuilder; import org.opencastproject.assetmanager.api.query.ASelectQuery; import org.opencastproject.assetmanager.api.query.Predicate; import org.opencastproject.assetmanager.api.query.PropertyField; import org.opencastproject.assetmanager.api.query.Target; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.security.api.AccessControlEntry; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlUtil; import org.opencastproject.security.api.AuthorizationService; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.Role; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UnauthorizedException; import org.opencastproject.security.api.User; import com.entwinemedia.fn.Fn2; import com.entwinemedia.fn.data.Opt; /** * Security layer. */ public class AssetManagerWithSecurity extends AssetManagerDecorator { public static final String WRITE_ACTION = "write"; public static final String READ_ACTION = "read"; public static final String SECURITY_NAMESPACE = "org.opencastproject.assetmanager.security"; private final AuthorizationService authSvc; private final SecurityService secSvc; public AssetManagerWithSecurity( AssetManager delegate, AuthorizationService authSvc, SecurityService secSvc) { super(delegate); this.authSvc = authSvc; this.secSvc = secSvc; } @Override public Snapshot takeSnapshot(String owner, MediaPackage mp) { final AccessControlList acl = authSvc.getActiveAcl(mp).getA(); final User user = secSvc.getUser(); final Organization org = secSvc.getOrganization(); if (AccessControlUtil.isAuthorized(acl, user, org, WRITE_ACTION)) { final Snapshot snapshot = super.takeSnapshot(owner, mp); storeAclAsProperties(snapshot, acl); return snapshot; } else { return chuck(new UnauthorizedException("Not allowed to take snapshot of media package " + mp.getIdentifier().toString())); } } @Override public void setAvailability(Version version, String mpId, Availability availability) { if (isAuthorized(mkAuthPredicate(mpId, WRITE_ACTION))) { super.setAvailability(version, mpId, availability); } else { chuck(new UnauthorizedException("Not allowed to set availability of episode " + mpId)); } } @Override public boolean setProperty(Property property) { final String mpId = property.getId().getMediaPackageId(); if (isAuthorized(mkAuthPredicate(mpId, WRITE_ACTION))) { return super.setProperty(property); } else { return chuck(new UnauthorizedException("Not allowed to set property on episode " + mpId)); } } @Override public Opt<Asset> getAsset(Version version, String mpId, String mpeId) { if (isAuthorized(mkAuthPredicate(mpId, READ_ACTION))) { return super.getAsset(version, mpId, mpeId); } else { return chuck(new UnauthorizedException(format("Not allowed to read assets of snapshot %s, version=%s", mpId, version))); } } @Override public AQueryBuilder createQuery() { return new AQueryBuilderDecorator(super.createQuery()) { @Override public ASelectQuery select(Target... target) { switch (isAdmin()) { case GLOBAL: return super.select(target); case ORGANIZATION: return super.select(target).where(restrictToUsersOrganization()); default: return super.select(target).where(mkAuthPredicate(READ_ACTION)); } } @Override public ADeleteQuery delete(String owner, Target target) { switch (isAdmin()) { case GLOBAL: return super.delete(owner, target); case ORGANIZATION: return super.delete(owner, target).where(restrictToUsersOrganization()); default: return super.delete(owner, target).where(mkAuthPredicate(WRITE_ACTION)); } } }; } /* ------------------------------------------------------------------------------------------------------------------ */ /** Create a new query builder. */ private AQueryBuilder q() { return delegate.createQuery(); } /** * Create an authorization predicate to be used with {@link #isAuthorized(Predicate)}, * restricting access to the user's organization and the given action. * * @param action * the action to restrict access to */ private Predicate mkAuthPredicate(final String action) { final AQueryBuilder q = q(); return $(secSvc.getUser().getRoles()) .foldl(q.always().not(), new Fn2<Predicate, Role, Predicate>() { @Override public Predicate apply(Predicate predicate, Role role) { return predicate.or(mkSecurityProperty(q, role.getName(), action).eq(true)); } }) .and(restrictToUsersOrganization()); } private Predicate mkAuthPredicate(final String mpId, final String action) { return q().mediaPackageId(mpId).and(mkAuthPredicate(action)); } /** Create a predicate that restricts access to the user's organization. */ private Predicate restrictToUsersOrganization() { return q().organizationId().eq(secSvc.getUser().getOrganization().getId()); } /** Check authorization based on the given predicate. */ private boolean isAuthorized(Predicate p) { switch (isAdmin()) { case GLOBAL: return true; case ORGANIZATION: return true; default: final AQueryBuilder q = delegate.createQuery(); return !q.select().where(p).run().getRecords().isEmpty(); } } private AdminRole isAdmin() { final User user = secSvc.getUser(); if (user.hasRole(GLOBAL_ADMIN_ROLE)) { return AdminRole.GLOBAL; } else if (user.hasRole(secSvc.getOrganization().getAdminRole())) { return AdminRole.ORGANIZATION; } else { return AdminRole.NONE; } } enum AdminRole { GLOBAL, ORGANIZATION, NONE } private void storeAclAsProperties(Snapshot snapshot, AccessControlList acl) { for (final AccessControlEntry ace : acl.getEntries()) { super.setProperty(Property.mk( PropertyId.mk( snapshot.getMediaPackage().getIdentifier().toString(), SECURITY_NAMESPACE, mkPropertyName(ace)), Value.mk(ace.isAllow()))); } } private PropertyField<Boolean> mkSecurityProperty(AQueryBuilder q, String role, String action) { return q.property(Value.BOOLEAN, SECURITY_NAMESPACE, mkPropertyName(role, action)); } private String mkPropertyName(AccessControlEntry ace) { return mkPropertyName(ace.getRole(), ace.getAction()); } private String mkPropertyName(String role, String action) { return role + " | " + action; } }