/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.controller.access.management;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.access.Authorizer;
import org.jboss.as.controller.access.AuthorizerConfiguration;
import org.jboss.as.controller.access.CombinationPolicy;
import org.jboss.as.controller.access.rbac.StandardRBACAuthorizer;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.authz.Roles;

/**
 * Standard {@link AuthorizerConfiguration} implementation that also exposes mutator APIs for use by
 * the WildFly management layer.
 *
 * @author Brian Stansberry (c) 2013 Red Hat Inc.
 */
public class WritableAuthorizerConfiguration implements AuthorizerConfiguration, AccessConstraintUtilizationRegistry {

    private volatile Map<String, RoleMappingImpl> roleMappings = new HashMap<String, RoleMappingImpl>();
    private final Map<Object, RoleMappingImpl> removedRoles = new WeakHashMap<Object, RoleMappingImpl>();
    private final Map<AccessConstraintKey, Map<PathAddress, AccessConstraintUtilization>> accessConstraintUtilization =
            new HashMap<AccessConstraintKey, Map<PathAddress, AccessConstraintUtilization>>();
    private volatile CombinationPolicy combinationPolicy = CombinationPolicy.PERMISSIVE;
    private volatile boolean useIdentityRoles;
    private volatile boolean nonFacadeMBeansSensitive;
    private volatile Authorizer.AuthorizerDescription authorizerDescription;
    private volatile RoleMaps roleMaps;
    private final Set<ScopedRoleListener> scopedRoleListeners = new LinkedHashSet<ScopedRoleListener>();

    public WritableAuthorizerConfiguration(Authorizer.AuthorizerDescription authorizerDescription) {
        this.authorizerDescription = authorizerDescription;
        this.roleMaps = new RoleMaps(authorizerDescription.getStandardRoles(), Collections.<String, ScopedRole>emptyMap());
    }

    /**
     * Reset the internal state of this object back to what it originally was.
     *
     * Used then reloading a server or in a slave host controller following a post-boot reconnect
     * to the master.
     */
    public synchronized void reset() {
        this.authorizerDescription = StandardRBACAuthorizer.AUTHORIZER_DESCRIPTION;
        this.useIdentityRoles = this.nonFacadeMBeansSensitive = false;
        this.roleMappings = new HashMap<String, RoleMappingImpl>();
        RoleMaps oldRoleMaps = this.roleMaps;
        this.roleMaps = new RoleMaps(authorizerDescription.getStandardRoles(), Collections.<String, ScopedRole>emptyMap());
        for (ScopedRole role : oldRoleMaps.scopedRoles.values()) {
            for (ScopedRoleListener listener : scopedRoleListeners) {
                try {
                    listener.scopedRoleRemoved(role);
                } catch (Exception ignored) {
                    // TODO log an ERROR
                }
            }
        }
    }

    public synchronized void registerScopedRoleListener(ScopedRoleListener listener) {
        scopedRoleListeners.add(listener);
    }

    public synchronized void unregisterScopedRoleListener(ScopedRoleListener listener) {
        scopedRoleListeners.remove(listener);
    }

    @Override
    public CombinationPolicy getPermissionCombinationPolicy() {
        return combinationPolicy;
    }

    @Override
    public boolean isRoleBased() {
        return authorizerDescription.isRoleBased();
    }

    @Override
    public boolean isMapUsingIdentityRoles() {
        return useIdentityRoles;
    }

    @Override
    public Set<String> getStandardRoles() {
        return roleMaps.standardRoles;
    }

    @Override
    public Map<String, ScopedRole> getScopedRoles() {
        return roleMaps.scopedRoles;
    }

    @Override
    public Set<String> getAllRoles() {
        return roleMaps.allRoles;
    }

    @Override
    public boolean hasRole(String roleName) {
        final Set<String> canonicalRoles = roleMaps.canonicalRoles;
        return canonicalRoles.contains(getOfficialForm(roleName));
    }

    @Override
    public Map<String, RoleMapping> getRoleMappings() {
        return Collections.<String, RoleMapping>unmodifiableMap(roleMappings);
    }

    public void setUseIdentityRoles(boolean useIdentityRoles) {
        this.useIdentityRoles = useIdentityRoles;
    }

    public synchronized void addScopedRole(ScopedRole toAdd) {
        for (ScopedRoleListener listener : scopedRoleListeners) {
            listener.scopedRoleAdded(toAdd);
        }
        Map<String, ScopedRole> newScopedRoles = new HashMap<String, ScopedRole>(roleMaps.scopedRoles);
        newScopedRoles.put(toAdd.getName(), toAdd);
        roleMaps = new RoleMaps(roleMaps.standardRoles, newScopedRoles);
    }

    public synchronized void removeScopedRole(String toRemove) {
        Map<String, ScopedRole> newScopedRoles = new HashMap<String, ScopedRole>(roleMaps.scopedRoles);
        ScopedRole removed = newScopedRoles.remove(toRemove);
        roleMaps = new RoleMaps(roleMaps.standardRoles, newScopedRoles);
        if (removed != null) {
            for (ScopedRoleListener listener : scopedRoleListeners) {
                listener.scopedRoleRemoved(removed);
            }
        }
    }

    @Override
    public boolean isNonFacadeMBeansSensitive() {
        return nonFacadeMBeansSensitive;
    }

    public void addRoleMappingImmediate(final String roleName) {
        roleMappings.put(roleName, new RoleMappingImpl(roleName));
    }

    /**
     * Adds a new role to the list of defined roles.
     *
     * @param roleName - The name of the role being added.
     */
    public synchronized void addRoleMapping(final String roleName) {
        HashMap<String, RoleMappingImpl> newRoles = new HashMap<String, RoleMappingImpl>(roleMappings);
        if (newRoles.containsKey(roleName) == false) {
            newRoles.put(roleName, new RoleMappingImpl(roleName));
            roleMappings = Collections.unmodifiableMap(newRoles);
        }
    }

    /**
     * Remove a role from the list of defined roles.
     *
     * @param roleName - The name of the role to be removed.
     * @return A key that can be used to undo the removal.
     */
    public synchronized Object removeRoleMapping(final String roleName) {
        /*
         * Would not expect this to happen during boot so don't offer the 'immediate' optimisation.
         */
        HashMap<String, RoleMappingImpl> newRoles = new HashMap<String, RoleMappingImpl>(roleMappings);
        if (newRoles.containsKey(roleName)) {
            RoleMappingImpl removed = newRoles.remove(roleName);
            Object removalKey = new Object();
            removedRoles.put(removalKey, removed);
            roleMappings = Collections.unmodifiableMap(newRoles);

            return removalKey;
        }

        return null;
    }

    /**
     * Undo a prior removal using the supplied undo key.
     *
     * @param removalKey - The key returned from the call to removeRoleMapping.
     * @return true if the undo was successful, false otherwise.
     */
    public synchronized boolean undoRoleMappingRemove(final Object removalKey) {
        HashMap<String, RoleMappingImpl> newRoles = new HashMap<String, RoleMappingImpl>(roleMappings);
        RoleMappingImpl toRestore = removedRoles.remove(removalKey);
        if (toRestore != null && newRoles.containsKey(toRestore.getName()) == false) {
            newRoles.put(toRestore.getName(), toRestore);
            roleMappings = Collections.unmodifiableMap(newRoles);
            return true;
        }

        return false;
    }

    public void setRoleMappingIncludeAll(final String roleName, final boolean includeAll) {
        RoleMappingImpl role = roleMappings.get(roleName);
        role.setIncludeAll(includeAll);
    }

    public boolean addRoleMappingPrincipal(final String roleName, final PrincipalType principalType, final MatchType matchType,
                                           final String name, final String realm, final boolean immediate) {
        RoleMappingImpl role = roleMappings.get(roleName);
        if (role != null) {
            if (immediate) {
                return role.addPrincipalImmediate(createPrincipal(principalType, name, realm), matchType);
            } else {
                return role.addPrincipal(createPrincipal(principalType, name, realm), matchType);
            }
        }
        return false;
    }

    public boolean removeRoleMappingPrincipal(final String roleName, final PrincipalType principalType, final MatchType matchType,
                                              final String name, final String realm) {
        RoleMappingImpl role = roleMappings.get(roleName);
        if (role != null) {
            return role.removePrincipal(createPrincipal(principalType, name, realm), matchType);
        }
        return false;
    }

    public MappingPrincipal createPrincipal(final PrincipalType principalType, final String name, final String realm) {
        return new MappingPrincipalImpl(principalType, name, realm);
    }

    public void setPermissionCombinationPolicy(CombinationPolicy combinationPolicy) {
        assert combinationPolicy != null : "combinationPolicy is null";
        this.combinationPolicy = combinationPolicy;
    }

    void setNonFacadeMBeansSensitive(boolean nonFacadeMBeansSensitive) {
        this.nonFacadeMBeansSensitive = nonFacadeMBeansSensitive;
    }

    synchronized void setAuthorizerDescription(Authorizer.AuthorizerDescription authorizerDescription) {
        this.authorizerDescription = authorizerDescription;
        this.roleMaps = new RoleMaps(authorizerDescription.getStandardRoles(), roleMaps.scopedRoles);
    }

    private static String getOfficialForm(String roleName) {
        return roleName == null ? null : roleName.toUpperCase(Locale.ENGLISH);
    }

    @Override
    public synchronized Map<PathAddress, AccessConstraintUtilization> getAccessConstraintUtilizations(AccessConstraintKey accessConstraintKey) {
        Map<PathAddress, AccessConstraintUtilization> result = getAccessConstraintUtilizations(accessConstraintKey, false);
        return result == null ? Collections.<PathAddress, AccessConstraintUtilization>emptyMap() : Collections.unmodifiableMap(result);
    }

    private Map<PathAddress, AccessConstraintUtilization> getAccessConstraintUtilizations(AccessConstraintKey constraintKey,
                                                                             boolean addIfNull) {
        Map<PathAddress, AccessConstraintUtilization> result = accessConstraintUtilization.get(constraintKey);
        if (result == null && addIfNull) {
            result = new HashMap<PathAddress, AccessConstraintUtilization>();
            accessConstraintUtilization.put(constraintKey, result);
        }
        return result;
    }

    @Override
    public synchronized void registerAccessConstraintResourceUtilization(AccessConstraintKey key, PathAddress address) {
        AccessConstraintUtilizationImpl acu = getAccessConstraintUtilizationImpl(key, address);
        acu.setResourceConstrained(true);
    }

    @Override
    public synchronized void registerAccessConstraintAttributeUtilization(AccessConstraintKey key,
                                                             PathAddress address, String attribute) {
        AccessConstraintUtilizationImpl acu = getAccessConstraintUtilizationImpl(key, address);
        acu.addAttribute(attribute);

    }

    @Override
    public synchronized void registerAccessConstraintOperationUtilization(AccessConstraintKey key,
                                                             PathAddress address, String operation) {
        AccessConstraintUtilizationImpl acu = getAccessConstraintUtilizationImpl(key, address);
        acu.addOperation(operation);

    }

    @Override
    public synchronized void unregisterAccessConstraintUtilizations(PathAddress address) {
        for (Map<PathAddress, AccessConstraintUtilization> map : accessConstraintUtilization.values()) {
            map.remove(address);
        }
    }

    private AccessConstraintUtilizationImpl getAccessConstraintUtilizationImpl(AccessConstraintKey key,
                                                                               PathAddress address) {
        Map<PathAddress, AccessConstraintUtilization> map = getAccessConstraintUtilizations(key, true);
        AccessConstraintUtilizationImpl acu = (AccessConstraintUtilizationImpl) map.get(address);
        if (acu == null) {
            // WFLY-1819. Validate that ApplicationTypeConfig.DEPLOYMENT isn't misused
            // A bit hacky, but this is as good a control point for this as any.
            if (key.isCore() && ApplicationTypeAccessConstraintDefinition.DEPLOYMENT.getName().equals(key.getName())
                    && ApplicationTypeAccessConstraintDefinition.DEPLOYMENT.getType().equals(key.getType())) {
                assert !address.toString().contains(ModelDescriptionConstants.SUBSYSTEM)
                        : "Invalid use of " + key + " in a subsystem; reserved for core use";
            }

            acu = new AccessConstraintUtilizationImpl(key, address);
            map.put(address, acu);
        }
        return acu;
    }

    /**
     * Types of matching strategies used in {@link org.jboss.as.controller.access.Caller} to {@link org.jboss.as.controller.access.AuthorizerConfiguration.RoleMapping} mapping.
     */
    public static enum MatchType {
        /** Any exclusive match, where a match precludes mapping a {@link org.jboss.as.controller.access.Caller} to a {@link org.jboss.as.controller.access.AuthorizerConfiguration.RoleMapping} */
        EXCLUDE,
        /** Any inclusive match, where a match allows mapping a {@link org.jboss.as.controller.access.Caller} to a {@link org.jboss.as.controller.access.AuthorizerConfiguration.RoleMapping} */
        INCLUDE
    }

    private static final class RoleMappingImpl implements RoleMapping {

        private final String name;
        private boolean includeAll;
        private volatile Set<MappingPrincipal> includes = new HashSet<MappingPrincipal>();
        private volatile Set<MappingPrincipal> excludes = new HashSet<MappingPrincipal>();

        private RoleMappingImpl(final String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("[Role name='" + name + "' ");
            sb.append("{Includes = ");
            for (MappingPrincipal current : includes) {
                sb.append(current.toString());
            }
            sb.append("}");
            sb.append("{Excludes = ");
            for (MappingPrincipal current : excludes) {
                sb.append(current.toString());
            }
            sb.append("}");
            sb.append("]");

            return sb.toString();
        }

        private boolean addPrincipalImmediate(final MappingPrincipal principal, final MatchType matchType) {
            Set<MappingPrincipal> set = getSet(matchType, true);
            try {
                return set.add(principal);
            } finally {
                setSet(set, matchType, true);
            }
        }

        private synchronized boolean addPrincipal(final MappingPrincipal principal, final MatchType matchType) {
            Set<MappingPrincipal> set = getSet(matchType, false);
            try {
                return set.add(principal);
            } finally {
                setSet(set, matchType, false);
            }
        }

        private void setIncludeAll(final boolean includeAll) {
            this.includeAll = includeAll;
        }

        public boolean includeAllAuthedUsers() {
            return includeAll;
        }

        private synchronized boolean removePrincipal(final MappingPrincipal principal, final MatchType matchType) {
            Set<MappingPrincipal> set = getSet(matchType, false);
            try {
                return set.remove(principal);
            } finally {
                setSet(set, matchType, false);
            }
        }

        @Override
        public MappingPrincipal isIncluded(SecurityIdentity identity) {
            return isInSet(identity, includes);
        }

        @Override
        public MappingPrincipal isExcluded(SecurityIdentity identity) {
            return isInSet(identity, excludes);
        }

        private MappingPrincipal isInSet(SecurityIdentity identity, Set<MappingPrincipal> theSet) {
            // One match is all it takes - return true on first match found.

            String accountName = null;
            String realm = null;
            Roles identityRoles = null;

            for (MappingPrincipal current : theSet) {
                switch (current.getType()) {
                    case USER:
                        if (current.getName().equals((accountName = getAccountName(identity, accountName)))) {
                            return current;
                        }
                        break;
                    case GROUP:
                            if ((identityRoles = getIdentityRoles(identity, identityRoles)).contains(current.getName())) {
                                return current;
                            }

                        break;
                }
            }

            return null;
        }

        private String getAccountName(final SecurityIdentity identity, final String currentValue) {
            return currentValue != null ? currentValue : identity.getPrincipal().getName();
        }

        private Roles getIdentityRoles(final SecurityIdentity identity, final Roles currentValue) {
            return currentValue != null ? currentValue : identity.getRoles();
        }

        private Set<MappingPrincipal> getSet(final MatchType matchType, final boolean immediate) {
            Set<MappingPrincipal> set;
            switch (matchType) {
                case INCLUDE:
                    set = includes;
                    break;
                default:
                    set = excludes;
            }

            return immediate ? set : new HashSet<MappingPrincipal>(set);
        }

        private void setSet(final Set<MappingPrincipal> set, final MatchType matchType, final boolean immediate) {
            if (immediate == false) {
                switch (matchType) {
                    case INCLUDE:
                        includes = Collections.<MappingPrincipal>unmodifiableSet(set);
                        break;
                    case EXCLUDE:
                        excludes = Collections.<MappingPrincipal>unmodifiableSet(set);
                }
            }
        }
    }

    private static final class MappingPrincipalImpl implements MappingPrincipal {
        private final PrincipalType type;
        private final String name;

        private final int hashCode; // Doesn't change and we know it is needed.

        private MappingPrincipalImpl(final PrincipalType type, final String name, final String realm) {
            this.type = type;
            this.name = name;

            hashCode = type.ordinal() * name.hashCode() * (realm == null ? 31 : realm.hashCode());
        }

        @Override
        public PrincipalType getType() {
            return type;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public int hashCode() {
            return hashCode;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof MappingPrincipalImpl && this.equals((MappingPrincipalImpl) obj);
        }

        private boolean equals(MappingPrincipalImpl obj) {
            return type == obj.type && name.equals(obj.name);
        }

        @Override
        public String toString() {
            return "Principal [type=" + type + ", name=" + name + "]";
        }

    }

    private static class RoleMaps {
        private final Set<String> standardRoles;
        private final Map<String, ScopedRole> scopedRoles;
        private final Set<String> allRoles;
        private final Set<String> canonicalRoles;

        private RoleMaps(Set<String> standardRoles, Map<String, ScopedRole> scopedRoles) {
            this.standardRoles = standardRoles;
            this.scopedRoles = scopedRoles;
            Set<String> allRoles = new HashSet<String>();
            allRoles.addAll(standardRoles);
            allRoles.addAll(scopedRoles.keySet());
            this.allRoles = Collections.unmodifiableSet(allRoles);
            Set<String> canonicalRoles = new HashSet<String>();
            for (String r : allRoles) {
                canonicalRoles.add(getOfficialForm(r));
            }
            this.canonicalRoles = Collections.unmodifiableSet(canonicalRoles);
        }
    }

}
