From 1b88065f9a522cf60c7c7824c200cd55201e97a1 Mon Sep 17 00:00:00 2001 From: Ashitha Santhosh <55257063+ashithasantosh@users.noreply.github.com> Date: Mon, 14 Feb 2022 07:10:18 -0800 Subject: [PATCH] authz: translate gRPC authz policy to Envoy RBAC proto (#8710) --- authz/build.gradle | 77 +++ .../authz/AuthorizationPolicyTranslator.java | 198 ++++++ .../AuthorizationPolicyTranslatorTest.java | 574 ++++++++++++++++++ settings.gradle | 2 + 4 files changed, 851 insertions(+) create mode 100644 authz/build.gradle create mode 100644 authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java create mode 100644 authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java diff --git a/authz/build.gradle b/authz/build.gradle new file mode 100644 index 0000000000..f6110a5850 --- /dev/null +++ b/authz/build.gradle @@ -0,0 +1,77 @@ +plugins { + id "java-library" + id "maven-publish" + + id "com.github.johnrengelman.shadow" + id "com.google.protobuf" + id "ru.vyarus.animalsniffer" +} + +description = "gRPC: Authorization" + +dependencies { + implementation project(':grpc-protobuf'), + project(':grpc-core') + + annotationProcessor libraries.autovalue + compileOnly libraries.javax_annotation + + testImplementation project(':grpc-testing'), + project(':grpc-testing-proto') + testImplementation (libraries.guava_testlib) { + exclude group: 'junit', module: 'junit' + } + + def xdsDependency = implementation project(':grpc-xds') + shadow configurations.implementation.getDependencies().minus([xdsDependency]) + shadow project(path: ':grpc-xds', configuration: 'shadow') + + signature "org.codehaus.mojo.signature:java17:1.0@signature" +} + +jar { + classifier = 'original' +} + +// TODO(ashithasantosh): Remove javadoc exclusion on adding authorization +// interceptor implementations. +javadoc { + exclude "io/grpc/authz/*" +} + +shadowJar { + classifier = null + dependencies { + exclude(dependency {true}) + } + relocate 'io.grpc.xds', 'io.grpc.xds.shaded.io.grpc.xds' + relocate 'udpa.annotations', 'io.grpc.xds.shaded.udpa.annotations' + relocate 'com.github.udpa', 'io.grpc.xds.shaded.com.github.udpa' + relocate 'envoy.annotations', 'io.grpc.xds.shaded.envoy.annotations' + relocate 'io.envoyproxy', 'io.grpc.xds.shaded.io.envoyproxy' + relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr' +} + +publishing { + publications { + maven(MavenPublication) { + // We want this to throw an exception if it isn't working + def originalJar = artifacts.find { dep -> dep.classifier == 'original'} + artifacts.remove(originalJar) + + pom.withXml { + def dependenciesNode = new Node(null, 'dependencies') + project.configurations.shadow.allDependencies.each { dep -> + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', 'compile') + } + asNode().dependencies[0].replaceNode(dependenciesNode) + } + } + } +} + +[publishMavenPublicationToMavenRepository]*.onlyIf {false} diff --git a/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java new file mode 100644 index 0000000000..1637af737a --- /dev/null +++ b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java @@ -0,0 +1,198 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.authz; + +import com.google.common.collect.ImmutableList; +import io.envoyproxy.envoy.config.rbac.v3.Permission; +import io.envoyproxy.envoy.config.rbac.v3.Policy; +import io.envoyproxy.envoy.config.rbac.v3.Principal; +import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated; +import io.envoyproxy.envoy.config.rbac.v3.RBAC; +import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.type.matcher.v3.PathMatcher; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.grpc.internal.JsonParser; +import io.grpc.internal.JsonUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. + */ +class AuthorizationPolicyTranslator { + private static final ImmutableList UNSUPPORTED_HEADERS = ImmutableList.of( + "host", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", + "te", "trailer", "transfer-encoding", "upgrade"); + + private static StringMatcher getStringMatcher(String value) { + if (value.equals("*")) { + return StringMatcher.newBuilder().setSafeRegex( + RegexMatcher.newBuilder().setRegex(".+").build()).build(); + } else if (value.startsWith("*")) { + return StringMatcher.newBuilder().setSuffix(value.substring(1)).build(); + } else if (value.endsWith("*")) { + return StringMatcher.newBuilder().setPrefix(value.substring(0, value.length() - 1)).build(); + } + return StringMatcher.newBuilder().setExact(value).build(); + } + + private static Principal parseSource(Map source) { + List principalsList = JsonUtil.getListOfStrings(source, "principals"); + if (principalsList == null || principalsList.isEmpty()) { + return Principal.newBuilder().setAny(true).build(); + } + Principal.Set.Builder principalsSet = Principal.Set.newBuilder(); + for (String principal: principalsList) { + principalsSet.addIds( + Principal.newBuilder().setAuthenticated( + Authenticated.newBuilder().setPrincipalName( + getStringMatcher(principal)).build()).build()); + } + return Principal.newBuilder().setOrIds(principalsSet.build()).build(); + } + + private static Permission parseHeader(Map header) throws IllegalArgumentException { + String key = JsonUtil.getString(header, "key"); + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("\"key\" is absent or empty"); + } + if (key.charAt(0) == ':' + || key.startsWith("grpc-") + || UNSUPPORTED_HEADERS.contains(key.toLowerCase())) { + throw new IllegalArgumentException(String.format("Unsupported \"key\" %s", key)); + } + List valuesList = JsonUtil.getListOfStrings(header, "values"); + if (valuesList == null || valuesList.isEmpty()) { + throw new IllegalArgumentException("\"values\" is absent or empty"); + } + Permission.Set.Builder orSet = Permission.Set.newBuilder(); + for (String value: valuesList) { + orSet.addRules( + Permission.newBuilder().setHeader( + HeaderMatcher.newBuilder() + .setName(key) + .setStringMatch(getStringMatcher(value)).build()).build()); + } + return Permission.newBuilder().setOrRules(orSet.build()).build(); + } + + private static Permission parseRequest(Map request) throws IllegalArgumentException { + Permission.Set.Builder andSet = Permission.Set.newBuilder(); + List pathsList = JsonUtil.getListOfStrings(request, "paths"); + if (pathsList != null && !pathsList.isEmpty()) { + Permission.Set.Builder pathsSet = Permission.Set.newBuilder(); + for (String path: pathsList) { + pathsSet.addRules( + Permission.newBuilder().setUrlPath( + PathMatcher.newBuilder().setPath( + getStringMatcher(path)).build()).build()); + } + andSet.addRules(Permission.newBuilder().setOrRules(pathsSet.build()).build()); + } + List> headersList = JsonUtil.getListOfObjects(request, "headers"); + if (headersList != null && !headersList.isEmpty()) { + Permission.Set.Builder headersSet = Permission.Set.newBuilder(); + for (Map header: headersList) { + headersSet.addRules(parseHeader(header)); + } + andSet.addRules(Permission.newBuilder().setAndRules(headersSet.build()).build()); + } + if (andSet.getRulesCount() == 0) { + return Permission.newBuilder().setAny(true).build(); + } + return Permission.newBuilder().setAndRules(andSet.build()).build(); + } + + private static Map parseRules( + List> objects, String name) throws IllegalArgumentException { + Map policies = new LinkedHashMap(); + for (Map object: objects) { + String policyName = JsonUtil.getString(object, "name"); + if (policyName == null || policyName.isEmpty()) { + throw new IllegalArgumentException("rule \"name\" is absent or empty"); + } + List principals = new ArrayList<>(); + Map source = JsonUtil.getObject(object, "source"); + if (source != null) { + principals.add(parseSource(source)); + } else { + principals.add(Principal.newBuilder().setAny(true).build()); + } + List permissions = new ArrayList<>(); + Map request = JsonUtil.getObject(object, "request"); + if (request != null) { + permissions.add(parseRequest(request)); + } else { + permissions.add(Permission.newBuilder().setAny(true).build()); + } + Policy policy = + Policy.newBuilder() + .addAllPermissions(permissions) + .addAllPrincipals(principals) + .build(); + policies.put(name + "_" + policyName, policy); + } + return policies; + } + + /** + * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. + * On success, will return one of the following - + * 1. One allow RBAC policy or, + * 2. Two RBAC policies, deny policy followed by allow policy. + * If the policy cannot be parsed or is invalid, an exception will be thrown. + */ + public static List translate(String authorizationPolicy) + throws IllegalArgumentException, IOException { + Object jsonObject = JsonParser.parse(authorizationPolicy); + if (!(jsonObject instanceof Map)) { + throw new IllegalArgumentException( + "Authorization policy should be a JSON object. Found: " + + (jsonObject == null ? null : jsonObject.getClass())); + } + @SuppressWarnings("unchecked") + Map json = (Map)jsonObject; + String name = JsonUtil.getString(json, "name"); + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("\"name\" is absent or empty"); + } + List rbacs = new ArrayList<>(); + List> objects = JsonUtil.getListOfObjects(json, "deny_rules"); + if (objects != null && !objects.isEmpty()) { + rbacs.add( + RBAC.newBuilder() + .setAction(Action.DENY) + .putAllPolicies(parseRules(objects, name)) + .build()); + } + objects = JsonUtil.getListOfObjects(json, "allow_rules"); + if (objects == null || objects.isEmpty()) { + throw new IllegalArgumentException("\"allow_rules\" is absent"); + } + rbacs.add( + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putAllPolicies(parseRules(objects, name)) + .build()); + return rbacs; + } +} diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java new file mode 100644 index 0000000000..b957bf283e --- /dev/null +++ b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java @@ -0,0 +1,574 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.authz; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import io.envoyproxy.envoy.config.rbac.v3.Permission; +import io.envoyproxy.envoy.config.rbac.v3.Policy; +import io.envoyproxy.envoy.config.rbac.v3.Principal; +import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated; +import io.envoyproxy.envoy.config.rbac.v3.RBAC; +import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.type.matcher.v3.PathMatcher; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import java.io.IOException; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AuthorizationPolicyTranslatorTest { + @Test + public void invalidPolicy() throws Exception { + String policy = "{ \"name\": \"abc\",, }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IOException ioe) { + assertThat(ioe).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON" + + " at line 1 column 18 path $.name"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingAuthorizationPolicyName() throws Exception { + String policy = "{}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"name\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void incorrectAuthorizationPolicyName() throws Exception { + String policy = "{ \"name\": [\"abc\"] }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (ClassCastException cce) { + assertThat(cce).hasMessageThat().isEqualTo( + "value '[abc]' for key 'name' in '{name=[abc]}' is not String"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingAllowRules() throws Exception { + String policy = "{ \"name\": \"authz\" }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"allow_rules\" is absent"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingRuleName() throws Exception { + String policy = "{" + + " \"name\" : \"abc\" ," + + " \"allow_rules\" : [" + + " {}" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("rule \"name\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingSourceAndRequest() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\" : [" + + " {" + + " \"name\": \"allow_all\"" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(1, rbacs.size()); + RBAC expected_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_all", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder().setAny(true)) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_rbac, rbacs.get(0)); + } + + @Test + public void emptySourceAndRequest() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\" : [" + + " {" + + " \"name\": \"allow_all\"," + + " \"source\": {}," + + " \"request\": {}" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(1, rbacs.size()); + RBAC expected_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_all", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder().setAny(true)) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_rbac, rbacs.get(0)); + } + + @Test + public void incorrectRulesType() throws Exception { + String policy = "{" + + " \"name\" : \"abc\" ," + + " \"allow_rules\" : {}" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (ClassCastException cce) { + assertThat(cce).hasMessageThat().isEqualTo( + "value '{}' for key 'allow_rules' in '{name=abc, allow_rules={}}' is not List"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void parseSourceSuccess() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"deny_rules\": [" + + " {" + + " \"name\": \"deny_users\"," + + " \"source\": {" + + " \"principals\": [" + + " \"spiffe://foo.com\"," + + " \"spiffe://bar*\"," + + " \"*baz\"," + + " \"spiffe://*.com\"" + + " ]" + + " }" + + " }" + + " ]," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_any\"," + + " \"source\": {" + + " \"principals\": [" + + " \"*\"" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(2, rbacs.size()); + RBAC expected_deny_rbac = + RBAC.newBuilder() + .setAction(Action.DENY) + .putPolicies("authz_deny_users", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder() + .setOrIds( + Principal.Set.newBuilder() + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setExact("spiffe://foo.com").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setPrefix("spiffe://bar").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setSuffix("baz").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setExact("spiffe://*.com").build()).build()).build()) + .build()).build()) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()).build(); + RBAC expected_allow_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_any", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder() + .setOrIds( + Principal.Set.newBuilder() + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder() + .setRegex(".+").build()).build()).build()) + .build()) + .build()).build()) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()).build(); + assertEquals(expected_deny_rbac, rbacs.get(0)); + assertEquals(expected_allow_rbac, rbacs.get(1)); + } + + @Test + public void unsupportedPseudoHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \":method\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" :method"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void unsupportedGrpcPrefixHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"grpc-xxx\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" grpc-xxx"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void unsupportedHostHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"Host\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" Host"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingHeaderKey() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {}" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"key\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingHeaderValues() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void emptyHeaderValues() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"," + + " \"values\": []" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void parseRequestSuccess() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"deny_rules\": [" + + " {" + + " \"name\": \"deny_access\"," + + " \"request\": {" + + " \"paths\": [" + + " \"/pkg.service/foo\"," + + " \"/pkg.service/bar*\"" + + " ]," + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"," + + " \"values\": [\"/dev/path/*\"]" + + " }" + + " ]" + + " }" + + " }" + + " ]," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access1\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"key-1\"," + + " \"values\": [" + + " \"foo\"," + + " \"*bar\"" + + " ]" + + " }," + + " {" + + " \"key\": \"key-2\"," + + " \"values\": [" + + " \"*\"" + + " ]" + + " }" + + " ]" + + " }" + + " }," + + " {" + + " \"name\": \"allow_access2\"," + + " \"request\": {" + + " \"paths\": [" + + " \"*baz\"" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(2, rbacs.size()); + RBAC expected_deny_rbac = + RBAC.newBuilder() + .setAction(Action.DENY) + .putPolicies("authz_deny_access", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setExact("/pkg.service/foo").build()).build()).build()) + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setPrefix("/pkg.service/bar").build()).build()).build()) + .build()).build()) + .addRules(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("dev-path") + .setStringMatch(StringMatcher.newBuilder() + .setPrefix("/dev/path/").build()) + .build()) + .build()) + .build()).build()) + .build()).build()) + .build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()).build(); + RBAC expected_allow_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_access1", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-1") + .setStringMatch(StringMatcher.newBuilder() + .setExact("foo").build()) + .build()) + .build()) + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-1") + .setStringMatch(StringMatcher.newBuilder() + .setSuffix("bar").build()) + .build()) + .build()) + .build()).build()) + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-2") + .setStringMatch(StringMatcher.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder() + .setRegex(".+").build()).build()) + .build()) + .build()) + .build()).build()).build()).build()).build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()) + .putPolicies("authz_allow_access2", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setSuffix("baz").build()).build()).build()) + .build()).build()) + .build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_deny_rbac, rbacs.get(0)); + assertEquals(expected_allow_rbac, rbacs.get(1)); + } +} diff --git a/settings.gradle b/settings.gradle index 3da79fce13..cd50337d5c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -49,6 +49,7 @@ include ":grpc-services" include ":grpc-xds" include ":grpc-bom" include ":grpc-rls" +include ":grpc-authz" include ":grpc-observability" project(':grpc-api').projectDir = "$rootDir/api" as File @@ -74,6 +75,7 @@ project(':grpc-services').projectDir = "$rootDir/services" as File project(':grpc-xds').projectDir = "$rootDir/xds" as File project(':grpc-bom').projectDir = "$rootDir/bom" as File project(':grpc-rls').projectDir = "$rootDir/rls" as File +project(':grpc-authz').projectDir = "$rootDir/authz" as File project(':grpc-observability').projectDir = "$rootDir/observability" as File if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) {