| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package examples.baku.io.permissions; |
| |
| import com.google.common.collect.UnmodifiableIterator; |
| import com.google.firebase.database.DataSnapshot; |
| import com.google.firebase.database.DatabaseError; |
| import com.google.firebase.database.DatabaseReference; |
| import com.google.firebase.database.ValueEventListener; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| import examples.baku.io.permissions.util.Utils; |
| |
| /** |
| * Created by phamilton on 7/9/16. |
| */ |
| public class Blessing implements Iterable<Blessing.Permission>, ValueEventListener { |
| |
| private static final String KEY_PERMISSIONS = "_permissions"; |
| private static final String KEY_RULES = "rule"; |
| |
| private PermissionManager permissionManager; |
| |
| private String id; |
| private String source; |
| private String target; |
| private DatabaseReference ref; |
| private DatabaseReference rulesRef; |
| private DataSnapshot snapshot; |
| |
| private Blessing parentBlessing; |
| private final Map<String, Integer> permissions = new HashMap<>(); |
| private final PermissionTree permissionTree = new PermissionTree(); |
| |
| private final Set<OnBlessingUpdatedListener> blessingListeners = new HashSet<>(); |
| |
| |
| public interface OnBlessingUpdatedListener { |
| void onBlessingUpdated(Blessing blessing); |
| |
| void onBlessingRemoved(Blessing blessing); |
| } |
| |
| private Blessing(PermissionManager permissionManager, String id, String source, String target) { |
| this.permissionManager = permissionManager; |
| if (id == null) { |
| // setRef(permissionManager.getBlessingsRef().push()); |
| //TEMP: use a combination of source and target for debugging |
| setRef(permissionManager.getBlessingsRef().child(source + "_" + target)); |
| id = this.ref.getKey(); |
| |
| } else { |
| setRef(permissionManager.getBlessingsRef().child(id)); |
| } |
| setId(id); |
| setSource(source); |
| setTarget(target); |
| } |
| |
| public static Blessing create(PermissionManager permissionManager, String id, String source, String target) { |
| Blessing blessing = permissionManager.getBlessing(source, target); |
| if (blessing == null) { |
| blessing = new Blessing(permissionManager, id, source, target); |
| permissionManager.putBlessing(blessing); |
| } |
| return blessing; |
| } |
| |
| //root blessings have no source blessing and their id is the same as their target |
| public static Blessing createRoot(PermissionManager permissionManager, String target) { |
| return create(permissionManager, target, null, target); |
| } |
| |
| public static Blessing fromSnapshot(PermissionManager permissionManager, DataSnapshot snapshot) { |
| String id = snapshot.getKey(); |
| String target = snapshot.child("target").getValue(String.class); |
| String source = null; |
| if (snapshot.hasChild("source")) |
| source = snapshot.child("source").getValue(String.class); |
| return create(permissionManager, id, source, target); |
| } |
| |
| public OnBlessingUpdatedListener addListener(OnBlessingUpdatedListener listener) { |
| blessingListeners.add(listener); |
| listener.onBlessingUpdated(this); |
| return listener; |
| } |
| |
| public boolean addListeners(Collection<OnBlessingUpdatedListener> listeners) { |
| return this.blessingListeners.addAll(listeners); |
| } |
| |
| public boolean removeListener(OnBlessingUpdatedListener listener) { |
| return blessingListeners.remove(listener); |
| } |
| |
| public boolean removeListeners(Collection<OnBlessingUpdatedListener> listeners) { |
| return blessingListeners.removeAll(listeners); |
| } |
| |
| private final OnBlessingUpdatedListener parentListener = new OnBlessingUpdatedListener() { |
| @Override |
| public void onBlessingUpdated(Blessing blessing) { |
| permissionTree.parentTree = parentBlessing.permissionTree; |
| notifyListeners(); |
| } |
| |
| @Override |
| public void onBlessingRemoved(Blessing blessing) { |
| //revoke self |
| revoke(); |
| } |
| }; |
| |
| public int getPermissions(String path) { |
| return permissionTree.getPermissions(path); |
| } |
| |
| public boolean isSynched() { |
| return snapshot != null; |
| } |
| |
| public String getId() { |
| return id; |
| } |
| |
| public String getSource() { |
| return source; |
| } |
| |
| public String getTarget() { |
| return target; |
| } |
| |
| public void setId(String id) { |
| this.id = id; |
| ref.child("id").setValue(id); |
| } |
| |
| private void setSource(String source) { |
| if (this.source == null && source != null) { |
| this.source = source; |
| ref.child("source").setValue(source); |
| parentBlessing = permissionManager.getBlessing(source); |
| if (parentBlessing == null) { //retrieve, if manager isn't tracking blessing |
| permissionManager.getBlessingsRef().child(source).addListenerForSingleValueEvent(new ValueEventListener() { |
| @Override |
| public void onDataChange(DataSnapshot dataSnapshot) { |
| if (dataSnapshot.exists()) { |
| parentBlessing = Blessing.fromSnapshot(permissionManager, dataSnapshot); |
| parentBlessing.addListener(parentListener); |
| } else { //destroy self if source doesn't exist |
| revoke(); |
| } |
| } |
| |
| @Override |
| public void onCancelled(DatabaseError databaseError) { |
| |
| } |
| }); |
| } else { |
| permissionTree.parentTree = parentBlessing.permissionTree; |
| parentBlessing.addListener(parentListener); |
| } |
| } |
| } |
| |
| private void notifyListeners() { |
| for (OnBlessingUpdatedListener listener : blessingListeners) { |
| listener.onBlessingUpdated(this); |
| } |
| } |
| |
| public void setTarget(String target) { |
| this.target = target; |
| ref.child("target").setValue(target); |
| } |
| |
| private void setSnapshot(DataSnapshot snapshot) { |
| if (!snapshot.exists()) { |
| throw new IllegalArgumentException("empty snapshot"); |
| } |
| this.snapshot = snapshot; |
| if (snapshot.hasChild(KEY_RULES)) { |
| this.permissionTree.setRoot(new Permission(snapshot.child(KEY_RULES), null, 0)); |
| } else { |
| this.permissionTree.setRoot(new Permission()); |
| } |
| } |
| |
| public Blessing setPermissions(String path, int permissions) { |
| this.permissions.put(path, permissions); |
| getRef(path).setPermission(permissions); |
| return this; |
| } |
| |
| public void setPermissions(Map<String, Integer> permissions) { |
| for (Map.Entry<String, Integer> entry : permissions.entrySet()) { |
| setPermissions(entry.getKey(), permissions.get(entry.getValue())); |
| } |
| } |
| |
| public Blessing clearPermissions(String path) { |
| getRef(path).clearPermission(); |
| this.permissions.remove(path); |
| return this; |
| } |
| |
| public Blessing revoke() { |
| if (parentBlessing != null) { |
| parentBlessing.removeListener(parentListener); |
| } |
| for (OnBlessingUpdatedListener listener : blessingListeners) { |
| listener.onBlessingRemoved(this); |
| } |
| ref.removeEventListener(this); |
| rulesRef.removeValue(); |
| return this; |
| } |
| |
| //delete all permission above path |
| public Blessing revokePermissions(String path) { |
| if (path != null) { |
| rulesRef.child(path).removeValue(); |
| } else { |
| rulesRef.removeValue(); |
| } |
| return this; |
| } |
| |
| private PermissionReference getRef(String path) { |
| return new PermissionReference(rulesRef, path); |
| } |
| |
| private void setRef(DatabaseReference ref) { |
| this.ref = ref; |
| this.rulesRef = ref.child(KEY_RULES); |
| |
| ref.addValueEventListener(this); |
| } |
| |
| @Override |
| public void onDataChange(DataSnapshot dataSnapshot) { |
| if (dataSnapshot.exists()) { |
| setSnapshot(dataSnapshot); |
| notifyListeners(); |
| } |
| } |
| |
| @Override |
| public void onCancelled(DatabaseError databaseError) { |
| databaseError.toException().printStackTrace(); |
| } |
| |
| //return a blessing interface for granting/revoking permissions |
| public Blessing bless(String target) { |
| Blessing result = getBlessing(target); |
| if (result == null) { |
| if (isDescendantOf(target) || target.equals(this.target)) { |
| throw new IllegalArgumentException("Can't bless a target that already exists in the blessing hiearchy."); |
| } |
| result = Blessing.create(permissionManager, null, getId(), target); |
| } |
| return result; |
| } |
| |
| public Blessing getBlessing(String target) { |
| return permissionManager.getBlessing(getId(), target); |
| } |
| |
| public boolean isDescendantOf(String target) { |
| return this.target.equals(target) || parentBlessing != null && parentBlessing.isDescendantOf(target); |
| } |
| |
| @Override |
| public Iterator<Permission> iterator() { |
| return isSynched() ? permissionTree.iterator() : null; |
| } |
| |
| public PermissionTree getPermissionTree() { |
| return permissionTree; |
| } |
| |
| public static class Permission implements Iterable<Permission> { |
| String key; |
| String path; |
| int inherited; |
| int permissions; |
| final Map<String, Permission> children = new HashMap(); |
| |
| |
| public Permission() { |
| } |
| |
| public Permission(DataSnapshot snapshot, String path, int inherited) { |
| this.path = path; |
| if (path != null) { |
| this.key = snapshot.getKey(); |
| } |
| this.inherited = inherited; |
| if (snapshot.hasChild(KEY_PERMISSIONS)) { |
| this.permissions |= snapshot.child(KEY_PERMISSIONS).getValue(Integer.class); |
| } |
| for (DataSnapshot child : snapshot.getChildren()) { |
| if (child.getKey().startsWith("_")) { //ignore keys with '_' prefix |
| continue; |
| } |
| String childPath = child.getKey(); |
| if (path != null) { |
| childPath = path + "/" + childPath; |
| } |
| children.put(child.getKey(), new Permission(child, childPath, this.permissions | this.inherited)); |
| } |
| } |
| |
| public Permission copy() { |
| Permission result = new Permission(); |
| result.key = key; |
| result.path = path; |
| result.inherited = inherited; |
| result.permissions = permissions; |
| for (Permission child : children.values()) { |
| result.children.put(child.key, child.copy()); |
| } |
| return result; |
| } |
| |
| public void addPermissions(int permission) { |
| this.permissions |= permission; |
| for (Permission child : children.values()) { |
| child.setInherited(getPermissions()); |
| } |
| } |
| |
| public void setInherited(int permission) { |
| if (this.inherited != permission) { |
| this.inherited = permission; |
| propagateInherited(); |
| } |
| } |
| |
| public void propagateInherited() { |
| for (Permission child : children.values()) { |
| child.setInherited(getPermissions()); |
| } |
| } |
| |
| public void removePermissions(int permission) { |
| this.permissions &= ~(permission); |
| propagateInherited(); |
| } |
| |
| public void checkPermissions(int reference) { |
| this.permissions &= reference; |
| propagateInherited(); |
| } |
| |
| public void checkPermissions(PermissionTree ref) { |
| for (Permission permission : this) { |
| permission.permissions &= ref.getPermissions(permission.path); |
| } |
| setInherited(inherited & ref.getPermissions(path)); |
| } |
| |
| |
| public Permission child(String path) { |
| int index = path.indexOf("/"); |
| if (index != -1) { |
| return this.children.get(path); |
| } |
| Permission child = this.children.get(path.substring(0, index)); |
| if (child != null && path.length() - index > 1) { |
| return child.child(path.substring(index)); |
| } |
| return null; |
| } |
| |
| |
| public int getPermissions() { |
| return permissions | inherited; |
| } |
| |
| |
| @Override |
| public Iterator<Permission> iterator() { |
| final Stack<Permission> nodeStack = new Stack<>(); |
| nodeStack.push(this); |
| |
| final Stack<String> pathStack = new Stack<>(); |
| pathStack.push(null); //default rule |
| |
| return new UnmodifiableIterator<Permission>() { |
| @Override |
| public boolean hasNext() { |
| return !nodeStack.isEmpty(); |
| } |
| |
| @Override |
| public Permission next() { |
| Permission node = nodeStack.pop(); |
| for (final Permission child : node.children.values()) { |
| nodeStack.push(child); |
| } |
| return node; |
| } |
| }; |
| } |
| } |
| |
| |
| public static class PermissionTree implements Iterable<Permission> { |
| Permission root; |
| final Map<String, Permission> rules = new HashMap<>(); |
| PermissionTree parentTree; |
| |
| public PermissionTree(DataSnapshot snapshot) { |
| setRoot(new Permission(snapshot, null, 0)); |
| } |
| |
| public PermissionTree() { |
| setRoot(new Permission()); |
| } |
| |
| public void setRoot(Permission root) { |
| this.root = root; |
| updateRules(); |
| } |
| |
| public void merge(PermissionTree tree) { |
| Permission permissionA; |
| Permission permissionB = tree.root; |
| permissionB.checkPermissions(tree); //check no permissions exceed parent |
| Queue<Permission> permissionQueue = new LinkedList<>(); |
| permissionQueue.add(permissionB); |
| |
| while (!permissionQueue.isEmpty()) { |
| permissionB = permissionQueue.remove(); |
| permissionA = rules.get(permissionB.path); |
| permissionA.addPermissions(tree.getPermissions(permissionB.path)); |
| for (Permission child : permissionB.children.values()) { |
| if (rules.containsKey(child.path)) { |
| permissionQueue.add(child); |
| } else { |
| Permission childCopy = child.copy(); |
| childCopy.setInherited(permissionA.getPermissions()); |
| permissionA.children.put(childCopy.key, childCopy); |
| } |
| } |
| } |
| updateRules(); |
| } |
| |
| private void updateRules() { |
| rules.clear(); |
| for (Permission permission : root) { |
| rules.put(permission.path, permission); |
| } |
| } |
| |
| public Permission get(String path) { |
| return rules.get(path); |
| } |
| |
| public int getPermissions(String path) { |
| path = Utils.getNearestCommonAncestor(path, keySet()); |
| Permission permission = get(path); |
| if (permission == null) { |
| return 0; |
| } |
| int result = permission.getPermissions(); |
| if (parentTree != null) { //validate |
| result &= parentTree.getPermissions(path); |
| } |
| return result; |
| } |
| |
| public Set<String> keySet() { |
| return rules.keySet(); |
| } |
| |
| public Collection<Permission> values() { |
| return rules.values(); |
| } |
| |
| @Override |
| public Iterator<Permission> iterator() { |
| return root.iterator(); |
| } |
| } |
| } |