xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationStack.java (revision 13048b5439f2a342452129c166ff82b5afe9266b)
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, 2020, 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.function.Predicate;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34 
35 import org.opengrok.indexer.configuration.Nameable;
36 import org.opengrok.indexer.logger.LoggerFactory;
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      * <p>
121      * <p>If the method is unable to load any of the entities contained in this
122      * stack then this stack is marked as failed. Note that it does not affect
123      * the authorization decision made by this stack.
124      *
125      * @param parameters parameters given in the configuration
126      *
127      * @see IAuthorizationPlugin#load(java.util.Map)
128      */
129     @Override
load(Map<String, Object> parameters)130     public void load(Map<String, Object> parameters) {
131         setCurrentSetup(new TreeMap<>());
132         getCurrentSetup().putAll(parameters);
133         getCurrentSetup().putAll(getSetup());
134 
135         LOGGER.log(Level.INFO, "[{0}] Stack \"{1}\" is loading.",
136                 new Object[]{getFlag().toString().toUpperCase(Locale.ROOT),
137                 getName()});
138 
139         // fill properly the "forGroups" and "forProjects" fields
140         processTargetGroupsAndProjects();
141 
142         setWorking();
143 
144         int cnt = 0;
145         for (AuthorizationEntity authEntity : getStack()) {
146             authEntity.load(getCurrentSetup());
147             if (authEntity.isWorking()) {
148                 cnt++;
149             }
150         }
151 
152         if (getStack().size() > 0 && cnt < getStack().size()) {
153             setFailed();
154         }
155 
156         LOGGER.log(Level.INFO, "[{0}] Stack \"{1}\" is {2}.",
157                 new Object[]{
158                     getFlag().toString().toUpperCase(Locale.ROOT),
159                     getName(),
160                     isWorking() ? "ready" : "not fully ok"});
161     }
162 
163     /**
164      * Unload all plugins contained in this stack.
165      *
166      * @see IAuthorizationPlugin#unload()
167      */
168     @Override
unload()169     public void unload() {
170         for (AuthorizationEntity plugin : getStack()) {
171             plugin.unload();
172         }
173     }
174 
175     /**
176      * Test the given entity if it should be allowed with in this stack context
177      * if and only if the stack is not marked as failed.
178      *
179      * @param entity the given entity - this is either group or project and is
180      * passed just for the logging purposes.
181      * @param pluginPredicate predicate returning true or false for the given
182      * entity which determines if the authorization for such entity is
183      * successful or failed for particular request and plugin
184      * @param skippingPredicate predicate returning true if this authorization
185      * entity should be omitted from the authorization process
186      * @return true if successful; false otherwise
187      */
188     @Override
isAllowed(Nameable entity, PluginDecisionPredicate pluginPredicate, PluginSkippingPredicate skippingPredicate)189     public boolean isAllowed(Nameable entity,
190             PluginDecisionPredicate pluginPredicate,
191             PluginSkippingPredicate skippingPredicate) {
192         boolean overallDecision = true;
193         LOGGER.log(Level.FINER, "Authorization for \"{0}\" in \"{1}\" [{2}]",
194                 new Object[]{entity.getName(), this.getName(), this.getFlag()});
195 
196         if (skippingPredicate.shouldSkip(this)) {
197             LOGGER.log(Level.FINER, "AuthEntity \"{0}\" [{1}] skipping testing of name \"{2}\"",
198                     new Object[]{this.getName(), this.getFlag(), entity.getName()});
199         } else {
200             overallDecision = processStack(entity, pluginPredicate, skippingPredicate);
201         }
202 
203         LOGGER.log(Level.FINER, "Authorization for \"{0}\" in \"{1}\" [{2}] => {3}",
204                 new Object[]{entity.getName(), this.getName(), this.getFlag(), overallDecision ? "true" : "false"});
205         return overallDecision;
206     }
207 
208     /**
209      * Process the stack.
210      *
211      * @param entity the given entity
212      * @param pluginPredicate predicate returning true or false for the given
213      * entity which determines if the authorization for such entity is
214      * successful or failed for particular request and plugin
215      * @param skippingPredicate predicate returning true if this authorization
216      * entity should be omitted from the authorization process
217      * @return true if entity is allowed; false otherwise
218      */
processStack(Nameable entity, PluginDecisionPredicate pluginPredicate, PluginSkippingPredicate skippingPredicate)219     protected boolean processStack(Nameable entity,
220             PluginDecisionPredicate pluginPredicate,
221             PluginSkippingPredicate skippingPredicate) {
222 
223         boolean overallDecision = true;
224         boolean optionalFailure = false;
225 
226         if (getStack().isEmpty()) {
227             return true;
228         }
229 
230         for (AuthorizationEntity authEntity : getStack()) {
231 
232             if (skippingPredicate.shouldSkip(authEntity)) {
233                 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] skipping testing of name \"{2}\"",
234                         new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName()});
235                 continue;
236             }
237             // run the plugin's test method
238             try {
239                 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\"",
240                         new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName()});
241 
242                 boolean entityDecision = authEntity.isAllowed(entity, pluginPredicate, skippingPredicate);
243 
244                 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\" => {3}",
245                         new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName(),
246                                 entityDecision ? "true" : "false"});
247 
248                 if (!entityDecision && authEntity.isRequired()) {
249                     // required sets a failure but still invokes all other plugins
250                     overallDecision = false;
251                 } else if (!entityDecision && authEntity.isRequisite()) {
252                     // requisite sets a failure and immediately returns the failure
253                     overallDecision = false;
254                     break;
255                 } else if (!entityDecision && authEntity.isOptional()) {
256                     optionalFailure = true;
257                 } else if (overallDecision && entityDecision && authEntity.isSufficient()) {
258                     // sufficient immediately returns the success
259                     break;
260                 }
261             } catch (AuthorizationException ex) {
262                 // Propagate up so that proper HTTP error can be given.
263                 LOGGER.log(Level.FINEST, "got authorization exception: " + ex.getMessage());
264                 throw ex;
265             } catch (Throwable ex) {
266                 LOGGER.log(Level.WARNING,
267                         String.format("AuthEntity \"%s\" has failed the testing of \"%s\" with an exception.",
268                                 authEntity.getName(),
269                                 entity.getName()),
270                         ex);
271 
272                 LOGGER.log(Level.FINEST, "AuthEntity \"{0}\" [{1}] testing a name \"{2}\" => {3}",
273                         new Object[]{authEntity.getName(), authEntity.getFlag(), entity.getName(),
274                                 "false (failed)"});
275 
276                 if (authEntity.isOptional()) {
277                     optionalFailure = true;
278                     continue;
279                 }
280                 // set the return value to false for this faulty plugin
281                 if (!authEntity.isSufficient()) {
282                     overallDecision = false;
283                 }
284                 // requisite plugin may immediately return the failure
285                 if (authEntity.isRequisite()) {
286                     break;
287                 }
288             }
289         }
290 
291         if (optionalFailure &&
292                 getStack().stream().filter(AuthorizationEntity::isOptional).count() == 1 &&
293                 getStack().stream().filter(Predicate.not(AuthorizationEntity::isOptional)).findAny().isEmpty()) {
294             return false;
295         }
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      * <p>
305      * <p>This is where the loaded plugin classes get to be a part of the
306      * authorization process. When the {@link AuthorizationPlugin} does not get
307      * its {@link IAuthorizationPlugin} it is marked as failed and returns false
308      * to all authorization decisions.
309      *
310      * @param plugin the new instance of a plugin
311      * @return true if there is such case; false otherwise
312      */
313     @Override
setPlugin(IAuthorizationPlugin plugin)314     public boolean setPlugin(IAuthorizationPlugin plugin) {
315         boolean ret = false;
316         for (AuthorizationEntity p : getStack()) {
317             ret = p.setPlugin(plugin) || ret;
318         }
319         return ret;
320     }
321 
322     /**
323      * Clones the stack. Performs:
324      * <ul>
325      * <li>copy the superclass {@link AuthorizationEntity}</li>
326      * <li>perform a deep copy of the contained stack</li>
327      * </ul>
328      *
329      * @return new instance of {@link AuthorizationStack}
330      */
331     @Override
clone()332     public AuthorizationStack clone() {
333         return new AuthorizationStack(this);
334     }
335 
336     /**
337      * Print the stack hierarchy. Process also all contained plugins and
338      * substacks.
339      *
340      * @param prefix this prefix should be prepended to every line produced by
341      * this stack
342      * @param colorElement a possible element where any occurrence of %color%
343      * will be replaced with a HTML HEX color representing this entity state.
344      * @return the string containing this stack representation
345      */
346     @Override
hierarchyToString(String prefix, String colorElement)347     public String hierarchyToString(String prefix, String colorElement) {
348         StringBuilder builder = new StringBuilder(prefix);
349 
350         builder.append(colorToString(colorElement));
351         builder.append(infoToString(prefix));
352         builder.append(" (stack ").append(isWorking() ? "ok" : "not fully ok").append(")");
353         builder.append("\n");
354 
355         builder.append(setupToString(prefix));
356         builder.append(targetsToString(prefix));
357 
358         for (AuthorizationEntity authEntity : getStack()) {
359             builder.append(authEntity.hierarchyToString(prefix + "    ", colorElement));
360         }
361         return builder.toString();
362     }
363 }
364