Skip to content

Commit

Permalink
Merge pull request #40 from dataquest-dev/feature/se-8-create-ACL-ele…
Browse files Browse the repository at this point in the history
…ment

| Phases            | MM  |  MB  | MR |   JM | Total  |
|-----------------|----:|----:|-----:|-----:|-------:|
| ETA                  | 20.8   |    0 |     0 |      0 |        0 |
| Developing      |  7+8+2.5+1  |    0 |    0 |      0 |         0 |
| Review             |  0  |    0 |    0 |      0 |         0 |
| Total                |   -  |   -   |  -    |   -    |         0 |
| ETA est.             |      |       |       |         |         0 |
| ETA cust.           |   -  |   -  |   -   |   -     |        0 |
## Problem description
The submission can contain admin only fields with fined-grained permissions.

## Problems
- I wanted to get the actual context by method _contextService.getContext()_ but it returns null. I found out the context could be obtained from the HttpRequest by method _ContextUtil.obtainCurrentRequestContext()_.
  • Loading branch information
milanmajchrak authored May 16, 2022
2 parents b6d47e4 + 8bc0832 commit b1dfa62
Show file tree
Hide file tree
Showing 7 changed files with 535 additions and 1 deletion.
170 changes: 170 additions & 0 deletions dspace-api/src/main/java/org/dspace/app/util/ACE.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.util;

import java.util.Set;

import org.apache.log4j.Logger;

/**
* Class that represents single Access Control Entry
*
* @author Michal Josífko
* Class is copied from the LINDAT/CLARIAH-CZ (https://github.com/ufal/clarin-dspace) and modified by
* @author Milan Majchrak (milan.majchrak at dataquest dot sk)
*/

public class ACE {

/** Logger */
private static final Logger log = Logger.getLogger(ACE.class);
private static final String POLICY_KEYWORD = "policy";
private static final String POLICY_DENY_KEYWORD = "deny";
private static final String POLICY_ALLOW_KEYWORD = "allow";
private static final String ACTION_KEYWORD = "action";
private static final String ACTION_READ_KEYWORD = "read";
private static final String ACTION_WRITE_KEYWORD = "write";
private static final String GRANTEE_TYPE_KEYWORD = "grantee-type";
private static final String GRANTEE_TYPE_USER_KEYWORD = "user";
private static final String GRANTEE_TYPE_GROUP_KEYWORD = "group";
private static final String GRANTEE_ID_KEYWORD = "grantee-id";
private static final String ANY_KEYWORD = "*";
public static final int ACTION_READ = 1;
public static final int ACTION_WRITE = 2;
private static final int POLICY_DENY = 1;
private static final int POLICY_ALLOW = 2;
private static final int GRANTEE_TYPE_USER = 1;
private static final int GRANTEE_TYPE_GROUP = 2;
private static final String GRANTEE_ID_ANY = "-1";
private int policy;
private int action;
private int granteeType;
private String granteeID;

/**
* Creates new ACE object from given String
*
* @param aceDefinition from the acl definition string
* @return ACE object or null
*/
public static ACE fromString(String aceDefinition) {
ACE ace = null;
String[] aceParts = aceDefinition.split(",");

int errors = 0;

int policy = 0;
int action = 0;
int granteeType = 0;
String granteeID = "";

for (int i = 0; i < aceParts.length; i++) {
String acePart = aceParts[i];
String keyValue[] = acePart.split("=");

if (keyValue.length != 2) {
log.error("Invalid ACE format: " + acePart);
errors++;
continue;
}

String key = keyValue[0].trim();
String value = keyValue[1].trim();

if (key.equals(POLICY_KEYWORD)) {
if (value.equals(POLICY_DENY_KEYWORD)) {
policy = POLICY_DENY;
} else if (value.equals(POLICY_ALLOW_KEYWORD)) {
policy = POLICY_ALLOW;
} else {
log.error("Invalid ACE policy value: " + value);
errors++;
}
} else if (key.equals(ACTION_KEYWORD)) {
if (value.equals(ACTION_READ_KEYWORD)) {
action = ACTION_READ;
} else if (value.equals(ACTION_WRITE_KEYWORD)) {
action = ACTION_WRITE;
} else {
log.error("Invalid ACE action value: " + value);
errors++;
}
} else if (key.equals(GRANTEE_TYPE_KEYWORD)) {
if (value.equals(GRANTEE_TYPE_USER_KEYWORD)) {
granteeType = GRANTEE_TYPE_USER;
} else if (value.equals(GRANTEE_TYPE_GROUP_KEYWORD)) {
granteeType = GRANTEE_TYPE_GROUP;
} else {
log.error("Invalid ACE grantee type value: " + value);
errors++;
}
} else if (key.equals(GRANTEE_ID_KEYWORD)) {
if (value.equals(ANY_KEYWORD)) {
granteeID = GRANTEE_ID_ANY;
} else {
granteeID = value;
}
} else {
log.error("Invalid ACE keyword: " + key);
errors++;
}
}
if (errors == 0) {
ace = new ACE(policy, action, granteeType, granteeID);
}
return ace;
}

/**
* Constructor for creating new Access Control Entry
*
* @param policy deny/allow
* @param action read/write
* @param granteeType user/group
* @param granteeID group UUID
*/
private ACE(int policy, int action, int granteeType, String granteeID) {
this.policy = policy;
this.action = action;
this.granteeType = granteeType;
this.granteeID = granteeID;
}

/**
* Method that checks whether the given inputs match this Access Control Entry
*
* @param userID of the current user
* @param groupIDs where is assigned the current user
* @param action could/couldn't be allowed for the user
* @return
*/
public boolean matches(String userID, Set<String> groupIDs, int action) {
if (this.action == action) {
if (granteeType == ACE.GRANTEE_TYPE_USER) {
if (granteeID.equals(GRANTEE_ID_ANY) || userID.equals(granteeID)) {
return true;
}
} else if (granteeType == ACE.GRANTEE_TYPE_GROUP) {
if (granteeID.equals(GRANTEE_ID_ANY) || groupIDs.contains(granteeID)) {
return true;
}
}
}
return false;
}

/**
* Convenience method to verify if this entry is allowing the action;
*
* @return the action is allowed or not
*/
public boolean isAllowed() {
return policy == ACE.POLICY_ALLOW;
}

}
145 changes: 145 additions & 0 deletions dspace-api/src/main/java/org/dspace/app/util/ACL.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.util;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.log4j.Logger;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.GroupService;
import org.springframework.stereotype.Component;

/**
* Class that represents Access Control List
*
* @author Michal Josífko
* Class is copied from the LINDAT/CLARIAH-CZ (https://github.com/ufal/clarin-dspace) and modified by
* @author Milan Majchrak (milan.majchrak at dataquest dot sk)
*/

@Component
public class ACL {

/** Logger */
private static final Logger log = Logger.getLogger(ACL.class);
public static final int ACTION_READ = ACE.ACTION_READ;
public static final int ACTION_WRITE = ACE.ACTION_WRITE;
/**
* List of single Access Control Entry
*/
private List<ACE> acl;
protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();

/**
* Creates new ACL object from given String
*
* @param aclDefinition of the field from the form definition file
* @return ACL object
*/
public static ACL fromString(String aclDefinition) {
List<ACE> acl = new ArrayList<ACE>();
if (aclDefinition != null) {
String[] aclEntries = aclDefinition.split(";");
for (int i = 0; i < aclEntries.length; i++) {
String aclEntry = aclEntries[i];
ACE ace = ACE.fromString(aclEntry);
if (ace != null) {
acl.add(ace);
}
}
}
return new ACL(acl);
}

/**
* Constructor for creating new Access Control List
*
* @param acl List of ACE
*/
ACL(List<ACE> acl) {
this.acl = acl;
}

/**
* Method to verify whether the the given user ID and set of group IDs is
* allowed to perform the given action
*
* @param userID current user
* @param groupIDs where is assigned the current user
* @param action read/write
* @return if user will see the input field
*/
private boolean isAllowedAction(String userID, Set<String> groupIDs, int action) {
for (ACE ace : acl) {
if (ace.matches(userID, groupIDs, action)) {
return ace.isAllowed();
}
}
return false;
}

/**
* Convenience method to verify whether the current user is allowed to
* perform given action based on current context
*
* @param c Current context, the user information are loaded from the context
* @param action read/write
* @return if user will see the input field
* @throws SQLException
*/
public boolean isAllowedAction(Context c, int action) {
boolean res = false;
if (acl.isEmpty()) {
// To maintain backwards compatibility allow everything if the ACL
// is empty
return true;
}
try {
if (authorizeService.isAdmin(c)) {
// Admin is always allowed
return true;
} else {
EPerson e = c.getCurrentUser();
if (e != null) {
UUID userID = e.getID();
List<Group> groups = groupService.allMemberGroups(c, c.getCurrentUser());

Set<String> groupIDs = groups.stream().flatMap(group -> Stream.of(group.getID().toString()))
.collect(Collectors.toSet());

return isAllowedAction(userID.toString(), groupIDs, action);
}
}
} catch (SQLException e) {
log.error(e);
}
return res;
}

/**
* Returns true is the ACL is empty set of rules
*
* @return contains some ACE elements
*/
public boolean isEmpty() {
return acl.isEmpty();
}

}
18 changes: 18 additions & 0 deletions dspace-api/src/main/java/org/dspace/app/util/DCInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import org.apache.commons.lang3.StringUtils;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -135,6 +136,11 @@ public class DCInput {
*/
private String regex = null;

/**
* Access Control List - is user allowed for particular ACL action on this input field in given
*/
private ACL acl = null;

/**
* allowed document types
*/
Expand Down Expand Up @@ -208,6 +214,7 @@ public DCInput(Map<String, String> fieldMap, Map<String, List<String>> listMap)
readOnly = fieldMap.get("readonly");
vocabulary = fieldMap.get("vocabulary");
regex = fieldMap.get("regex");
acl = ACL.fromString(fieldMap.get("acl"));
String closedVocabularyStr = fieldMap.get("closedVocabulary");
closedVocabulary = "true".equalsIgnoreCase(closedVocabularyStr)
|| "yes".equalsIgnoreCase(closedVocabularyStr);
Expand Down Expand Up @@ -536,6 +543,17 @@ public List<String> getExternalSources() {
return externalSources;
}

/**
* Is user allowed for particular ACL action on this input field in given Context?
*
* @param c current Context, load the user data based on the current Context
* @param action read/write
* @return true if allowed, false otherwise
*/
public boolean isAllowedAction(Context c, int action) {
return acl.isAllowedAction(c, action);
}

public boolean isQualdropValue() {
if ("qualdrop_value".equals(getInputType())) {
return true;
Expand Down
Loading

0 comments on commit b1dfa62

Please sign in to comment.