ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseACL.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));
}
}
}