Since I need to implement an authorization layer, and move past the current uncertainty surrounding authorization and authentication in Java, I think I'll start small and completely independent and learn from history.

Requirements:

1. Ability to perform authorization checks on code and Subjects.
2. Compatible with all current Java LTS releases.
3. Will need to use Java API's, nothing platform specific.

Uncertainty:

1. New JAAS API's are unknown, will they be suitable for our
   application? Unclear.
2. Will hooks be provided in OpenJDK for Guard checks or not? Unclear.
3. Lack of support for new features with existing API's going forward,
   eg virtual threads.
4. How to authenticate TLS and Kerberos connections without using
   deprecated API's?
5. Avoid other API's that may be removed in future due to
   under-utilisation.

What can we do with what we know, how can we do it better?

1. Create a new Authorization.class with methods that return Callable
   objects that can be submitted to Executors, including virtual
   threads (assuming virtual threads support StackWalker).
2. Methods:
     * Callable<T> privilegedCall(Callable<T> c) // Always preserves
       subject, if any.
     * Callable<T> privilegedCall(Subject s, Callable<T> c) // Uses
       provided Subject, from LoginModule, or executing in the context
       of an authenticated client over a secure connection.
     * Callable<T> privilegedCall(AuthorizationContext ac, Callable<T>
       c) // Uses provided AuthorizationContext combined with context
       of the privileged caller.
     * AuthorizationContext  getContext(); // Returns an optimized
       context (combines the inherited and calculated contexts, injects
       Principal[]'s from the Subject into each domain. Stored for
       later use when making privilegedCall's eg for TLS connection.

3. The Authorization.class implementation will enure that the inherited
   context is stored in a ThreadLocal variable which is restored to
   it's original value using a try finally block to ensure the
   inherited context is only present during the wrapped Callable's
   call.   It's possible that privilegedCall's are nested in one
   thread, in which case the ThreadLocal value will be changed after
   each privilegedCall to the outer calls context, until it is set to
   null, by the outermost privilegedCall.
4. Callable returned can be submitted to an Executor, eg as a
   privileged task.
5. Create a new AuthorizationContext class

     * Encapsulates the current Subject
     * Contains a snapshot of the ProtectionDomains when the Callable
       was passed to privilegedCall and includes the domain of the
       class that called the Authorization.privileged method as well as
       any domains of Callable implementation parameter classes, this
       is the inherited context.
     * Methods:
         o Subject getSubject() // For secure connections.
         o void checkEach(Consumer<ProtectionDomain> consumer) throws
           AuthorizationException // Consumer::andThen allows for
           debugging information to be printed to error, such as print
           the privileges of a domain, or to record the required
           privileges of each domain, without throwing a
           SecurityException (when recording allowed operations).
           AuthorizationContext implementation determines how to
           execute.  No mutable shared state.

4. Use Agents to instrument Java Public API until hooks are provided by
   OpenJDK.
     * *ONLY* target LTS releases to minimize API analysis required.
     * Use static analysis to located methods in Java API's that return
       sensitive classes, eg ClassLoader, ProtectionDomain.
     * We could use some hooks here OpenJDK?
5. How to capture domains and privileged scope?

     * It is not possible to inherit a call stack from a previous
       thread, so either the thread executes only platform code and is
       checked, assuming the platform code can be trusted to not allow
       sensitive object references to escape, or if application code is
       present, then it is unprivileged unless a privilegedCall is
       made.  It will be known by the presence of a ThreadLocal
       inherited context whether a privilegedCall is being executed.
     * The stack context is only captured after a privilegedCall is
       made on a Thread's call stack, with the exception of a call
       stack that contains *only* platform code.
     * If an inherited ThreadLocal AuthorizationContext isn't present,
       when checked, then an unprivileged domain will represent the
       entire stack, with the following exception:
         o Unless all code on the thread stack is platform code, in
           this case, capture all domains on the call stack since the
           thread started.   This is a code only check, as no Subject
           will be present.
         o For bootloader system code construct ProtectionDomain, with
           CodeSource URL[] containing a jrt:/$MODULE , if Module
           isNamed(), retrieved from Class.getModule().
         o Authorization agents will be considered platform code.
     * At the time Authorization.getContext() is called, the inherited
       context will be combined with the stack walk from the current
       thread and the all domains will include the Subject's principals.

6. GuardFactory and GuardFactorySpi, Authorization agents will need to
   call GuardFactory, to obtain Guard instances, the Guard
   implementation can call:
     * Authorization::getContext()checkEach(domain -> doSomething(domain));

--
Regards,
Peter Firmstone

On 26/06/2021 10:05 pm, Peter Firmstone wrote:


On 26/06/2021 3:41 pm, Peter Firmstone wrote:

Apologies for multiple earlier emails, please ignore and read this instead.

This proposal is about stripping out and simplifying as much of the dilapidated and complex SecurityManager infrastructure as possible, while retaining the ability for developers to implement a better high scaling and performant Authorization layer, without prohibitively preventing it.

Summary of Proposed Changes:

 1. GuardFactory & GuardFactorySpi to provide hooks for authorization
    checks without SecurityManager or Policy. (Note GuardFactory
    should never return null and instead return a no-op Guard that
    hotspot can optimize out.
 2. Existing Permission implementations to be obtained using
    GuardFactorySpi implementations, until their removal.  Note that
    when SecurityManager is stubbed out and Permission
    implementations are deprecated for removal, these should no
    longer be provided by default, but instead need to be enabled by
    entries in the java.security config file, in preparation for
    their removal.
 3. JDK code to no longer call Permission implementations directly,
    instances obtained using GuardFactory, when enabled in the
    java.security configuration file.
 4. Threads (system and virtual) updated to use a singleton
    *unprivileged* AccessControlContext, instead of inherited
    AccessControlContext, this is more appropriate for Executors, the
    original inherited context was designed before Executors were
    introduced.
 5. Deprecation for removal of all Permission implementations from
    the JDK platform.   The existing implementations of Permission
    introduce unnecessary complexity; they lack sufficient
    flexibility resulting in a proliferation of Permission grants
    required in policy files and some make blocking network calls.
 6. Introduce a system property to change AccessController's default
    behaviour, disable the stack walk by default, but allow it to be
    re-enabled with a system property, replace the stack walk array
    result of ProtectionDomains with an *unprivileged*
    AccessControlContext, the SubjectDomainCombiner can replace it
    with a, AccessControlContext containing a single element array,
    containing one ProtectionDomain with Principals.
 7. AccessController::doPrivileged erases the DomainCombiner by
    default, deprecate these methods for removal (make private),
    retain doPrivilegedWithCombiner methods that preserve the
    SubjectDomainCombiner.   Developers should replace their
    doPrivileged methods with doPrivilegedWithCombiner.   Create a
    new method AccessController::doUnprivileged, clear intent, to
    erase the DomainCombiner, and use the *unprivileged*
    AccessControlContext.  Update
    AccessController.AccHolder.innocuousAcc to refer to an
    *unprivileged* context, as per the definition below.
 8. Deprecate for removal the CodeSource::implies method.
 9. Give unique ProtectionDomain's with a meaninful CodeSource to
    Java modules mapped to the boot loader, rather than using a
    Shared ProtectionDomain with a null CodeSource.
10. Deprecate for removal AccessController::checkPermission and
    AccessControlContext::checkPermission methods.

AccessController.checkPermission calls AccessControlContext.optimize, which invokes the DomainCombiner, prior to calling AccessControlContext.checkPermission

In my implementation of SecurityManager, I call AccessController.getContext from within a PrivilegedAction, to optimise a newly created AccessControlContext, (AccessController.getContext also calls AcessControlConext.optimize), prior to calling AccessControlContext.checkPermission.

I think it would be simpler however, to create a new method in AccessController to replace checkPermission which also calls optimize.

I think something could be done here with Stream and Consumer to perform the function checking ProtectionDomain's.  Needs a little more thought, but basically we want to be able to check each ProtectionDomain, without being restricted to Permission or Policy implementations.

Eg:

AccessController.stream(AccessControlContext context).forEach(domain -> Check::domain)

Or

AccessController.checkDomains(AccessControlContext context, Consumer<ProtectionDomain>)

This method would have a relevant Guard.check "RUNTIME" "getProtectionDomain" prior to calling AccessControlContext.optimize and the developer would need to make the call from a PrivilegedAction, and remember pass the relevant guard check for it's own AccessControlContext.

11. Undeprecate AccessController, AccessControlContext,
    DomainCombiner, SubjectDomainCombiner and Subject::doAs methods,
    while deprecating for removal methods stated in items above.
12. Deprecate for removal ProtectionDomain::implies,
    ProtectionDomain::getPermissions and
    ProtectionDomain::staticPermissionsOnly
13. Replace PermissionCollection type argument with Object in
    ProtectionDomain constructors, ignore the permissions parameter,
    and deprecate existing constructors.   Deprecate
    PermissionCollection for removal.
14. Create a new constructor: ProtectionDomain(CodeSource cs,
    ClassLoader cl, Principal[] p).
15. Create a new factory method
    ProtectionDomain::newInstance(Principal[] p), to allow a weak
    cache of ProtectionDomain instances for each Principal[], to be
    utilised by SubjectDomainCombiner to avoid unnecessary
    duplication of objects.   This is an optimization for
    AccessControlContext::equals and ::hashCode methods.   Using a
    cache of AccessControlContext, it is possible to avoid rechecking
    authorization that has already been checked.  For example, when
    using an Executor with many tasks, all with the same
    AccessControlContext, you only need to check once and return the
    same result for subsequent checks.   This is an optimization I
    have used previously to great effect.

The ProtectionDomain::newInstance is just a nice to have, SubjectDomainCombiner already caches PD's, just seems a better place to cache for the following reasons:

  * Cache can be utilised by other implementations.
  * Simplification of SubjectDomainCombiner.
  * Responsibility of ProtectionDomain.

It's not an essential item, however, just seems like an opportunity for a little refactoring.

To clarify what an *unprivileged* AccessControlContext is:

    An instance of AccessControlContext, that contains a single
    element array, containing a ProtectionDomain, with a null
    ClassLoader, null Principal[] and a *non-null* CodeSource,
    containing a null URL.

    So as to distinguish between what is traditionally a JDK
    bootstrap ProtectionDomain and unprivileged domain after
    ProtectionDomain::getPermissions is removed.

Stubbing of SecurityManager and Policy, for runtime backward compatibility. Update ProtectionDomain::implies method, to *not* consult with the Policy.  Note it's possible to get access to the ProtectionDomain array contained within AccessControlContext using a DomainCombiner.

This is backward compatible with existing usages of JAAS and least painful method of transition for all concerned as well as allowing complete flexibility of implementation.

Regards,

Peter Firmstone.

On 25/06/2021 3:59 pm, Peter Firmstone wrote:
Thanks Dalibor,

Would targeting Java 18 be practical?

Also it won't take long to code a prototype, just not sure of the process.

Cheers,

Peter.


On 24/06/2021 9:30 pm, Dalibor Topic wrote:
On 24.06.2021 04:24, Peter Firmstone wrote:
Thanks Andrew,

For the simple case, of replacing the SecurityManager stack walk, one could use reflection.

Thank you for also confirming that is not possible (or at least very unlikely) to add a GuardBuilder to Java 8, the proposal is for JDK code to use a provider mechanism, to intercept permission checks, so custom authentication layers can be implemented, this could be accepted in future versions of Java, but not existing. As it is said, there is no harm in asking.

Generally speaking, adding any public APIs to a platform release after its specification has been published, is always going to be a very tall order involving the JCP, among other things. It's not really worth it, when other technical solutions, such as multi-release JARs, already exist, that alleviate the necessity.

cheers,
dalibor topic

--
Regards,
Peter Firmstone

Reply via email to