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.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028import org.jgrapes.core.Channel; 029import org.jgrapes.core.Component; 030import org.jgrapes.core.Event; 031import org.jgrapes.core.Manager; 032import org.jgrapes.core.annotation.Handler; 033import org.jgrapes.util.events.ConfigurationUpdate; 034import org.jgrapes.webconsole.base.ConsoleConnection; 035import org.jgrapes.webconsole.base.WebConsoleUtils; 036import org.jgrapes.webconsole.base.events.AddConletRequest; 037import org.jgrapes.webconsole.base.events.AddConletType; 038import org.jgrapes.webconsole.base.events.ConsolePrepared; 039import org.jgrapes.webconsole.base.events.UpdateConletType; 040 041/** 042 * Configures the conlets available based on the user currently logged in. 043 */ 044@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 045public class RoleConletFilter extends Component { 046 047 @SuppressWarnings("PMD.UseConcurrentHashMap") 048 private final Map<String, List<String>> acl = new HashMap<>(); 049 private final Set<String> knownTypes = new HashSet<>(); 050 051 /** 052 * Creates a new component with its channel set to the given 053 * channel. 054 * 055 * @param componentChannel the channel that the component's 056 * handlers listen on by default and that 057 * {@link Manager#fire(Event, Channel...)} sends the event to 058 */ 059 public RoleConletFilter(Channel componentChannel) { 060 super(componentChannel); 061 } 062 063 /** 064 * Creates a new component with its channel set to the given 065 * channel. 066 * 067 * Supported properties are: 068 * 069 * * *conletTypesByRole*: see {@link #setConletTypesByRole(Map)}. 070 * 071 * @param componentChannel the channel that the component's 072 * handlers listen on by default and that 073 * {@link Manager#fire(Event, Channel...)} sends the event to 074 * @param properties the properties used to configure the component 075 */ 076 @SuppressWarnings({ "unchecked", "PMD.ConstructorCallsOverridableMethod" }) 077 public RoleConletFilter(Channel componentChannel, 078 Map<?, ?> properties) { 079 super(componentChannel); 080 setConletTypesByRole((Map<String, List<String>>) properties 081 .get("conletTypesByRole")); 082 } 083 084 /** 085 * Sets the permitted conlet types by role. The parameter 086 * is a Map<String, List<String>> holding the conlet permissions 087 * to be added for the given role. The permissions can be an 088 * asterisk ("*") to allow usage of all conlet types. It can be 089 * a conlet type as reported by {@link AddConletType#conletType()} 090 * to allow the usage of a particular conlet. The type may be 091 * prefixed with an exclamation mark to deny the usage (default). 092 * 093 * The first match is used, i.e. the asterisk makes only sense 094 * as the last element in the list. 095 * 096 * @param acl the acl 097 * @return the user role conlet filter 098 */ 099 @SuppressWarnings({ "PMD.LinguisticNaming", 100 "PMD.AvoidInstantiatingObjectsInLoops" }) 101 public RoleConletFilter 102 setConletTypesByRole(Map<String, List<String>> acl) { 103 // Deep copy (and cleanup) 104 this.acl.clear(); 105 this.acl.putAll(acl); 106 for (var e : this.acl.entrySet()) { 107 e.setValue(e.getValue().stream().map(String::trim) 108 .collect(Collectors.toList())); 109 } 110 return this; 111 } 112 113 /** 114 * The component can be configured with events that include 115 * a path (see @link {@link ConfigurationUpdate#paths()}) 116 * that matches this components path (see {@link Manager#componentPath()}). 117 * 118 * The following properties are recognized: 119 * 120 * `conletTypesByRole` 121 * : Invokes {@link #setConletTypesByRole(Map)} with the 122 * given values. 123 * 124 * @param event the event 125 */ 126 @SuppressWarnings("unchecked") 127 @Handler 128 public void onConfigUpdate(ConfigurationUpdate event) { 129 event.structured(componentPath()) 130 .map(c -> (Map<String, List<String>>) c 131 .get("conletTypesByRole")) 132 .ifPresent(this::setConletTypesByRole); 133 } 134 135 /** 136 * Collect known types for wildcard handling 137 * 138 * @param event the event 139 */ 140 @Handler 141 public void onAddConletType(AddConletType event) { 142 knownTypes.add(event.conletType()); 143 } 144 145 /** 146 * Disable all conlets that the user is not allowed to use 147 * by firing {@link UpdateConletType} events with no render modes. 148 * The current user is 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 = 800) 155 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 156 public void onConsolePrepared(ConsolePrepared event, 157 ConsoleConnection channel) { 158 var allowed = new HashSet<String>(); 159 WebConsoleUtils.rolesFromSession(channel.session()).forEach( 160 role -> { 161 var maybe = new HashSet<>(knownTypes); 162 acl.getOrDefault(role.getName(), Collections.emptyList()) 163 .forEach(p -> { 164 if (p.startsWith("!")) { 165 maybe.remove(p.substring(1).trim()); 166 } else if ("*".equals(p)) { 167 allowed.addAll(maybe); 168 } else { 169 if (knownTypes.contains(p)) { 170 allowed.add(p); 171 } 172 } 173 }); 174 }); 175 channel.setAssociated(this, allowed); 176 Set<String> toRemove = new HashSet<>(knownTypes); 177 toRemove.removeAll(allowed); 178 for (var type : toRemove) { 179 channel.respond(new UpdateConletType(type)); 180 } 181 } 182 183 /** 184 * If the request originates from a client 185 * (see {@link AddConletRequest#isFrontendRequest()}, verifies that 186 * the user is allowed to create a conlet of the given type. 187 * 188 * As the conlets that he user is not allowed to use are disabled, 189 * it should be impossible to create such requests in the first place. 190 * However, the frontend code is open to manipulation and therefore 191 * this additional check is introduced to increase security. 192 * 193 * @param event the event 194 * @param channel the channel 195 */ 196 @Handler(priority = 1000) 197 public void onAddConlet(AddConletRequest event, ConsoleConnection channel) { 198 if (!event.isFrontendRequest()) { 199 return; 200 } 201 var allowed = channel.associated(this, Set.class); 202 if (allowed.isEmpty() || !allowed.get().contains(event.conletType())) { 203 event.cancel(true); 204 } 205 } 206}