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