Commit e7528a9c authored by mhellka's avatar mhellka
Browse files

Fixed/Refactored ACL handling.

parent 11a7a6f5
......@@ -346,5 +346,4 @@ public class Utils {
}
return true;
}
}
......@@ -111,19 +111,18 @@ public class Archive {
modified = true;
}
authPolicy = new AuthPolicy(getAuthContext(), ext.owner, ext.acl);
acl = new ArchiveAccessControl(this, ext.acl);
if (modified) {
authPolicy.suspend(true);
// Set owner and grant OWNER permissions to current user.
setOwner(getAuthContext().getPrincipal().getFullId());
getACL().forOwner().permit(ArchivePermissionSet.OWNER);
// Run create filters.
vaultConfig.runFilters("create", f -> f.onCreate(this));
authPolicy.suspend(false);
}
authPolicy = new AuthPolicy(getAuthContext(), ext.owner, ext.acl);
vaultHandle.getSession().getTransaction().onPrepare(t -> {
authPolicy.suspend(true);
vaultConfig.runFilters("prepare", f -> f.onCommit(this));
......@@ -170,17 +169,24 @@ public class Archive {
return ext.revision;
}
public synchronized List<ArchiveFile> getFiles() {
private List<ArchiveFile> getInternalFileList() {
if (files == null) {
checkPermission(ArchivePermission.LIST_ITEM);
files = new ArrayList<>();
for (final Resource r : sto.getResourcesByPrefix(ArchiveFile.RESOURCE_PREFIX)) {
String name = r.getName();
name = name.substring(ArchiveFile.RESOURCE_PREFIX.length(), name.length());
files.add(new ArchiveFile(this, r, name));
synchronized (this) {
if (files == null) {
files = new ArrayList<>();
for (final Resource r : sto.getResourcesByPrefix(ArchiveFile.RESOURCE_PREFIX)) {
String name = r.getName();
name = name.substring(ArchiveFile.RESOURCE_PREFIX.length(), name.length());
files.add(new ArchiveFile(this, r, name));
}
}
}
}
return Collections.unmodifiableList(files);
return files;
}
public List<ArchiveFile> getFiles() {
return Collections.unmodifiableList(getInternalFileList());
}
public ArchiveFile getFile(String name) {
......@@ -198,7 +204,7 @@ public class Archive {
final Resource r = sto.createResource(ArchiveFile.RESOURCE_PREFIX + name);
final ArchiveFile file = new ArchiveFile(this, r, name);
file.setMediaType(mediaType);
files.add(file);
getInternalFileList().add(file);
afterModify();
return file;
......@@ -208,25 +214,33 @@ public class Archive {
beforeModify();
checkPermission(ArchivePermission.DELETE_ITEM);
if (files.remove(file)) {
if (getInternalFileList().remove(file)) {
file.resource.remove();
removedStuff.add(file);
afterModify();
}
}
public synchronized List<ArchiveDocument> getDocuments() {
private List<ArchiveDocument> getInternalDocumentList() {
if (docs == null) {
checkPermission(ArchivePermission.LIST_ITEM);
docs = new ArrayList<>();
for (final Resource r : sto.getResourcesByPrefix(ArchiveDocument.RESOURCE_PREFIX)) {
String name = r.getName();
name = name.substring(ArchiveDocument.RESOURCE_PREFIX.length(),
name.length() - ArchiveDocument.RESOURCE_SUFFIX.length());
docs.add(new ArchiveDocument(this, r, name));
synchronized (this) {
if (docs == null) {
docs = new ArrayList<>();
for (final Resource r : sto.getResourcesByPrefix(ArchiveDocument.RESOURCE_PREFIX)) {
String name = r.getName();
name = name.substring(ArchiveDocument.RESOURCE_PREFIX.length(),
name.length() - ArchiveDocument.RESOURCE_SUFFIX.length());
docs.add(new ArchiveDocument(this, r, name));
}
}
}
}
return Collections.unmodifiableList(docs);
return docs;
}
public List<ArchiveDocument> getDocuments() {
checkPermission(ArchivePermission.LIST_ITEM);
return Collections.unmodifiableList(getInternalDocumentList());
}
public ArchiveDocument getDocument(String name) {
......@@ -244,17 +258,17 @@ public class Archive {
final Resource r = sto.createResource(ArchiveDocument.RESOURCE_PREFIX + name + ArchiveDocument.RESOURCE_SUFFIX);
r.setMediaType("application/json");
final ArchiveDocument doc = new ArchiveDocument(this, r, name);
docs.add(doc);
getInternalDocumentList().add(doc);
afterModify();
return doc;
}
void removeDocument(ArchiveDocument archiveDocument) {
synchronized void removeDocument(ArchiveDocument archiveDocument) {
beforeModify();
checkPermission(ArchivePermission.DELETE_ITEM);
if (docs.remove(archiveDocument)) {
if (getInternalDocumentList().remove(archiveDocument)) {
archiveDocument.resource.remove();
removedStuff.add(archiveDocument);
afterModify();
......@@ -303,38 +317,45 @@ public class Archive {
}
}
private List<ArchiveLink> getLinksInternal() {
if (links == null) {
synchronized (this) {
if (links == null) {
links = new ArrayList<>();
for (final Link link : ext.links) {
links.add(new ArchiveLink(this, link));
}
}
}
}
return links;
}
/**
* Returns a copy of the list of archive links.
*/
public synchronized List<ArchiveLink> getLinks() {
public List<ArchiveLink> getLinks() {
checkPermission(ArchivePermission.LIST_ITEM);
if (links == null) {
links = new ArrayList<>();
for (final Link link : ext.links) {
links.add(new ArchiveLink(this, link));
}
}
return Collections.unmodifiableList(links);
return Collections.unmodifiableList(getLinksInternal());
}
public ArchiveLink addLink(String target) {
public synchronized ArchiveLink addLink(String target) {
beforeModify();
checkPermission(ArchivePermission.CREATE_ITEM);
final ArchiveLink wrapped = new ArchiveLink(this, new Link());
ext.links.add(wrapped.value);
if (links != null)
links.add(wrapped);
getLinksInternal().add(wrapped);
wrapped.setTarget(target);
afterModify();
return wrapped;
}
void removeLink(ArchiveLink link) {
synchronized void removeLink(ArchiveLink link) {
beforeModify();
checkPermission(ArchivePermission.DELETE_ITEM);
if (links.remove(link)) {
if (getLinksInternal().remove(link)) {
ext.links.remove(link.value);
removedStuff.add(link);
afterModify();
......
......@@ -11,6 +11,9 @@ import de.gwdg.cdstar.auth.ArchivePermission;
import de.gwdg.cdstar.auth.ArchivePermissionSet;
import de.gwdg.cdstar.auth.Principal;
import de.gwdg.cdstar.runtime.beans.AccessGrant;
import de.gwdg.cdstar.runtime.beans.BaseSubject.GroupSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.PrincipalSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.SpecialSubject;
public class ArchiveACLEntry {
......@@ -31,7 +34,7 @@ public class ArchiveACLEntry {
ArchiveACLEntry(Archive archive, AccessGrant accessGrant) {
this.archive = archive;
acl = accessGrant;
for (final String g : acl.grants) {
for (final String g : acl.getGrants()) {
try {
if (Utils.isLowerCase(g)) {
permissions.add(ArchivePermission.valueOf(g.toUpperCase()));
......@@ -45,27 +48,33 @@ public class ArchiveACLEntry {
}
public boolean isGroupSubject() {
return acl.isGroupSubject();
return acl.getSubject() instanceof GroupSubject;
}
public boolean isOwnerSubject() {
return acl.isOwnerSubject();
return acl.getSubject().equals(SpecialSubject.OWNER);
}
public boolean isAnonSubject() {
return acl.isAnonSubject();
return acl.getSubject().equals(SpecialSubject.ANY);
}
public boolean isKnownSubject() {
return acl.getSubject().equals(SpecialSubject.USER);
}
public boolean isPrincipalSubject() {
return acl.isPrincipalSubject();
return acl.getSubject() instanceof PrincipalSubject;
}
public boolean matchesPrincipal(Principal principal) {
return acl.matchesPrincipal(principal.getFullId());
return acl.getSubject() instanceof PrincipalSubject && acl.getSubject().getName().equals(principal.getFullId());
}
public String getGroupName() {
return acl.getGroupName();
if (!(acl.getSubject() instanceof GroupSubject))
return null;
return acl.getSubject().getName();
}
public boolean permits(ArchivePermission permit) {
......@@ -88,7 +97,7 @@ public class ArchiveACLEntry {
archive.beforeModify();
archive.checkPermission(ArchivePermission.CHANGE_ACL);
if (permissions.add(permit)) {
acl.grants.add(permit.name().toLowerCase());
acl.getGrants().add(permit.name().toLowerCase());
archive.afterModify();
}
}
......@@ -97,7 +106,7 @@ public class ArchiveACLEntry {
archive.beforeModify();
archive.checkPermission(ArchivePermission.CHANGE_ACL);
if (permissionSets.add(permit)) {
acl.grants.add(permit.name());
acl.getGrants().add(permit.name());
archive.afterModify();
}
}
......@@ -106,7 +115,7 @@ public class ArchiveACLEntry {
archive.beforeModify();
archive.checkPermission(ArchivePermission.CHANGE_ACL);
if (permissions.remove(permit)) {
acl.grants.remove(permit.name().toLowerCase());
acl.getGrants().remove(permit.name().toLowerCase());
archive.afterModify();
}
}
......@@ -115,7 +124,7 @@ public class ArchiveACLEntry {
archive.beforeModify();
archive.checkPermission(ArchivePermission.CHANGE_ACL);
if (permissionSets.remove(permit)) {
acl.grants.remove(permit.name());
acl.getGrants().remove(permit.name());
archive.afterModify();
}
}
......@@ -125,6 +134,7 @@ public class ArchiveACLEntry {
archive.checkPermission(ArchivePermission.CHANGE_ACL);
permissions.clear();
permissionSets.clear();
acl.getGrants().clear();
archive.afterModify();
}
......@@ -135,12 +145,12 @@ public class ArchiveACLEntry {
*/
static boolean extractPermissionsInto(EnumSet<ArchivePermission> all, AccessGrant grant) {
boolean changed = false;
for (final String item : grant.grants) {
for (final String item : grant.getGrants()) {
final EnumSet<ArchivePermission> permissions = ArchiveACLEntry.validGrants.get(item);
if (permissions == null) {
throw new RuntimeException("Unknown grant: " + item);
}
changed = changed || all.addAll(permissions);
changed = all.addAll(permissions) || changed;
}
return changed;
}
......@@ -151,6 +161,6 @@ public class ArchiveACLEntry {
*/
public String getSubject() {
return acl.subject;
return acl.getSubject().toString();
}
}
\ No newline at end of file
......@@ -7,6 +7,9 @@ import java.util.List;
import de.gwdg.cdstar.auth.ArchivePermission;
import de.gwdg.cdstar.auth.Principal;
import de.gwdg.cdstar.runtime.beans.AccessGrant;
import de.gwdg.cdstar.runtime.beans.BaseSubject.GroupSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.PrincipalSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.SpecialSubject;
public class ArchiveAccessControl {
......@@ -37,7 +40,7 @@ public class ArchiveAccessControl {
archive.beforeModify();
archive.checkPermission(ArchivePermission.CHANGE_ACL);
for (final ArchiveACLEntry acl : getWrapperList()) {
if (acl.acl.subject.equals(grant.subject))
if (acl.acl.getSubject().equals(grant.getSubject()))
return acl;
}
final ArchiveACLEntry acl = new ArchiveACLEntry(archive, grant);
......@@ -50,28 +53,28 @@ public class ArchiveAccessControl {
* Return the ACL entry that matches the owner of the object.
*/
public ArchiveACLEntry forOwner() {
return findOrCreate(AccessGrant.forOwnerSubject());
return findOrCreate(new AccessGrant(SpecialSubject.OWNER));
}
/**
* Return the ACL entry that matches any user, known or anonymous.
*/
public ArchiveACLEntry forAny() {
return findOrCreate(AccessGrant.forAnonSubject());
return findOrCreate(new AccessGrant(SpecialSubject.ANY));
}
/**
* Return the ACL entry that matches any known (non-anonymous) user.
*/
public ArchiveACLEntry forKnown() {
return findOrCreate(AccessGrant.forKnownSubject());
return findOrCreate(new AccessGrant(SpecialSubject.USER));
}
/**
* Return the ACL entry that matches members of a group.
*/
public ArchiveACLEntry forGroup(String name) {
return findOrCreate(AccessGrant.forGroupSubject(name));
return findOrCreate(new AccessGrant(new GroupSubject(name)));
}
/**
......@@ -85,7 +88,7 @@ public class ArchiveAccessControl {
* Return the ACL entry that matches a specific user.
*/
public ArchiveACLEntry forPrincipal(String principal) {
return findOrCreate(AccessGrant.forPrincipalSubject(principal));
return findOrCreate(new AccessGrant(new PrincipalSubject(principal)));
}
/**
......
......@@ -3,13 +3,22 @@ package de.gwdg.cdstar.runtime;
import java.util.EnumSet;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.gwdg.cdstar.Utils;
import de.gwdg.cdstar.auth.ArchivePermission;
import de.gwdg.cdstar.auth.Subject;
import de.gwdg.cdstar.runtime.beans.AccessGrant;
import de.gwdg.cdstar.runtime.beans.BaseSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.GroupSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.PrincipalSubject;
import de.gwdg.cdstar.runtime.beans.BaseSubject.SpecialSubject;
import de.gwdg.cdstar.runtime.exc.AccessError;
class AuthPolicy {
private static final Logger log = LoggerFactory.getLogger(AuthPolicy.class);
private final Subject subject;
private final String owner;
private final AccessGrant[] originalAclList;
......@@ -58,12 +67,13 @@ class AuthPolicy {
final String subjectId = subject.getPrincipal().getFullId();
final boolean isOwner = Utils.equalNotNull(owner, subjectId);
for (final AccessGrant grant : originalAclList) {
if (grant.isGroupSubject())
final BaseSubject gs = grant.getSubject();
if (gs instanceof GroupSubject)
continue;
boolean match = false;
match = match || grant.isAnonSubject();
match = match || isOwner && grant.isOwnerSubject();
match = match || (grant.isPrincipalSubject() && grant.matchesPrincipal(subjectId));
match = match || gs == SpecialSubject.ANY;
match = match || isOwner && gs == SpecialSubject.OWNER;
match = match || ((gs instanceof PrincipalSubject) && gs.getName().equals(subjectId));
if (match)
ArchiveACLEntry.extractPermissionsInto(cachedPermissions, grant);
}
......@@ -80,7 +90,7 @@ class AuthPolicy {
grantsCheckedUntil++;
final AccessGrant grant = originalAclList[grantsCheckedUntil];
if (!grant.isGroupSubject())
if (!(grant.getSubject() instanceof GroupSubject))
continue; // Already checked above
final EnumSet<ArchivePermission> newPermissions = cachedPermissions.clone();
......@@ -88,7 +98,7 @@ class AuthPolicy {
if (!ArchiveACLEntry.extractPermissionsInto(newPermissions, grant))
continue; // Would not add any new permissions
if (!subject.isMemberOf(grant.getGroupName()))
if (!subject.isMemberOf(grant.getSubject().getName()))
continue; // Does not apply to subject.
cachedPermissions.addAll(newPermissions);
......@@ -103,7 +113,10 @@ class AuthPolicy {
* {@link AccessError} instead of returning false.
*/
void checkPermission(ArchivePermission permit) {
if (!isPermitted(permit))
if (!isPermitted(permit)) {
log.warn("Permission check failed for subject {}, permission {}", subject, permit);
log.warn("Permissions: {}", cachedPermissions);
throw new AccessError(permit.name());
}
}
}
......@@ -5,7 +5,8 @@ import java.util.Set;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* <code>
......@@ -19,74 +20,25 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
@JsonAutoDetect(isGetterVisibility = Visibility.NONE)
public class AccessGrant {
private static final String SPECIAL_PREFIX = "$";
private static final String SPECIAL_ANY = SPECIAL_PREFIX + "any";
private static final String SPECIAL_KNOWN = SPECIAL_PREFIX + "known";
private static final String SPECIAL_OWNER = SPECIAL_PREFIX + "owner";
private static final String GROUP_PREFIX = "@";
public String subject;
public Set<String> grants;
private final BaseSubject subject;
private final Set<String> grants;
public AccessGrant() {
public AccessGrant(BaseSubject subject) {
this(subject, null);
}
public AccessGrant(String subject) {
@JsonCreator
public AccessGrant(@JsonProperty("subject") BaseSubject subject, @JsonProperty("grants") Set<String> grants) {
this.subject = subject;
grants = new HashSet<>();
this.grants = grants == null ? new HashSet<>() : grants;
}
public AccessGrant(String subject, Set<String> grants) {
this.subject = subject;
this.grants = grants;
}
public boolean isGroupSubject() {
return subject.startsWith(GROUP_PREFIX);
}
public static AccessGrant forGroupSubject(String group) {
return new AccessGrant(GROUP_PREFIX + group);
}
public boolean isOwnerSubject() {
return SPECIAL_OWNER.equals(subject);
}
public static AccessGrant forOwnerSubject() {
return new AccessGrant(SPECIAL_OWNER);
}
public boolean isKnownSubject() {
return SPECIAL_KNOWN.equals(subject);
}
public static AccessGrant forKnownSubject() {
return new AccessGrant(SPECIAL_KNOWN);
}
public boolean isAnonSubject() {
return SPECIAL_ANY.equals(subject);
}
public static AccessGrant forAnonSubject() {
return new AccessGrant(SPECIAL_ANY);
}
public boolean isPrincipalSubject() {
return !(subject.startsWith(GROUP_PREFIX) || subject.startsWith(SPECIAL_PREFIX));
}
public static AccessGrant forPrincipalSubject(String name) {
return new AccessGrant(name);
}
public boolean matchesPrincipal(String subjectId) {
return isPrincipalSubject() && subject.equals(subjectId);
public BaseSubject getSubject() {
return subject;
}
@JsonIgnore
public String getGroupName() {
return isGroupSubject() ? subject.substring(GROUP_PREFIX.length()) : null;
public Set<String> getGrants() {
return grants;
}
public AccessGrant copy() {
......
......@@ -53,7 +53,7 @@ public class ArchiveExt {
public void flush(StorageObject sto) {
try {
acl.removeIf(p -> p.grants.isEmpty());
acl.removeIf(p -> p.getGrants().isEmpty());
sto.getResource(RESOURCE_NAME).truncate(0).write(SharedObjectMapper.json.writeValueAsBytes(this));
} catch (final JsonProcessingException e) {
throw new BackendException("Failed to serialize archive attributes.", e);
......
package de.gwdg.cdstar.runtime.beans;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Value types for string based grant subjects for CDSTAR object-bound access
* control.
*
* Subjects are immutable, and in case of {@link SpecialPrefix} even singletons.
* {@link #toString()} and {@link #fromString(String)} can be used to convert
* between subjects and a type-aware string representation.
*
*/
public abstract class BaseSubject {
private static final String GROUP_PREFIX = "@";
private static final String SPECIAL_PREFIX = "$";
private final String name;
private BaseSubject(String name) {
this.name = name;
}
/**
* Return the name of the principal, group or special without any prefix.