org.opengroupware.logic.authz.OGoAuthzFetchContext.java Source code

Java tutorial

Introduction

Here is the source code for org.opengroupware.logic.authz.OGoAuthzFetchContext.java

Source

/*
  Copyright (C) 2007-2014 Helge Hess
    
  This file is part of OpenGroupware.org (OGo)
    
  OGo 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; either version 2, or (at your option) any
  later version.
    
  OGo 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 OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.opengroupware.logic.authz;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.getobjects.eoaccess.EOAccessDataSource;
import org.getobjects.eoaccess.EOAdaptorDataSource;
import org.getobjects.eoaccess.EODatabase;
import org.getobjects.eoaccess.EOEntity;
import org.getobjects.eocontrol.EOGlobalID;
import org.getobjects.eocontrol.EOKeyGlobalID;
import org.getobjects.foundation.NSKeyValueCoding;
import org.getobjects.foundation.NSObject;
import org.getobjects.foundation.UString;
import org.opengroupware.logic.core.OGoObjectContext;
import org.opengroupware.logic.db.OGoDatabase;

/**
 * OGoAuthzFetchContext
 * <p>
 * Object used to process the permissions of OGo objects stored in an
 * editing context. The permission resolution itself is handled by
 * IOGoPermissionHandler objects. Each OGo entity is assigned to a specific
 * permission handler class.
 * 
 * <p>
 * This object and the whole process is quite complicated since the permission
 * of an OGo object can depend on permissions of other objects.
 * And then the whole process needs to be very efficient since it needs to be
 * performed very often. Which is why we group fetch requests and run them in
 * a bulk way.
 * 
 * <p>
 * @author helge
 */
public class OGoAuthzFetchContext extends NSObject {
    protected static final Log log = LogFactory.getLog("OGoAuthzFetchContext");
    static boolean doDebug = true;

    protected OGoObjectContext oCtx;
    protected Number[] authIds;
    protected Number[] personAuthIds;

    protected Map<EOGlobalID, String> gidToPermission;
    protected Map<String, IOGoPermissionHandler> entityToHandler;

    protected Map<EOGlobalID, Object> gidToFragment;
    protected Map<EOGlobalID, String> gidToACLPermission;
    protected Map<EOGlobalID, Boolean> gidHasACL;

    /* ACLs required to resolve the object */
    protected Set<EOKeyGlobalID> requestedACLs;
    /* ACLs to fetch only if there is a fetch above */
    protected Set<EOKeyGlobalID> optionalACLs;

    /* GIDs where we need to fetch the required security attributes */
    protected Map<IOGoPermissionHandler, Set<EOKeyGlobalID>> fetchGlobalIDs;

    /* if an object depends on the permissions of another one, it puts the
     * other's GID into this queue.
     */
    protected Set<EOKeyGlobalID> requestedGlobalIDs;

    public static final String noPermission = "";

    /* constructor */

    /**
     * Setup a new OGoAuthzFetchContext.
     * <p>
     * This is invoked by fetchPermissionsForGlobalIDs() in OGoObjectContext.
     * 
     * @param _ctx - the associated OGoObjectContext (used for fetching things)
     * @param _g2p - the gidToPermission Map which is filled by this handler
     */
    public OGoAuthzFetchContext(final OGoObjectContext _ctx, final Map<EOGlobalID, String> _g2p) {
        super();

        this.oCtx = _ctx;
        this.authIds = _ctx.authenticatedIDs();
        this.personAuthIds = this.authIds; // TBD: fix me, this is an optimization
        this.gidToPermission = _g2p;
        this.entityToHandler = defaultEntityToHandler;

        /* prepare state */
        this.requestedGlobalIDs = new HashSet<EOKeyGlobalID>(128);
        this.requestedACLs = new HashSet<EOKeyGlobalID>(128);
        this.optionalACLs = new HashSet<EOKeyGlobalID>(128);
        this.gidToFragment = new HashMap<EOGlobalID, Object>(16);
        this.gidToACLPermission = new HashMap<EOGlobalID, String>(128);
        this.gidHasACL = new HashMap<EOGlobalID, Boolean>(128);
        this.fetchGlobalIDs = new HashMap<IOGoPermissionHandler, Set<EOKeyGlobalID>>(16);
    }

    /* processing permissions */

    /**
     * Fetches the permissions of all objects in the OGoObjectContext.
     */
    public Map<EOGlobalID, String> processPermissions() {
        this.processPermissionsOfGlobalIDs(this.oCtx.globalIDsForObjects(this.oCtx.registeredObjects().toArray()));
        return this.gidToPermission;
    }

    /**
     * Processes the permissions of the given global-ids.
     * 
     * @param _gids - the global-ids to check
     * @return true if all GIDs could be checked, false otherwise
     */
    public boolean processPermissionsOfGlobalIDs(final EOGlobalID[] _gids) {
        // returns true if anything was resolved
        if (_gids == null || _gids.length == 0)
            return false;

        final boolean debugOn = log.isDebugEnabled();

        if (debugOn)
            log.debug("process objects: #" + _gids.length);

        /* setup initial set of pending objects */

        final Set<EOKeyGlobalID> pending = new HashSet<EOKeyGlobalID>(_gids.length);
        for (int i = _gids.length - 1; i >= 0; i--) {
            if (!(_gids[i] instanceof EOKeyGlobalID)) {
                log.error("unexpected GID: " + _gids[i]);
                continue;
            }

            pending.add((EOKeyGlobalID) _gids[i]);
        }

        int maxFetchIterations = 10;
        for (int fetchIteration = 1; pending.size() > 0 && fetchIteration <= maxFetchIterations; fetchIteration++) {
            if (fetchIteration > maxFetchIterations) {
                log.error("stopping processing, max fetch iteration count reached: " + maxFetchIterations
                        + ", pending: " + pending);
                break;
            }

            for (int iteration = 1; pending.size() > 0 && iteration < 8; iteration++) {
                if (debugOn) {
                    log.debug("  * start iteration " + fetchIteration + "/" + iteration + ", #" + pending.size()
                            + " pending:");
                }

                /* Scan pending GIDs and check whether we can resolve anything. Collect
                 * requests for additional data.
                 */
                Set<EOKeyGlobalID> resolvedGlobalIDs = this.prescanGlobalIDs(pending);

                if (debugOn) {
                    String s;
                    log.debug("  = prescan done, resolved: #" + resolvedGlobalIDs.size() + ", pending: #"
                            + pending.size());

                    s = "#" + this.requestedGlobalIDs.size();
                    if (this.requestedGlobalIDs.size() < 10)
                        s += ": " + this.requestedGlobalIDs;
                    log.debug("    dependencies: " + s);

                    s = "handler#" + this.fetchGlobalIDs.size();
                    if (this.fetchGlobalIDs.size() < 10)
                        s += ": " + this.fetchGlobalIDs;
                    log.debug("    fetch-info:   " + s);

                    log.debug("    fetch-acl:    #" + this.requestedACLs.size() + " / consider: #"
                            + this.optionalACLs.size());
                }

                /* add requested GIDs to new pending list */
                final int sizeBefore = pending.size();
                pending.addAll(this.requestedGlobalIDs);
                this.requestedGlobalIDs.clear();

                /* check whether we added GIDs, if so, reiterate even if no
                 * objects got resolved! (to add fetch requests for the new GID)
                 */
                final boolean newStuffGotRequested = sizeBefore < pending.size();

                /* Check whether we resolved or requested anything. If not, we need to
                 * fetch things to allow the handlers to proceed.
                 */
                if ((resolvedGlobalIDs == null || resolvedGlobalIDs.size() == 0) && !newStuffGotRequested) {
                    break;
                }

                /* remove resolved records from 'pending' list */
                pending.removeAll(resolvedGlobalIDs);
            }

            /* perform necessary fetches */

            if (pending.size() > 0) {

                if (this.requestedACLs.size() > 0) {
                    /* In case we must fetch ACLs, fetch the optional ones in one run,
                     * otherwise delay fetch of optional ACLs until they are actually
                     * needed.
                     */
                    this.requestedACLs.addAll(this.optionalACLs);
                }

                /* clear optional fetch requests */
                this.optionalACLs.clear();

                if (this.requestedACLs.size() == 0 && this.fetchGlobalIDs.size() == 0) {
                    log.error("no fetch requests issued, but objects still unprocessed:" + pending);
                    break;
                }

                if (this.requestedACLs.size() > 0) {
                    /* this makes the fetched ACLs available to the next iteration */
                    final Exception error = this.fetchACLsForGlobalIDs(this.requestedACLs);

                    if (error == null) {
                        /* we fetched them :-), remove the request */
                        this.requestedACLs.clear();
                    } else {
                        log.error("error fetching ACLs for global ids: " + this.requestedACLs);
                    }
                }

                if (this.fetchGlobalIDs.size() > 0) {
                    this.fetchRequestedAuthInfos(this.fetchGlobalIDs);
                    this.fetchGlobalIDs.clear();
                }
            }
        }

        if (pending.size() > 0)
            log.error("could not fetch permissions of all objects:\n  " + pending);

        return pending.size() == 0;
    }

    protected void fetchRequestedAuthInfos(final Map<IOGoPermissionHandler, Set<EOKeyGlobalID>> _handlerToGIDs) {
        /* Our fetches are grouped by handler, which is great as it allows us to do
         * bulk fetches :-)
         */

        final OGoDatabase db = this.oCtx != null ? (OGoDatabase) this.oCtx.database() : null;

        for (final IOGoPermissionHandler handler : _handlerToGIDs.keySet()) {
            final Set<EOKeyGlobalID> gids = _handlerToGIDs.get(handler);
            if (gids == null || gids.size() == 0)
                continue;

            if (log.isDebugEnabled()) {
                log.debug("  will fetch infos for #" + gids.size() + " gids using: " + handler);
            }

            final Map<EOKeyGlobalID, Object> results = handler.fetchInfosForGlobalIDs(this, db, gids);
            if (results == null) {
                log.error("failed to fetch auth-infos using handler: " + handler + "\n  gids: " + gids);
                continue;
            }

            /* remove keys (whether we returned infos or not) */
            gids.clear();

            /* add to info map */
            this.gidToFragment.putAll(results);
        }
    }

    /**
     * This method fetches object_acl records for all global-ids which are passed
     * in as a parameter. Only records of the authz-ctx' authids are fetched and
     * then compressed into a single permission.
     * 
     * @param _gids - the GIDs we want to have the ACLs for
     * @return a Map containing the GIDs as keys and their permission as the value
     */
    @SuppressWarnings("unchecked")
    public Exception fetchACLsForGlobalIDs(final Collection<EOKeyGlobalID> _gids) {
        if (_gids == null)
            return null;

        final int gidCount = _gids.size();

        if (gidCount == 0)
            return null; /* everything ok */

        /* Extract primary key values from global-ids. This works because pkeys
         * are unique across tables in OGo.
         * Also: we set the minimum access-level for each gid in the map, so that
         * we can be sure that entries w/o ACEs do get the no-access permission. 
         */
        final Map<Number, EOKeyGlobalID> pkeyToGlobalID = new HashMap<Number, EOKeyGlobalID>(gidCount + 1);
        final Collection<Number> pkeysToProcess = new HashSet<Number>(gidCount);

        for (final EOKeyGlobalID gid : _gids) {
            /* default access */
            this.gidToACLPermission.put(gid, noPermission);
            this.gidHasACL.put(gid, Boolean.FALSE);

            /* extract key */
            final Number oid = (Number) gid.toNumber();
            pkeyToGlobalID.put(oid, gid);

            /* fetch this */
            pkeysToProcess.add(oid);
        }

        /* setup fetch environment */

        final EODatabase db = this.oCtx != null ? this.oCtx.database() : null;
        final EOEntity aclEntity = db.entityNamed("ACLEntries");
        EOAccessDataSource ads = new EOAdaptorDataSource(db.adaptor(), aclEntity);

        List<Map<String, Object>> results = ads.fetchObjects("authzFetch", "authIds", this.authIds, "ids",
                pkeysToProcess);
        if (results == null)
            return ads.consumeLastException();

        for (int i = results.size() - 1; i >= 0; i--) {
            final Map<String, Object> ace = results.get(i);
            final String perms = (String) ace.get("permissions");
            final Number oid = (Number) ace.get("object_id"); // watch for Go changes
            if (perms == null || oid == null) {
                log.warn("found ACE w/o oid or permissions: " + ace);
                continue;
            }

            EOKeyGlobalID gid = pkeyToGlobalID.get(oid);

            /* there where results for the GID, remember this fact */
            this.gidHasACL.put(gid, true);
            pkeysToProcess.remove(oid);

            /* process permissions */

            final int permsLen = perms.length();
            if (permsLen == 0)
                continue; /* perm empty, won't add anything */

            final String currentPerms = this.gidToACLPermission.get(gid);

            this.gidToACLPermission.put(gid, UString.unionCharacterSets(perms, currentPerms));
        }

        /* fetch counts */
        if (pkeysToProcess.size() > 0) {
            // TBD: Could we do this in some kind of join with the fetch above?
            //      Probably not, but maybe we could make a union.
            //      We also have the option to do the GROUP BY in memory at the
            //      expense of having to transfer all ACEs of an ACL.

            if (log.isDebugEnabled())
                log.debug("      fetch-acl-count: #" + pkeysToProcess.size());

            results = ads.fetchObjects("authzCountFetch", "ids", pkeysToProcess);
            if (results == null)
                return ads.consumeLastException();

            for (int i = results.size() - 1; i >= 0; i--) {
                final Map<String, Object> objectAclCount = results.get(i);
                final Number oid = (Number) objectAclCount.get("object_id");
                final EOKeyGlobalID gid = pkeyToGlobalID.get(oid);
                this.gidHasACL.put(gid, true);
            }
        }

        return null; /* everything OK */
    }

    /**
     * This method tries to resolve the permissions of the given GIDs in-memory
     * and invokes the per-entity handlers to make them register their
     * requirements.
     * The method does just one pass, so you might want to call the method
     * multiple times if it managed to resolve objects.
     * 
     * @param _gids - set of global-ids to check
     * @return the set of global-ids which could be resolved successfully 
     */
    public Set<EOKeyGlobalID> prescanGlobalIDs(final Set<EOKeyGlobalID> _gids) {
        if (_gids == null || _gids.size() == 0)
            return null;

        final boolean debugOn = log.isDebugEnabled();

        /* GIDs which could be resolved during this run */
        Set<EOKeyGlobalID> resolvedGlobalIDs = new HashSet<EOKeyGlobalID>(128);

        if (debugOn)
            log.debug("    scan objects: #" + _gids.size());

        for (final EOGlobalID gid : _gids) {
            final EOKeyGlobalID kgid = (EOKeyGlobalID) gid;

            /* first check cache */

            final String permission = this.gidToPermission.get(gid);
            if (permission != null) {
                /* we already have permissions for this object :-) */

                if (debugOn) {
                    log.debug("      done: permission '" + permission + "' is cached: " + gid);
                }

                /* mark as resolved (because it was in the requested input?!) */
                resolvedGlobalIDs.add(kgid);
                continue;
            }

            /* next we retrieve the cached object and info for the handler */

            final NSKeyValueCoding object = (NSKeyValueCoding) this.oCtx.objectForGlobalID(gid);
            final Object objectInfo = this.gidToFragment.get(gid);

            /* OK, we have the object or the fetched object info, process it */

            final boolean didResolve;
            IOGoPermissionHandler handler = this.permissionHandlerForGlobalID(kgid);
            if (debugOn)
                log.debug("      process " + gid + " using " + handler);

            if (handler == null) {
                log.warn("found no permission handler for object: " + kgid);
                this.recordPermissionsForGlobalID(noPermission, kgid);
                didResolve = true;
            } else {
                didResolve = handler.process(this, kgid, object, objectInfo);
            }

            /* post */

            if (didResolve) {
                resolvedGlobalIDs.add(kgid);
                if (debugOn)
                    log.debug("        handler resolved permission: " + kgid);
                // TBD: we could also remove temporary state? (ACL perms etc?)
            } else if (debugOn)
                if (debugOn)
                    log.debug("        object still pending: " + kgid);
        }

        return resolvedGlobalIDs;
    }

    @SuppressWarnings("rawtypes")
    public String processACLOfObject(EOGlobalID _gid, Collection _objectACL) {
        if (_objectACL != null) {
            /* Object has the ACL relationship fetched! So we can directly check the
             * setup. */
            final List<String> perms = new ArrayList<String>(8);
            for (final Object aceO : _objectACL) {
                final NSKeyValueCoding ace = (NSKeyValueCoding) aceO;
                if (this.contextHasPrincipalId((Number) ace.valueForKey("principalId")))
                    perms.add((String) ace.valueForKey("permissions"));
            }
            return this.unionPermissions(perms);
        }

        /* null means we need to fetch the ACL! */
        return _gid != null ? this.gidToACLPermission.get(_gid) : null;
    }

    /**
     * Checks whether login-ctx has authenticated the given id. It could be a
     * team id or a person(/account) id. If you just need to check person ids
     * (eg 'ownerId' like fields) use the contextHasAccountId() method. 
     * 
     * @param  _id - the primary key to be checked
     * @return true if this ID is authenticated, false otherwise
     */
    public boolean contextHasPrincipalId(final Number _id) {
        if (_id == null || this.authIds == null)
            return false;

        for (final Number p : this.authIds) {
            if (p != null && p.equals(_id))
                return true;
        }
        return false;
    }

    /**
     * Checks whether login-ctx has authenticated the given account id.
     * 
     * @param  _id - the person contact's primary key to be checked
     * @return true if this ID is authenticated, false otherwise
     */
    public boolean contextHasAccountId(final Number _id) {
        if (_id == null || this.personAuthIds == null)
            return false;

        for (final Number p : this.personAuthIds) {
            if (p != null && p.equals(_id))
                return true;
        }
        return false;
    }

    /**
     * Returns whether the account has minimum access to the project with the
     * given ID.
     * 
     * @param _id - the primary key of the project
     * @return true if the account has access, false if not and null if we need to
     *   fetch the permissions
     */
    public Boolean contextHasAccessToProjectWithPrimaryKey(final Number _id) {
        if (_id == null)
            return true; /* sure, we have access to the 'null' project ;-) */

        // TBD: here we could just fetch the 'myProjects' lists?!
        // => but this would be more expensive for documents *with a project*, in
        //    this case we need to fetch the whole project-permission

        /*
         * This could be optimized. We don't need the full project permission, we
         * just need to know whether a project_company_assignment entry 'EXISTS'.
         * The idea is that the context could be given the active project list,
         * which in turn might be cached by the caller.
         */
        final EOKeyGlobalID projectGID = EOKeyGlobalID.globalIDWithEntityName("Projects", new Object[] { _id });

        final String projectPermission = this.gidToPermission.get(projectGID);
        if (projectPermission != null)
            return (noPermission.equals(projectPermission)) ? false : true;

        return null; /* means: don't know yet, fetch the project-permissions! */
    }

    /**
     * Checks the gidToPermission cache for the given entity/id combination. If
     * no permission set is stored, null is returned. If it contains 'r' (read),
     * true is returned.
     * 
     * @param _entityName
     * @param _id
     * @return null on cache miss, true if we have 'r' access, false otherwise
     */
    public String permissionsForObject(String _entityName, final Number _id) {
        if (_id == null) {
            log.error("invoked hasReadAccessToObject() w/o an id, entity: " + _entityName);
            return null; // TBD: what to return?
        }

        final EOKeyGlobalID gid = EOKeyGlobalID.globalIDWithEntityName(_entityName, new Object[] { _id });

        return this.gidToPermission.get(gid);
    }

    /**
     * Checks the gidToPermission cache for the given entity/id combination. If
     * no permission set is stored, null is returned. If it contains 'r' (read),
     * true is returned.
     * 
     * @param _entityName
     * @param _id
     * @return null on cache miss, true if we have 'r' access, false otherwise
     */
    public Boolean hasReadAccessToObject(String _entityName, final Number _id) {
        if (_id == null) {
            log.error("invoked hasReadAccessToObject() w/o an id, entity: " + _entityName);
            return null; // TBD: what to return?
        }

        final EOKeyGlobalID gid = EOKeyGlobalID.globalIDWithEntityName(_entityName, new Object[] { _id });

        final String permissions = this.gidToPermission.get(gid);
        if (permissions == null)
            return null; /* means: don't know yet, fetch the project-permissions! */

        if (permissions.length() == 0)
            return false;

        return permissions.indexOf('r') >= 0 ? true : false;
    }

    /**
     * Combines the given permission sets into a single one.
     * Example: 'lr', 'r', 'rw' will return 'lrw'.
     * 
     * @param _perms - a collection of permission sets
     * @return the combined set of permission characters
     */
    public String unionPermissions(final Collection<String> _perms) {
        int size;
        if (_perms == null || (size = _perms.size()) == 0)
            return noPermission;

        /* shortcut */
        if (size == 1 && _perms instanceof List<?>)
            return ((List<String>) _perms).get(0);

        // TBD: make this stuff sane
        final Set<Character> a = new HashSet<Character>(8);
        for (final String perms : _perms) {
            for (char permission : perms.toCharArray())
                a.add(permission);
        }

        if ((size = a.size()) == 0)
            return noPermission;

        final StringBuilder sb = new StringBuilder(size);
        for (Character c : a)
            sb.append(c.charValue());
        return sb.toString();
    }

    /**
     * Returns the permission handler which is considered responsible for the
     * given gid.
     * <p>
     * This is called by prescanGlobalIDs().
     * 
     * @param _gid - the EOGlobalID to process
     * @return the permission handler, or null
     */
    public IOGoPermissionHandler permissionHandlerForGlobalID(EOGlobalID _gid) {
        if (_gid == null)
            return null;

        /* check entity */

        final String entityName = (_gid instanceof EOKeyGlobalID) ? ((EOKeyGlobalID) _gid).entityName()
                : (String) _gid.valueForKey("entityName");
        if (entityName == null) {
            log.warn("cannot determine entity of GID: " + _gid);
            return null;
        }

        /* find handler for entity */

        IOGoPermissionHandler handler = this.entityToHandler.get(entityName);
        if (handler != null)
            return handler;

        /* did not find a handler */

        if (log.isInfoEnabled())
            log.info("did not find permission handler for entity: " + entityName);

        final EOEntity entity = this.oCtx.database().entityNamed(entityName);
        if (entity != null) {
            if ((handler = new OGoGenericPermissionHandler(entity)) != null)
                this.entityToHandler.put(entityName, handler);
        }

        return handler;
    }

    /* callbacks for handler */

    /**
     * Registers a global-id for a fetch. The context will group GIDs for a fetch.
     * 
     * @param _requester - the handler which needs the info 
     * @param _gid - the global-id which we need the info for
     */
    public void requestFetchOfInfo(final IOGoPermissionHandler _requester, final EOKeyGlobalID _gid) {
        Set<EOKeyGlobalID> gids = this.fetchGlobalIDs.get(_requester);
        if (gids == null) {
            gids = new HashSet<EOKeyGlobalID>(64);
            this.fetchGlobalIDs.put(_requester, gids);
        }
        gids.add(_gid);
    }

    public void registerObjectDependency(final IOGoPermissionHandler _requester,
            final EOKeyGlobalID _sourceGlobalID, final EOKeyGlobalID _requiredGlobalID) {
        this.requestedGlobalIDs.add(_requiredGlobalID);
    }

    /**
     * Tells the authz context that an ACL is *required* to determine the
     * permissions of an object.
     * 
     * @param _requester - the handler which needs the ACL
     * @param _gid - the global-id which we need the ACL for
     */
    public void requestFetchOfACL(final IOGoPermissionHandler _requester, final EOKeyGlobalID _gid) {
        this.requestedACLs.add(_gid);
    }

    /**
     * To give the authz context a hint that the ACL of an object might be
     * required. The context will fetch the ACL of the GID if there are also
     * *required* ACLs.
     * 
     * @param _requester - the permission handler which would like to have the ACL
     * @param _gid - the global-id we need the ACL for
     */
    public void considerFetchOfACL(final IOGoPermissionHandler _requester, final EOKeyGlobalID _gid) {
        this.optionalACLs.add(_gid);
    }

    /**
     * The handler calls this method if it has successfully determined the
     * permissions of an object.
     * 
     * @param _perms a string representing the permissions (eg "rw")
     * @param _gid the global-id which has the given permissions
     */
    public void recordPermissionsForGlobalID(String _perms, EOGlobalID _gid) {
        this.gidToPermission.put(_gid, _perms);
    }

    /* default handler mapping */

    protected static final Map<String, IOGoPermissionHandler> defaultEntityToHandler;

    protected static final IOGoPermissionHandler sharedContactPermissionHandler = new OGoContactPermissionHandler();
    protected static final IOGoPermissionHandler sharedContactOwnedPermHandler = new OGoContactOwnedObjectPermissionHandler();

    protected static final Object[] defaultEntityToHandlerList = {
            /* this should be somehow attached to the model? yes! */
            "Persons", sharedContactPermissionHandler, "Accounts", sharedContactPermissionHandler, "Teams",
            sharedContactPermissionHandler, "Companies", sharedContactPermissionHandler,

            "PersonPhones", OGoPhoneNumberPermissionHandler.personPhone, "PersonEMails",
            OGoEMailAddressPermissionHandler.personEMail, "PersonAddresses",
            OGoAddressPermissionHandler.personAddress, "CompanyPhones",
            OGoPhoneNumberPermissionHandler.companyPhone, "CompanyEMails",
            OGoEMailAddressPermissionHandler.companyEMail, "CompanyAddresses",
            OGoAddressPermissionHandler.companyAddress, "PersonComments", sharedContactOwnedPermHandler,
            "CompanyComments", sharedContactOwnedPermHandler, "TeamComments", sharedContactOwnedPermHandler,

            "Tasks", OGoTaskPermissionHandler.defaultHandler, "Projects",
            OGoProjectPermissionHandler.defaultHandler, "Documents", OGoDocumentPermissionHandler.defaultHandler,
            "Notes", OGoDocumentPermissionHandler.defaultHandler,

            // TBD: fix this, should have a proper, own association
            "ProjectPersons", OGoProjectOwnedObjectPermissionHandler.defaultHandler, "ProjectTeams",
            OGoProjectOwnedObjectPermissionHandler.defaultHandler, "ProjectCompanies",
            OGoProjectOwnedObjectPermissionHandler.defaultHandler, "ProjectsToCompany",
            OGoProjectOwnedObjectPermissionHandler.defaultHandler,

            // TBD
            "ACLEntries", OGoPublicObjectPermissionHandler.defaultHandler, "TeamMemberships",
            OGoPublicObjectPermissionHandler.defaultHandler, "Employments",
            OGoPublicObjectPermissionHandler.defaultHandler };

    static {
        defaultEntityToHandler = new HashMap<String, IOGoPermissionHandler>(32);
        for (int i = 0; i < defaultEntityToHandlerList.length; i += 2) {
            defaultEntityToHandler.put((String) defaultEntityToHandlerList[i],
                    (IOGoPermissionHandler) defaultEntityToHandlerList[i + 1]);
        }
    }

}