/* * Copyright 2004 - 2009 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation. * * PowerFolder is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id: Folder.java 8681 2009-07-19 00:07:45Z tot $ */ package de.dal33t.powerfolder.security; import java.awt.EventQueue; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import de.dal33t.powerfolder.ConfigurationEntry; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.Member; import de.dal33t.powerfolder.PFComponent; import de.dal33t.powerfolder.clientserver.RemoteCallException; import de.dal33t.powerfolder.clientserver.ServerClient; import de.dal33t.powerfolder.clientserver.ServerClientEvent; import de.dal33t.powerfolder.clientserver.ServerClientListener; import de.dal33t.powerfolder.event.ListenerSupportFactory; import de.dal33t.powerfolder.light.AccountInfo; import de.dal33t.powerfolder.light.FolderInfo; import de.dal33t.powerfolder.light.MemberInfo; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.Util; /** * The security manager for the client. * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc</a> * @version $Revision: 1.5 $ */ public class SecurityManagerClient extends PFComponent implements SecurityManager { private static final boolean CACHE_ENABLED = true; private static final int MAX_REQUEST_ACCOUNT_INFOS = 21; private static final AccountInfo NULL_ACCOUNT = new AccountInfo(null, null) { private static final long serialVersionUID = 1L; public String toString() { return "Default"; } }; private SecurityManagerListener listners; private ServerClient client; private Map<Member, Session> sessions; private Map<AccountInfo, PermissionsCacheSegment> permissionsCacheAccounts; public SecurityManagerClient(Controller controller, ServerClient client) { super(controller); Reject.ifNull(client, "Client is null"); this.client = client; this.sessions = new ConcurrentHashMap<Member, Session>(); this.permissionsCacheAccounts = new ConcurrentHashMap<AccountInfo, PermissionsCacheSegment>(); this.listners = ListenerSupportFactory .createListenerSupport(SecurityManagerListener.class); this.client.addListener(new MyServerClientListener()); } public Account authenticate(String username, char[] password) { Account a = client.login(username, password); if (!a.isValid()) { return null; } return a; } public Account authenticate(String username, String passwordMD5, String salt) { // TRAC #1921 throw new UnsupportedOperationException( "Authentication with md5 encoded password not supported at client for " + username); } public void logout() { client.logout(); } private final Object requestPermissionLock = new Object(); public boolean hasPermission(MemberInfo memberInfo, Permission permission) { Member m = memberInfo.getNode(getController(), true); if (client.isClusterServer(m)) { return true; } if (!client.isConnected() || !client.isLoggedIn()) { return hasPermissionDisconnected(permission); } AccountInfo a = m.getAccountInfo(); if (a == null) { // Not logged in return false; } return hasPermission(m.getAccountInfo(), permission); } public boolean hasPermission(Account account, Permission permission) { return hasPermission(account != null ? account.createInfo() : null, permission); } public boolean hasPermission(AccountInfo accountInfo, Permission permission) { if (accountInfo != null && client.isLoggedIn() && client.getAccount().createInfo().equals(accountInfo)) { if (client.getAccount().hasPermission(permission)) { // Optimize. Local answer. return true; } } try { Boolean hasPermission; PermissionsCacheSegment cache = permissionsCacheAccounts .get(nullSafeGet(accountInfo)); if (cache != null) { hasPermission = cache.hasPermission(permission); } else { // Create cache hasPermission = null; cache = new PermissionsCacheSegment(); permissionsCacheAccounts.put(nullSafeGet(accountInfo), cache); } String source; if (!CACHE_ENABLED) { hasPermission = null; } if (hasPermission == null) { if (client.isConnected() && client.isLoggedIn()) { synchronized (requestPermissionLock) { // Re-check cache PermissionsCacheSegment secondCheck = permissionsCacheAccounts .get(nullSafeGet(accountInfo)); hasPermission = secondCheck != null ? secondCheck .hasPermission(permission) : null; if (!CACHE_ENABLED) { hasPermission = null; } if (hasPermission == null) { hasPermission = retrievePermission(accountInfo, permission, cache); source = "recvd"; if (isFine()) { logFine("(" + source + ") " + nullSafeGet(accountInfo) + " has " + (hasPermission ? "" : "NOT ") + permission); } } else { source = "cache"; } } } else { hasPermission = hasPermissionDisconnected(permission); source = "nocon"; } } else { source = "cache"; } return hasPermission; } catch (RemoteCallException e) { if (isWarning()) { logWarning("Unable to check " + permission + " for " + nullSafeGet(accountInfo) + ". " + e); } if (isFiner()) { logFiner(e); } return hasPermissionDisconnected(permission).booleanValue(); } } private Boolean retrievePermission(AccountInfo aInfo, Permission permission, PermissionsCacheSegment cache) { if (aInfo == null || aInfo.getOID() == null) { return Boolean.FALSE; } boolean supportsBulkRequest = false; try { supportsBulkRequest = Util.compareVersions(client.getServer() .getIdentity().getProgramVersion(), "4.2.9"); } catch (Exception e) { } if (supportsBulkRequest && permission instanceof FolderPermission) { // Optimization. Request all folder permissions in bulk FolderPermission fp = (FolderPermission) permission; FolderInfo foInfo = fp.folder; if (isFine()) { logFine("Using bulk permission request for " + aInfo + " on " + foInfo); } List<Permission> permissions = new ArrayList<Permission>(5); permissions.add(permission); permissions.add(FolderPermission.read(foInfo)); permissions.add(FolderPermission.readWrite(foInfo)); permissions.add(FolderPermission.admin(foInfo)); permissions.add(FolderPermission.owner(foInfo)); try { List<Boolean> result = client.getSecurityService() .hasPermissions(aInfo, permissions); cache.set(FolderPermission.read(foInfo), result.get(1)); cache.set(FolderPermission.readWrite(foInfo), result.get(2)); cache.set(FolderPermission.admin(foInfo), result.get(3)); cache.set(FolderPermission.owner(foInfo), result.get(4)); // Original request. return result.get(0); } catch (RemoteCallException e) { if (e.getCause() instanceof NoSuchMethodException) { // Fallthrough. Use single method. logWarning("Unable to retrieve permissions in bulk. Falling back to legacy call. " + aInfo + " has? " + permission); } else { throw e; } } } // Single request / Legacy call. boolean singleResult = client.getSecurityService().hasPermission(aInfo, permission); cache.set(permission, singleResult); return singleResult; } private Boolean hasPermissionDisconnected(Permission permission) { boolean noConnectPossible = getController().isLanOnly() && !client.getServer().isOnLAN() && !ConfigurationEntry.SERVER_CONNECT_FROM_LAN_TO_INTERNET .getValueBoolean(getController()); if (noConnectPossible) { // Server is not on LAN, but running in LAN only mode. Allow all // since we will never connect at all return Boolean.TRUE; } if (permission instanceof FolderPermission) { return ConfigurationEntry.SERVER_DISCONNECT_SYNC_ANYWAYS .getValueBoolean(getController()); } else { return !ConfigurationEntry.SECURITY_PERMISSIONS_STRICT .getValueBoolean(getController()); } } private final Object requestAccountInfoLock = new Object(); /** * Gets the {@link AccountInfo} for the given node. Retrieves it from server * if necessary. NEVER refreshes from server when running in EDT thread. * * @see de.dal33t.powerfolder.security.SecurityManager#getAccountInfo(de.dal33t.powerfolder.Member) */ public AccountInfo getAccountInfo(Member node) { if (client.isPrimaryServer(node)) { return NULL_ACCOUNT; } Session session = sessions.get(node); // Cache hit if (session != null) { if (isFiner()) { logFiner("Retured cached account for " + node + " : " + session.getAccountInfo()); } return session.getAccountInfo(); } // Smells like hack if (Util.isAwtAvailable() && EventQueue.isDispatchThread()) { if (isFiner()) { logFiner("Not trying to refresh account of " + node + ". Running in EDT thread"); } return null; } if (ServerClient.SERVER_HANDLE_MESSAGE_THREAD.get()) { if (isWarning()) { logWarning("Not trying to refresh account of " + node + ". Running handleMessage thread of Server"); } return null; } if (!client.isConnected()) { // Not available yet. return null; } AccountInfo aInfo; try { // TODO Check if really required synchronized (requestAccountInfoLock) { // After we are request lock owner. Check if other thread // probably has refreshed the session we are looking for. session = sessions.get(node); // Cache hit if (session != null) { if (isFiner()) { logFiner("Retured cached account for " + node + " : " + session.getAccountInfo()); } return session.getAccountInfo(); } Map<MemberInfo, AccountInfo> res = client.getSecurityService() .getAccountInfos(Collections.singleton(node.getInfo())); aInfo = res.get(node.getInfo()); if (isFiner()) { logFiner("Retrieved account " + aInfo + " for " + node); } if (CACHE_ENABLED) { sessions.put(node, new Session(aInfo)); } } } catch (RemoteCallException e) { logWarning("Unable to retrieve account info for " + node + ". " + e); logFiner(e); aInfo = null; } return aInfo; } private final Map<String, Member> refreshing = Util.createConcurrentHashMap(); public void nodeAccountStateChanged(final Member node, boolean refreshFolderMemberships) { if (!getController().isStarted()) { return; } String key = node.getId() + refreshFolderMemberships; if (refreshing.containsKey(key)) { // Currently refreshing return; } Runnable refresher = new Refresher(node, refreshFolderMemberships); if (getController().isStarted()) { refreshing.put(key, node); getController().getIOProvider().startIO(refresher); } } public void fetchAccountInfos(Collection<Member> nodes, boolean forceRefresh) { Reject.ifNull(nodes, "Nodes is null"); try { if (!client.isConnected()) { return; } Collection<MemberInfo> reqNodes = new ArrayList<MemberInfo>( nodes.size()); Map<MemberInfo, AccountInfo> res = new HashMap<MemberInfo, AccountInfo>(); for (Member node : nodes) { if (forceRefresh || !sessions.containsKey(node)) { reqNodes.add(node.getInfo()); if (reqNodes.size() >= MAX_REQUEST_ACCOUNT_INFOS) { res.putAll(client.getSecurityService().getAccountInfos( reqNodes)); reqNodes.clear(); } } } if (reqNodes.isEmpty()) { return; } if (isFine()) { logFine("Pre-fetching account infos for " + nodes.size() + " nodes"); } if (reqNodes.size() > MAX_REQUEST_ACCOUNT_INFOS) { logWarning("Pre-fetching account infos for many nodes (" + nodes.size() + ")"); } res.putAll(client.getSecurityService().getAccountInfos(reqNodes)); if (isFine()) { logFine("Retrieved " + res.size() + " AccountInfos for " + nodes.size() + " nodes: " + res); } for (Entry<MemberInfo, AccountInfo> entry : res.entrySet()) { Member node = entry.getKey().getNode(getController(), false); if (node == null) { continue; } AccountInfo aInfo = res.get(node.getInfo()); if (CACHE_ENABLED) { sessions.put(node, new Session(aInfo)); } // logWarning("Fire account state change on " + node + " - " // + aInfo); fireNodeAccountStateChanged(node); } } catch (RemoteCallException e) { logWarning("Unable to retrieve account info for " + nodes.size() + " nodes. " + e); logFiner(e); } } // Internal helper ******************************************************** private AccountInfo nullSafeGet(AccountInfo aInfo) { if (aInfo == null) { return NULL_ACCOUNT; } return aInfo; } private void clearNodeCache(Member node) { Session s = sessions.remove(node); if (isFiner()) { logFiner("Clearing cache on " + node + ": " + s); } permissionsCacheAccounts.remove(nullSafeGet(s != null ? s .getAccountInfo() : null)); } /** * Handle server connect: Refresh/Precache AccountInfos of friends and * members on our folders. */ private void prefetchAccountInfos() { Collection<Member> nodesToRefresh = new LinkedList<Member>(); for (Member node : getController().getNodeManager() .getNodesAsCollection()) { if (shouldAutoRefresh(node)) { nodesToRefresh.add(node); } } fetchAccountInfos(nodesToRefresh, true); } /** * Refreshes a AccountInfo for the given node if it should be pre-fetched. * * @param node */ private void refresh(Member node) { if (shouldAutoRefresh(node)) { getAccountInfo(node); } fireNodeAccountStateChanged(node); } private boolean shouldAutoRefresh(Member node) { return node.isMySelf() || node.isFriend() || node.hasJoinedAnyFolder() || (node.isOnLAN() && node.isCompletelyConnected()); } // Event handling ********************************************************* protected void fireNodeAccountStateChanged(Member node) { listners.nodeAccountStateChanged(new SecurityManagerEvent(node)); } public void addListener(SecurityManagerListener listner) { ListenerSupportFactory.addListener(listners, listner); } public void removeListener(SecurityManagerListener listner) { ListenerSupportFactory.removeListener(listners, listner); } // Inner classes ********************************************************** private final class Refresher implements Runnable { private final Member node; private final boolean syncFolderMemberships; private Refresher(Member node, boolean syncFolderMemberships) { this.node = node; this.syncFolderMemberships = syncFolderMemberships; } public void run() { try { // The server connected! boolean server = false; if (client.isPrimaryServer(node) && node.isConnected()) { prefetchAccountInfos(); server = true; } // Myself changed! if (node.isMySelf() && client.isConnected()) { try { client.refreshAccountDetails(); } catch (Exception e) { logWarning("Unable to refresh account details. " + e); logFiner(e); } } // Refresh MemberInfo->AccountInfo cache clearNodeCache(node); refresh(node); // This is required because of probably changed access // permissions to any folder. if (syncFolderMemberships) { if (node.isMySelf() || server) { getController().getFolderRepository() .triggerSynchronizeAllFolderMemberships(); } else if (node.isCompletelyConnected()) { node.synchronizeFolderMemberships(); } } } finally { // Not longer refreshing this node. refreshing.remove(node.getId() + syncFolderMemberships); } } } private class MyServerClientListener implements ServerClientListener { public boolean fireInEventDispatchThread() { return false; } public void login(ServerClientEvent event) { if (event.isLoginSuccess()) { permissionsCacheAccounts.clear(); } } public void accountUpdated(ServerClientEvent event) { if (event.isLoginSuccess()) { permissionsCacheAccounts.clear(); } } public void serverConnected(ServerClientEvent event) { } public void serverDisconnected(ServerClientEvent event) { } public void nodeServerStatusChanged(ServerClientEvent event) { } } private class Session { private AccountInfo info; public Session(AccountInfo info) { super(); // Info CAN be null! Means no login this.info = info; } public AccountInfo getAccountInfo() { return info; } } private class PermissionsCacheSegment { Map<Permission, Boolean> permissions = new ConcurrentHashMap<Permission, Boolean>(); void set(Permission permission, Boolean hasPermission) { Reject.ifNull(permission, "Permission is null"); permissions.put(permission, hasPermission); } Boolean hasPermission(Permission permission) { Reject.ifNull(permission, "Permission is null"); return permissions.get(permission); } } }