/*
  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
 * 
 * This class manages security declarations.
 * 
 * TODO: document more
 */
public class JoSecurityManager extends NSObject {
  protected static final Log log = LogFactory.getLog("JoSecurityManager");
  
  protected WOApplication application;
  
  public JoSecurityManager(WOApplication _app) {
    this.application = _app;
  }
  
  /* accessors */
  
  public WOApplication application() {
    return this.application;
  }
  
  /* validation checks (called by object traversal) */

  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 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;
    }
    if (sinfo == null) { /* found none for the key */
      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);
    
    /* check protections */
    
    if (sinfo.isKeyPrivate(_name)) {
      if (isInfoOn) 
        log.info("name "+ _name + " is marked private in info: " + sinfo);
      
      return new JoAccessDeniedException("attempt to access private key");
    }
    
    if (sinfo.isKeyPublic(_name)) {
      if (isInfoOn) 
        log.info("name "+ _name + " is marked public in info: " + sinfo);
      
      return null; /* key is explicitly declared as public */
    }
    
    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;
  }
  
  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;
    
    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());
    }
    
    if (sinfo.isObjectPublic())
      return null; /* we are public */
    if (sinfo.isObjectPrivate())
      return new JoAccessDeniedException("attempt to access private object");
    
    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;
  }

  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.login();
      
      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 */
  
  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;
  }
  
  public boolean isUserOwnerOfObjectInContext
    (IJoUser _user, Object _obj, IJoContext _ctx)
  {
    // TODO: implement me
    return false;
  }
  
  public boolean isObjectOwnedInContext(Object _obj, IJoContext _ctx) {
    // TODO: implement me
    return false;
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
  }
}
