xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationEntity.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
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