| // 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.HashBasedTable; |
| import com.google.common.collect.HashMultimap; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Table; |
| import com.google.firebase.database.ChildEventListener; |
| import com.google.firebase.database.DataSnapshot; |
| import com.google.firebase.database.DatabaseError; |
| import com.google.firebase.database.DatabaseReference; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import examples.baku.io.permissions.util.Utils; |
| |
| |
| /** |
| * Created by phamilton on 6/28/16. |
| */ |
| public class PermissionManager { |
| |
| public static final String EXTRA_TIMEOUT = "extraTimeout"; |
| public static final String EXTRA_COLOR = "extraColor"; |
| private DatabaseReference mDatabaseRef; |
| private DatabaseReference mBlessingsRef; |
| private DatabaseReference mRequestsRef; |
| |
| public static final int FLAG_DEFAULT = 0; |
| public static final int FLAG_WRITE = 1 << 0; |
| public static final int FLAG_READ = 1 << 1; |
| |
| static final String KEY_PERMISSIONS = "_permissions"; |
| static final String KEY_REQUESTS = "_requests"; |
| static final String KEY_BLESSINGS = "_blessings"; |
| |
| private static final String KEY_ROOT = "root"; |
| |
| private String mId; |
| private Blessing rootBlessing; |
| |
| //<blessing id, blessing> |
| private final Map<String, Blessing> mBlessings = new HashMap<>(); |
| //<source, target, blessing> |
| private final Table<String, String, Blessing> mBlessingsTable = HashBasedTable.create(); |
| private final Set<String> mBlessingTargets = new HashSet(); |
| |
| private final Map<String, PermissionRequest> mRequests = new HashMap<>(); |
| private final Table<String, String, PermissionRequest.Builder> mActiveRequests = HashBasedTable.create(); |
| |
| private final Multimap<String, OnRequestListener> mRequestListeners = HashMultimap.create(); //<path,, > |
| private final Multimap<String, OnRequestListener> mSubscribedRequests = HashMultimap.create(); //<request id, > |
| |
| private Blessing.PermissionTree mPermissionTree = new Blessing.PermissionTree(); |
| private final Multimap<String, OnPermissionChangeListener> mPermissionValueEventListeners = HashMultimap.create(); |
| private final Multimap<String, String> mNearestAncestors = HashMultimap.create(); |
| |
| |
| //TODO: replace string ownerId with Auth |
| public PermissionManager(final DatabaseReference databaseReference, String owner) { |
| this.mDatabaseRef = databaseReference; |
| this.mId = owner; |
| |
| mRequestsRef = databaseReference.child(KEY_REQUESTS); |
| //TODO: only consider requests from sources within the constellation |
| mRequestsRef.addChildEventListener(requestListener); |
| mBlessingsRef = mDatabaseRef.child(KEY_BLESSINGS); |
| |
| this.mId = owner; |
| initRootBlessing(); |
| join(mId); |
| |
| } |
| |
| public void join(String group) { |
| mBlessingsRef.orderByChild("target").equalTo(group).addChildEventListener(blessingListener); |
| mBlessingTargets.add(group); |
| } |
| |
| public void leave(String group) { |
| mBlessingsRef.orderByChild("target").equalTo(group).removeEventListener(blessingListener); |
| mBlessingTargets.remove(group); |
| } |
| |
| public void initRootBlessing() { |
| rootBlessing = Blessing.createRoot(this, mId); |
| } |
| |
| //TODO: optimize this mess. Currently, recalculating entire permission tree. |
| void refreshPermissions() { |
| Blessing.PermissionTree updatedPermissionTree = new Blessing.PermissionTree(); |
| //received blessings |
| for (Blessing blessing : getReceivedBlessings()) { |
| if (blessing.isSynched()) { |
| updatedPermissionTree.merge(blessing.getPermissionTree()); |
| } |
| } |
| |
| //re-associate listeners with rules |
| mNearestAncestors.clear(); |
| for (String path : mPermissionValueEventListeners.keySet()) { |
| String nearestAncestor = Utils.getNearestCommonAncestor(path, updatedPermissionTree.keySet()); |
| if (nearestAncestor != null) { |
| mNearestAncestors.put(nearestAncestor, path); |
| } |
| } |
| |
| Set<String> changedPermissions = new HashSet<>(); |
| |
| //determine removed permissions |
| Sets.SetView<String> removedPermissions = Sets.difference(mPermissionTree.keySet(), updatedPermissionTree.keySet()); |
| for (String path : removedPermissions) { |
| String newPath = Utils.getNearestCommonAncestor(path, updatedPermissionTree.keySet()); |
| int previous = mPermissionTree.getPermissions(path); |
| int current = updatedPermissionTree.getPermissions(newPath); |
| if (previous != current) { |
| changedPermissions.add(path); |
| } |
| } |
| |
| //compare previous tree |
| for (Blessing.Permission permission : updatedPermissionTree.values()) { |
| int previous = mPermissionTree.getPermissions(permission.path); |
| int current = updatedPermissionTree.getPermissions(permission.path); |
| if (previous != current) { |
| changedPermissions.add(permission.path); |
| } |
| } |
| |
| mPermissionTree = updatedPermissionTree; |
| |
| //notify listeners |
| for (String path : changedPermissions) { |
| onPermissionsChange(path); |
| } |
| } |
| |
| //call all the listeners effected by a permission change at this path |
| void onPermissionsChange(String path) { |
| int permission = getPermissions(path); |
| if (mNearestAncestors.containsKey(path)) { |
| for (String listenerPath : mNearestAncestors.get(path)) { |
| if (mPermissionValueEventListeners.containsKey(listenerPath)) { |
| for (OnPermissionChangeListener listener : mPermissionValueEventListeners.get(listenerPath)) { |
| listener.onPermissionChange(permission); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| public Set<PermissionRequest> getRequests(String path) { |
| Set<PermissionRequest> result = new HashSet<>(); |
| for (PermissionRequest request : mRequests.values()) { |
| if (getAllPaths(request.getPath()).contains(path)) { |
| result.add(request); |
| } |
| } |
| return result; |
| } |
| |
| public PermissionRequest getRequest(String rId) { |
| return mRequests.get(rId); |
| } |
| |
| |
| public Blessing getRootBlessing() { |
| return rootBlessing; |
| } |
| |
| public Set<Blessing> getReceivedBlessings() { |
| Set<Blessing> result = new HashSet<>(); |
| for (String target : mBlessingTargets) { |
| result.addAll(mBlessingsTable.column(target).values()); |
| } |
| return result; |
| } |
| |
| public Set<Blessing> getGrantedBlessings(String src) { |
| return new HashSet<>(mBlessingsTable.row(src).values()); |
| } |
| |
| public Blessing putBlessing(Blessing blessing) { |
| String source = blessing.getSource(); |
| String target = blessing.getTarget(); |
| mBlessings.put(blessing.getId(), blessing); |
| if (source == null) { |
| source = KEY_ROOT; |
| } |
| return mBlessingsTable.put(source, target, blessing); |
| } |
| |
| public Blessing getBlessing(String id) { |
| return mBlessings.get(id); |
| } |
| |
| public Blessing getBlessing(String source, String target) { |
| if (source == null) { |
| source = KEY_ROOT; |
| } |
| return mBlessingsTable.get(source, target); |
| } |
| |
| public void removeBlessing(String rId) { |
| Blessing removedBlessing = mBlessings.remove(rId); |
| if (removedBlessing != null) { |
| mBlessingsTable.remove(removedBlessing.getSource(), removedBlessing.getTarget()); |
| |
| } |
| } |
| |
| //return a blessing interface for granting/revoking permissions |
| //uses local device blessing as root |
| public Blessing bless(String target) { |
| return rootBlessing.bless(target); |
| } |
| |
| public DatabaseReference getBlessingsRef() { |
| return mBlessingsRef; |
| } |
| |
| private ChildEventListener requestListener = new ChildEventListener() { |
| @Override |
| public void onChildAdded(DataSnapshot dataSnapshot, String s) { |
| onRequestUpdated(dataSnapshot); |
| } |
| |
| @Override |
| public void onChildChanged(DataSnapshot dataSnapshot, String s) { |
| onRequestUpdated(dataSnapshot); |
| } |
| |
| @Override |
| public void onChildRemoved(DataSnapshot dataSnapshot) { |
| onRequestRemoved(dataSnapshot); |
| } |
| |
| @Override |
| public void onChildMoved(DataSnapshot dataSnapshot, String s) { |
| |
| } |
| |
| @Override |
| public void onCancelled(DatabaseError databaseError) { |
| |
| } |
| }; |
| |
| public void finishRequest(String rId) { |
| //TODO: notify source entity and ignore instead of removing |
| mRequestsRef.child(rId).removeValue(); |
| } |
| |
| public void grantRequest(PermissionRequest request) { |
| Blessing blessing = bless(request.getSource()); |
| blessing.setPermissions(request.getPath(), request.getPermissions()); |
| finishRequest(request.getId()); |
| } |
| |
| private void onRequestUpdated(DataSnapshot snapshot) { |
| if (!snapshot.exists()) { |
| return; |
| } |
| |
| PermissionRequest request = snapshot.getValue(PermissionRequest.class); |
| if (request == null) { |
| return; |
| } |
| |
| String requestPath = request.getPath(); |
| if (requestPath == null) { |
| return; |
| } |
| |
| //ignore local requests |
| if (mId.equals(request.getSource())) { |
| return; |
| } |
| |
| //Check if request permissions can be granted by this instance |
| if ((getPermissions(requestPath) & request.getPermissions()) != request.getPermissions()) { |
| return; |
| } |
| |
| String rId = request.getId(); |
| String source = request.getSource(); |
| mRequests.put(rId, request); |
| |
| if (mSubscribedRequests.containsKey(rId)) { |
| for (OnRequestListener listener : new HashSet<>(mSubscribedRequests.get(rId))) { |
| if (!listener.onRequest(request, bless(source))) { |
| //cancel subscription |
| mSubscribedRequests.remove(rId, listener); |
| } |
| } |
| } else { |
| for (String path : getAllPaths(request.getPath())) { |
| for (OnRequestListener listener : mRequestListeners.get(path)) { |
| if (listener.onRequest(request, bless(source))) { |
| //add subscription |
| mSubscribedRequests.put(request.getId(), listener); |
| } |
| } |
| } |
| } |
| } |
| |
| private void onRequestRemoved(DataSnapshot snapshot) { |
| mRequests.remove(snapshot.getKey()); |
| PermissionRequest request = snapshot.getValue(PermissionRequest.class); |
| String source = request.getSource(); |
| if (request != null && !mId.equals(source)) { //ignore local requests |
| for (OnRequestListener listener : mSubscribedRequests.removeAll(request.getId())) { |
| listener.onRequestRemoved(request, bless(source)); |
| } |
| } |
| } |
| |
| //allows |
| private Set<String> getAllPaths(String path) { |
| Set<String> result = new HashSet<>(); |
| result.add(path); |
| result.add("*"); |
| String subpath = path; |
| int index; |
| while ((index = subpath.lastIndexOf("/")) != -1) { |
| subpath = subpath.substring(0, index); |
| result.add(subpath + "/*"); |
| } |
| return result; |
| } |
| |
| private ChildEventListener blessingListener = new ChildEventListener() { |
| @Override |
| public void onChildAdded(DataSnapshot snapshot, String s) { |
| Blessing receivedBlessing = Blessing.fromSnapshot(PermissionManager.this, snapshot); |
| receivedBlessing.addListener(blessingChangedListner); |
| } |
| |
| @Override |
| public void onChildChanged(DataSnapshot dataSnapshot, String s) { |
| } |
| |
| @Override |
| public void onChildRemoved(DataSnapshot dataSnapshot) { |
| Blessing removedBlessing = mBlessings.remove(dataSnapshot.getKey()); |
| if (removedBlessing != null) { |
| removedBlessing.removeListener(blessingChangedListner); |
| mBlessingsTable.remove(removedBlessing.getSource(), removedBlessing.getTarget()); |
| refreshPermissions(); |
| } |
| } |
| |
| @Override |
| public void onChildMoved(DataSnapshot dataSnapshot, String s) { |
| |
| } |
| |
| @Override |
| public void onCancelled(DatabaseError databaseError) { |
| |
| } |
| }; |
| |
| private Blessing.OnBlessingUpdatedListener blessingChangedListner = new Blessing.OnBlessingUpdatedListener() { |
| @Override |
| public void onBlessingUpdated(Blessing blessing) { |
| refreshPermissions(); |
| } |
| |
| @Override |
| public void onBlessingRemoved(Blessing blessing) { |
| refreshPermissions(); |
| } |
| }; |
| |
| |
| public int getPermissions(String path) { |
| return mPermissionTree.getPermissions(path); |
| } |
| |
| public OnPermissionChangeListener addPermissionEventListener(String path, OnPermissionChangeListener listener) { |
| int current = FLAG_DEFAULT; |
| mPermissionValueEventListeners.put(path, listener); |
| |
| String nearestAncestor = Utils.getNearestCommonAncestor(path, mPermissionTree.keySet()); |
| if (nearestAncestor != null) { |
| current = getPermissions(nearestAncestor); |
| mNearestAncestors.put(nearestAncestor, path); |
| } |
| listener.onPermissionChange(current); |
| return listener; |
| } |
| |
| public void removePermissionEventListener(String path, OnPermissionChangeListener listener) { |
| mPermissionValueEventListeners.remove(path, listener); |
| String nca = Utils.getNearestCommonAncestor(path, mPermissionTree.keySet()); |
| mNearestAncestors.remove(nca, path); |
| |
| } |
| |
| |
| public void removeOnRequestListener(String path, OnRequestListener requestListener) { |
| mRequestListeners.remove(path, requestListener); |
| if (mRequestListeners.values().contains(requestListener)) { |
| //TODO: this doesn't catch cases where one request listener unsubscribed |
| for (Map.Entry<String, OnRequestListener> entry : mSubscribedRequests.entries()) { |
| if (entry.getValue().equals(requestListener)) { |
| String rId = entry.getKey(); |
| PermissionRequest request = mRequests.get(rId); |
| if (getAllPaths(request.getPath()).contains(path)) { |
| mSubscribedRequests.remove(rId, requestListener); |
| } |
| } |
| } |
| |
| } else { |
| mSubscribedRequests.values().remove(requestListener); |
| } |
| } |
| |
| public OnRequestListener addOnRequestListener(String path, OnRequestListener requestListener) { |
| mRequestListeners.put(path, requestListener); |
| for (Map.Entry<String, PermissionRequest> entry : mRequests.entrySet()) { |
| String rId = entry.getKey(); |
| PermissionRequest request = entry.getValue(); |
| Set<String> requestPaths = getAllPaths(request.getPath()); |
| if (requestPaths.contains(path)) { |
| String source = request.getSource(); |
| if (requestListener.onRequest(request, bless(source))) { |
| mSubscribedRequests.put(rId, requestListener); |
| } |
| } |
| } |
| return requestListener; |
| } |
| |
| public PermissionRequest.Builder request(String path, String group) { |
| PermissionRequest.Builder builder = mActiveRequests.get(group, path); |
| if (builder == null) { |
| builder = new PermissionRequest.Builder(mRequestsRef.push(), path, mId); |
| mActiveRequests.put(group, path, builder); |
| } |
| return builder; |
| } |
| |
| public void cancelRequests(String group) { |
| Collection<PermissionRequest.Builder> builders = mActiveRequests.row(group).values(); |
| for (PermissionRequest.Builder builder : builders) { |
| builder.cancel(); |
| } |
| builders.clear(); |
| } |
| |
| public void cancelRequest(String group, String path) { |
| PermissionRequest.Builder builder = mActiveRequests.remove(group, path); |
| if (builder != null) { |
| builder.cancel(); |
| } |
| } |
| |
| public void onDestroy() { |
| mBlessingsRef.removeEventListener(blessingListener); |
| mRequestsRef.removeEventListener(requestListener); |
| for (Blessing blessing : new HashSet<Blessing>(mBlessings.values())) { |
| blessing.revoke(); |
| } |
| } |
| |
| public interface OnRequestListener { |
| boolean onRequest(PermissionRequest request, Blessing blessing); |
| |
| void onRequestRemoved(PermissionRequest request, Blessing blessing); |
| } |
| |
| public interface OnPermissionChangeListener { |
| void onPermissionChange(int current); |
| |
| void onCancelled(DatabaseError databaseError); |
| } |
| } |