/*
  Copyright (C) 2006-2007 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE 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 Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; 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.jope.appserver.publisher;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.core.WOApplication;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/**
 * JoSecurityManager
 * <p>
 * This class manages security declarations.
 * <p>
 * Note: you probably do not want to access this class from your own code but
 * rather use the static methods in IJoSecuredObject.Utility.
 */
public class JoSecurityManager extends NSObject {
//TODO: document more
//TBD: the unclear thing about this class is whether it should ask/wrap the
//     object or whether its just the fallback. In the latter case it should
//     be renamed "DefaultImplementation" I guess (similiar to KVC fallback)
  protected static final Log log = LogFactory.getLog("JoSecurityManager");
  
  protected WOApplication application;
  
  public JoSecurityManager(WOApplication _app) {
    this.application = _app;
  }
  
  
  /* accessors */
  
  public WOApplication application() {
    // TBD: what is this used for?
    return this.application;
  }
  
  
  /* validation checks (called by object traversal) */

  /**
   * The method first validates the '_self' object using validateObject(). It
   * then retrieves the JoClass of the object and extracts the security info for
   * the given '_name'.
   * <p>
   * If the class has no security info, the default access (allow) will get
   * checked (Note: only the value 'allow' is relevant, hence everything else
   * means deny).
   * <p>
   * If it has a security info, it can be 'private', 'public' or some
   * permission. For the latter validatePermissionOnObject() will be called to
   * determine whether the user has that permission (in the context of the
   * object).
   * <p>
   * Note: the name can be protected separately from the value the name points
   * to. That is, even if access to the name is allowed, the object stored
   * under this name can be protected (this is checked by validateObject()).
   * 
   * @param _self - the object to be checked
   * @param _name - the name which is requested
   * @param _ctx  - the context the object lives in
   * @return a security exception if the access was denied
   */
  public Exception validateNameOfObject
    (Object _self, String _name, IJoContext _ctx)
  {
    boolean isInfoOn = log.isInfoEnabled();
    Exception error;
    
    /* find out permission required for object itself */
    
    if ((error = this.validateObject(_self, _ctx)) != null) {
      if (isInfoOn)
        log.info("object did not validate: " + _self + ": " + error);
      return error;
    }
    
    
    /* find security info for _name */
    /* Note: Why do we scan the hierarchy and have no consolidated 
     * "securityInfoForClass()" method which traverses the hierarchy for "the"
     * info. This is because each info must be checked for the _name. Ie the
     * security info of a superclass could contain the protections on the
     * given key.
     */ 
    
    JoClass cls = _ctx.joClassRegistry().joClassForJavaObject(_self, _ctx);
    JoClassSecurityInfo sinfo = null;
    JoClassSecurityInfo sDefaultInfo = null;
    
    for (JoClass pcls = cls; pcls != null; pcls = pcls.joSuperClass()) {
      sinfo = this.securityInfoForClass(pcls);
      if (sinfo != null) {
        if (sinfo.hasProtectionsForKey(_name))
          break;
        else if (isInfoOn) {
          log.info("security info of class '" + pcls.className() + 
                   "'\n  has no protections for name '" + _name +
                   "'\n  info: " + sinfo);
        }
      }
      
      if (sDefaultInfo == null && sinfo.hasDefaultAccessDeclaration())
        sDefaultInfo = sinfo;
      
      sinfo = null;
    }
    
    
    /* Process default security declaration in case we found no security info
     * for the key.
     */
    
    if (sinfo == null) { /* found none for _name */
      /* We found no key specific security info. Hence we use the default
       * access declaration which can be "allow".
       */
      if (sDefaultInfo != null) { /* but we found a default access */
        if (isInfoOn)
          log.info("using default info for name " + _name + ": " +sDefaultInfo);
        
        if ("allow".equals(sDefaultInfo.defaultAccess())) {
          if (isInfoOn)
            log.info("default access is set to 'allow', name: " + _name);
          return null /* everything allowed, no specific protection */;
        }
        
        if (isInfoOn) {
          log.info("rejected because default info is not allow: " + 
                   sDefaultInfo);
        }
      }
      else if (isInfoOn)
        log.info("no name and no default info for name: " + _name);
      
      return new JoAccessDeniedException
        ("attempt to access private name '" + _name + "' in class: " +
         _self.getClass().getSimpleName());
    }
    else if (isInfoOn)
      log.info("found security info for name " + _name + ": " + sinfo);
    
    
    /* We found a security declaration for the given name. Check the
     * protections.
     */
    
    if (sinfo.isKeyPrivate(_name)) {
      /* What does it mean to be 'private'. Using private you can always
       * explicitly forbid access to a JOPE name. Eg if you want that 'abc'
       * is *never ever* accessed using JoLookup (exposed to the web!), you
       * can declare it private.
       * 
       * In JOPE this is the default (if no security info was found and
       * no default access was defined). I think Zope2 allows access to all
       * Python slots per default (restricted by ZODB ownership of course).
       * (just like KVC)
       * Important: be careful with setting the default access to "allow"! This
       * reverses the process. 
       */
      if (isInfoOn) 
        log.info("name "+ _name + " is marked private in info: " + sinfo);
      
      return new JoAccessDeniedException("attempt to access private key");
    }
    
    if (sinfo.isKeyPublic(_name)) {
      /* Simple case, public is just that, public :-) */
      if (isInfoOn) 
        log.info("name "+ _name + " is marked public in info: " + sinfo);
      
      return null; /* key is explicitly declared as public */
    }
    
    /* OK, name was protected with an explicit permission, eg 'View' or
     * 'Edit'. In this case we call validatePermissionOnObject() to check
     * whether the current user has a role which has the necessary permission.
     */
    String permission = sinfo.permissionRequiredForKey(_name);
    error = this.validatePermissionOnObject(permission, _self, _ctx);
    if (error != null) {
      if (isInfoOn) {
        log.info("could not valid permission for name " + _name + 
                 ": " + permission);
      }
      return error;
    }

    /* validation was ok, we are done. */
    if (log.isDebugEnabled())
      log.debug("  object/key did validate: " + _self + ": " + _name);
    return null;
  }
  
  
  /**
   * Checks whether the current user is allowed to access the given object in
   * the given context.
   * <p>
   * This works by retrieving the security info of the objects JoClass. If the
   * object has no security info, access is rejected. That is access defaults to
   * "&lt;private&gt;".
   * <p>
   * If an explicit permission is required to use objects of the JoClass
   * validatePermissionOnObject() will get called.
   * 
   * <p>
   * Eg called by validateNameOfObject() on the given object. 
   * 
   * @param _self - the object to be checked
   * @param _ctx  - the context the object lives in
   * @return a security exception if the access was denied
   */
  public Exception validateObject(Object _self, IJoContext _ctx) {
    // TODO: in SOPE we also ask _self isPublicInContext:
    
    JoClass cls = _ctx.joClassRegistry().joClassForJavaObject(_self, _ctx);
    JoClassSecurityInfo sinfo = null;
    
    /* first find security info */
    
    for (JoClass pcls = cls; pcls != null; pcls = pcls.joSuperClass()) {
      sinfo = this.securityInfoForClass(pcls);
      
      if (log.isDebugEnabled()) {
        log.debug("CLASS: " + pcls);
        log.debug("  CHECK: " + sinfo);
      }
      
      if (sinfo != null && sinfo.hasObjectProtections())
        break;
      sinfo = null;
    }
    if (sinfo == null) {
      log.error("attempt to access private object:\n" + 
                "  object: " + _self + "\n" +
                "  class:  " + cls);
      return new JoAccessDeniedException
        ("attempt to access private object: " + cls.className());
    }
    
    /* check private/public */
    
    if (sinfo.isObjectPrivate())
      return new JoAccessDeniedException("attempt to access private object");
    if (sinfo.isObjectPublic())
      return null; /* we are public */
    
    /* check explicit permission */
    
    String permission = sinfo.permissionRequiredForObject();
    Exception error = this.validatePermissionOnObject(permission, _self, _ctx);
    if (error != null) return error;
    
    /* validation was ok, we are done. */
    if (log.isDebugEnabled())
      log.debug("  object did validate: " + _self + ": " + permission);
    return null;
  }


  /**
   * First checks whether the user is allowed to access _value by calling
   * validateObject() with the _value. It then validates the name
   * (during traversal the reverse process is performed, first name then value).
   * 
   * @param _self  - the object the value was looked up in
   * @param _name  - the name the value was looked up with
   * @param _value - the value which was returned by the lookup
   * @param _ctx   - the context all this happens in
   * @return a security exception if the access was denied
   */
  public Exception validateValueForNameOfObject
    (Object _self, String _name, Object _value, IJoContext _ctx)
  {
    /* this additionally checks object restrictions of the value */
    if (_value != null) {
      Exception error = this.validateObject(_value, _ctx);
      if (error != null) return error;
    }

    return this.validateNameOfObject(_self, _name, _ctx);
  }
  
  public Exception validatePermissionOnObject
    (String _permission, Object _self, IJoContext _ctx)
  {
    boolean isInfoOn = log.isInfoEnabled();
    
    if (_permission == null) {
      if (isInfoOn) log.info("got no permission to validate on " + _self);
      return null;
    }
    
    if ("<public>".equals(_permission)) {
      if (isInfoOn) log.info("got <public> permission to validate ...");
      return null;
    }
    
    /* process roles defined in the object (/acquired) */
    
    String[] rolesHavingPermission = null;
    // TDB
    
    /* process default roles */
    
    if (rolesHavingPermission == null) {
      if (isInfoOn) {
        log.info("found no local roles for permission, " +
                 "locating default roles of permission '" + _permission +
                 "' on object: " + _self);
      }
      
      JoClass cls = _ctx.joClassRegistry().joClassForJavaObject(_self, _ctx);
      JoClassSecurityInfo sinfo = null;

      for (JoClass pcls = cls; pcls != null; pcls = pcls.joSuperClass()) {
        sinfo = this.securityInfoForClass(pcls);
        if (sinfo != null && sinfo.hasDefaultRoleForPermission(_permission))
          break;
        sinfo = null;
      }

      if (sinfo == null) {
        log.warn("found no default roles for permission: " + _permission);
        rolesHavingPermission = JoClassSecurityInfo.defaultViewRoles;
      }
      else {
        rolesHavingPermission = sinfo.defaultRolesForPermission(_permission);
        if (isInfoOn) {
          log.info("found default roles for permission '" + _permission + 
                   "': " +
                   UString.componentsJoinedByString(rolesHavingPermission,",") +
                   " in " + sinfo);
        }
      }
    }
    
    /* scan for anonymous */
    
    boolean containsOwnerRole = false;
    for (int i = 0; i < rolesHavingPermission.length; i++) {
      if (JoRole.Anonymous.equals(rolesHavingPermission[i])) {
        if (isInfoOn)
          log.info("anonymous role has permission: " + _permission);
        return null; /* no user checks required */
      }
      
      if (!containsOwnerRole && JoRole.Owner.equals(rolesHavingPermission[i])) {
        containsOwnerRole = true;
        if (isInfoOn)
          log.info("permission is associated with owner role: " + _permission);
      }
    }
    
    /* process roles against user */
    
    IJoUser user = _ctx != null ? _ctx.activeUser() : null;
    if (user == null) {
      /* In SOPE we attach the authenticator, but I suppose we want to do this
       * at rendering time in JOPE.
       */
      if (log.isWarnEnabled())
        log.warn("got no active user, no authenticator configured?: " + _ctx);
      return new JoAuthRequiredException(null, "could not determine user");
    }
    
    String[] userRoles = user.rolesForObjectInContext(_self, _ctx);
    if (userRoles == null || userRoles.length == 0) {
      if (log.isWarnEnabled())
        log.warn("user has no associated roles: " + user);
      return new JoAccessDeniedException("attempt to access protected object");
    }
    
    /* check whether we have one of the required roles */
    
    String matchingRole = null;
    for (String role: rolesHavingPermission) {
      for (String userRole: userRoles) {
        if (role.equals(userRole)) {
          matchingRole = role; /* user has a proper role */
          break;
        }
      }
      if (matchingRole != null) {
        if (isInfoOn) {
          log.info("user has role " + matchingRole + " for permission: " +
                   _permission);
        }
        break;
      }
    }
    
    /* if no role was found, check whether the user is the owner */
    
    if (matchingRole == null && containsOwnerRole) {
      if (this.isUserOwnerOfObjectInContext(user, _self, _ctx)) {
        if (isInfoOn) log.info("user is the owner of the object");
        matchingRole = JoRole.Owner;
      }
      else if (!this.isObjectOwnedInContext(_self, _ctx)) {
        if (isInfoOn) log.info("object is not owned: " + _self);
        matchingRole = JoRole.Owner; /* hm ... */
      }
    }
    
    /* check whether we found a role and raise if not */
    
    if (matchingRole == null) {
      String login = user.getName();
      
      if (login == null || "anonymous".equals(login)) {
        /* still anonymous, requesting login */
        if (isInfoOn) {
          log.info("found no matching role for anonymous user, " +
                   "requesting authentication for permission: " +
                   _permission);
        }
        return new JoAuthRequiredException
          (user.authenticator(), "need authentication to access object");
      }
      
      /* 
       * Note: AFAIK Zope will present the user a login panel in any
       * case. IMHO this is not good in practice (you don't change
       * identities very often ;-), and the 403 code has it's value too.
       */
      
      if (isInfoOn) {
        log.info("authenticated user does not have the necessary role to " +
                 "access the object protected by permission: " + _permission);
      }
      
      return new JoAccessDeniedException
        (user.authenticator(), "attempt to access protected object");
    }
    
    /* found a role, return */
    
    if (isInfoOn)
      log.info("found role " + matchingRole + " for permission " + _permission);

    return null /* everything is fine */;
  }
  
  
  /* security info */
  
  public JoClassSecurityInfo securityInfoForClass(JoClass _cls) {
    return _cls != null ? _cls.securityInfo() : null;
  }
  
  
  /* user */
  
  /**
   * Returns the IJoUser object which got authenticated in the context. The
   * given object is ignored / not relevant for the processing.
   * <p>
   * The method currently calls _ctx.activeUser().
   */
  public IJoUser userInContextForObject(IJoContext _ctx, Object _obj) {
    // TODO: in SOPE this also processes the authenticator, which seems
    //       superflous given that activeUser already does it?
    return _ctx != null ? _ctx.activeUser() : null;
  }

  /**
   * Checks whether the given _user is the owner of the given _object in the
   * given context.
   * 
   * @param _user - an IJoUser object
   * @param _obj  - the object to be checked for ownership
   * @param _ctx  - the context for the operation
   * @return true if the given user owns the given object, no otherwise
   */
  public boolean isUserOwnerOfObjectInContext
    (IJoUser _user, Object _obj, IJoContext _ctx)
  {
    // TODO: implement me
    return false;
  }
  
  /**
   * Checks whether the user authenticated in _ctx owns the given object.
   * 
   * @param _obj - the object to be checked
   * @param _ctx - the context which contains the authentication information
   * @return true if the object is owned by the user or false if not
   */
  public boolean isObjectOwnedInContext(Object _obj, IJoContext _ctx) {
    if (_obj == null)
      return false; // null is not owned by anyone
    
    return this.isUserOwnerOfObjectInContext
      (_ctx != null ? _ctx.activeUser() : null, _obj, _ctx);
  }

  
  /* description */
  
  @Override
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.application == null)
      _d.append(" no-app");
  }
}
