Authorization layer API and low level access checks.
Peter Firmstone
peter.firmstone at zeus.net.au
Wed Jun 30 11:38:06 UTC 2021
A draft Authorization implementation, untested.
--
Regards,
Peter Firmstone
/**
* Authorization class, instances contain the domains and Subject of the
* Authorization context, used for Authorization decisions by Guard
* implementations. Provides static utility methods to make
privilgedCall's
* and record the current context.
*
* @author peter
*/
public final class Authorization {
private static final ProtectionDomain MY_DOMAIN =
Authorization.class.getProtectionDomain();
private static final Authorization PRIVILEGED =
new Authorization(new ProtectionDomain []{ MY_DOMAIN });
private static final Authorization UNPRIVILEGED
= new Authorization(
new ProtectionDomain[]{
new ProtectionDomain(
new CodeSource(null, (Certificate [])null), null
)
}
);
private static final ThreadLocal<Authorization> INHERITED_CONTEXT
= new ThreadLocal();
private static final Guard GUARD_REGISTER_CHECK =
GuardBuilder.getInstance("RUNTIME").get("registerGuard",
(String) null);
private static final Guard GUARD_SUBJECT =
GuardBuilder.getInstance("AUTH").get("getSubjectFromAuthorization", null);
private static final Set<Class<? extends Guard>> GUARDS =
RC.set(Collections.newSetFromMap(new
ConcurrentHashMap<>()), Ref.WEAK, 0);
/**
* Elevates the privileges of the Callable to those granted to the
Subject
* and ProtectionDomain's of the Callable and it's call stack,
including the
* ProtectionDomain of the caller of this method.
*
* @param <V>
* @param c
* @return
*/
public static <V> Callable<V> privilegedCall(Callable<V> c){
Authorization auth = INHERITED_CONTEXT.get();
try {
INHERITED_CONTEXT.set(PRIVILEGED);
if (auth != null){
return privilegedCall(auth.getSubject(), c);
} else {
return new CallableWrapper<>(new
Authorization(captureCallerDomain(null), null), c);
}
} finally {
INHERITED_CONTEXT.set(auth);
}
}
/**
* Elevates the privileges of the Callable to those granted to the
Subject
* and ProtectionDomain's of the Callable and it's call stack,
including the
* ProtectionDomain of the caller of this method.
*
* @param <V>
* @param subject
* @param c
* @return
*/
public static <V> Callable<V> privilegedCall(Subject subject,
Callable<V> c){
Authorization authorization = INHERITED_CONTEXT.get();
try {
INHERITED_CONTEXT.set(PRIVILEGED);
Set<Principal> p = subject != null ?
subject.getPrincipals() : null;
Principal [] principals = p != null ? p.toArray(new
Principal[p.size()]) : null;
return new CallableWrapper<>(new
Authorization(captureCallerDomain(principals), subject), c);
} finally {
INHERITED_CONTEXT.set(authorization);
}
}
/**
* Elevates the privileges of the Callable to those granted to the
Subject
* and ProtectionDomain's of the Callable and it's call stack,
including the
* ProtectionDomain of the caller of this method and the Authorization
* context provided.
*
* @param <V>
* @param ac
* @param c
* @return
*/
public static <V> Callable<V> privilegedCall(Authorization ac,
Callable<V> c){
if (c == null) throw new IllegalArgumentException("Callable
cannot be null");
if (ac != null){
Authorization authorization = INHERITED_CONTEXT.get();
try {
INHERITED_CONTEXT.set(PRIVILEGED);
Subject subject = ac.getSubject();
Set<Principal> p = subject != null ?
subject.getPrincipals() : null;
Principal [] principals = p != null ? p.toArray(new
Principal[p.size()]) : null;
Set<ProtectionDomain> domains =
captureCallerDomain(principals);
ac.checkEach((ProtectionDomain t) -> {
if (MY_DOMAIN.equals(t)) return;
if (principals != null){
domains.add(
new ProtectionDomainKey(t, principals)
);
} else {
domains.add(new ProtectionDomainKey(t));
}
});
Authorization auth = new Authorization(domains, subject);
return new CallableWrapper<>(auth, c);
} finally {
INHERITED_CONTEXT.set(authorization);
}
} else {
return privilegedCall(c);
}
}
private static Set<ProtectionDomain> captureCallerDomain(Principal
[] principals){
Set<Option> options = new HashSet<>();
options.add(Option.RETAIN_CLASS_REFERENCE);
StackWalker walker = StackWalker.getInstance(options);
List<StackFrame> frames = walker.walk(s ->
s.dropWhile(f ->
f.getClassName().equals(Authorization.class.getName()))
.limit(1L) // Grab the caller who called privilegedCall.
.collect(Collectors.toList()));
Set<ProtectionDomain> domains = new HashSet<>();
Iterator<StackFrame> it = frames.iterator();
while (it.hasNext()){
ProtectionDomain t =
it.next().getDeclaringClass().getProtectionDomain();
if (MY_DOMAIN.equals(t)) continue;
if (principals != null){
domains.add(new ProtectionDomainKey(t, principals));
} else {
domains.add(new ProtectionDomainKey(t));
}
}
return domains;
}
/**
* Avoids stack walk, returns an Authorization containing a
ProtectionDomain
* with the Principal [] of the current Subject, if any. The
CodeSource
* of this domain contains a <code>null</code> URL. If there is no
current
* Subject, this domain will be unprivileged.
*
* @return
*/
public static Authorization getSubjectAuthorization(){
Authorization inherited = INHERITED_CONTEXT.get();
if (inherited == null) return UNPRIVILEGED;
try {
INHERITED_CONTEXT.set(PRIVILEGED);
Subject subject = inherited.getSubject();
Set<Principal> p = subject != null ?
subject.getPrincipals() : null;
Principal [] principals = p != null ? p.toArray(new
Principal[p.size()]) : null;
Set<ProtectionDomain> domains = new HashSet<>(1);
domains.add(
new ProtectionDomainKey(
new CodeSource(null, (Certificate[]) null),
null,
null,
principals
)
);
return new Authorization(domains, subject);
} finally {
INHERITED_CONTEXT.set(inherited);
}
}
/**
* Performs a stack walk to obtain all domains since the {@link
Callable#call() }
* method was made, includes the domain of the caller of any of the
three
* {@link #privilegedCall(javax.security.auth.Subject,
java.util.concurrent.Callable)
* methods as well as the {@link Subject}. All domains on the
stack contain the
* {@link Principal} of the Subject.
*
* If a privilegedCall wasn't made, then an unprivileged Authorization
* instance is returned.
*
* @return
*/
public static Authorization getAuthorization(){
// Optimise, avoid stack walk if UNPRIVILEGED.
Authorization inherited = INHERITED_CONTEXT.get();
if (inherited == null) return UNPRIVILEGED;
try {
INHERITED_CONTEXT.set(PRIVILEGED);
Subject subject = inherited.getSubject();
Set<Principal> p = subject != null ?
subject.getPrincipals() : null;
Principal [] principals = p != null ? p.toArray(new
Principal[p.size()]) : null;
Set<Option> options = new HashSet<>();
options.add(Option.RETAIN_CLASS_REFERENCE);
StackWalker walker = StackWalker.getInstance(options);
List<StackFrame> frames = walker.walk(s ->
s.skip(1) //Skips getAuthorization()
.takeWhile(f ->
!f.getClassName().equals(CallableWrapper.class.getName())))
.collect(Collectors.toList());
Set<ProtectionDomain> domains = new HashSet<>(frames.size());
inherited.checkEach((ProtectionDomain t) -> {
if (MY_DOMAIN.equals(t)) return;
if (principals != null){
domains.add(new ProtectionDomainKey(t, principals));
} else {
domains.add(new ProtectionDomainKey(t));
}
});
Iterator<StackFrame> it = frames.iterator();
while (it.hasNext()){
Class declaringClass = it.next().getDeclaringClass();
ProtectionDomain t = declaringClass.getProtectionDomain();
if (MY_DOMAIN.equals(t)) continue;
CodeSource cs = t.getCodeSource();
if (cs == null){ // Bootstrap ClassLoader?
Module module = declaringClass.getModule();
if (module.isNamed()){
try {
cs = new CodeSource( new URL("jrt:/" +
module.getName()), (Certificate[]) null);
} catch (MalformedURLException ex) {
Logger.getLogger(Authorization.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
if (principals != null){
domains.add(new ProtectionDomainKey(cs,
t.getPermissions(), t.getClassLoader(), principals));
} else {
domains.add(new ProtectionDomainKey(cs,
t.getPermissions(), t.getClassLoader(), t.getPrincipals()));
}
}
return new Authorization(domains, subject);
} finally {
INHERITED_CONTEXT.set(inherited);
}
}
/**
* Register the calling Class type for a Guard implementation.
*
* Prior to calling {@link
Authorization#checkEach(java.util.function.Consumer)
* a guard must register, this should be during initialization the
ProtectionDomain
* of the guard will be checked. This should occur prior to
*
*
* @param guardClass
*/
public static void registerGuard(Class<? extends Guard> guardClass){
GUARD_REGISTER_CHECK.checkGuard(guardClass);
GUARDS.add(guardClass);
}
private final Set<ProtectionDomain> context;
private final Subject subject;
private final int hashCode;
private Authorization(Set<ProtectionDomain> context, Subject s) {
this.context = context;
this.subject = s;
int hash = 7;
hash = 11 * hash + Objects.hashCode(context);
hash = 11 * hash + Objects.hashCode(s);
this.hashCode = hash;
}
private Authorization(ProtectionDomain [] context){
this(new HashSet<ProtectionDomain>(Arrays.asList(context)), null);
}
public Subject getSubject(){
if (!PRIVILEGED.equals(INHERITED_CONTEXT.get()))
GUARD_SUBJECT.checkGuard(null);
return subject;
}
/**
*
* @param consumer
* @throws AuthorizationException
*/
public void checkEach(Consumer<ProtectionDomain> consumer) throws
AuthorizationException {
Authorization authorization = INHERITED_CONTEXT.get();
if (UNPRIVILEGED.equals(authorization)) throw new
AuthorizationException("A privilegedCall is required to enable
privileges.");
if (PRIVILEGED.equals(authorization)) return; // Avoids
circular checks.
try {
INHERITED_CONTEXT.set(PRIVILEGED);
Set<Option> options = new HashSet<>();
options.add(Option.RETAIN_CLASS_REFERENCE);
StackWalker walker = StackWalker.getInstance(options);
List<StackFrame> frames = walker.walk(s ->
s.dropWhile(f ->
f.getClassName().equals(Authorization.class.getName()))
.limit(1L) // Grab the caller who called privilegedCall.
.collect(Collectors.toList()));
frames.stream().forEach((StackFrame t) -> {
Class cl = t.getDeclaringClass();
if (!Guard.class.isAssignableFrom(cl) ||
!GUARDS.contains(cl)){
throw new AuthorizationException("Guard not
registered: " + cl.getCanonicalName());
}
});
} finally {
INHERITED_CONTEXT.set(authorization);
}
// The actual check for privileged code.
context.stream().forEach(consumer);
}
@Override
public boolean equals(Object o){
if (this == o) return true;
if (!(o instanceof Authorization)) return false;
Authorization that = (Authorization) o;
if (!this.subject.equals(that.subject)) return false;
return this.context.equals(that.context);
}
@Override
public int hashCode() {
return hashCode;
}
private static class CallableWrapper<V> implements Callable<V> {
private final Authorization authorization;
private final Callable<V> callable;
CallableWrapper(Authorization a, Callable<V> c){
this.authorization = a;
this.callable = c;
}
@Override
public V call() throws Exception {
Authorization existingContext = INHERITED_CONTEXT.get();
INHERITED_CONTEXT.set(authorization);
try {
return callable.call();
} finally {
INHERITED_CONTEXT.set(existingContext);
}
}
}
/**
* ProtectionDomainKey identity .
*/
private static class ProtectionDomainKey extends ProtectionDomain{
private static UriCodeSource getCodeSource(CodeSource cs){
if (cs != null) return new UriCodeSource(cs);
return null;
}
private final CodeSource codeSource;
private final Principal[] princiPals;
private final int hashCode;
ProtectionDomainKey(ProtectionDomain pd){
this(getCodeSource(pd.getCodeSource()),
pd.getPermissions(), pd.getClassLoader(), pd.getPrincipals());
}
ProtectionDomainKey(ProtectionDomain pd, Principal [] p) {
this(getCodeSource(pd.getCodeSource()),
pd.getPermissions(), pd.getClassLoader(), p);
}
ProtectionDomainKey(CodeSource cs, PermissionCollection perms,
ClassLoader cl, Principal [] p){
this(getCodeSource(cs), perms, cl, p);
}
private ProtectionDomainKey(UriCodeSource urics,
PermissionCollection perms, ClassLoader cl, Principal [] p){
super(urics, perms, cl, p);
this.codeSource = urics;
this.princiPals = p;
int hash = 7;
hash = 29 * hash + Objects.hashCode(this.codeSource);
hash = 29 * hash + Objects.hashCode(cl);
hash = 29 * hash + Arrays.deepHashCode(this.princiPals);
this.hashCode = hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final ProtectionDomainKey other = (ProtectionDomainKey) obj;
if (!Objects.equals(getClassLoader(),
other.getClassLoader())) return false;
if (!Objects.equals(this.codeSource, other.codeSource))
return false;
return Arrays.deepEquals(this.princiPals, other.princiPals);
}
@Override
public int hashCode() {
return hashCode;
}
}
/**
* To avoid CodeSource equals and hashCode methods.
*
* Shamelessly stolen from RFC3986URLClassLoader
*
* CodeSource uses DNS lookup calls to check location IP addresses are
* equal.
*
* This class must not be serialized.
*/
private static class UriCodeSource extends CodeSource {
private static final long serialVersionUID = 1L;
private final Uri uri;
private final int hashCode;
UriCodeSource(CodeSource cs){
this(cs.getLocation(), cs.getCertificates());
}
UriCodeSource(URL url, Certificate [] certs){
super(url, certs);
Uri uRi = null;
if (url != null){
try {
uRi = Uri.urlToUri(url);
} catch (URISyntaxException ex) { }//Ignore
}
this.uri = uRi;
int hash = 7;
hash = 23 * hash + (this.uri != null ? this.uri.hashCode()
: 0);
hash = 23 * hash + (certs != null ? Arrays.hashCode(certs)
: 0);
hashCode = hash;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o){
if (!(o instanceof UriCodeSource)) return false;
if (uri == null) return super.equals(o); // In case of
URISyntaxException
UriCodeSource that = (UriCodeSource) o;
if ( !uri.equals(that.uri)) return false;
Certificate [] mine = getCertificates();
Certificate [] theirs = that.getCertificates();
return Arrays.equals(mine, theirs);
}
public Object writeReplace() throws ObjectStreamException {
return new CodeSource(getLocation(), getCertificates());
}
}
}
On 30/06/2021 10:45 am, Peter Firmstone wrote:
> Hi Daniel,
>
> That is the current intent, however identifying all methods which
> require protecting isn't a simple process and will change with each
> release.
>
> The simplest part is determining whether the combination of User and
> domains calling have the necessary privileges (my current focus), the
> difficulty is in determining the methods that require protection for
> which Agents must be written. It would be nice if OpenJDK could
> create check points through which security sensitive objects are
> passed and ensure these checkpoints are always used for passing
> references for those types of objects, so that any new JVM code also
> uses these methods, so that it would be easier to control and limit
> the number of locations for which we need to write agents, or other
> mechanisms, such as provider interfaces. Otherwise there is a lot of
> work involved in auditing every JVM release for new code, which has
> the potential to pass security sensitive object references.
>
> It is not advisable to run untrusted code, the JDK doesn't adequately
> defend against untrusted code execution, eg memory consumption,
> throwing Errors, spawning Threads, the intent is to capture the
> privileged execution paths of software's intended functionality, using
> integration tests, recording and constraining the software by
> preventing other unintended privileged execution paths from being
> executed.
>
> A use case for this is restricting (narrowing the audit scope) parsing
> of data to the combination of audited code in a server with
> authenticated client subjects. The authenticated subject, represents
> the source of the data, so if there's no authenticated client subject,
> then we don't grant the privilege to avoid parsing of untrusted data,
> but in addition to that, we want to also constrain parsing to a
> particular domain / scope, which has been audited and approved for
> parsing authenticated user data. While we trust other libraries
> utilised to generally do the right thing, it isn't practical to audit
> all code that might also be capable of parsing data that we don't
> intend to utilise, so we don't grant those libraries privileges they
> are not required to have, in accordance with principles of least
> privilege.
>
> The intent is simply to limit scope to make security auditing
> practical and affordable. Java has a very large ecosystem, so it
> isn't practical to audit everything. We also want authorization to be
> simpler to deploy and less complex to utilize.
>
> Regards,
>
> Peter.
>
> On 30/06/2021 4:42 am, Daniel Latrémolière wrote:
>> Hello,
>>
>> Just for my knowledge, and if I understand your need to enforce a
>> security policy on code potentially untrusted.
>>
>> Isn't it possible to simply create a Java agent instrumenting
>> bytecode [1], which will replace [2] each Java method invocation, in
>> untrusted bytecode, which is returning a potentially sensitive object
>> [3], by a call to a generated adapter.
>>
>> In the generated adapter, you can add all useful code to validate if
>> corresponding code is allowed to see this object, potentially sensitive.
>>
>> A Java agent, would be compatible with all Java versions and I think
>> it would be possible to add exactly the permissions needed.
>>
>> Thanks,
>>
>> Daniel.
>>
>> [1]:
>> https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/ClassFileTransformer.html
>>
>> [2]: Bytecode transformation, by example with ASM:
>> https://stackoverflow.com/a/35635682
>>
>> [3]: By example, proxying each constructor or method returning an
>> instance of class like java.io.File and java.nio.file.Path (if you
>> want to do something like FilePermission).
>>
>>
>>
>> Le 29/06/2021 à 00:44, Peter Firmstone a écrit :
>>> I'm currently playing around with a simpler security model, where
>>> one must escalate privileges with a privilegedCall, designed to be
>>> submitted to an Executor, which is task / thread confined.
>>>
More information about the discuss
mailing list