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.ConsoleConnection;
037import org.jgrapes.webconsole.base.ConsoleRole;
038import org.jgrapes.webconsole.base.ConsoleUser;
039import org.jgrapes.webconsole.base.WebConsoleUtils;
040import org.jgrapes.webconsole.base.events.ConsolePrepared;
041
042/**
043 * Configures roles (of type {@link ConsoleRole)} 
044 * for the user currently logged in.
045 */
046public class RoleConfigurator extends Component {
047
048    @SuppressWarnings("PMD.UseConcurrentHashMap")
049    private final Map<String, Set<String>> roles = new HashMap<>();
050    private boolean replace;
051
052    /**
053     * Creates a new component with its channel set to the given 
054     * channel.
055     *
056     * @param componentChannel the channel that the component's
057     * handlers listen on by default and that 
058     * {@link Manager#fire(Event, Channel...)} sends the event to
059     */
060    public RoleConfigurator(Channel componentChannel) {
061        super(componentChannel);
062    }
063
064    /**
065     * Creates a new component with its channel set to the given 
066     * channel.
067     *
068     * Supported properties are:
069     * 
070     *  * *rolesByUser*: see {@link #setRolesByUser(Map)}.
071     *
072     * @param componentChannel the channel that the component's
073     * handlers listen on by default and that 
074     * {@link Manager#fire(Event, Channel...)} sends the event to
075     * @param properties the properties used to configure the component
076     */
077    @SuppressWarnings({ "unchecked", "PMD.ConstructorCallsOverridableMethod" })
078    public RoleConfigurator(Channel componentChannel,
079            Map<?, ?> properties) {
080        super(componentChannel);
081        setRolesByUser((Map<String, Set<String>>) properties
082            .get("rolesByUser"));
083    }
084
085    /**
086     * Sets the roles associated with a user. The parameter
087     * is a Map<String, Set<String>> holding the roles to be  
088     * associated with a given user. The special key "*" may
089     * be used to specify roles that are to be added to any user.
090     *
091     * @param roles the roles
092     * @return the user role conlet filter
093     */
094    @SuppressWarnings({ "PMD.LinguisticNaming",
095        "PMD.AvoidInstantiatingObjectsInLoops" })
096    public RoleConfigurator setRolesByUser(Map<String, Set<String>> roles) {
097        this.roles.clear();
098        this.roles.putAll(roles);
099        for (var e : this.roles.entrySet()) {
100            e.setValue(new HashSet<>(e.getValue()));
101        }
102        return this;
103    }
104
105    /**
106     * Control whether the component replaces all {@link ConsoleRole}s
107     * or adds to the existing roles (default).
108     *
109     * @param replace the replace
110     * @return the role configurator
111     */
112    @SuppressWarnings("PMD.LinguisticNaming")
113    public RoleConfigurator setReplace(boolean replace) {
114        this.replace = replace;
115        return this;
116    }
117
118    /**
119     * The component can be configured with events that include
120     * a path (see @link {@link ConfigurationUpdate#paths()})
121     * that matches this components path (see {@link Manager#componentPath()}).
122     * 
123     * The following properties are recognized:
124     * 
125     * `rolesByUser`
126     * : Invokes {@link #setRolesByUser(Map)} with the given values.
127     *
128     * `replace`
129     * : Invokes {@link #setReplace(boolean)} with the given value.
130     * 
131     * @param event the event
132     */
133    @SuppressWarnings("unchecked")
134    @Handler
135    public void onConfigUpdate(ConfigurationUpdate event) {
136        event.structured(componentPath())
137            .map(c -> (Map<String, Collection<String>>) c.get("rolesByUser"))
138            .map(m -> m.entrySet().stream()
139                .collect(Collectors.toMap(Map.Entry::getKey,
140                    e -> e.getValue().stream().collect(Collectors.toSet()))))
141            .ifPresent(this::setRolesByUser);
142        event.value(componentPath(), "replace").map(Boolean::valueOf)
143            .ifPresent(this::setReplace);
144    }
145
146    /**
147     * Sets the roles in the session's subject. The current user is 
148     * obtained from 
149     * {@link WebConsoleUtils#userFromSession(org.jgrapes.http.Session)}.
150     *
151     * @param event the event
152     * @param channel the channel
153     */
154    @Handler(priority = 900)
155    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
156    public void onConsolePrepared(ConsolePrepared event,
157            ConsoleConnection channel) {
158        Subject subject = (Subject) channel.session().get(Subject.class);
159        if (subject == null) {
160            return;
161        }
162        if (replace) {
163            for (var itr = subject.getPrincipals().iterator(); itr.hasNext();) {
164                if (itr.next() instanceof ConsoleRole) {
165                    itr.remove();
166                }
167            }
168        }
169        Stream.concat(
170            WebConsoleUtils.userFromSession(channel.session())
171                .map(ConsoleUser::getName).stream(),
172            Stream.of("*")).map(roles::get).filter(Objects::nonNull)
173            .flatMap(Set::stream).map(ConsoleRole::new)
174            .forEach(p -> subject.getPrincipals().add(p));
175    }
176}