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.AvoidInstantiatingObjectsInLoops" })
093    public RoleConfigurator setRolesByUser(Map<String, Set<String>> roles) {
094        this.roles.clear();
095        this.roles.putAll(roles);
096        for (var e : this.roles.entrySet()) {
097            e.setValue(new HashSet<>(e.getValue()));
098        }
099        return this;
100    }
101
102    /**
103     * Control whether the component replaces all {@link ConsoleRole}s
104     * or adds to the existing roles (default).
105     *
106     * @param replace the replace
107     * @return the role configurator
108     */
109    public RoleConfigurator setReplace(boolean replace) {
110        this.replace = replace;
111        return this;
112    }
113
114    /**
115     * The component can be configured with events that include
116     * a path (see @link {@link ConfigurationUpdate#paths()})
117     * that matches this components path (see {@link Manager#componentPath()}).
118     * 
119     * The following properties are recognized:
120     * 
121     * `rolesByUser`
122     * : Invokes {@link #setRolesByUser(Map)} with the given values.
123     *
124     * `replace`
125     * : Invokes {@link #setReplace(boolean)} with the given value.
126     * 
127     * @param event the event
128     */
129    @SuppressWarnings("unchecked")
130    @Handler
131    public void onConfigUpdate(ConfigurationUpdate event) {
132        event.structured(componentPath())
133            .map(c -> (Map<String, Collection<String>>) c.get("rolesByUser"))
134            .map(m -> m.entrySet().stream()
135                .collect(Collectors.toMap(Map.Entry::getKey,
136                    e -> e.getValue().stream().collect(Collectors.toSet()))))
137            .ifPresent(this::setRolesByUser);
138        event.value(componentPath(), "replace").map(Boolean::valueOf)
139            .ifPresent(this::setReplace);
140    }
141
142    /**
143     * Sets the roles in the subject with the authenticated user.
144     *
145     * @param event the event
146     * @param channel the channel
147     */
148    @Handler(priority = 900)
149    public void onUserAuthenticated(UserAuthenticated event, Channel channel) {
150        Subject subject = event.subject();
151        if (replace) {
152            for (var itr = subject.getPrincipals().iterator(); itr.hasNext();) {
153                if (itr.next() instanceof ConsoleRole) {
154                    itr.remove();
155                }
156            }
157        }
158        Stream.concat(
159            subject.getPrincipals(ConsoleUser.class).stream()
160                .findFirst().map(ConsoleUser::getName).stream(),
161            Stream.of("*")).map(roles::get).filter(Objects::nonNull)
162            .flatMap(Set::stream).map(ConsoleRole::new)
163            .forEach(p -> subject.getPrincipals().add(p));
164        event.by("Role Configurator");
165    }
166}