Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

binder: Add SecurityPolicies#anyOf(), similar to allOf() #9147

Merged
merged 3 commits into from May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 63 additions & 0 deletions binder/src/main/java/io/grpc/binder/SecurityPolicies.java
Expand Up @@ -29,7 +29,10 @@
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/** Static factory methods for creating standard security policies. */
@CheckReturnValue
Expand Down Expand Up @@ -191,6 +194,12 @@ private static boolean checkPackageSignature(
* Creates a {@link SecurityPolicy} that allows access if and only if *all* of the specified
* {@code securityPolicies} allow access.
*
* <p>Policies will be checked in the order that they are passed. If a policy denies access,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about adding this to the contract too but wasn't sure since it would preclude ever (optimistically) checking the policies in parallel. Is there a specific need for or benefit of this guarantee that you have in mind?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. And I'm happy not committing to a particular behavior, but in that case I would prefer to be explicit that we intentionally make no guarantees. Something like "subsequent policies may be checked in any order and on any thread" so that we are clear you should not depend on that behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it thanks. Thinking about this more with http://www.hyrumslaw.com in mind, I doubt anyone would ever be brave enough to make such a change even if we added an explicit non-guarantee to this javadoc. So maybe your proposal to guarantee the order is for the best since it is the simplest. Should be a separate PR though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all allOf() changes from this PR.

* subsequent policies will not be checked.
*
* <p>If a policy denies access, the {@link io.grpc.Status} returned by {@code checkAuthorization}
* will be that of the failed policy.
*
* @param securityPolicies the security policies that all must allow access.
* @throws NullPointerException if any of the inputs are {@code null}.
* @throws IllegalArgumentException if {@code securityPolicies} is empty.
Expand Down Expand Up @@ -218,6 +227,60 @@ public Status checkAuthorization(int uid) {
};
}

/**
* Creates a {@link SecurityPolicy} that allows access if *any* of the specified {@code
* securityPolicies} allow access.
*
* <p>Policies will be checked in the order that they are passed. If a policy allows access,
* subsequent policies will not be checked.
*
* <p>If all policies deny access, the {@link io.grpc.Status} returned by {@code
* checkAuthorization} will included the concatenated descriptions of the failed policies and
* attach any additional causes as suppressed throwables. The status code will be that of the
* first failed policy.
*
* @param securityPolicies the security policies that will be checked.
* @throws NullPointerException if any of the inputs are {@code null}.
* @throws IllegalArgumentException if {@code securityPolicies} is empty.
*/
public static SecurityPolicy anyOf(SecurityPolicy... securityPolicies) {
Preconditions.checkNotNull(securityPolicies, "securityPolicies");
Preconditions.checkArgument(securityPolicies.length > 0, "securityPolicies must not be empty");

return anyOfSecurityPolicy(securityPolicies);
}

private static SecurityPolicy anyOfSecurityPolicy(SecurityPolicy... securityPolicies) {
return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
List<Status> failed = new ArrayList<>();
for (SecurityPolicy policy : securityPolicies) {
Status checkAuth = policy.checkAuthorization(uid);
if (checkAuth.isOk()) {
return checkAuth;
}
failed.add(checkAuth);
}

Iterator<Status> iter = failed.iterator();
Status toReturn = iter.next();
while (iter.hasNext()) {
Status append = iter.next();
toReturn = toReturn.augmentDescription(append.getDescription());
if (append.getCause() != null) {
if (toReturn.getCause() != null) {
toReturn.getCause().addSuppressed(append.getCause());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want test coverage for this cause accumulation logic?

} else {
toReturn = toReturn.withCause(append.getCause());
}
}
}
return toReturn;
}
};
}

/**
* Creates a {@link SecurityPolicy} which checks if the caller has all of the given permissions
* from {@code permissions}.
Expand Down
53 changes: 52 additions & 1 deletion binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java
Expand Up @@ -34,6 +34,7 @@
import com.google.common.collect.ImmutableSet;
import io.grpc.Status;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -382,14 +383,64 @@ public void testAllOf_succeedsIfAllSecurityPoliciesAllowed() throws Exception {

@Test
public void testAllOf_failsIfOneSecurityPoliciesNotAllowed() throws Exception {
final AtomicBoolean policyCalled = new AtomicBoolean(false);
policy =
SecurityPolicies.allOf(
SecurityPolicies.internalOnly(),
SecurityPolicies.permissionDenied("Not allowed SecurityPolicy"));
SecurityPolicies.permissionDenied("Not allowed SecurityPolicy"),
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
policyCalled.set(true);
throw new AssertionError();
jdcormie marked this conversation as resolved.
Show resolved Hide resolved
}
});

assertThat(policy.checkAuthorization(MY_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(MY_UID).getDescription())
.contains("Not allowed SecurityPolicy");
assertThat(policyCalled.get()).isFalse();
}

@Test
public void testAnyOf_succeedsIfAnySecurityPoliciesAllowed() throws Exception {
final AtomicBoolean policyCalled = new AtomicBoolean(false);
policy =
SecurityPolicies.anyOf(
SecurityPolicies.internalOnly(),
new SecurityPolicy() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly but if we have two copies of this fake perhaps making it a class named RecordingSecurityPolicy or similar would be more self-documenting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a helper class to the test.

@Override
public Status checkAuthorization(int uid) {
policyCalled.set(true);
throw new AssertionError();
}
});

assertThat(policy.checkAuthorization(MY_UID).getCode()).isEqualTo(Status.OK.getCode());
assertThat(policyCalled.get()).isFalse();
}

@Test
public void testAnyOf_failsIfNoSecurityPolicyIsAllowed() throws Exception {
policy =
SecurityPolicies.anyOf(
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.PERMISSION_DENIED.withDescription("Not allowed: first");
}
},
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.UNAUTHENTICATED.withDescription("Not allowed: second");
}
});

assertThat(policy.checkAuthorization(MY_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(MY_UID).getDescription()).contains("Not allowed: first");
assertThat(policy.checkAuthorization(MY_UID).getDescription()).contains("Not allowed: second");
}
}