001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2022 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconsole.rbac;
020
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029import javax.security.auth.Subject;
030import org.jgrapes.core.Channel;
031import org.jgrapes.core.Component;
032import org.jgrapes.core.Event;
033import org.jgrapes.core.Manager;
034import org.jgrapes.core.annotation.Handler;
035import org.jgrapes.util.events.ConfigurationUpdate;
036import org.jgrapes.webconsole.base.ConsoleRole;
037import org.jgrapes.webconsole.base.ConsoleUser;
038import org.jgrapes.webconsole.base.events.UserAuthenticated;
039
040/**
041 * Configures roles (of type {@link ConsoleRole)} 
042 * for the user currently logged in.
043 */
044public class RoleConfigurator extends Component {
045
046    @SuppressWarnings("PMD.UseConcurrentHashMap")
047    private final Map<String, Set<String>> roles = new HashMap<>();
048    private boolean replace;
049
050    /**
051     * Creates a new component with its channel set to the given 
052     * channel.
053     *
054     * @param componentChannel the channel that the component's
055     * handlers listen on by default and that 
056     * {@link Manager#fire(Event, Channel...)} sends the event to
057     */
058    public RoleConfigurator(Channel componentChannel) {
059        super(componentChannel);
060    }
061
062    /**
063     * Creates a new component with its channel set to the given 
064     * channel.
065     *
066     * Supported properties are:
067     * 
068     *  * *rolesByUser*: see {@link #setRolesByUser(Map)}.
069     *
070     * @param componentChannel the channel that the component's
071     * handlers listen on by default and that 
072     * {@link Manager#fire(Event, Channel...)} sends the event to
073     * @param properties the properties used to configure the component
074     */
075    @SuppressWarnings({ "unchecked", "PMD.ConstructorCallsOverridableMethod" })
076    public RoleConfigurator(Channel componentChannel,
077            Map<?, ?> properties) {
078        super(componentChannel);
079        setRolesByUser((Map<String, Set<String>>) properties
080            .get("rolesByUser"));
081    }
082
083    /**
084     * Sets the roles associated with a user. The parameter
085     * is a Map<String, Set<String>> holding the roles to be  
086     * associated with a given user. The special key "*" may
087     * be used to specify roles that are to be added to any user.
088     *
089     * @param roles the roles
090     * @return the user role conlet filter
091     */
092    @SuppressWarnings({ "PMD.LinguisticNaming",
093        "PMD.AvoidInstantiatingObjectsInLoops" })
094    public RoleConfigurator setRolesByUser(Map<String, Set<String>> roles) {
095        this.roles.clear();
096        this.roles.putAll(roles);
097        for (var e : this.roles.entrySet()) {
098            e.setValue(new HashSet<>(e.getValue()));
099        }
100        return this;
101    }
102
103    /**
104     * Control whether the component replaces all {@link ConsoleRole}s
105     * or adds to the existing roles (default).
106     *
107     * @param replace the replace
108     * @return the role configurator
109     */
110    @SuppressWarnings("PMD.LinguisticNaming")
111    public RoleConfigurator setReplace(boolean replace) {
112        this.replace = replace;
113        return this;
114    }
115
116    /**
117     * The component can be configured with events that include
118     * a path (see @link {@link ConfigurationUpdate#paths()})
119     * that matches this components path (see {@link Manager#componentPath()}).
120     * 
121     * The following properties are recognized:
122     * 
123     * `rolesByUser`
124     * : Invokes {@link #setRolesByUser(Map)} with the given values.
125     *
126     * `replace`
127     * : Invokes {@link #setReplace(boolean)} with the given value.
128     * 
129     * @param event the event
130     */
131    @SuppressWarnings("unchecked")
132    @Handler
133    public void onConfigUpdate(ConfigurationUpdate event) {
134        event.structured(componentPath())
135            .map(c -> (Map<String, Collection<String>>) c.get("rolesByUser"))
136            .map(m -> m.entrySet().stream()
137                .collect(Collectors.toMap(Map.Entry::getKey,
138                    e -> e.getValue().stream().collect(Collectors.toSet()))))
139            .ifPresent(this::setRolesByUser);
140        event.value(componentPath(), "replace").map(Boolean::valueOf)
141            .ifPresent(this::setReplace);
142    }
143
144    /**
145     * Sets the roles in the subject with the authenticated user.
146     *
147     * @param event the event
148     * @param channel the channel
149     */
150    @Handler(priority = 900)
151    public void onUserAuthenticated(UserAuthenticated event, Channel channel) {
152        Subject subject = event.subject();
153        if (replace) {
154            for (var itr = subject.getPrincipals().iterator(); itr.hasNext();) {
155                if (itr.next() instanceof ConsoleRole) {
156                    itr.remove();
157                }
158            }
159        }
160        Stream.concat(
161            subject.getPrincipals(ConsoleUser.class).stream()
162                .findFirst().map(ConsoleUser::getName).stream(),
163            Stream.of("*")).map(roles::get).filter(Objects::nonNull)
164            .flatMap(Set::stream).map(ConsoleRole::new)
165            .forEach(p -> subject.getPrincipals().add(p));
166        event.by("Role Configurator");
167    }
168}