1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2018, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.indexer.authorization; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Map; 30 import java.util.TreeMap; 31 import java.util.logging.Level; 32 import java.util.logging.Logger; 33 import org.opengrok.indexer.configuration.Nameable; 34 import org.opengrok.indexer.configuration.RuntimeEnvironment; 35 import org.opengrok.indexer.logger.LoggerFactory; 36 import org.opengrok.indexer.web.Statistics; 37 38 /** 39 * Subclass of {@link AuthorizationEntity}. It implements the methods to 40 * be able to contain and making decision for: 41 * <ul> 42 * <li>other stacks</li> 43 * <li>plugins</li> 44 * </ul> 45 * 46 * @author Krystof Tulinger 47 */ 48 public class AuthorizationStack extends AuthorizationEntity { 49 50 private static final long serialVersionUID = -2116160303238347415L; 51 52 private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationStack.class); 53 54 private List<AuthorizationEntity> stack = new ArrayList<>(); 55 AuthorizationStack()56 public AuthorizationStack() { 57 } 58 59 /** 60 * Copy constructor from another stack. 61 * <ul> 62 * <li>copy the superclass {@link AuthorizationEntity}</li> 63 * <li>perform a deep copy of the contained stack (using 64 * {@link AuthorizationEntity#clone()}</li> 65 * </ul> 66 * 67 * @param x the stack to be copied 68 */ AuthorizationStack(AuthorizationStack x)69 public AuthorizationStack(AuthorizationStack x) { 70 super(x); 71 stack = new ArrayList<>(x.stack.size()); 72 for (AuthorizationEntity e : x.getStack()) { 73 stack.add(e.clone()); 74 } 75 } 76 AuthorizationStack(AuthControlFlag flag, String name)77 public AuthorizationStack(AuthControlFlag flag, String name) { 78 super(flag, name); 79 } 80 81 /** 82 * Get the value of {@code stack}. 83 * 84 * @return the current stack 85 */ getStack()86 public List<AuthorizationEntity> getStack() { 87 return stack; 88 } 89 90 /** 91 * Set the value of {@code stack}. 92 * 93 * @param s the new stack 94 */ setStack(List<AuthorizationEntity> s)95 public void setStack(List<AuthorizationEntity> s) { 96 this.stack = s; 97 } 98 99 /** 100 * Add a new authorization entity into this stack. 101 * 102 * @param s new entity 103 */ add(AuthorizationEntity s)104 public void add(AuthorizationEntity s) { 105 this.stack.add(s); 106 } 107 108 /** 109 * Remove the given authorization entity from this stack. 110 * 111 * @param s the entity to remove 112 */ remove(AuthorizationEntity s)113 public void remove(AuthorizationEntity s) { 114 s.unload(); 115 this.stack.remove(s); 116 } 117 118 /** 119 * Load all authorization entities in this stack. 120 * 121 * <p> 122 * If the method is unable to load any of the entities contained in this 123 * stack then this stack is marked as failed. Note that it does not affect 124 * the authorization decision made by this stack. 125 * </p> 126 * 127 * @param parameters parameters given in the configuration 128 * 129 * @see IAuthorizationPlugin#load(java.util.Map) 130 */ 131 @Override load(Map<String, Object> parameters)132 public void load(Map<String, Object> parameters) { 133 setCurrentSetup(new TreeMap<>()); 134 getCurrentSetup().putAll(parameters); 135 getCurrentSetup().putAll(getSetup()); 136 137 LOGGER.log(Level.INFO, "[{0}] Stack \"{1}\" is loading.", 138 new Object[]{getFlag().toString().toUpperCase(Locale.ROOT), 139 getName()}); 140 141 // fill properly the "forGroups" and "forProjects" fields 142 processTargetGroupsAndProjects(); 143 144 setWorking(); 145 146 int cnt = 0; 147 for (AuthorizationEntity authEntity : getStack()) { 148 authEntity.load(getCurrentSetup()); 149 if (authEntity.isWorking()) { 150 cnt++; 151 } 152 } 153 154 if (getStack().size() > 0 && cnt < getStack().size()) { 155 setFailed(); 156 } 157 158 LOGGER.log(Level.INFO, "[{0}] Stack \"{1}\" is {2}.", 159 new Object[]{ 160 getFlag().toString().toUpperCase(Locale.ROOT), 161 getName(), 162 isWorking() ? "ready" : "not fully ok"}); 163 } 164 165 /** 166 * Unload all plugins contained in this stack. 167 * 168 * @see IAuthorizationPlugin#unload() 169 */ 170 @Override unload()171 public void unload() { 172 for (AuthorizationEntity plugin : getStack()) { 173 plugin.unload(); 174 } 175 } 176 177 /** 178 * Test the given entity if it should be allowed with in this stack context 179 * if and only if the stack is not marked as failed. 180 * 181 * @param entity the given entity - this is either group or project and is 182 * passed just for the logging purposes. 183 * @param pluginPredicate predicate returning true or false for the given 184 * entity which determines if the authorization for such entity is 185 * successful or failed for particular request and plugin 186 * @param skippingPredicate predicate returning true if this authorization 187 * entity should be omitted from the authorization process 188 * @return true if successful; false otherwise 189 */ 190 @Override isAllowed(Nameable entity, PluginDecisionPredicate pluginPredicate, PluginSkippingPredicate skippingPredicate)191 public boolean isAllowed(Nameable entity, 192 PluginDecisionPredicate pluginPredicate, 193 PluginSkippingPredicate skippingPredicate) { 194 boolean overallDecision = true; 195 LOGGER.log(Level.FINER, "Authorization for \"{0}\" in \"{1}\" [{2}]", 196 new Object[]{entity.getName(), this.getName(), this.getFlag()}); 197 198 if (skippingPredicate.shouldSkip(this)) { 199 LOGGER.log(Level.FINER, "AuthEntity \"{0}\" [{1}] skipping testing of name \"{2}\"", 200 new Object[]{this.getName(), this.getFlag(), entity.getName()}); 201 } else { 202 overallDecision = processStack(entity, pluginPredicate, skippingPredicate); 203 } 204 205 LOGGER.log(Level.FINER, "Authorization for \"{0}\" in \"{1}\" [{2}] => {3}", 206 new Object[]{entity.getName(), this.getName(), this.getFlag(), overallDecision ? "true" : "false"}); 207 return overallDecision; 208 } 209 210 /** 211 * Process the stack. 212 * 213 * @param entity the given entity 214 * @param pluginPredicate predicate returning true or false for the given 215 * entity which determines if the authorization for such entity is 216 * successful or failed for particular request and plugin 217 * @param skippingPredicate predicate returning true if this authorization 218 * entity should be omitted from the authorization process 219 * @return true if entity is allowed; false otherwise 220 */ processStack(Nameable entity, PluginDecisionPredicate pluginPredicate, PluginSkippingPredicate skippingPredicate)221 protected boolean processStack(Nameable entity, 222 PluginDecisionPredicate pluginPredicate, 223 PluginSkippingPredicate skippingPredicate) { 224 225 Statistics stats = RuntimeEnvironment.getInstance().getStatistics(); 226 long time = System.currentTimeMillis(); 227 228 boolean overallDecision = true; 229 for (AuthorizationEntity authEntity : getStack()) { 230 231 if (skippingPredicate.shouldSkip(authEntity)) { 232 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] skipping testing of name \"{2}\"", 233 new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName()}); 234 continue; 235 } 236 // run the plugin's test method 237 try { 238 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\"", 239 new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName()}); 240 241 boolean pluginDecision = authEntity.isAllowed(entity, pluginPredicate, skippingPredicate); 242 243 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\" => {3}", 244 new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName(), 245 pluginDecision ? "true" : "false"}); 246 247 if (!pluginDecision && authEntity.isRequired()) { 248 // required sets a failure but still invokes all other plugins 249 overallDecision = false; 250 continue; 251 } else if (!pluginDecision && authEntity.isRequisite()) { 252 // requisite sets a failure and immediately returns the failure 253 overallDecision = false; 254 break; 255 } else if (overallDecision && pluginDecision && authEntity.isSufficient()) { 256 // sufficient immediately returns the success 257 overallDecision = true; 258 break; 259 } 260 } catch (AuthorizationException ex) { 261 // Propagate up so that proper HTTP error can be given. 262 LOGGER.log(Level.FINEST, "got authorization exception: " + ex.getMessage()); 263 throw ex; 264 } catch (Throwable ex) { 265 LOGGER.log(Level.WARNING, 266 String.format("AuthEntity \"%s\" has failed the testing of \"%s\" with an exception.", 267 authEntity.getName(), 268 entity.getName()), 269 ex); 270 271 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\" => {3}", 272 new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName(), 273 "false (failed)"}); 274 275 // set the return value to false for this faulty plugin 276 if (!authEntity.isSufficient()) { 277 overallDecision = false; 278 } 279 // requisite plugin may immediately return the failure 280 if (authEntity.isRequisite()) { 281 break; 282 } 283 } 284 } 285 time = System.currentTimeMillis() - time; 286 287 stats.addRequestTime( 288 String.format("authorization_in_stack_%s_%s", getName(), overallDecision ? "positive" : "negative"), 289 time); 290 stats.addRequestTime( 291 String.format("authorization_in_stack_%s_%s_of_%s", getName(), overallDecision ? "positive" : "negative", entity.getName()), 292 time); 293 stats.addRequestTime( 294 String.format("authorization_in_stack_%s_of_%s", getName(), entity.getName()), 295 time); 296 297 return overallDecision; 298 } 299 300 /** 301 * Set the plugin to all classes in this stack which requires this class in 302 * the configuration. This creates a new instance of the plugin for each 303 * class which needs it. 304 * 305 * <p> 306 * This is where the loaded plugin classes get to be a part of the 307 * authorization process. When the {@link AuthorizationPlugin} does not get 308 * its {@link IAuthorizationPlugin} it is marked as failed and returns false 309 * to all authorization decisions. 310 * </p> 311 * 312 * @param plugin the new instance of a plugin 313 * @return true if there is such case; false otherwise 314 */ 315 @Override setPlugin(IAuthorizationPlugin plugin)316 public boolean setPlugin(IAuthorizationPlugin plugin) { 317 boolean ret = false; 318 for (AuthorizationEntity p : getStack()) { 319 ret = p.setPlugin(plugin) || ret; 320 } 321 return ret; 322 } 323 324 /** 325 * Clones the stack. Performs: 326 * <ul> 327 * <li>copy the superclass {@link AuthorizationEntity}</li> 328 * <li>perform a deep copy of the contained stack</li> 329 * </ul> 330 * 331 * @return new instance of {@link AuthorizationStack} 332 */ 333 @Override clone()334 public AuthorizationStack clone() { 335 return new AuthorizationStack(this); 336 } 337 338 /** 339 * Print the stack hierarchy. Process also all contained plugins and 340 * substacks. 341 * 342 * @param prefix this prefix should be prepended to every line produced by 343 * this stack 344 * @param colorElement a possible element where any occurrence of %color% 345 * will be replaced with a HTML HEX color representing this entity state. 346 * @return the string containing this stack representation 347 */ 348 @Override hierarchyToString(String prefix, String colorElement)349 public String hierarchyToString(String prefix, String colorElement) { 350 StringBuilder builder = new StringBuilder(prefix); 351 352 builder.append(colorToString(colorElement)); 353 builder.append(infoToString(prefix)); 354 builder.append(" (stack ").append(isWorking() ? "ok" : "not fully ok").append(")"); 355 builder.append("\n"); 356 357 builder.append(setupToString(prefix)); 358 builder.append(targetsToString(prefix)); 359 360 for (AuthorizationEntity authEntity : getStack()) { 361 builder.append(authEntity.hierarchyToString(prefix + " ", colorElement)); 362 } 363 return builder.toString(); 364 } 365 } 366