All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.enterprise.security.ee.acl.RoleMapper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation
 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.enterprise.security.ee.acl;

import com.sun.enterprise.config.serverbeans.SecurityService;
import com.sun.enterprise.security.auth.login.DistinguishedPrincipalCredential;
import com.sun.logging.LogDomains;

import java.io.Serializable;
import java.security.Principal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.security.auth.Subject;

import org.glassfish.api.admin.ServerEnvironment;
import org.glassfish.deployment.common.RootDeploymentDescriptor;
import org.glassfish.deployment.common.SecurityRoleMapper;
import org.glassfish.internal.api.Globals;
import org.glassfish.security.common.Group;
import org.glassfish.security.common.Role;
import org.glassfish.security.common.UserNameAndPassword;
import org.glassfish.security.common.UserPrincipal;

import static com.sun.enterprise.util.Utility.isEmpty;
import static java.util.Collections.emptySet;
import static java.util.Collections.enumeration;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toSet;

/**
 * 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 Jakarta Authorization related changes.
 *
 * @author Harpreet Singh
 */
public class RoleMapper implements Serializable, SecurityRoleMapper {

    private static final long serialVersionUID = -4455830942007736853L;
    private static final Logger LOG = LogDomains.getLogger(RoleMapper.class, LogDomains.SECURITY_LOGGER, false);

    private String appName;
    private final Map roleToSubject = new HashMap<>();

    // Default mapper to emulate Servlet default p2r mapping semantics
    private String defaultPrincipalToRoleMappingClassName;
    private final DefaultRoleToSubjectMapping defaultRoleToSubjectMapping = new DefaultRoleToSubjectMapping();

    /*
     * The following 2 Maps are a copy of roleToSubject. This is added as a support for deployment. Should think of
     * optimizing this.
     */
    private final Map> roleToPrincipal = new HashMap<>();
    private final Map> roleToGroup = new HashMap<>();

    /* The following objects are used to detect conflicts during deployment */
    /*
     * .....Mapping of module (or application) that is presently calling assignRole(). It is set by the startMappingFor()
     * method. After all the subjects have been assigned, stopMappingFor() is called and then the mappings can be checked
     * against those previously assigned.
     */
    private Mapping currentMapping;

    /**
     * These override roles mapped in submodules.
     */
    private Set topLevelRoles;

    /**
     * Used to identify the application level mapping file
     */
    private static final String TOP_LEVEL = "sun-application.xml mapping file";

    // used to log a warning only one time
    private boolean conflictLogged;

    // store roles that have a conflict so they are not re-mapped
    private Set conflictedRoles;

    private transient SecurityService securityService;

    RoleMapper(String appName) {
        this.appName = appName;
        securityService = Globals.getDefaultBaseServiceLocator().getService(SecurityService.class, ServerEnvironment.DEFAULT_INSTANCE_NAME);
        defaultPrincipalToRoleMappingClassName = getDefaultPrincipalToRoleMappingClassName();
    }

    /**
     * Copy constructor. This is called from the JSR88 implementation. This is not stored into the internal rolemapper maps.
     */
    public RoleMapper(RoleMapper other) {
        this.appName = other.getName();
        for (Iterator it = other.getRoles(); it.hasNext();) {
            String role = it.next();

            // Recover groups
            Enumeration groups = other.getGroupsAssignedTo(new Role(role));
            Set groupsToRole = new HashSet<>();
            for (; groups.hasMoreElements();) {
                Group gp = groups.nextElement();
                groupsToRole.add(new Group(gp.getName()));
                addRoleToSubject(gp, role);
            }
            this.roleToGroup.put(role, groupsToRole);

            // Recover principles
            Enumeration users = other.getUsersAssignedTo(new Role(role));
            Set usersToRole = new HashSet<>();
            while (users.hasMoreElements()) {
                UserPrincipal principal = users.nextElement();
                usersToRole.add(new UserNameAndPassword(principal.getName()));
                addRoleToSubject(principal, role);
            }

            this.roleToPrincipal.put(role, usersToRole);
        }
    }

    /**
     * @return The application/module name for this RoleMapper
     */
    @Override
    public String getName() {
        return appName;
    }

    /**
     * @param name The application/module name
     */
    @Override
    public void setName(String name) {
        this.appName = name;
    }

    /**
     * Returns the RoleToSubjectMapping for the RoleMapping
     *
     * @return Map of role->subject mapping
     */
    @Override
    public Map getRoleToSubjectMapping() {
        // This causes the last currentMapping information to be added
        checkAndAddMappings();

        if (roleToSubject.isEmpty() && isDefaultPrincipalToRoleMapping()) {
            return defaultRoleToSubjectMapping;
        }

        return roleToSubject;
    }

    @Override
    public Map> getGroupToRolesMapping() {
        Map> groupToRoles = new HashMap<>();

        for (Map.Entry roleToSubject : getRoleToSubjectMapping().entrySet()) {
            for (String group : getGroups(roleToSubject.getValue())) {
                groupToRoles.computeIfAbsent(group, g -> new HashSet<>())
                            .add(roleToSubject.getKey());
            }
        }

        return groupToRoles;
    }

    @Override
    public Map> getCallerToRolesMapping() {
        Map> callerToRoles = new HashMap<>();

        for (Map.Entry roleToSubject : getRoleToSubjectMapping().entrySet()) {
            for (String callerName : getCallerPrincipalNames(roleToSubject.getValue())) {
                callerToRoles.computeIfAbsent(callerName, g -> new HashSet<>())
                            .add(roleToSubject.getKey());
            }
        }

        return callerToRoles;
    }

    /**
     * Assigns a Principal to the specified role. This method delegates work to internalAssignRole() after checking for
     * conflicts. RootDeploymentDescriptor added as a fix for: https://glassfish.dev.java.net/issues/show_bug.cgi?id=2475
     *
     * The first time this is called, a new Mapping object is created to store the role mapping information. When called
     * again from a different module, the old mapping information is checked and stored and a new Mapping object is created.
     *
     * @param principal The principal that needs to be assigned to the role.
     * @param role The Role the principal is being assigned to.
     * @param rootDeploymentDescriptor The descriptor of the module containing the role mapping
     */
    @Override
    public void assignRole(Principal principal, Role role, RootDeploymentDescriptor rootDeploymentDescriptor) {
        String callingModuleID = getModuleID(rootDeploymentDescriptor);

        if (currentMapping == null) {
            currentMapping = new Mapping(callingModuleID);
        } else if (!callingModuleID.equals(currentMapping.owner)) {
            checkAndAddMappings();
            currentMapping = new Mapping(callingModuleID);
        }

        // When using the top level mapping
        if (callingModuleID.equals(TOP_LEVEL) && topLevelRoles == null) {
            topLevelRoles = new HashSet<>();
        }

        // Store principal and role temporarily until stopMappingFor called
        currentMapping.addMapping(principal, role);
    }

    /**
     * Remove the given role-principal mapping
     *
     * @param role Role object
     * @param principal the principal
     */
    @Override
    public void unassignPrincipalFromRole(Role role, Principal principal) {
        String roleName = role.getName();
        Subject subject = roleToSubject.get(roleName);
        if (subject != null) {
            subject.getPrincipals().remove(principal);
            roleToSubject.put(roleName, subject);
        }

        if (principal instanceof Group) {
            Set groups = roleToGroup.get(roleName);
            if (groups != null) {
                groups.remove(principal);
                roleToGroup.put(roleName, groups);
            }
        } else {
            Set principals = roleToPrincipal.get(roleName);
            if (principals != null) {
                principals.remove(principal);
                roleToPrincipal.put(roleName, principals);
            }
        }
    }

    @Override
    public Iterator getRoles() {
        return roleToSubject.keySet().iterator(); // All the roles
    }

    @Override
    public Enumeration getGroupsAssignedTo(Role role) {
        Set groups = roleToGroup.get(role.getName());
        if (groups == null) {
            return enumeration(emptySet());
        }

        return enumeration(groups);
    }


    @Override
    public Enumeration getUsersAssignedTo(Role role) {
        Set principals = roleToPrincipal.get(role.getName());
        if (principals == null) {
            return enumeration(emptySet());
        }

        return enumeration(principals);
    }

    @Override
    public void unassignRole(Role role) {
        if (role != null) {
            String roleName = role.getName();
            roleToSubject.remove(roleName);
            roleToPrincipal.remove(roleName);
            roleToGroup.remove(roleName);
        }
    }

    /**
     * @return String. String representation of the RoleToPrincipal Mapping
     */
    @Override
    public String toString() {
        StringBuilder s = new StringBuilder("RoleMapper:");

        for (Iterator e = this.getRoles(); e.hasNext();) {
            String r = e.next();
            s.append("\n\tRole (").append(r).append(") has Principals(");
            Subject sub = roleToSubject.get(r);
            Iterator it = sub.getPrincipals().iterator();
            for (; it.hasNext();) {
                Principal p = it.next();
                s.append(p.getName()).append(" ");
            }
            s.append(")");
        }
        return s.toString();
    }

    /**
     * @param principal A principal that corresponds to the role
     * @param role A role corresponding to this principal
     */
    private void addRoleToSubject(final Principal principal, String role) {
        roleToSubject.computeIfAbsent(role, e -> new Subject())
                     .getPrincipals()
                     .add(principal);
    }

    /**
     * @returns the class name used for default Principal to role mapping return null if default P2R mapping is not
     * supported.
     */
    private String getDefaultPrincipalToRoleMappingClassName() {
        String className = null;
        try {
            if (securityService != null && Boolean.parseBoolean(securityService.getActivateDefaultPrincipalToRoleMapping())) {
                className = securityService.getMappedPrincipalClass();
                if (isEmpty(className)) {
                    className = Group.class.getName();
                }
            }

            if (className == null) {
                return null;
            }

            // To avoid a failure later make sure we can instantiate now
            Class.forName(className)
                 .getConstructor(String.class)
                 .newInstance("anystring");

            return className;
        } catch (Exception e) {
            LOG.log(SEVERE, "pc.getDefaultP2RMappingClass: " + className, e);
            return null;
        }
    }

    // @return true or false depending on activation of
    // the mapping via domain.xml.
    @Override
    public boolean isDefaultPrincipalToRoleMapping() {
        return defaultPrincipalToRoleMappingClassName != null;
    }

    @Override
    public Set getGroups(Subject subject) {
        return
            subject.getPrincipals()
                   .stream()
                   .filter(e -> e instanceof Group)
                   .map(e -> e.getName())
                   .collect(toSet());
    }

    @Override
    public Principal getCallerPrincipal(Subject subject) {
        return
            subject.getPublicCredentials()
                   .stream()
                   .filter(DistinguishedPrincipalCredential.class::isInstance)
                   .map(Principal.class::cast)
                   .findAny()
                   .orElse(subject.getPrincipals()
                       .stream()
                       .filter(UserPrincipal.class::isInstance)
                       .findAny()
                       .orElse(null));
    }

    private Set getCallerPrincipalNames(Subject subject) {
        return
            subject.getPrincipals()
                   .stream()
                   .filter(UserPrincipal.class::isInstance)
                   .map(Principal::getName)
                   .collect(toSet());
    }

    // The method that does the work for assignRole().
    private void internalAssignRole(Principal principal, Role role) {
        String roleName = role.getName();
        LOG.log(FINE, "SECURITY:RoleMapper Assigning Role {0} to {1}", new Object[] {roleName, principal});

        addRoleToSubject(principal, roleName);

        if (principal instanceof Group) {
            roleToGroup.computeIfAbsent(roleName, e -> new HashSet<>())
                       .add((Group) principal);
        } else if (principal instanceof UserPrincipal) {
            roleToPrincipal.computeIfAbsent(roleName, e -> new HashSet<>())
                           .add((UserPrincipal) principal);
        } else {
            throw new IllegalArgumentException("Unknown principal class: " + principal.getClass());
        }
    }

    /**
     * Only web/ejb BundleDescriptor and Application descriptor objects are used for role mapping currently. If other
     * subtypes of RootDeploymentDescriptor are used in the future, they should be added here.
     */
    private String getModuleID(RootDeploymentDescriptor rootDeploymentDescriptor) {
        // V3: Can we use this : return rdd.getModuleID();

        if (rootDeploymentDescriptor.isApplication()) {
            return TOP_LEVEL;
        }

        if (rootDeploymentDescriptor.getModuleDescriptor() != null) {
            return rootDeploymentDescriptor.getModuleDescriptor().getArchiveUri();
        }

        // Cannot happen unless glassfish code is changed
        throw new AssertionError(rootDeploymentDescriptor.getClass() + " is not a known descriptor type");

    }

    /**
     * For each role in the current mapping:
     *
     * First check that the role does not already exist in the top-level mapping. If it does, then the top-level role
     * mapping overrides the current one and we do not need to check if they conflict. Just continue with the next role.
     *
     * If the current mapping is from the top-level file, then check to see if the role has already been mapped. If so, do
     * not need to check for conflicts. Simply override and assign the role.
     *
     * If the above cases do not apply, check for conflicts with roles already set. If there is a conflict, it is between
     * two submodules, so the role should be unmapped in the existing role mappings.
     *
     */
    private void checkAndAddMappings() {
        if (currentMapping == null) {
            return;
        }

        for (Role role : currentMapping.getRoles()) {

            if (topLevelRoles != null && topLevelRoles.contains(role)) {
                logConflictWarning();
                LOG.log(FINE, "Role {0} from module {1} is being overridden by top-level mapping.",
                    new Object[] {role, currentMapping.owner});
                continue;
            }

            if (currentMapping.owner.equals(TOP_LEVEL)) {
                topLevelRoles.add(role);
                if (roleToSubject.keySet().contains(role.getName())) {
                    logConflictWarning();
                    LOG.log(FINE,
                        "Role {0} from top-level mapping descriptor is overriding existing role in sub module.", role);
                    unassignRole(role);
                }

            } else if (roleConflicts(role, currentMapping.getPrincipals(role))) {
                // Detail message already logged
                logConflictWarning();
                unassignRole(role);
                continue;
            }

            // No problems, so assign role
            for (Principal principal : currentMapping.getPrincipals(role)) {
                internalAssignRole(principal, role);
            }

        }

        // Clear current mapping
        currentMapping = null;
    }

    private boolean roleConflicts(Role r, Set ps) {
        // check to see if there has been a previous conflict
        if (conflictedRoles != null && conflictedRoles.contains(r)) {
            LOG.log(FINE, "Role {0} from module {1} has already had a conflict with other modules.",
                new Object[] {r, currentMapping.owner});
            return true;
        }

        // If role not previously mapped, no conflict
        if (!roleToSubject.keySet().contains(r.getName())) {
            return false;
        }

        // check number of mappings first
        int targetNumPrin = ps.size();
        int actualNum = 0;
        Set pSet = roleToPrincipal.get(r.getName());
        Set gSet = roleToGroup.get(r.getName());
        actualNum += pSet == null ? 0 : pSet.size();
        actualNum += gSet == null ? 0 : gSet.size();
        if (targetNumPrin != actualNum) {
            LOG.log(FINE, "Module {0} has different number of mappings for role {1} than other mapping files",
                new Object[] {currentMapping.owner, r.getName()});

            if (conflictedRoles == null) {
                conflictedRoles = new HashSet<>();
            }

            conflictedRoles.add(r);
            return true;
        }

        // check the principals and groups
        boolean fail = false;
        for (Principal p : ps) {
            if (p instanceof Group) {
                if (gSet != null && !gSet.contains(p)) {
                    fail = true;
                }

            } else if (pSet != null && !pSet.contains(p)) {
                fail = true;
            }

            if (fail) {
                LOG.log(FINE, "Role {0} in module {1} is not included in other modules.",
                    new Object[] {r, currentMapping.owner});

                if (conflictedRoles == null) {
                    conflictedRoles = new HashSet<>();
                }

                conflictedRoles.add(r);
                return true;
            }

        }

        // no conflicts
        return false;
    }

    private void logConflictWarning() {
        if (!conflictLogged) {
            LOG.log(WARNING, "Role mapping conflicts found in application {0}. Some roles may not be mapped.", getName());
            conflictLogged = true;
        }
    }

    /**
     * Used to represent the role mapping of a single descriptor file.
     */
    private static class Mapping implements Serializable {
        private static final long serialVersionUID = 5863982599500877228L;

        private final String owner;
        private final Map> roleMap;

        Mapping(String owner) {
            this.owner = owner;
            roleMap = new HashMap<>();
        }

        void addMapping(Principal principal, Role role) {
            roleMap.computeIfAbsent(role, e -> new HashSet<>())
                   .add(principal);
        }

        Set getRoles() {
            return roleMap.keySet();
        }

        Set getPrincipals(Role r) {
            return roleMap.get(r);
        }
    }

    class DefaultRoleToSubjectMapping extends HashMap {
        private static final long serialVersionUID = 3074733840327132690L;

        private final Map roleMap = new HashMap<>();

        DefaultRoleToSubjectMapping() {
        }

        // Do not map '**' to a Principal as this represents the any authenticated user role
        @Override
        public Subject get(Object key) {
            synchronized (roleMap) {
                Subject subject = roleMap.get(key);
                if (subject == null && key instanceof String && !"**".equals(key)) {
                    subject = new Subject();
                    String roleName = (String) key;
                    subject.getPrincipals().add(getSameNamedPrincipal(roleName));
                    roleMap.put(roleName, subject);
                }

                return subject;
            }
        }

        Principal getSameNamedPrincipal(String roleName) {
            try {
                return (Principal)
                    Class.forName(defaultPrincipalToRoleMappingClassName)
                         .getConstructor(String.class)
                         .newInstance(roleName);
            } catch (Exception e) {
                LOG.log(SEVERE, "rm.getSameNamedPrincipal", new Object[] { roleName, e });
                throw new RuntimeException("Unable to get principal by default p2r mapping");
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy