/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package org.apache.wiki.auth;

import org.apache.log4j.Logger;
import org.apache.wiki.api.core.Acl;
import org.apache.wiki.api.core.AclEntry;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.ContextEnum;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.api.core.Page;
import org.apache.wiki.api.core.Session;
import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
import org.apache.wiki.api.exceptions.WikiException;
import org.apache.wiki.auth.acl.AclManager;
import org.apache.wiki.auth.acl.UnresolvedPrincipal;
import org.apache.wiki.auth.authorize.GroupManager;
import org.apache.wiki.auth.authorize.Role;
import org.apache.wiki.auth.permissions.AllPermission;
import org.apache.wiki.auth.permissions.PagePermission;
import org.apache.wiki.auth.user.UserDatabase;
import org.apache.wiki.auth.user.UserProfile;
import org.apache.wiki.event.WikiEventListener;
import org.apache.wiki.event.WikiEventManager;
import org.apache.wiki.event.WikiSecurityEvent;
import org.apache.wiki.i18n.InternationalizationManager;
import org.apache.wiki.pages.PageManager;
import org.apache.wiki.preferences.Preferences;
import org.apache.wiki.util.ClassUtil;
import org.freshcookies.security.policy.LocalPolicy;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.Permission;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.WeakHashMap;


/**
 * <p>Default implementation for {@link AuthorizationManager}</p>
 * {@inheritDoc}
 *
 * <p>See the {@link #checkPermission(Session, Permission)} and {@link #hasRoleOrPrincipal(Session, Principal)} methods for more
 * information on the authorization logic.</p>
 * @since 2.3
 * @see AuthenticationManager
 */
public class DefaultAuthorizationManager implements AuthorizationManager {

    private static final Logger log = Logger.getLogger( DefaultAuthorizationManager.class );

    private Authorizer m_authorizer = null;

    /** Cache for storing ProtectionDomains used to evaluate the local policy. */
    private Map< Principal, ProtectionDomain > m_cachedPds = new WeakHashMap<>();

    private Engine m_engine = null;

    private LocalPolicy m_localPolicy = null;

    /**
     * Constructs a new DefaultAuthorizationManager instance.
     */
    public DefaultAuthorizationManager() {
    }

    /** {@inheritDoc} */
    @Override
    public boolean checkPermission( final Session session, final Permission permission ) {
        // A slight sanity check.
        if( session == null || permission == null ) {
            fireEvent( WikiSecurityEvent.ACCESS_DENIED, null, permission );
            return false;
        }

        final Principal user = session.getLoginPrincipal();

        // Always allow the action if user has AllPermission
        final Permission allPermission = new AllPermission( m_engine.getApplicationName() );
        final boolean hasAllPermission = checkStaticPermission( session, allPermission );
        if( hasAllPermission ) {
            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
            return true;
        }

        // If the user doesn't have *at least* the permission granted by policy, return false.
        final boolean hasPolicyPermission = checkStaticPermission( session, permission );
        if( !hasPolicyPermission ) {
            fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission );
            return false;
        }

        // If this isn't a PagePermission, it's allowed
        if( !( permission instanceof PagePermission ) ) {
            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
            return true;
        }

        // If the page or ACL is null, it's allowed.
        final String pageName = ((PagePermission)permission).getPage();
        final Page page = m_engine.getManager( PageManager.class ).getPage( pageName );
        final Acl acl = ( page == null) ? null : m_engine.getManager( AclManager.class ).getPermissions( page );
        if( page == null ||  acl == null || acl.isEmpty() ) {
            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
            return true;
        }

        // Next, iterate through the Principal objects assigned this permission. If the context's subject possesses
        // any of these, the action is allowed.
        final Principal[] aclPrincipals = acl.findPrincipals( permission );

        log.debug( "Checking ACL entries..." );
        log.debug( "Acl for this page is: " + acl );
        log.debug( "Checking for principal: " + Arrays.toString( aclPrincipals ) );
        log.debug( "Permission: " + permission );

        for( Principal aclPrincipal : aclPrincipals ) {
            // If the ACL principal we're looking at is unresolved, try to resolve it here & correct the Acl
            if ( aclPrincipal instanceof UnresolvedPrincipal ) {
                final AclEntry aclEntry = acl.getAclEntry( aclPrincipal );
                aclPrincipal = resolvePrincipal( aclPrincipal.getName() );
                if ( aclEntry != null && !( aclPrincipal instanceof UnresolvedPrincipal ) ) {
                    aclEntry.setPrincipal( aclPrincipal );
                }
            }

            if ( hasRoleOrPrincipal( session, aclPrincipal ) ) {
                fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
                return true;
            }
        }
        fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission );
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public Authorizer getAuthorizer() throws WikiSecurityException {
        if ( m_authorizer != null ) {
            return m_authorizer;
        }
        throw new WikiSecurityException( "Authorizer did not initialize properly. Check the logs." );
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasRoleOrPrincipal( final Session session, final Principal principal ) {
        // If either parameter is null, always deny
        if( session == null || principal == null ) {
            return false;
        }

        // If principal is role, delegate to isUserInRole
        if( AuthenticationManager.isRolePrincipal( principal ) ) {
            return isUserInRole( session, principal );
        }

        // We must be looking for a user principal, assuming that the user has been properly logged in. So just look for a name match.
        if( session.isAuthenticated() && AuthenticationManager.isUserPrincipal( principal ) ) {
            final String principalName = principal.getName();
            final Principal[] userPrincipals = session.getPrincipals();
            for( final Principal userPrincipal : userPrincipals ) {
                if( userPrincipal.getName().equals( principalName ) ) {
                    return true;
                }
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasAccess( final Context context, final HttpServletResponse response, final boolean redirect ) throws IOException {
        final boolean allowed = checkPermission( context.getWikiSession(), context.requiredPermission() );
        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );

        // Stash the wiki context
        if ( context.getHttpRequest() != null && context.getHttpRequest().getAttribute( Context.ATTR_CONTEXT ) == null ) {
            context.getHttpRequest().setAttribute( Context.ATTR_CONTEXT, context );
        }

        // If access not allowed, redirect
        if( !allowed && redirect ) {
            final Principal currentUser  = context.getWikiSession().getUserPrincipal();
            final String pageurl = context.getPage().getName();
            if( context.getWikiSession().isAuthenticated() ) {
                log.info( "User " + currentUser.getName() + " has no access - forbidden (permission=" + context.requiredPermission() + ")" );
                context.getWikiSession().addMessage( MessageFormat.format( rb.getString( "security.error.noaccess.logged" ),
                                                     context.getName()) );
            } else {
                log.info( "User " + currentUser.getName() + " has no access - redirecting (permission=" + context.requiredPermission() + ")" );
                context.getWikiSession().addMessage( MessageFormat.format( rb.getString("security.error.noaccess"), context.getName() ) );
            }
            response.sendRedirect( m_engine.getURL( ContextEnum.WIKI_LOGIN.getRequestContext(), pageurl, null ) );
        }
        return allowed;
    }

    /**
     * {@inheritDoc}
     *
     * Expects to find property 'jspwiki.authorizer' with a valid Authorizer implementation name to take care of role lookup operations.
     */
    @Override
    public void initialize( final Engine engine, final Properties properties ) throws WikiException {
        m_engine = engine;

        //  JAAS authorization continues
        m_authorizer = getAuthorizerImplementation( properties );
        m_authorizer.initialize( engine, properties );

        // Initialize local security policy
        try {
            final String policyFileName = properties.getProperty( POLICY, DEFAULT_POLICY );
            final URL policyURL = engine.findConfigFile( policyFileName );

            if (policyURL != null) {
                final File policyFile = new File( policyURL.toURI().getPath() );
                log.info("We found security policy URL: " + policyURL + " and transformed it to file " + policyFile.getAbsolutePath());
                m_localPolicy = new LocalPolicy( policyFile, engine.getContentEncoding().displayName() );
                m_localPolicy.refresh();
                log.info( "Initialized default security policy: " + policyFile.getAbsolutePath() );
            } else {
                final String sb = "JSPWiki was unable to initialize the default security policy (WEB-INF/jspwiki.policy) file. " +
                                  "Please ensure that the jspwiki.policy file exists in the default location. " +
                		          "This file should exist regardless of the existance of a global policy file. " +
                                  "The global policy file is identified by the java.security.policy variable. ";
                final WikiSecurityException wse = new WikiSecurityException( sb );
                log.fatal( sb, wse );
                throw wse;
            }
        } catch ( final Exception e) {
            log.error("Could not initialize local security policy: " + e.getMessage() );
            throw new WikiException( "Could not initialize local security policy: " + e.getMessage(), e );
        }
    }

    /**
     * Attempts to locate and initialize a Authorizer to use with this manager. Throws a WikiException if no entry is found, or if one
     * fails to initialize.
     *
     * @param props jspwiki.properties, containing a 'jspwiki.authorization.provider' class name.
     * @return a Authorizer used to get page authorization information.
     * @throws WikiException if there are problems finding the authorizer implementation.
     */
    private Authorizer getAuthorizerImplementation( final Properties props ) throws WikiException {
        final String authClassName = props.getProperty( PROP_AUTHORIZER, DEFAULT_AUTHORIZER );
        return ( Authorizer )locateImplementation( authClassName );
    }

    private Object locateImplementation( final String clazz ) throws WikiException {
        if ( clazz != null ) {
            try {
                final Class< ? > authClass = ClassUtil.findClass( "org.apache.wiki.auth.authorize", clazz );
                return authClass.newInstance();
            } catch( final ClassNotFoundException e ) {
                log.fatal( "Authorizer " + clazz + " cannot be found", e );
                throw new WikiException( "Authorizer " + clazz + " cannot be found", e );
            } catch( final InstantiationException e ) {
                log.fatal( "Authorizer " + clazz + " cannot be created", e );
                throw new WikiException( "Authorizer " + clazz + " cannot be created", e );
            } catch( final IllegalAccessException e ) {
                log.fatal( "You are not allowed to access this authorizer class", e );
                throw new WikiException( "You are not allowed to access this authorizer class", e );
            }
        }

        throw new NoRequiredPropertyException( "Unable to find a " + PROP_AUTHORIZER + " entry in the properties.", PROP_AUTHORIZER );
    }

    /** {@inheritDoc} */
    @Override
    public boolean allowedByLocalPolicy( final Principal[] principals, final Permission permission ) {
        for ( final Principal principal : principals ) {
            // Get ProtectionDomain for this Principal from cache, or create new one
            ProtectionDomain pd = m_cachedPds.get( principal );
            if ( pd == null ) {
                final ClassLoader cl = this.getClass().getClassLoader();
                final CodeSource cs = new CodeSource( null, (Certificate[])null );
                pd = new ProtectionDomain( cs, null, cl, new Principal[]{ principal } );
                m_cachedPds.put( principal, pd );
            }

            // Consult the local policy and get the answer
            if ( m_localPolicy.implies( pd, permission ) ) {
                return true;
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public boolean checkStaticPermission( final Session session, final Permission permission ) {
        return ( Boolean )Session.doPrivileged( session, ( PrivilegedAction< Boolean > )() -> {
            try {
                // Check the JVM-wide security policy first
                AccessController.checkPermission( permission );
                return Boolean.TRUE;
            } catch( final AccessControlException e ) {
                // Global policy denied the permission
            }

            // Try the local policy - check each Role/Group and User Principal
            if ( allowedByLocalPolicy( session.getRoles(), permission ) || allowedByLocalPolicy( session.getPrincipals(), permission ) ) {
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        } );
    }

    /** {@inheritDoc} */
    @Override
    public Principal resolvePrincipal( final String name ) {
        // Check built-in Roles first
        final Role role = new Role(name);
        if ( Role.isBuiltInRole( role ) ) {
            return role;
        }

        // Check Authorizer Roles
        Principal principal = m_authorizer.findRole( name );
        if ( principal != null ) {
            return principal;
        }

        // Check Groups
        principal = m_engine.getManager( GroupManager.class ).findRole( name );
        if ( principal != null ) {
            return principal;
        }

        // Ok, no luck---this must be a user principal
        final Principal[] principals;
        final UserProfile profile;
        final UserDatabase db = m_engine.getManager( UserManager.class ).getUserDatabase();
        try {
            profile = db.find( name );
            principals = db.getPrincipals( profile.getLoginName() );
            for( final Principal value : principals ) {
                principal = value;
                if( principal.getName().equals( name ) ) {
                    return principal;
                }
            }
        } catch( final NoSuchPrincipalException e ) {
            // We couldn't find the user...
        }
        // Ok, no luck---mark this as unresolved and move on
        return new UnresolvedPrincipal( name );
    }


    // events processing .......................................................

    /** {@inheritDoc} */
    @Override
    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
        WikiEventManager.addWikiEventListener( this, listener );
    }

    /** {@inheritDoc} */
    @Override
    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
        WikiEventManager.removeWikiEventListener( this, listener );
    }

}
