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, 2021, 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.io.Serializable; 27 import java.util.Locale; 28 import java.util.Map; 29 import java.util.Map.Entry; 30 import java.util.Set; 31 import java.util.TreeMap; 32 import java.util.TreeSet; 33 import java.util.function.Predicate; 34 import java.util.logging.Level; 35 import java.util.logging.Logger; 36 import java.util.regex.PatternSyntaxException; 37 import java.util.stream.Collectors; 38 import org.opengrok.indexer.configuration.Group; 39 import org.opengrok.indexer.configuration.Nameable; 40 import org.opengrok.indexer.configuration.Project; 41 import org.opengrok.indexer.logger.LoggerFactory; 42 43 /** 44 * This class covers authorization entities used in opengrok. 45 * 46 * Currently there are two: 47 * <ul> 48 * <li>stack of plugins</li> 49 * <li>plugin</li> 50 * </ul> 51 * 52 * The purpose is to extract common member variables and methods into an class, 53 * namely: 54 * <ul> 55 * <li>name</li> 56 * <li>role - sufficient/required/requisite</li> 57 * <li>state - working/failed</li> 58 * <li>setup - from configuration</li> 59 * </ul> 60 * and let the subclasses implement the important abstract methods. 61 * 62 * This class is intended to be read from a configuration. 63 * 64 * @author Krystof Tulinger 65 */ 66 public abstract class AuthorizationEntity implements Nameable, Serializable, Cloneable { 67 68 private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationEntity.class); 69 70 /** 71 * Predicate specialized for the the plugin decisions. The caller should 72 * implement the <code>decision</code> method. Returning true if the plugin 73 * allows the action or false when the plugin forbids the action. 74 */ 75 public abstract static class PluginDecisionPredicate implements Predicate<IAuthorizationPlugin> { 76 77 @Override test(IAuthorizationPlugin t)78 public boolean test(IAuthorizationPlugin t) { 79 return decision(t); 80 } 81 82 /** 83 * Perform the authorization check for this plugin. 84 * 85 * @param t the plugin 86 * @return true if plugin allows the action; false otherwise 87 */ decision(IAuthorizationPlugin t)88 public abstract boolean decision(IAuthorizationPlugin t); 89 } 90 91 /** 92 * Predicate specialized for the the entity skipping decisions. The caller 93 * should implement the <code>shouldSkip</code> method. Returning true if 94 * the entity should be skipped for this action and false if the entity 95 * should be used. 96 */ 97 public abstract static class PluginSkippingPredicate implements Predicate<AuthorizationEntity> { 98 99 @Override test(AuthorizationEntity t)100 public boolean test(AuthorizationEntity t) { 101 return shouldSkip(t); 102 } 103 104 /** 105 * Decide if the entity should be skipped in this step of authorization. 106 * 107 * @param t the entity 108 * @return true if skipped (authorization decision will not be affected 109 * by this entity) or false if it should be used (authorization decision 110 * will be affected by this entity) 111 */ shouldSkip(AuthorizationEntity t)112 public abstract boolean shouldSkip(AuthorizationEntity t); 113 } 114 115 private static final long serialVersionUID = 1L; 116 /** 117 * One of "required", "requisite", "sufficient". 118 */ 119 protected AuthControlFlag flag; 120 protected String name; 121 protected Map<String, Object> setup = new TreeMap<>(); 122 /** 123 * Hold current setup - merged with all ancestor's stacks. 124 */ 125 protected transient Map<String, Object> currentSetup = new TreeMap<>(); 126 127 private Set<String> forProjects = new TreeSet<>(); 128 private Set<String> forGroups = new TreeSet<>(); 129 130 protected transient boolean working = true; 131 AuthorizationEntity()132 public AuthorizationEntity() { 133 } 134 135 /** 136 * Copy constructor for the entity. 137 * <ul> 138 * <li>copy flag</li> 139 * <li>copy name</li> 140 * <li>deep copy of the setup</li> 141 * <li>copy the working attribute</li> 142 * </ul> 143 * 144 * @param x the entity to be copied 145 */ AuthorizationEntity(AuthorizationEntity x)146 public AuthorizationEntity(AuthorizationEntity x) { 147 flag = x.flag; 148 name = x.name; 149 setup = new TreeMap<>(x.setup); 150 working = x.working; 151 forGroups = new TreeSet<>(x.forGroups); 152 forProjects = new TreeSet<>(x.forProjects); 153 } 154 AuthorizationEntity(AuthControlFlag flag, String name)155 public AuthorizationEntity(AuthControlFlag flag, String name) { 156 this.flag = flag; 157 this.name = name; 158 } 159 160 /** 161 * Load this entity with given parameters. 162 * 163 * @param parameters given parameters passed to the plugin's load method 164 * 165 * @see IAuthorizationPlugin#load(java.util.Map) 166 */ load(Map<String, Object> parameters)167 public abstract void load(Map<String, Object> parameters); 168 169 /** 170 * Unload this entity. 171 * 172 * @see IAuthorizationPlugin#unload() 173 */ unload()174 public abstract void unload(); 175 176 /** 177 * Test the given entity if it should be allowed with this authorization 178 * check. 179 * 180 * @param entity the given entity - this is either group or project and is 181 * passed just for the logging purposes. 182 * @param pluginPredicate predicate returning true or false for the given 183 * entity which determines if the authorization for such entity is 184 * successful or failed 185 * @param skippingPredicate predicate returning true if this authorization 186 * entity should be omitted from the authorization process 187 * @return true if successful; false otherwise 188 */ isAllowed(Nameable entity, PluginDecisionPredicate pluginPredicate, PluginSkippingPredicate skippingPredicate)189 public abstract boolean isAllowed(Nameable entity, 190 PluginDecisionPredicate pluginPredicate, 191 PluginSkippingPredicate skippingPredicate); 192 193 /** 194 * Set the plugin to all classes which requires this class in the 195 * configuration. This creates a new instance of the plugin for each class 196 * which needs it. 197 * 198 * @param plugin the new instance of a plugin 199 * @return true if there is such case; false otherwise 200 */ setPlugin(IAuthorizationPlugin plugin)201 public abstract boolean setPlugin(IAuthorizationPlugin plugin); 202 203 /** 204 * Perform a deep copy of the entity. 205 * 206 * @return the new instance of this entity 207 */ 208 @Override clone()209 public abstract AuthorizationEntity clone(); 210 211 /** 212 * Print the entity hierarchy. 213 * 214 * @param prefix this prefix should be prepended to every line produced by 215 * this entity 216 * @param colorElement a possible element where any occurrence of %color% 217 * will be replaced with a HTML HEX color representing this entity state. 218 * @return the string containing this entity representation 219 */ hierarchyToString(String prefix, String colorElement)220 public abstract String hierarchyToString(String prefix, String colorElement); 221 222 /** 223 * Get the value of {@code flag}. 224 * 225 * @return the value of flag 226 */ getFlag()227 public AuthControlFlag getFlag() { 228 return flag; 229 } 230 231 /** 232 * Set the value of {@code flag}. 233 * 234 * @param flag new value of flag 235 */ setFlag(AuthControlFlag flag)236 public void setFlag(AuthControlFlag flag) { 237 this.flag = flag; 238 } 239 240 /** 241 * Set the value of {@code flag}. 242 * 243 * @param flag new value of flag 244 */ setFlag(String flag)245 public void setFlag(String flag) { 246 this.flag = AuthControlFlag.get(flag); 247 } 248 249 /** 250 * Get the value of {@code name}. 251 * 252 * @return the value of name 253 */ 254 @Override getName()255 public String getName() { 256 return name; 257 } 258 259 /** 260 * Set the value of {@code name}. 261 * 262 * @param name new value of name 263 */ 264 @Override setName(String name)265 public void setName(String name) { 266 this.name = name; 267 } 268 269 /** 270 * Get the value of {@code setup}. 271 * 272 * @return the value of setup 273 */ getSetup()274 public Map<String, Object> getSetup() { 275 return setup; 276 } 277 278 /** 279 * Set the value of {@code setup}. 280 * 281 * @param setup new value of setup 282 */ setSetup(Map<String, Object> setup)283 public void setSetup(Map<String, Object> setup) { 284 this.setup = setup; 285 } 286 287 /** 288 * Get the value of current setup. 289 * 290 * @return the value of current setup 291 */ getCurrentSetup()292 public Map<String, Object> getCurrentSetup() { 293 return currentSetup; 294 } 295 296 /** 297 * Set the value of current setup. 298 * 299 * @param currentSetup new value of current setup 300 */ setCurrentSetup(Map<String, Object> currentSetup)301 public void setCurrentSetup(Map<String, Object> currentSetup) { 302 this.currentSetup = currentSetup; 303 } 304 305 /** 306 * Get the value of {@code forProjects}. 307 * 308 * @return the value of forProjects 309 */ forProjects()310 public Set<String> forProjects() { 311 return getForProjects(); 312 } 313 314 /** 315 * Get the value of {@code forProjects}. 316 * 317 * @return the value of forProjects 318 */ getForProjects()319 public Set<String> getForProjects() { 320 return forProjects; 321 } 322 323 /** 324 * Set the value of {@code forProjects}. 325 * 326 * @param forProjects new value of forProjects 327 */ setForProjects(Set<String> forProjects)328 public void setForProjects(Set<String> forProjects) { 329 this.forProjects = forProjects; 330 } 331 332 /** 333 * Set the value of {@code forProjects}. 334 * 335 * @param project add this project into the set 336 */ setForProjects(String project)337 public void setForProjects(String project) { 338 this.forProjects.add(project); 339 } 340 341 /** 342 * Set the value of {@code forProjects}. 343 * 344 * @param projects add all projects in this array into the set 345 * 346 * @see #setForProjects(java.lang.String) 347 */ setForProjects(String[] projects)348 public void setForProjects(String[] projects) { 349 for (String project : projects) { 350 setForProjects(project); 351 } 352 } 353 354 /** 355 * Get the value of {@code forGroups}. 356 * 357 * @return the value of forGroups 358 */ forGroups()359 public Set<String> forGroups() { 360 return getForGroups(); 361 } 362 363 /** 364 * Get the value of {@code forGroups}. 365 * 366 * @return the value of forGroups 367 */ getForGroups()368 public Set<String> getForGroups() { 369 return forGroups; 370 } 371 372 /** 373 * Set the value of {@code forGroups}. 374 * 375 * @param forGroups new value of forGroups 376 */ setForGroups(Set<String> forGroups)377 public void setForGroups(Set<String> forGroups) { 378 this.forGroups = forGroups; 379 } 380 381 /** 382 * Set the value of {@code forGroups}. 383 * 384 * @param group add this group into the set 385 */ setForGroups(String group)386 public void setForGroups(String group) { 387 this.forGroups.add(group); 388 } 389 390 /** 391 * Set the value of {@code forGroups}. 392 * 393 * @param groups add all groups in this array into the set 394 * 395 * @see #setForGroups(java.lang.String) 396 */ setForGroups(String[] groups)397 public void setForGroups(String[] groups) { 398 for (String group : groups) { 399 setForGroups(group); 400 } 401 } 402 403 /** 404 * Discover all targeted groups and projects for every group given by 405 * {@link #forGroups()}. 406 * 407 * <ul> 408 * <li>add to the {@link #forGroups()} all groups which are descendant 409 * groups to the group</li> 410 * <li>add to the {@link #forGroups()} all groups which are parent groups to 411 * the group</li> 412 * <li>add to the {@link #forProjects()} all projects and repositories which 413 * are in the descendant groups or in the group itself</li> 414 * <li>issue a warning for non-existent groups</li> 415 * <li>issue a warning for non-existent projects</li> 416 * </ul> 417 */ processTargetGroupsAndProjects()418 protected void processTargetGroupsAndProjects() { 419 Set<String> groups = new TreeSet<>(); 420 421 for (String x : forGroups()) { 422 /** 423 * Full group discovery takes place here. All projects/repositories 424 * in the group are added into "forProjects" and all subgroups 425 * (including projects/repositories) and parent groups (excluding 426 * the projects/repositories) are added into "forGroups". 427 * 428 * If the group does not exist then a warning is issued. 429 */ 430 Group g; 431 if ((g = Group.getByName(x)) != null) { 432 forProjects().addAll(g.getAllProjects().stream().map(Project::getName).collect(Collectors.toSet())); 433 groups.addAll(g.getRelatedGroups().stream().map(Group::getName).collect(Collectors.toSet())); 434 groups.add(x); 435 } else { 436 LOGGER.log(Level.WARNING, "Configured group \"{0}\" in forGroups section" 437 + " for name \"{1}\" does not exist", 438 new Object[]{x, getName()}); 439 } 440 } 441 setForGroups(groups); 442 443 forProjects().removeIf((t) -> { 444 /** 445 * Check the existence of the projects and issue a warning if there 446 * is no such project. 447 */ 448 Project p; 449 if ((p = Project.getByName(t)) == null) { 450 LOGGER.log(Level.WARNING, "Configured project \"{0}\" in forProjects" 451 + " section for name \"{1}\" does not exist", 452 new Object[]{t, getName()}); 453 return true; 454 } 455 return false; 456 }); 457 } 458 459 /** 460 * Check if the plugin exists and has not failed while loading. 461 * 462 * @return true if working, false otherwise 463 */ isWorking()464 public boolean isWorking() { 465 return working; 466 } 467 468 /** 469 * Mark this entity as working. 470 */ setWorking()471 public synchronized void setWorking() { 472 working = true; 473 } 474 475 /** 476 * Check if this plugin has failed during loading or is missing. 477 * 478 * This method has the same effect as !{@link isWorking()}. 479 * 480 * @return true if failed, true otherwise 481 * @see #isWorking() 482 */ isFailed()483 public boolean isFailed() { 484 return !isWorking(); 485 } 486 487 /** 488 * Set this plugin as failed. This plugin will no more call the underlying 489 * plugin isAllowed methods. 490 * 491 * @see IAuthorizationPlugin#isAllowed(jakarta.servlet.http.HttpServletRequest, Group) 492 * @see IAuthorizationPlugin#isAllowed(jakarta.servlet.http.HttpServletRequest, Project) 493 */ setFailed()494 public synchronized void setFailed() { 495 working = false; 496 } 497 498 /** 499 * Check if this plugin is marked as required. 500 * 501 * @return true if is required; false otherwise 502 */ isRequired()503 public boolean isRequired() { 504 return getFlag().isRequired(); 505 } 506 507 /** 508 * Check if this plugin is marked as sufficient. 509 * 510 * @return true if is sufficient; false otherwise 511 */ isSufficient()512 public boolean isSufficient() { 513 return getFlag().isSufficient(); 514 } 515 516 /** 517 * Check if this plugin is marked as requisite. 518 * 519 * @return true if is requisite; false otherwise 520 */ isRequisite()521 public boolean isRequisite() { 522 return getFlag().isRequisite(); 523 } 524 525 /** 526 * Check if this plugin is marked as optional. 527 * 528 * @return true if is optional; false otherwise 529 */ isOptional()530 public boolean isOptional() { 531 return getFlag().isOptional(); 532 } 533 534 /** 535 * Print the entity hierarchy. 536 * 537 * @return the string containing this entity representation 538 */ hierarchyToString()539 public String hierarchyToString() { 540 return hierarchyToString("", "<span style=\"background-color: %color%;\"> </span>"); 541 } 542 543 /** 544 * Print the color element for this entity. Replace all occurrences of 545 * %color% in the input string by the current state color in the HTML HEX 546 * format. 547 * 548 * @param colorElement the string, possibly an HTML element, describing the 549 * color (can use %color%) to inject the true color of this entity state. 550 * @return the color element with filled color 551 */ colorToString(String colorElement)552 protected String colorToString(String colorElement) { 553 StringBuilder builder = new StringBuilder(colorElement.length() + 10); 554 String tmp; 555 try { 556 // #66ff33 - green 557 // #ff0000 - red 558 tmp = colorElement.replaceAll("(?<!\\\\)%color(?<!\\\\)%", isWorking() ? "#66ff33" : "#ff0000"); 559 if (tmp.isEmpty()) { 560 builder.append(" "); 561 } else { 562 builder.append(tmp); 563 } 564 } catch (PatternSyntaxException ex) { 565 builder.append(" "); 566 } 567 return builder.toString(); 568 } 569 570 /** 571 * Print the basic information about this entity. 572 * 573 * @param prefix prepend this value to each line produced 574 * @return the string containing the information. 575 */ infoToString(String prefix)576 protected String infoToString(String prefix) { 577 StringBuilder builder = new StringBuilder(40); 578 String flup = getFlag().toString().toUpperCase(Locale.ROOT); 579 String nm = getName(); 580 builder.append(" ").append(flup).append(" '").append(nm).append("'"); 581 return builder.toString(); 582 } 583 584 /** 585 * Print the setup into a string. 586 * 587 * @param prefix prepend this value to each line produced 588 * @return the string representing the entity setup 589 */ setupToString(String prefix)590 protected String setupToString(String prefix) { 591 StringBuilder builder = new StringBuilder(); 592 if (!currentSetup.isEmpty()) { 593 builder.append(prefix).append(" setup:\n"); 594 for (Entry<String, Object> entry : currentSetup.entrySet()) { 595 builder.append(prefix) 596 .append(" ") 597 .append(entry.getKey()) 598 .append(": ") 599 .append(entry.getValue()) 600 .append("\n"); 601 } 602 } 603 return builder.toString(); 604 } 605 606 /** 607 * Print the targets for groups and projects into a string. 608 * 609 * @param prefix prepend this value to each line produced 610 * @return the string representing targeted the groups and projects 611 */ targetsToString(String prefix)612 protected String targetsToString(String prefix) { 613 StringBuilder builder = new StringBuilder(); 614 if (forGroups().size() > 0) { 615 builder.append(prefix).append(" only for groups:\n"); 616 for (String x : forGroups()) { 617 builder.append(prefix).append(" ").append(x).append("\n"); 618 } 619 } 620 if (forProjects().size() > 0) { 621 builder.append(prefix).append(" only for projects:\n"); 622 for (String x : forProjects()) { 623 builder.append(prefix).append(" ").append(x).append("\n"); 624 } 625 } 626 return builder.toString(); 627 } 628 } 629