612 lines
20 KiB
Java
612 lines
20 KiB
Java
/*
|
|
* Copyright (c) 2015-present, Parse, LLC.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
package com.parse;
|
|
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* A {@code ParseACL} is used to control which users can access or modify a particular object. Each
|
|
* {@link ParseObject} can have its own {@code ParseACL}. You can grant read and write permissions
|
|
* separately to specific users, to groups of users that belong to roles, or you can grant
|
|
* permissions to "the public" so that, for example, any user could read a particular object but
|
|
* only a particular set of users could write to that object.
|
|
*/
|
|
public class ParseACL implements Parcelable {
|
|
private static final String PUBLIC_KEY = "*";
|
|
private final static String UNRESOLVED_KEY = "*unresolved";
|
|
private static final String KEY_ROLE_PREFIX = "role:";
|
|
private static final String UNRESOLVED_USER_JSON_KEY = "unresolvedUser";
|
|
|
|
private static class Permissions {
|
|
private static final String READ_PERMISSION = "read";
|
|
private static final String WRITE_PERMISSION = "write";
|
|
|
|
private final boolean readPermission;
|
|
private final boolean writePermission;
|
|
|
|
/* package */ Permissions(boolean readPermission, boolean write) {
|
|
this.readPermission = readPermission;
|
|
this.writePermission = write;
|
|
}
|
|
|
|
/* package */ Permissions(Permissions permissions) {
|
|
this.readPermission = permissions.readPermission;
|
|
this.writePermission = permissions.writePermission;
|
|
}
|
|
|
|
/* package */ JSONObject toJSONObject() {
|
|
JSONObject json = new JSONObject();
|
|
|
|
try {
|
|
if (readPermission) {
|
|
json.put(READ_PERMISSION, true);
|
|
}
|
|
if (writePermission) {
|
|
json.put(WRITE_PERMISSION, true);
|
|
}
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
/* package */ void toParcel(Parcel parcel) {
|
|
parcel.writeByte(readPermission ? (byte) 1 : 0);
|
|
parcel.writeByte(writePermission ? (byte) 1 : 0);
|
|
}
|
|
|
|
/* package */ boolean getReadPermission() {
|
|
return readPermission;
|
|
}
|
|
|
|
/* package */ boolean getWritePermission() {
|
|
return writePermission;
|
|
}
|
|
|
|
/* package */ static Permissions createPermissionsFromJSONObject(JSONObject object) {
|
|
boolean read = object.optBoolean(READ_PERMISSION, false);
|
|
boolean write = object.optBoolean(WRITE_PERMISSION, false);
|
|
return new Permissions(read, write);
|
|
}
|
|
|
|
/* package */ static Permissions createPermissionsFromParcel(Parcel parcel) {
|
|
return new Permissions(parcel.readByte() == 1, parcel.readByte() == 1);
|
|
}
|
|
}
|
|
|
|
private static ParseDefaultACLController getDefaultACLController() {
|
|
return ParseCorePlugins.getInstance().getDefaultACLController();
|
|
}
|
|
|
|
/**
|
|
* Sets a default ACL that will be applied to all {@link ParseObject}s when they are created.
|
|
*
|
|
* @param acl
|
|
* The ACL to use as a template for all {@link ParseObject}s created after setDefaultACL
|
|
* has been called. This value will be copied and used as a template for the creation of
|
|
* new ACLs, so changes to the instance after {@code setDefaultACL(ParseACL, boolean)}
|
|
* has been called will not be reflected in new {@link ParseObject}s.
|
|
* @param withAccessForCurrentUser
|
|
* If {@code true}, the {@code ParseACL} that is applied to newly-created
|
|
* {@link ParseObject}s will provide read and write access to the
|
|
* {@link ParseUser#getCurrentUser()} at the time of creation. If {@code false}, the
|
|
* provided ACL will be used without modification. If acl is {@code null}, this value is
|
|
* ignored.
|
|
*/
|
|
public static void setDefaultACL(ParseACL acl, boolean withAccessForCurrentUser) {
|
|
getDefaultACLController().set(acl, withAccessForCurrentUser);
|
|
}
|
|
|
|
/* package */ static ParseACL getDefaultACL() {
|
|
return getDefaultACLController().get();
|
|
}
|
|
|
|
// State
|
|
private final Map<String, Permissions> permissionsById = new HashMap<>();
|
|
private boolean shared;
|
|
|
|
/**
|
|
* A lazy user that hasn't been saved to Parse.
|
|
*/
|
|
//TODO (grantland): This should be a list for multiple lazy users with read/write permissions.
|
|
private ParseUser unresolvedUser;
|
|
|
|
/**
|
|
* Creates an ACL with no permissions granted.
|
|
*/
|
|
public ParseACL() {
|
|
// do nothing
|
|
}
|
|
|
|
/**
|
|
* Creates a copy of {@code acl}.
|
|
*
|
|
* @param acl
|
|
* The acl to copy.
|
|
*/
|
|
public ParseACL(ParseACL acl) {
|
|
for (String id : acl.permissionsById.keySet()) {
|
|
permissionsById.put(id, new Permissions(acl.permissionsById.get(id)));
|
|
}
|
|
unresolvedUser = acl.unresolvedUser;
|
|
if (unresolvedUser != null) {
|
|
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
|
|
}
|
|
}
|
|
|
|
/* package for tests */ ParseACL copy() {
|
|
return new ParseACL(this);
|
|
}
|
|
|
|
boolean isShared() {
|
|
return shared;
|
|
}
|
|
|
|
void setShared(boolean shared) {
|
|
this.shared = shared;
|
|
}
|
|
|
|
// Internally we expose the json object this wraps
|
|
/* package */ JSONObject toJSONObject(ParseEncoder objectEncoder) {
|
|
JSONObject json = new JSONObject();
|
|
try {
|
|
for (String id: permissionsById.keySet()) {
|
|
json.put(id, permissionsById.get(id).toJSONObject());
|
|
}
|
|
if (unresolvedUser != null) {
|
|
Object encoded = objectEncoder.encode(unresolvedUser);
|
|
json.put(UNRESOLVED_USER_JSON_KEY, encoded);
|
|
}
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
// A helper for creating a ParseACL from the wire.
|
|
// We iterate over it rather than just copying to permissionsById so that we
|
|
// can ensure it's the right format.
|
|
/* package */ static ParseACL createACLFromJSONObject(JSONObject object, ParseDecoder decoder) {
|
|
ParseACL acl = new ParseACL();
|
|
|
|
for (String key : ParseJSONUtils.keys(object)) {
|
|
if (key.equals(UNRESOLVED_USER_JSON_KEY)) {
|
|
JSONObject unresolvedUser;
|
|
try {
|
|
unresolvedUser = object.getJSONObject(key);
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
acl.unresolvedUser = (ParseUser) decoder.decode(unresolvedUser);
|
|
} else {
|
|
try {
|
|
Permissions permissions = Permissions.createPermissionsFromJSONObject(object.getJSONObject(key));
|
|
acl.permissionsById.put(key, permissions);
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException("could not decode ACL: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
return acl;
|
|
}
|
|
|
|
/**
|
|
* Creates an ACL where only the provided user has access.
|
|
*
|
|
* @param owner
|
|
* The only user that can read or write objects governed by this ACL.
|
|
*/
|
|
public ParseACL(ParseUser owner) {
|
|
this();
|
|
setReadAccess(owner, true);
|
|
setWriteAccess(owner, true);
|
|
}
|
|
|
|
/* package for tests */ void resolveUser(ParseUser user) {
|
|
if (!isUnresolvedUser(user)) {
|
|
return;
|
|
}
|
|
if (permissionsById.containsKey(UNRESOLVED_KEY)) {
|
|
permissionsById.put(user.getObjectId(), permissionsById.get(UNRESOLVED_KEY));
|
|
permissionsById.remove(UNRESOLVED_KEY);
|
|
}
|
|
unresolvedUser = null;
|
|
}
|
|
|
|
/* package */ boolean hasUnresolvedUser() {
|
|
return unresolvedUser != null;
|
|
}
|
|
|
|
/* package */ ParseUser getUnresolvedUser() {
|
|
return unresolvedUser;
|
|
}
|
|
|
|
// Helper for setting stuff
|
|
private void setPermissionsIfNonEmpty(String userId, boolean readPermission, boolean writePermission) {
|
|
if (!(readPermission || writePermission)) {
|
|
permissionsById.remove(userId);
|
|
}
|
|
else {
|
|
permissionsById.put(userId, new Permissions(readPermission, writePermission));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set whether the public is allowed to read this object.
|
|
*/
|
|
public void setPublicReadAccess(boolean allowed) {
|
|
setReadAccess(PUBLIC_KEY, allowed);
|
|
}
|
|
|
|
/**
|
|
* Get whether the public is allowed to read this object.
|
|
*/
|
|
public boolean getPublicReadAccess() {
|
|
return getReadAccess(PUBLIC_KEY);
|
|
}
|
|
|
|
/**
|
|
* Set whether the public is allowed to write this object.
|
|
*/
|
|
public void setPublicWriteAccess(boolean allowed) {
|
|
setWriteAccess(PUBLIC_KEY, allowed);
|
|
}
|
|
|
|
/**
|
|
* Set whether the public is allowed to write this object.
|
|
*/
|
|
public boolean getPublicWriteAccess() {
|
|
return getWriteAccess(PUBLIC_KEY);
|
|
}
|
|
|
|
/**
|
|
* Set whether the given user id is allowed to read this object.
|
|
*/
|
|
public void setReadAccess(String userId, boolean allowed) {
|
|
if (userId == null) {
|
|
throw new IllegalArgumentException("cannot setReadAccess for null userId");
|
|
}
|
|
boolean writePermission = getWriteAccess(userId);
|
|
setPermissionsIfNonEmpty(userId, allowed, writePermission);
|
|
}
|
|
|
|
/**
|
|
* Get whether the given user id is *explicitly* allowed to read this object. Even if this returns
|
|
* {@code false}, the user may still be able to access it if getPublicReadAccess returns
|
|
* {@code true} or a role that the user belongs to has read access.
|
|
*/
|
|
public boolean getReadAccess(String userId) {
|
|
if (userId == null) {
|
|
throw new IllegalArgumentException("cannot getReadAccess for null userId");
|
|
}
|
|
Permissions permissions = permissionsById.get(userId);
|
|
return permissions != null && permissions.getReadPermission();
|
|
}
|
|
|
|
/**
|
|
* Set whether the given user id is allowed to write this object.
|
|
*/
|
|
public void setWriteAccess(String userId, boolean allowed) {
|
|
if (userId == null) {
|
|
throw new IllegalArgumentException("cannot setWriteAccess for null userId");
|
|
}
|
|
boolean readPermission = getReadAccess(userId);
|
|
setPermissionsIfNonEmpty(userId, readPermission, allowed);
|
|
}
|
|
|
|
/**
|
|
* Get whether the given user id is *explicitly* allowed to write this object. Even if this
|
|
* returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns
|
|
* {@code true} or a role that the user belongs to has write access.
|
|
*/
|
|
public boolean getWriteAccess(String userId) {
|
|
if (userId == null) {
|
|
throw new IllegalArgumentException("cannot getWriteAccess for null userId");
|
|
}
|
|
Permissions permissions = permissionsById.get(userId);
|
|
return permissions != null && permissions.getWritePermission();
|
|
}
|
|
|
|
/**
|
|
* Set whether the given user is allowed to read this object.
|
|
*/
|
|
public void setReadAccess(ParseUser user, boolean allowed) {
|
|
if (user.getObjectId() == null) {
|
|
if (user.isLazy()) {
|
|
setUnresolvedReadAccess(user, allowed);
|
|
return;
|
|
}
|
|
throw new IllegalArgumentException("cannot setReadAccess for a user with null id");
|
|
}
|
|
setReadAccess(user.getObjectId(), allowed);
|
|
}
|
|
|
|
private void setUnresolvedReadAccess(ParseUser user, boolean allowed) {
|
|
prepareUnresolvedUser(user);
|
|
setReadAccess(UNRESOLVED_KEY, allowed);
|
|
}
|
|
|
|
private void setUnresolvedWriteAccess(ParseUser user, boolean allowed) {
|
|
prepareUnresolvedUser(user);
|
|
setWriteAccess(UNRESOLVED_KEY, allowed);
|
|
}
|
|
|
|
private void prepareUnresolvedUser(ParseUser user) {
|
|
// Registers a listener for the user so that when it is saved, the
|
|
// unresolved ACL will be resolved.
|
|
if (!isUnresolvedUser(user)) {
|
|
permissionsById.remove(UNRESOLVED_KEY);
|
|
unresolvedUser = user;
|
|
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
|
|
}
|
|
}
|
|
|
|
private boolean isUnresolvedUser(ParseUser other) {
|
|
// This might be a different instance, but if they have the same local id, assume it's correct.
|
|
if (other == null || unresolvedUser == null) return false;
|
|
return other == unresolvedUser || (other.getObjectId() == null &&
|
|
other.getOrCreateLocalId().equals(unresolvedUser.getOrCreateLocalId()));
|
|
}
|
|
|
|
/**
|
|
* Get whether the given user id is *explicitly* allowed to read this object. Even if this returns
|
|
* {@code false}, the user may still be able to access it if getPublicReadAccess returns
|
|
* {@code true} or a role that the user belongs to has read access.
|
|
*/
|
|
public boolean getReadAccess(ParseUser user) {
|
|
if (isUnresolvedUser(user)) {
|
|
return getReadAccess(UNRESOLVED_KEY);
|
|
}
|
|
if (user.isLazy()) {
|
|
return false;
|
|
}
|
|
if (user.getObjectId() == null) {
|
|
throw new IllegalArgumentException("cannot getReadAccess for a user with null id");
|
|
}
|
|
return getReadAccess(user.getObjectId());
|
|
}
|
|
|
|
/**
|
|
* Set whether the given user is allowed to write this object.
|
|
*/
|
|
public void setWriteAccess(ParseUser user, boolean allowed) {
|
|
if (user.getObjectId() == null) {
|
|
if (user.isLazy()) {
|
|
setUnresolvedWriteAccess(user, allowed);
|
|
return;
|
|
}
|
|
throw new IllegalArgumentException("cannot setWriteAccess for a user with null id");
|
|
}
|
|
setWriteAccess(user.getObjectId(), allowed);
|
|
}
|
|
|
|
/**
|
|
* Get whether the given user id is *explicitly* allowed to write this object. Even if this
|
|
* returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns
|
|
* {@code true} or a role that the user belongs to has write access.
|
|
*/
|
|
public boolean getWriteAccess(ParseUser user) {
|
|
if (isUnresolvedUser(user)) {
|
|
return getWriteAccess(UNRESOLVED_KEY);
|
|
}
|
|
if (user.isLazy()) {
|
|
return false;
|
|
}
|
|
if (user.getObjectId() == null) {
|
|
throw new IllegalArgumentException("cannot getWriteAccess for a user with null id");
|
|
}
|
|
return getWriteAccess(user.getObjectId());
|
|
}
|
|
|
|
/**
|
|
* Get whether users belonging to the role with the given roleName are allowed to read this
|
|
* object. Even if this returns {@code false}, the role may still be able to read it if a parent
|
|
* role has read access.
|
|
*
|
|
* @param roleName
|
|
* The name of the role.
|
|
* @return {@code true} if the role has read access. {@code false} otherwise.
|
|
*/
|
|
public boolean getRoleReadAccess(String roleName) {
|
|
return getReadAccess(KEY_ROLE_PREFIX + roleName);
|
|
}
|
|
|
|
/**
|
|
* Set whether users belonging to the role with the given roleName are allowed to read this
|
|
* object.
|
|
*
|
|
* @param roleName
|
|
* The name of the role.
|
|
* @param allowed
|
|
* Whether the given role can read this object.
|
|
*/
|
|
public void setRoleReadAccess(String roleName, boolean allowed) {
|
|
setReadAccess(KEY_ROLE_PREFIX + roleName, allowed);
|
|
}
|
|
|
|
/**
|
|
* Get whether users belonging to the role with the given roleName are allowed to write this
|
|
* object. Even if this returns {@code false}, the role may still be able to write it if a parent
|
|
* role has write access.
|
|
*
|
|
* @param roleName
|
|
* The name of the role.
|
|
* @return {@code true} if the role has write access. {@code false} otherwise.
|
|
*/
|
|
public boolean getRoleWriteAccess(String roleName) {
|
|
return getWriteAccess(KEY_ROLE_PREFIX + roleName);
|
|
}
|
|
|
|
/**
|
|
* Set whether users belonging to the role with the given roleName are allowed to write this
|
|
* object.
|
|
*
|
|
* @param roleName
|
|
* The name of the role.
|
|
* @param allowed
|
|
* Whether the given role can write this object.
|
|
*/
|
|
public void setRoleWriteAccess(String roleName, boolean allowed) {
|
|
setWriteAccess(KEY_ROLE_PREFIX + roleName, allowed);
|
|
}
|
|
|
|
private static void validateRoleState(ParseRole role) {
|
|
if (role == null || role.getObjectId() == null) {
|
|
throw new IllegalArgumentException(
|
|
"Roles must be saved to the server before they can be used in an ACL.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get whether users belonging to the given role are allowed to read this object. Even if this
|
|
* returns {@code false}, the role may still be able to read it if a parent role has read access.
|
|
* The role must already be saved on the server and its data must have been fetched in order to
|
|
* use this method.
|
|
*
|
|
* @param role
|
|
* The role to check for access.
|
|
* @return {@code true} if the role has read access. {@code false} otherwise.
|
|
*/
|
|
public boolean getRoleReadAccess(ParseRole role) {
|
|
validateRoleState(role);
|
|
return getRoleReadAccess(role.getName());
|
|
}
|
|
|
|
/**
|
|
* Set whether users belonging to the given role are allowed to read this object. The role must
|
|
* already be saved on the server and its data must have been fetched in order to use this method.
|
|
*
|
|
* @param role
|
|
* The role to assign access.
|
|
* @param allowed
|
|
* Whether the given role can read this object.
|
|
*/
|
|
public void setRoleReadAccess(ParseRole role, boolean allowed) {
|
|
validateRoleState(role);
|
|
setRoleReadAccess(role.getName(), allowed);
|
|
}
|
|
|
|
/**
|
|
* Get whether users belonging to the given role are allowed to write this object. Even if this
|
|
* returns {@code false}, the role may still be able to write it if a parent role has write
|
|
* access. The role must already be saved on the server and its data must have been fetched in
|
|
* order to use this method.
|
|
*
|
|
* @param role
|
|
* The role to check for access.
|
|
* @return {@code true} if the role has write access. {@code false} otherwise.
|
|
*/
|
|
public boolean getRoleWriteAccess(ParseRole role) {
|
|
validateRoleState(role);
|
|
return getRoleWriteAccess(role.getName());
|
|
}
|
|
|
|
/**
|
|
* Set whether users belonging to the given role are allowed to write this object. The role must
|
|
* already be saved on the server and its data must have been fetched in order to use this method.
|
|
*
|
|
* @param role
|
|
* The role to assign access.
|
|
* @param allowed
|
|
* Whether the given role can write this object.
|
|
*/
|
|
public void setRoleWriteAccess(ParseRole role, boolean allowed) {
|
|
validateRoleState(role);
|
|
setRoleWriteAccess(role.getName(), allowed);
|
|
}
|
|
|
|
private static class UserResolutionListener implements GetCallback<ParseObject> {
|
|
private final WeakReference<ParseACL> parent;
|
|
|
|
public UserResolutionListener(ParseACL parent) {
|
|
this.parent = new WeakReference<>(parent);
|
|
}
|
|
|
|
@Override
|
|
public void done(ParseObject object, ParseException e) {
|
|
// A callback that will resolve the user when it is saved for any
|
|
// ACL that is listening to it.
|
|
try {
|
|
ParseACL parent = this.parent.get();
|
|
if (parent != null) {
|
|
parent.resolveUser((ParseUser) object);
|
|
}
|
|
} finally {
|
|
object.unregisterSaveListener(this);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/* package for tests */ Map<String, Permissions> getPermissionsById() {
|
|
return permissionsById;
|
|
}
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
writeToParcel(dest, new ParseObjectParcelEncoder());
|
|
}
|
|
|
|
/* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
|
|
dest.writeByte(shared ? (byte) 1 : 0);
|
|
dest.writeInt(permissionsById.size());
|
|
Set<String> keys = permissionsById.keySet();
|
|
for (String key : keys) {
|
|
dest.writeString(key);
|
|
Permissions permissions = permissionsById.get(key);
|
|
permissions.toParcel(dest);
|
|
}
|
|
dest.writeByte(unresolvedUser != null ? (byte) 1 : 0);
|
|
if (unresolvedUser != null) {
|
|
// Encoder will create a local id for unresolvedUser, so we recognize it after unparcel.
|
|
encoder.encode(unresolvedUser, dest);
|
|
}
|
|
}
|
|
|
|
public final static Creator<ParseACL> CREATOR = new Creator<ParseACL>() {
|
|
@Override
|
|
public ParseACL createFromParcel(Parcel source) {
|
|
return new ParseACL(source, new ParseObjectParcelDecoder());
|
|
}
|
|
|
|
@Override
|
|
public ParseACL[] newArray(int size) {
|
|
return new ParseACL[size];
|
|
}
|
|
};
|
|
|
|
/* package */ ParseACL(Parcel source, ParseParcelDecoder decoder) {
|
|
shared = source.readByte() == 1;
|
|
int size = source.readInt();
|
|
for (int i = 0; i < size; i++) {
|
|
String key = source.readString();
|
|
Permissions permissions = Permissions.createPermissionsFromParcel(source);
|
|
permissionsById.put(key, permissions);
|
|
}
|
|
if (source.readByte() == 1) {
|
|
unresolvedUser = (ParseUser) decoder.decode(source);
|
|
unresolvedUser.registerSaveListener(new UserResolutionListener(this));
|
|
}
|
|
}
|
|
}
|