/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.security.acl;

import java.util.*;
import java.lang.reflect.Constructor;
import java.security.Principal;
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.sun.enterprise.deployment.Role;
import com.sun.enterprise.deployment.interfaces.SecurityRoleMapper;
import com.sun.enterprise.deployment.PrincipalImpl;
import com.sun.enterprise.deployment.Group;
import com.sun.enterprise.config.serverbeans.Server;
import com.sun.enterprise.config.serverbeans.ServerBeansFactory;
import com.sun.enterprise.config.serverbeans.SecurityService;
import com.sun.enterprise.config.ConfigContext;
import com.sun.enterprise.server.ApplicationServer;
import com.sun.enterprise.server.ServerContext;
import com.sun.enterprise.Switch;
import java.util.logging.*;
import com.sun.logging.*;
import javax.security.auth.Subject;


/** This Object maintains  a mapping of users and groups to application
 * specific Roles.
 * Using this object this mapping information could be maintained and
 * queried at a later time. This is a complete rewrite of the previous
 * RoleMapper for JACC related changes.
 * @author Harpreet Singh
 */
public class RoleMapper implements java.io.Serializable, SecurityRoleMapper {
    
    private static Map ROLEMAPPER = new HashMap();
    private static final String DEFAULT_ROLE_NAME = "ANYONE";
    private static Role defaultRole = null;
    private static String defaultRoleName = null;
    private String appName;
    private final Map roleToSubject = new HashMap(); // role->subject

    // default mapper to emulate Servlet default p2r mapping semantics
    private static String defaultP2RMappingClassName = null;
    private DefaultRoleToSubjectMapping defaultRTSM = 
         new DefaultRoleToSubjectMapping();
    
    // the following 2 Maps are a copy of roleToSubject.
    // This is added as a support for deployment.
    // Should think of optimizing this
    // role-to-principal
    private final Map roleToPrincipal = new HashMap();
    // role-to-group
    private final Map roleToGroup = new HashMap();
    
    private static Logger _logger =
    LogDomains.getLogger(LogDomains.SECURITY_LOGGER);
    
    private RoleMapper(String appName) {
        this.appName = appName;
        Switch sw = Switch.getSwitch();
        if (sw.getContainerType() == Switch.EJBWEB_CONTAINER) { // 4735725
            initDefaultRole();
        }
        if (defaultP2RMappingClassName==null)
            defaultP2RMappingClassName = getDefaultP2RMappingClassName();
    }
    
    private static synchronized void initDefaultRole() {
        
        if(defaultRole == null) {
            
            defaultRoleName = DEFAULT_ROLE_NAME;
            
            try {
                ConfigContext configContext =
                ApplicationServer.getServerContext().getConfigContext();
                assert(configContext != null);
                
                Server configBean =
                ServerBeansFactory.getServerBean(configContext);
                assert(configBean != null);
                
                SecurityService securityBean =
                ServerBeansFactory.getSecurityServiceBean(configContext);
                assert(securityBean != null);
                
                defaultRoleName = securityBean.getAnonymousRole();
                
            } catch (Exception e) {
                _logger.log(Level.WARNING,
                "java_security.anonymous_role_reading_exception",
                e);
            }
            
            if(_logger.isLoggable(Level.FINE)){
                _logger.log(Level.FINE, "Default role is: " +  defaultRoleName);
            }
            defaultRole = new Role(defaultRoleName);
        }
    }
    
    /** Returns a RoleMapper corresponding to the AppName.
     * @param appName Application Name of this RoleMapper.
     * @return SecurityRoleMapper for the application
     */
    public static RoleMapper getRoleMapper(String appName) {
        RoleMapper r = (RoleMapper)ROLEMAPPER.get(appName);
        if(r == null){
            r = new RoleMapper(appName);
            synchronized(RoleMapper.class){
                ROLEMAPPER.put(appName,r);
            }
        }
        return r;
    }
    
    /** Set a RoleMapper for the application
     * @param appName Application or module name
     * @param rmap <I>SecurityRoleMapper</I> for the application or the module
     */    
    public static void setRoleMapper(String appName, SecurityRoleMapper rmap) {
        synchronized(RoleMapper.class){
            ROLEMAPPER.put(appName, rmap);
        }
    }
    
    /**
     * @param appName Application/module name.
     */    
    public static void removeRoleMapper(String appName) {
        
        if( ROLEMAPPER.containsKey(appName)){
            synchronized(RoleMapper.class){
                ROLEMAPPER.remove(appName);
            }
        }
    }
    
    /**
     * @return The application/module name for this RoleMapper
     */    
    public String getName() {
        return appName;
    }

    /**
     * @param name The application/module name
     */    
    public void setName(String name) {        
        this.appName = name;
    }
    
    /**
     * @param principal A principal that corresponds to the role
     * @param role A role corresponding to this principal
     */    
    private void addRoleToPrincipal(final Principal principal, String role){
        assert roleToSubject != null;
        Subject subject = (Subject)roleToSubject.get(role);
        final Subject sub = (subject == null)?new Subject(): subject;
        AccessController.doPrivileged(new PrivilegedAction() {
            public java.lang.Object run() {
                sub.getPrincipals().add(principal);
                return null;
            }
        });
        roleToSubject.put(role, sub);
    }
       
    /**
     * Remove the given role-principal mapping
     * @param role, Role object
     * @param principal, the principal
     */    
    public void unassignPrincipalFromRole(Role role, Principal principal){
        assert roleToSubject != null;
        String mrole = role.getName();
        final Subject sub = (Subject)roleToSubject.get(mrole);
        final Principal p = principal;
        if (sub != null){
            AccessController.doPrivileged(new PrivilegedAction() {
                public java.lang.Object run() {
                    sub.getPrincipals().remove(p);
                    return null;
                }
            });
            roleToSubject.put(mrole, sub);
        }
        Map tmp = null; // shorthand
        tmp = (principal instanceof Group)? roleToGroup: roleToPrincipal;
        Vector v = (Vector)tmp.get(mrole);
        if(v != null){
            v.remove(principal);
            tmp.put(mrole, v);
        }        
    }

    // @return true or false depending on activation of
    // the mapping via domain.xml.
    static boolean isDefaultRTSMActivated() {
	return (defaultP2RMappingClassName!=null);
    }
    
    /**
     * Returns the RoleToSubjectMapping for the RoleMapping
     * @return Map of role->subject mapping
     */    
    public Map getRoleToSubjectMapping(){
        assert roleToSubject != null;
        if (roleToSubject.isEmpty() && isDefaultRTSMActivated()) {
            return defaultRTSM;
        }
        return roleToSubject;
    }
    
    /**
     * Assigns a Principal to the specified role.
     * @param The principal that needs to be assigned to the role.
     * @param The Role the principal is being assigned to.
     */
    public void assignRole(Principal p, Role r) {     
        String role = r.getName();
        Map tmp_map = null;
        if(_logger.isLoggable(Level.FINE)){
            _logger.log(Level.FINE, "SECURITY:RoleMapper Assigning Role "+ role +
                " to  "+ p.getName());
        }
        addRoleToPrincipal(p, role);
        // tmp_map will never ever be null
        tmp_map = (p instanceof Group)? roleToGroup:roleToPrincipal;        
        Vector _principals = (Vector)tmp_map.get(role) ;
        if(_principals == null) {
            _principals = new Vector();
        }
        _principals.add(p);
        tmp_map.put(role, _principals);
    }
     
    /**
     * Returns an enumeration of roles for this rolemapper.
     */
    public Iterator getRoles() {
        assert roleToSubject != null;
        return roleToSubject.keySet().iterator(); // All the roles
    }
        
    /**
     * Returns an enumeration of Groups assigned to the given role
     * @param The Role to which the groups are assigned to.
     */
    public Enumeration getGroupsAssignedTo(Role r) {
        assert roleToGroup != null;
        Vector v = (Vector)roleToGroup.get(r.getName());
        v = (v == null)? new Vector(): v;
        return v.elements();
    }
    
    /**
     * Returns an enumeration of Principals assigned to the given role
     * @param The Role to which the principals are assigned to.
     */
    public Enumeration getUsersAssignedTo(Role r) {        
        assert roleToPrincipal != null;
        Vector v =  (Vector)roleToPrincipal.get(r.getName());
        v =  (v == null)? new Vector(): v;
        return v.elements();
    }
    
    public void unassignRole(Role r) {
        if (r != null){
            String role = r.getName();
            roleToSubject.remove(role);
            roleToPrincipal.remove(role);
            roleToGroup.remove(role);
        }
    }


    /**
     * @return String. String representation of the RoleToPrincipal Mapping
     */    
    public String toString() {
        
        StringBuffer s = new StringBuffer("RoleMapper:");
        for (Iterator e = this.getRoles(); e.hasNext();) {
            String r = (String) e.next();
            s.append("\n\tRole (" + r + ") has Principals(");
            Subject sub = (Subject)roleToSubject.get(r);
            Iterator it = sub.getPrincipals().iterator();
            for(; it.hasNext(); ){
                Principal p = (Principal) it.next();
                s.append(p.getName()+" ");
            }
            s.append(")");
        }
        if(_logger.isLoggable(Level.FINER)){
            _logger.log(Level.FINER,s.toString());
        }
        return s.toString();
    }
    
    
    /** Copy constructor. This is called from the JSR88 implementation.
     * This is not stored into the internal rolemapper maps.
     */
    public RoleMapper(RoleMapper r){
        this.appName = r.getName();
        for(Iterator it = r.getRoles();it.hasNext(); ){
            String role = (String) it.next();
            // recover groups
            Enumeration groups = r.getGroupsAssignedTo(new Role(role));
            Vector groupsToRole = new Vector();
            for(;groups.hasMoreElements(); ){
                Group gp = (Group) groups.nextElement();
                groupsToRole.add(new Group(gp.getName()));
                addRoleToPrincipal(gp,role);
            }
            this.roleToGroup.put(role,groupsToRole);
            
            // recover principles
            Enumeration users = r.getUsersAssignedTo(new Role(role));
            Vector usersToRole = new Vector();
            for(;users.hasMoreElements(); ){
                PrincipalImpl gp = (PrincipalImpl) users.nextElement();
                usersToRole.add(new PrincipalImpl(gp.getName()));
                addRoleToPrincipal(gp,role);
            }
            this.roleToPrincipal.put(role,usersToRole);
        }
    }

    /**
     * @returns the class name used for default Principal to role mapping
     *          return null if default P2R mapping is not supported.
     */
    static private String getDefaultP2RMappingClassName() {
        if (defaultP2RMappingClassName != null )
            return defaultP2RMappingClassName;

        String className=null;
        try {
            ServerContext serverContext = ApplicationServer.getServerContext();
            if (serverContext != null) {
                ConfigContext configContext = serverContext.getConfigContext();
                if (configContext != null) {
                    SecurityService securityService =
                        ServerBeansFactory.getSecurityServiceBean(configContext);
                    if (securityService != null && 
                        securityService.isActivateDefaultPrincipalToRoleMapping()==true) {
                        className = securityService.getMappedPrincipalClass();
                        if (className==null || "".equals(className))
			    className = "com.sun.enterprise.deployment.Group";
                    }
                }
            }
            if (className==null)
                return null;
            Class clazz = Class.forName(className);
            Class[] argClasses = new Class[]  { String.class };
            Object[] arg       = new Object[] { "anystring" };
            Constructor c = clazz.getConstructor(argClasses); 
            Principal principal = (Principal) c.newInstance(arg);
                  //verify that this class is a Principal class and has a constructor(string)
            return className;
        } catch (Exception e) {
            _logger.log(Level.SEVERE,"pc.getDefaultP2RMappingClass: " + e);
            return null;
        }
    }

    class DefaultRoleToSubjectMapping extends HashMap {
        private HashMap roleMap = new HashMap();

        DefaultRoleToSubjectMapping() {
            super();
        }

        Principal getSameNamedPrincipal(String roleName) {
            Subject subject = new Subject();    
            try {
                Class clazz = Class.forName(defaultP2RMappingClassName); 
                Class[] argClasses = new Class[]  { String.class };
                Object[] arg       = new Object[] { new String(roleName) };
                Constructor c = clazz.getConstructor(argClasses);
                Principal principal = (Principal) c.newInstance(arg);
                return principal;
            } catch (Exception e) {
                _logger.log(Level.SEVERE,"rm.getSameNamedPrincipal", new Object[]{roleName, e});
                throw new RuntimeException("Unable to get principal by default p2r mapping");
            }
        }

        public Object get(Object key) {
            assert key instanceof String;
            synchronized (roleMap) {
                Subject s = (Subject)roleMap.get(key);
                if (s == null && key instanceof String) {
                    final Subject fs = new Subject();
                    final String roleName = (String) key;
                    AccessController.doPrivileged(new PrivilegedAction() {
                        public java.lang.Object run() {
                            fs.getPrincipals().add(getSameNamedPrincipal(roleName));
                            return null;
                        }
                    });
                    roleMap.put(key,fs);
                    s = fs;
                }
                return s;
            }
        }
    }    
}
