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}