To be Reviewed By: July 12th 2019

Authors: Juan José Ramos

Status: Draft | Discussion | Development | Active | Dropped | Superseded

Superseded by: N/A

Related: N/A



Introduction

The query engine allows applications to access public field values or execute methods on the objects stored within a Geode Region. If a field used in query is not declared as public within the object, the query engine automatically tries to get the value for the field using accessor methods. Prior to the release of Geode 1.3.0, OQL used to allow any method invocation on objects present in the member’s classpath, including mutators and, through the usage of Java Reflection, internal Geode, and even JDK or external library (Spring, Tomcat, etc.), methods. This could impact the integrity of the data, the region, and the platform on which Geode is running (for more details see CVE-2017-9795).

As part of GEODE-3247, several options were analysed and, after considering the weight of security holes and the difficulty of determining which methods deployed by the developer were intended to be available for queries and which were not, the decision was made to tighten up the Security and, by default, disallow any method call not explicitly added to an acceptance list.

The main threats, already addressed through GEODE-3247, are shown below:

  1. Reflection (Allows the user to do everything)
      SELECT * FROM /region r WHERE r.getClass().forName('java.lang.Runtime').getDeclaredMethods()[0].invoke()
  2. Cache Access (Allows the user to do anything with the cache: closing, accessing all regions, etc.)
      SELECT * FROM /region.getCache().close()
  3. Region Access (Allows the user to destroy, add or invalidate the entire region or specific entries)
      SELECT * FROM /region.destroyRegion()
      SELECT * FROM /region.invalidate('xyz')
      SELECT * FROM /region.put('xyz','abc')
  4. Entry Modification (Allows the user to modify an object in place)
      SELECT r.setName('Zaraza') FROM /region r

Problem

After the solution was released and deployed by several users, the feedback received wasn’t the best and the overall feeling was that enabling this feature made OQL unusable. Our customers ended up changing the access of the attributes on their domain model to be public so the Field can be directly accessed by the query engine without requiring a method invocation (we shouldn't impose this restriction on the data model!!) or disabling OQL security altogether through the system property QueryService.allowUntrustedMethodInvocation.

We certainly know that malicious users can write complex OQL expression and potentially compromise the entire system, but just disallowing everything is not the best solution; allowing method invocations on the user’s domain model should be configurable and easily achieved. That said, we also know that we can’t automatically define the trust boundaries for deployed code, and that trusting by default all code deployed by a user with enough privileges is not feasible either: the user might have deployed the code but it might not want that code to be invoked through OQL by a READ only user. What we can do, however, is to make the integration easier and provide our customers with the right tools and means to easily decide which methods should be allowed and which ones should be denied.

As a summary, the current OQL security implementation is too restrictive and turns the whole OQL feature almost unusable when security is enabled. The experience has also demonstrated that there is no "One size fits all" solution for this requirement, so we need to analyze possible options to loosen the security restrictions and facilitate the developer’s experience, along with taking a final step to decide which option to pursue once the community has approved the proposal.

From now on and in the context of this document, Method Authorization Implementation refers to the actual mechanism/execution of the logic involved in determining whether a specific method should be allowed or denied during the execution of a particular query.

Goals

  • Method Authorization Implementation should be pluggable.
  • Method Authorization Implementation should be "on" by default when Security is enabled at cluster level.
  • Method Authorization Implementation should be stored and retrieved through the cluster configuration service.
  • The system property QueryService.allowUntrustedMethodInvocation should be kept but marked as deprecated.
  • Geode should prevent RCE (Remote Code Execution) exploits and other vulnerabilities in OQL expressions when Security is enabled at cluster level.
  • Method Authorization Implementation should be configurable/exchangeable in runtime (doesn’t need to be instantaneous update, eventual consistency is tolerable).
  • Users should be able to invoke methods on domain classes (present on the system class path or deployed through gfsh) as part of OQL, relatively easy and with little to no configuration changes.

Current Implementation

In order to execute any query, the user executing it needs to have the DATA:READ:regionName ResourcePermission assigned to it, that's the first thing checked no matter where the query execution comes from. Once the verification succeeds, the query engine proceeds to execute the query and, whenever needed, executes the Method Authorization Implementation.

MethodInvocationAuthorizer

The single internal interface responsible of verifying whether a method can be invoked on a certain object or not:

public interface MethodInvocationAuthorizer {
void authorizeMethodInvocatioon(Method method, Object target);
}

The single implementation (RestrictedMethodInvocationAuthorizer) first checks whether the method is part of the allowed list. If it is and the target object is an actual Geode region (passed as a bind parameter to the query), it proceeds to check whether the user has the required privileges to execute queries on that particular region (DATA:READ:RegionName). The current approach denies everything and only allows some methods to be executed, this list can not be configured or changed in runtime, it’s hard coded and any addition/deletion requires a new release of the productThe list of currently allowed methods is attached below.

Class

Method

Object

toString, equals, compareTo

Boolean

booleanValue

Number

byteValue, intValue, doubleValue, floatValue, longValue, shortValue

Collection, Map

get, entrySet, keySet, values, getEntries, getValues, containsKey

Map.Entry

getKey, getValue

Date

after, before, getNanos, getTime

String

chartAt, codePointAt, codePointBefore, codePointCount, compareToIgnoreCase, concat, contains, contentEquals, endsWith, equalsIgnoreCase, getBytes, hashCode, indexOf, intern, isEmpty, lastIndexOf, length, matches, offsetByCodePoints, replace, replaceAll, replaceFirst, split, startsWith, substring, toCharArray, toLowerCase, toUpperCase, trim

There is one instance of MethodInvocationAuthorizer per QueryService: Geode creates a new one every time the user requests a QueryService from the Cache instance. Within the initialization:

  • If the SecurityManager is not enabled, or if the flag QueryService.allowUntrustedMethodInvocation is set as true, the MethodInvocationAuthorizer is created as a no-op (no security).
  • If the SecurityManager is enabled and the flag QueryService.allowUntrustedMethodInvocation is set as false (default), the RestrictedMethodInvocationAuthorizer is configured.

The method authorizeMethodInvocation is invoked during the OQL execution through the MethodDispatch and AttributeDescriptor classes. It's important to note that the query engine doesn't have any information about the actual type of the objects while pre processing or parsing the query string (neither can it be obtained before executing the query), the actual check to determine whether a method is allowed or not (authorizeMethodInvocation) is executed for every object traversed by the query while it's processing the intermediate results.

MethodDispatch

The class is used to execute an arbitrary method on a target object while an OQL is being executed, is worth noticing that currently there is no context nor metadata about where the target class comes from.

The class is only used for explicit method invocations. As an example, within the expression user.isEnabled() the method would be isEnabled() and the target object would be the actual runtime type/class for user. The single entry point to this class is the invoke() method, which is called through CompiledOperation.evaluate(ExecutionContext context). This particular class, CompiledOperation, is just a node within the AST tree generated by the OQLParser, and it basically represents a method invocation in OQL.

Continuing with the example and assuming that the OQL contains the user.isEnabled() expression, below is a simplified pseudo code of how this is achieved in runtime:


invoke {

  methodInvocationAuthorizer.authorize("isEnabled", "user")

  return Method.invoke("user")

}


The MethodDispatch class is created based on the method name, the class on which the method will be invoked and the type of its arguments, and it is stored within an internal local cache that never expires. The authorize method, however (and no matter that the current acceptance list is immutable), is called by the MethodDispatch class for every method invocation and every object traversed by the query.

AttributeDescriptor

The class is used to read a specific attribute from a target object while an OQL is being executed, is worth noticing again that currently there is no context nor metadata about where the target class comes from.

The class is used for accessing attributes directly and implicit method invocations. As an example, within the expression user.name the attribute would be name and the target object would be the actual runtime type/class for user. The single entry point to this class is the read(Object target) method, which is basically invoked through CompiledPath.evaluate(ExecutionContext context). This class, CompiledPath, is just a node within the AST tree generated by the OQLParser, and it represents an identifier that follows a “dot” operator.

Assuming that the OQL is looking for user.name, below is a simplified pseudo code of how this is achieved in runtime:


read {

  if "user" instance of PdxInstance

    if "name" is a field directly accessible through the PdxInstance

      read "name" using only the PdxInstance (getRawField, getPdxField, etc)

    else

      deserialize the PDX object and get an instance of "user"

      readReflection

  else

    readReflection

}


readReflection {

  member = getReadMember() // get available access strategy to "name" attribute on "user" instance through reflection

  switch (member):

    case Field: {

      return Field.get("user")

    }

    case Method: {

      methodInvocationAuthorizer.authorize("getName()" / "name()", "user")

      return Method.invoke(“user”)

    }

}


The AttributeDescriptor class stores within an internal local cache that never expires the actual Member read by reflection; the authorize method, however (and no matter that the current acceptance list is immutable), is called by the AttributeDescriptor class for every non public field for which there's a accessor method available on every object traversed by the query.

Solution

Summary

Make the currently internal MethodInvocationAuthorizer interface public and allow users to provide their own, it will hold the entire Method Authorization ImplementationThe QueryService will use an empty implementation of the interface if security is disabled or the flag QueryService.allowUntrustedMethodInvocation is set as true (as it works right now). If security is enabled, the default RestrictedMethodAuthorizer will be set or, if configured, the custom implementation will be used.

Based on the comments added to the original proposal and the feedback gathered through the Geode dev list within the relevant thread, the following implementations should be provided out of the box:

  • RestrictedMethodAuthorizer
  • UnrestrictedMethodAuthorizer
  • RegExMethodAuthorizer
  • JavaBeanAccessorMethodAuthorizer

The RestrictedMethodAuthorizer will be the authorizer used by default out of the box, as it contains the list of methods per object type that are currently considered safe and it also prevents any possible re-introduction of CVE-2017-9795.

The UnrestrictedMethodAuthorizer will basically allow any method execution as long as the target object does not belong to a Geode package or, if it does belong to a geode package, it's considered safe (sub-set of methods already allowed by the RestrictedMethodAuthorizer). The other two authorizers will always delegate to RestrictedMethodAuthorizer and expand the set of allowed methods with whatever the internal implementation is configured to do. The JavaBeanAccessorMethodAuthorizer covers the most common use cases and requires little to no configuration effort. For those use cases not covered, the users can choose to use the RegExMethodAuthorizer, which allows them to configure which methods to allow directly through regex expressions. If none of the above is a good fit for a particular situation, the user ultimately has the option to provide its own implementation of the MethodInvocationAuthorizer interface and do whatever needed in order to allow/deny the execution of particular methods.

All out of the box authorizers will be implemented to prevent security problems but, due to the fact that we can't automatically detect in place modifications nor automatically define the trust boundary, the configurable ones will require extra care regarding the configuration or domain model design on the user side. The following table presents a brief summary of what "threats" (of the ones shown within the Introduction) are fully addressed by each implementation, and which ones might be exploitable depending on how the administrator configures the authorizer (the details will be described in each individual section when applicable, and clear documentation around this should be added to the user guide if we choose to implement these authorizers).

Authorizer \ ThreatReflectionCache AccessRegion AccessEntry Modification
RestrictedMethodAuthorizer(tick)(tick)(tick)(tick)
UnrestrictedMethodAuthorizer
(tick)(tick)(tick)(error)
RegExMethodAuthorizer(tick)(tick)(tick)(error)
JavaBeanAccessorMethodAuthorizer(tick)(tick)(tick)(error)

Implementation Details

This section is just an overview and it contains some ideas of how the proposal could be achieved, no PoC has been done so far so the implementations details might change.


Interface MethodInvocationAuthorizer (Public)

This interface is intended to be implemented by users that want a custom authorization mechanism, and by the out of the box implementations as well. The interface will have only one method and it should return a boolean indicating whether the specified method is allowed to be executed on the target object or not. For those situations on which the authorization can not be determined, a non-checked exception should be thrown.

The authorize method will be called for every traversed object as part of the query execution, so it's extremely important that the implementation is lighting fast.


public interface MethodInvocationAuthorizer  {

  boolean authorize(Method method, Object target);

}


Implementations of this interface must be thread-safe as more than one thread might invoke the method at the same time.

The MethodDispatch class will be modified to remember whether the method was allowed by a previous invocation, preventing further calls to the respective authorizer if it was; this basically means that the authorize method for explicit method invocations will be called once in the lifetime of a Geode member for every new method seen while traversing the objects.

The AttributeDescriptor class will be modified to remember whether the accessor method was allowed by a previous invocation, preventing further calls to the respective authorizer if it was; this basically means that the authorize method for implicit method invocations will be called once in the lifetime of a Geode member for every new accessor method to a non-public field seen while traversing the objects.


Class RestrictedMethodAuthorizer (Public)

The current RestrictedMethodInvocationAuthorizer implementation logic, but made public so applications can delegate to this class and use it as the starting point for providing custom authorizers. It twill be immutable and thread safe, and provide two new public methods (isAllowedGeodeMethod and isPermanentlyForbiddenMethod) that can be used by other classes in order to know whether a particular method on a particular object is already marked as safe or not.

Configuration Options

Create a new QueryServiceConfig element at the CacheConfig level to contain any configuration related to OQL, including the custom MethodInvocationAuthorizer. Even though at the beginning this new configuration element will be used to configure only the MethodInvocationAuthorizer implementation, it’s worth noting that it provides a single configuration entry point for the whole query engine. This basically means that it can also be used in the future to allow further additions and configuration options, even replacing the current system properties used to configure the QueryService with new child elements and/or attributes.

The resulting XML element would look something like the following (the properties are just examples):


<query-service>

 <queryVerbose>true</queryVerbose>

 <allowUntrustedMethodInvocation>false</allowUntrustedMethodInvocation>

 <method-authorizer>

   <class-name>test.Authorizer</class-name>

   <parameter name="allowedMethodsDataBaseUrl">

     <string>jdbc:mysql://myHost/allowedMethodsDatabase</string>

   </parameter>

 </method-authorizer>

</query-service>


This new configuration element and its properties should be stored and retrieved through the cluster configuration service, and must also be modifiable through gfsh commands:

  • alter query-service --allowUntrustedMethodInvocation=false
  • alter query-service --method-authorizer=test.Authorizer{'param':'value'}

Out of the Box Implementations

The code below is shown only as an example, the final implementation might differ.

RestrictedMethodAuthorizer

The public implementation of the current RestrictedMethodInvocationAuthorizer, basically denying everything except a small list of methods already known to be safe.

Advantages

  • No extra configuration needed (plus).
  • Already implemented and tested (plus).
  • No chance of re-introducing CVE-2017-9795 (plus).

Risks / Unknowns / Disadvantages

  • Too restrictive (minus).
  • User can't use methods in queries (minus).
  • User can't use non-public fields in queries as the implicit method invocation is also denied (minus).

UnrestrictedMethodAuthorizer

Allow any method execution as long as the target object does not belong to a Geode package, or does belong but it's marked as safe (Region.get, Region.entrySet, Region.keySet, Region.values, Region.getEntries, Region.getValues, Region.containsKey, Region.getKey and Region.getValue). Some known dangerous methods (like getClass) will also be rejected, no matter whether the target object belongs to a Geode package or not. The implementation will be immutable and thread-safe.

When using the methodAuthorizer, users with DATA:READ:RegionName privileges will be able to execute ANY method (even those modifying the entry) on the objects stored within the region and on instances used as bind parameters of the query, so it must be used with extreme care and only in cases on which only trusted users and applications have access to the cluster.


@Override

public boolean authorize(Method method, Object target) throws NotAuthorizedException {

  String packageName = target.getClass().getPackage().getName().toLowerCase();


  if (restrictedAuthorizer.isPermanentlyForbiddenMethod(method, target) {

    return false;

  }


  if (!packageName.startsWith("org.apache.geode")) {

    return true;

  }


  return restrictedAuthorizer.isAllowedGeodeMethod(method, target);

}

Advantages

  • Easy to use (plus).
  • No extra configuration needed (plus).
  • Implicit and Explicit methods can be executed on objects stored within the regions (plus).

Risks / Unknowns / Disadvantages

  • In place modifications are allowed (minus).
  • Users with DATA:READ:RegionName privileges can modify the entries within the region through methods that mutate the object, thus part of CVE-2017-9795 can be re-introduced (minus).

RegExMethodAuthorizer

Methods allowed to be executed should match some regex expression(s) configured by the user, similar to what we currently do today with the PDX ReflectionBasedAutoSerializer. The implementation will have an internal structure containing already compiled Pattern instances and use them to verify the actual method that should be executed by the OQL engine, denying or allowing the execution based on the match result. If there is no match, it will delegate the final decision to the RestrictedMethodAuthorizer. The implementation will be immutable and thread-safe.

When using the methodAuthorizer, if the regular expression is not restrictive enough, users with DATA:READ:RegionName privileges will be able to execute methods (even those modifying the entry) on the objects stored within the region and on instances used as bind parameters of the query, so this authorizer must be used with extreme care.


@Override

public boolean authorize(Method method, Object target) throws NotAuthorizedException {

  boolean matches = false;

  String methodName = target.getClass().getName() + "." + method.getName();


  if (restrictedAuthorizer.isPermanentlyForbiddenMethod(method, target) {

    return false;

  }


  Iterator<Pattern> iterator = this.patternsCache.iterator();

  while (iterator.hasNext() && !matches) {

    matches = iterator.next().matcher(methodName).matches();

  }


  if (!matches) {

    matches = restrictedAuthorizer.authorize(method, target);

  }


  return matches;

}

Advantages

  • Easy to use and configure what to allow/deny (plus).
  • Regular expressions are standard, everyone “should know” how to use them (plus).

Risks / Unknowns / Disadvantages

  • Performance impact (minus).
  • Customers still need to configure “something” (the regex) (minus).
  • Customers need to learn regex expressions, if they don't do already (minus).
  • Operators with little Regex knowledge can accidentally allow everything depending on which wildcards are used and, thus, reintroduce part of CVE-2017-9795 (minus).

JavaBeanAccessorMethodAuthorizer

Allow the OQL engine to execute any method that follows the design patterns for accessor methods described in the JavaBean specification 1.01; that is, basically, allow any method starting with get or is. For extra security, only methods belonging to classes under certain packages (configured by the user) should be allowed, and some known dangerous methods (like getClass) should be disabled. If there is no match, it will delegate the final decision to the RestrictedMethodAuthorizer. The implementation will be immutable and thread-safe.

This methodAuthorizer is fully based on the design patterns described in JavaBean specification 1.01 and, by definition, in the fact that accessor methods don't modify the instance's state. If an accessor method follows the naming conventions described in the JavaBean specification 1.01 but actually change the instance's state, users with with DATA:READ:RegionName privileges will be able to make in place modifications, so this methodAuthorizer must not be used in those scenarios.

@Override

public boolean authorize(Method method, Object target) throws NotAuthorizedException {

  boolean matches = false;

  String methodName = method.getName().toLowerCase();

  String packageName = target.getClass().getPackage().getName().toLowerCase();


  if (restrictedAuthorizer.isPermanentlyForbiddenMethod(method, target) {

    return false;

  }


  if ((methodName.startsWith("get") || methodName.startsWith("is"))) {

    Iterator<String> iterator = this.packagesCache.iterator();

    while (iterator.hasNext() && !matches) {

      matches = iterator.next().startsWith(packageName);

    }

  }


  if (!matches) {

    matches = restrictedAuthorizer.authorize(method, target);

  }


  return matches;

}

Advantages

  • No major changes needed (plus).
  • Solves the general use case: most customers use get*/is* as the name for accessor methods and the configurable package restricts access only to methods from the domain model (plus).

Risks / Unknowns / Disadvantages

  • Customers need to configure “something” (the package) (minus).

  • There might be some safe accessor methods that do not start with get*/is* (minus).

  • Not every method starting with get*/is* might be safe to invoke and, thus, this authorizer can reintroduce part of CVE-2017-9795 (minus).

Examples

This section contains some examples showing how the feature should work, once it’s implemented, for different use cases. All examples assume that the cluster is already up and running with security enabled and that the current default method authorizer is configured (nothing is allowed). For the sake of simplicity, let’s also assume that the domain model is entirely contained within packages "order.model" and "tickets.model".

Happy Path

# The cluster is secured, only in-house developed apps execute queries and operators/developers know which methods can be invoked or not.

# Customer deploys the jar and configures the UnrestrictedMethodAuthorizer

$> deploy --jar=/tmp/model-1.0.0.jar

$> alter query-service --method-authorizer=UnrestrictedMethodAuthorizer


# All classes follow the JavaBean specification 1.01 for accessor methods

# Customer deploys the jar and configures the JavaBeanAccessorMethodAuthorizer

$> deploy --jar=/tmp/model-1.0.0.jar

$> alter query-service --method-authorizer=JavaBeanAccessorMethodAuthorizer{'packages' : 'order.model,tickets.model'}

Change Method Authorizer in Runtime

# All classes follow the JavaBean specification 1.01 for accessor methods

# Customer deploys the jar and configures the JavaBeanAccessorMethodAuthorizer

$> deploy --jar=/tmp/model-1.0.0.jar

$> alter query-service --method-authorizer=JavaBeanAccessorMethodAuthorizer{'packages' : 'order.model,tickets.model'}


# Customer realizes that one developer did not follow the specification and multiple "calculateXXXX" methods exist in several classes

# These methods need to be accessed through OQL right away in production, so they configure RegExMethodAuthorizer with the required regex to allow default java bean accessors (get*|is*) + methods starting with "calculate*"

alter query-service --method-authorizer=RegExMethodAuthorizer{'patterns' : 'model.*(get|is|calculate)'}


# After removing all "calculateXXXX" methods the customer deploys the new model and re-configures the JavaBeanAccessorMethodAuthorizer

deploy --jar=/tmp/model-2.0.0.jar

alter query-service --method-authorizer=JavaBeanAccessorMethodAuthorizer{'packages' : 'order.model,tickets.model'}

Custom Method Authorizer with Hot Deploy

# Out of the box implementations are not suitable for the use case

# A method needs to be allowed or denied based on runtime configuration

# Customer develops a custom method authorizer that gets the list of allowed methods from an external environment variable.

public class EnvironmentAwareAuthorizer implements MethodInvocationAuthorizer {

  private final static String METHODS_ALLOWED = "myEnvironmentVariable";

  private final boolean configured;

  private Map<String, List<String>> acceptList;


  private void parseEnvironment(String variableName) {

    // Parse JSON environment variable and populate internal map.

  }


  public EnvironmentAwareAuthorizer() {

    try {

      parseEnvironment(METHODS_ALLOWED);

      configured = true;

    } catch (Exception parseException) {

      configured = false;

    }

  }


  @Override

  public boolean authorize(Method method, Object target) throws NotAuthorizedException {

    if (!configured) {

      throw new NotAuthorizedException("Configuration not loaded.");

    }


    String typeName = target.getName().toLowerCase();

    String methodName = method.getName().toLowerCase();

    List<String> methodsAllowedPerType = acceptList.get(typeName);


    if ((methodsAllowedPerType != null) && (methodsAllowedPerType.contains(methodName))) {

      return true;

    }


    return false;

  }

}


# Customer packages the custom implementation + data-model, deploys the single JAR file and configures the custom authorizer

$> deploy --jar=/tmp/library-1.0.0.jar

$> alter query-service --method-authorizer=io.company.EnvironmentAwareAuthorizer

Discarded Options

The following approaches were discarded for several reasons, they’re included here only for historic purposes.

PDXBasedMethodAuthorizer

We can’t invoke actual methods on objects stored as PdxInstance without deserializing them, that’s a fact, BUT we could have a Method Authorization Implementation that automatically trusts all methods invoked on objects that were deserialized from a PdxInstance.

Pros and Cons

  • Easy to implement (plus).
  • No major changes needed (plus).
  • Not every method on the object might be safe to execute (minus).
  • Customer needs to configure PDX AND also deploy the domain model (minus).

DataAwareBasedMethodAuthorizer

Allow the OQL engine to execute any method on any instance that is part of the object hierarchy inserted into the Geode Region. The idea is, basically, to allow everything as long as it has been inserted into the region by an authenticated user. It's the responsibility of the user to train operators and developers to not execute dangerous methods or mutators on their own objects (if any). Some known dangerous methods (like getClass) should be disabled by default, however.

Pros and Cons

  • No extra configuration needed (plus).

  • Solves the general use case: deploy the domain model, start executing OQL and invoking methods without further changes (other than setting this authorizer through configuration) (plus).

  • How would it work for method invocation chains, like user.getAddress().getZipCode().getId()(minus).

  • How to get extra metadata about the object on which the method will be executed?. With the current implementation is not possible (minus).

  • We could be invoking methods on an object in lots of places in the query tree, is it even possible to always have the region during the authorization?. (minus).
  • Java Reflection is already expensive, going up through the object hierarchy to find out whether the object is part of the region or not will definitely and negatively impact performance (minus).

AnnotationBasedMethodAuthorizer

Add a new annotation to Geode (@Authorized, @OQLMethod, other?) so users can annotate within their domain model which methods are safe to invoke during OQL execution. In order to decide whether the method is allowed or denied, the implementation will need to check whether the method is annotated with the marker annotation or not.

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@interface Authorized {

}


@Override

public boolean authorize(Method method, Object target) throws NotAuthorizedException {

  return (method.isAnnotationPresent(Authorized.class));

}

Pros and Cons

  • Easy to use and implement (plus).
  • Annotations are becoming the standard, developers are comfortable with them (plus).
  • No extra configuration needed: the deployed domain model contains what we need in runtime (plus).
  • The domain model classes must be present on the server’s classpath (minus).
  • An unnecessary level of coupling is added between the user’s code and Geode (minus).
  • Code is not code anymore, there is embedded configuration in the domain model (minus).

ConfigurableAcceptListMethodAuthorizer

Keep the Method Authorization Implementation details internal and only provide support to configure the list of allowed methods to the users. The approach basically implies keeping the actual behaviour and RestrictedMethodInvocationAuthorizer implementation, but improving the class to make it "mutable" in terms of configuration so users can add or remove custom methods to the list of allowed methods in runtime. The methods actually allowed should match exactly the ones to be executed by OQL, any difference will result in the method execution to be rejected by the query engine.

Pros and Cons

  • No major changes needed (plus).
  • Easy to understand and configure (plus).
  • Cumbersome Configuration (minus).
    • Becomes a nightmare for huge data models.
    • User needs to add methods to the accepted list one by one, per class.
    • Maintenance complexity when the amount of methods to allow increases.

ResourcePermissionBasedMethodAuthorizer

Allow any method invocation for users that have the DATA:QUERY:RegionName role. This approach basically allows the operator/developer to decide which users have rights to execute any method for queries on a particular region (DATA:READ:RegionName already allows OQL execution, DATA:QUERY:RegionName will also allow method execution as part of the OQL on that particular region).

Pros and Cons

  • Easy to implement (plus).
  • No extra configuration or changes needed (plus).
  • Addition of new resource permission DATA:QUERY:RegionName (minus).
  • Confusing. Multiple roles are required for “the same” OQL execution operation (minus).

Prior Art

There are some existing frameworks/solutions that might accomplish the same as this proposal. However, we believe that those solutions are inferior for the reasons below.

Spring Method Security & Shiro Annotation-based Authorization

Both Spring Method Security and Shiro Annotation-based Authorization allow the user to annotate the classes in order to explicitly configure which roles/permissions are required to execute the relevant method, similar to what this proposal tries to accomplish through the discarded AnnotationBasedMethodAuthorizer. Annotations are really popular within the Java world and these approaches are extremely powerful and configurable.

The primary problem with these solutions is that they force the user to modify the domain model and, also, add extra unnecessary coupling. With this proposal, anyway, the user can ultimately use these frameworks by just providing their own authorizer implementation and check the annotation in order to allow/deny the method execution.

Errata

  1. The contract for the interface MethodInvocationAuthorizer won't include a throws clause for the NotAuthorizedException class, that exception was designed to indicate that the subject is not allowed to execute a particular operation, not to indicate that a problem has occurred and that the authorization can not be determined. Since Geode can't do anything to recover from such errors and doesn't have any insights about the actual implementation, a non checked exception should be thrown whenever there's an error while executing the authorization logic.
  2. Authorizer Implementations won't have Based as part of the actual name since the word doesn't add anything useful to the class name.
  3. Class name for GeodeBasedMethodAuthorizer was changed to MethodUnrestrictedMethodAuthorizer.
  4. Method name isKnownDangerousMethod was changed to isPermanentlyForbiddenMethod.
  5. The IndexManager was modified to throw an exception and mark the index as invalid whenever the removal of an entry from an index fails. This was the behaviour used when adding mappings to an index, so the class was fixed to keep consistency between the different operations and to be able to mark existing indexes as invalid whenever a newly configured MethodInvocationAuthorizer doesn't allow the method invocations included within the index expression (see GEODE-7486 and GEODE-7351).
  6. The CQ Engine was modified to always use the most up to date configured MethodInvocationAuthorizer. Whenever the MethodInvocationAuthorizer is changed in runtime, all running CQs are updated to use it in order to avoid security issues and previously cached results are invalidated/cleared as cached keys may not be valid anymore (see GEODE-7487GEODE-7497 and GEODE-7351).
  • No labels