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}