Skip to content

Commit 1b88065

Browse files
authz: translate gRPC authz policy to Envoy RBAC proto (#8710)
1 parent bb33657 commit 1b88065

File tree

4 files changed

+851
-0
lines changed

4 files changed

+851
-0
lines changed

authz/build.gradle

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
plugins {
2+
id "java-library"
3+
id "maven-publish"
4+
5+
id "com.github.johnrengelman.shadow"
6+
id "com.google.protobuf"
7+
id "ru.vyarus.animalsniffer"
8+
}
9+
10+
description = "gRPC: Authorization"
11+
12+
dependencies {
13+
implementation project(':grpc-protobuf'),
14+
project(':grpc-core')
15+
16+
annotationProcessor libraries.autovalue
17+
compileOnly libraries.javax_annotation
18+
19+
testImplementation project(':grpc-testing'),
20+
project(':grpc-testing-proto')
21+
testImplementation (libraries.guava_testlib) {
22+
exclude group: 'junit', module: 'junit'
23+
}
24+
25+
def xdsDependency = implementation project(':grpc-xds')
26+
shadow configurations.implementation.getDependencies().minus([xdsDependency])
27+
shadow project(path: ':grpc-xds', configuration: 'shadow')
28+
29+
signature "org.codehaus.mojo.signature:java17:1.0@signature"
30+
}
31+
32+
jar {
33+
classifier = 'original'
34+
}
35+
36+
// TODO(ashithasantosh): Remove javadoc exclusion on adding authorization
37+
// interceptor implementations.
38+
javadoc {
39+
exclude "io/grpc/authz/*"
40+
}
41+
42+
shadowJar {
43+
classifier = null
44+
dependencies {
45+
exclude(dependency {true})
46+
}
47+
relocate 'io.grpc.xds', 'io.grpc.xds.shaded.io.grpc.xds'
48+
relocate 'udpa.annotations', 'io.grpc.xds.shaded.udpa.annotations'
49+
relocate 'com.github.udpa', 'io.grpc.xds.shaded.com.github.udpa'
50+
relocate 'envoy.annotations', 'io.grpc.xds.shaded.envoy.annotations'
51+
relocate 'io.envoyproxy', 'io.grpc.xds.shaded.io.envoyproxy'
52+
relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr'
53+
}
54+
55+
publishing {
56+
publications {
57+
maven(MavenPublication) {
58+
// We want this to throw an exception if it isn't working
59+
def originalJar = artifacts.find { dep -> dep.classifier == 'original'}
60+
artifacts.remove(originalJar)
61+
62+
pom.withXml {
63+
def dependenciesNode = new Node(null, 'dependencies')
64+
project.configurations.shadow.allDependencies.each { dep ->
65+
def dependencyNode = dependenciesNode.appendNode('dependency')
66+
dependencyNode.appendNode('groupId', dep.group)
67+
dependencyNode.appendNode('artifactId', dep.name)
68+
dependencyNode.appendNode('version', dep.version)
69+
dependencyNode.appendNode('scope', 'compile')
70+
}
71+
asNode().dependencies[0].replaceNode(dependenciesNode)
72+
}
73+
}
74+
}
75+
}
76+
77+
[publishMavenPublicationToMavenRepository]*.onlyIf {false}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2021 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.authz;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import io.envoyproxy.envoy.config.rbac.v3.Permission;
21+
import io.envoyproxy.envoy.config.rbac.v3.Policy;
22+
import io.envoyproxy.envoy.config.rbac.v3.Principal;
23+
import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated;
24+
import io.envoyproxy.envoy.config.rbac.v3.RBAC;
25+
import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action;
26+
import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
27+
import io.envoyproxy.envoy.type.matcher.v3.PathMatcher;
28+
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
29+
import io.envoyproxy.envoy.type.matcher.v3.StringMatcher;
30+
import io.grpc.internal.JsonParser;
31+
import io.grpc.internal.JsonUtil;
32+
import java.io.IOException;
33+
import java.util.ArrayList;
34+
import java.util.LinkedHashMap;
35+
import java.util.List;
36+
import java.util.Map;
37+
38+
/**
39+
* Translates a gRPC authorization policy in JSON string to Envoy RBAC policies.
40+
*/
41+
class AuthorizationPolicyTranslator {
42+
private static final ImmutableList<String> UNSUPPORTED_HEADERS = ImmutableList.of(
43+
"host", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
44+
"te", "trailer", "transfer-encoding", "upgrade");
45+
46+
private static StringMatcher getStringMatcher(String value) {
47+
if (value.equals("*")) {
48+
return StringMatcher.newBuilder().setSafeRegex(
49+
RegexMatcher.newBuilder().setRegex(".+").build()).build();
50+
} else if (value.startsWith("*")) {
51+
return StringMatcher.newBuilder().setSuffix(value.substring(1)).build();
52+
} else if (value.endsWith("*")) {
53+
return StringMatcher.newBuilder().setPrefix(value.substring(0, value.length() - 1)).build();
54+
}
55+
return StringMatcher.newBuilder().setExact(value).build();
56+
}
57+
58+
private static Principal parseSource(Map<String, ?> source) {
59+
List<String> principalsList = JsonUtil.getListOfStrings(source, "principals");
60+
if (principalsList == null || principalsList.isEmpty()) {
61+
return Principal.newBuilder().setAny(true).build();
62+
}
63+
Principal.Set.Builder principalsSet = Principal.Set.newBuilder();
64+
for (String principal: principalsList) {
65+
principalsSet.addIds(
66+
Principal.newBuilder().setAuthenticated(
67+
Authenticated.newBuilder().setPrincipalName(
68+
getStringMatcher(principal)).build()).build());
69+
}
70+
return Principal.newBuilder().setOrIds(principalsSet.build()).build();
71+
}
72+
73+
private static Permission parseHeader(Map<String, ?> header) throws IllegalArgumentException {
74+
String key = JsonUtil.getString(header, "key");
75+
if (key == null || key.isEmpty()) {
76+
throw new IllegalArgumentException("\"key\" is absent or empty");
77+
}
78+
if (key.charAt(0) == ':'
79+
|| key.startsWith("grpc-")
80+
|| UNSUPPORTED_HEADERS.contains(key.toLowerCase())) {
81+
throw new IllegalArgumentException(String.format("Unsupported \"key\" %s", key));
82+
}
83+
List<String> valuesList = JsonUtil.getListOfStrings(header, "values");
84+
if (valuesList == null || valuesList.isEmpty()) {
85+
throw new IllegalArgumentException("\"values\" is absent or empty");
86+
}
87+
Permission.Set.Builder orSet = Permission.Set.newBuilder();
88+
for (String value: valuesList) {
89+
orSet.addRules(
90+
Permission.newBuilder().setHeader(
91+
HeaderMatcher.newBuilder()
92+
.setName(key)
93+
.setStringMatch(getStringMatcher(value)).build()).build());
94+
}
95+
return Permission.newBuilder().setOrRules(orSet.build()).build();
96+
}
97+
98+
private static Permission parseRequest(Map<String, ?> request) throws IllegalArgumentException {
99+
Permission.Set.Builder andSet = Permission.Set.newBuilder();
100+
List<String> pathsList = JsonUtil.getListOfStrings(request, "paths");
101+
if (pathsList != null && !pathsList.isEmpty()) {
102+
Permission.Set.Builder pathsSet = Permission.Set.newBuilder();
103+
for (String path: pathsList) {
104+
pathsSet.addRules(
105+
Permission.newBuilder().setUrlPath(
106+
PathMatcher.newBuilder().setPath(
107+
getStringMatcher(path)).build()).build());
108+
}
109+
andSet.addRules(Permission.newBuilder().setOrRules(pathsSet.build()).build());
110+
}
111+
List<Map<String, ?>> headersList = JsonUtil.getListOfObjects(request, "headers");
112+
if (headersList != null && !headersList.isEmpty()) {
113+
Permission.Set.Builder headersSet = Permission.Set.newBuilder();
114+
for (Map<String, ?> header: headersList) {
115+
headersSet.addRules(parseHeader(header));
116+
}
117+
andSet.addRules(Permission.newBuilder().setAndRules(headersSet.build()).build());
118+
}
119+
if (andSet.getRulesCount() == 0) {
120+
return Permission.newBuilder().setAny(true).build();
121+
}
122+
return Permission.newBuilder().setAndRules(andSet.build()).build();
123+
}
124+
125+
private static Map<String, Policy> parseRules(
126+
List<Map<String, ?>> objects, String name) throws IllegalArgumentException {
127+
Map<String, Policy> policies = new LinkedHashMap<String, Policy>();
128+
for (Map<String, ?> object: objects) {
129+
String policyName = JsonUtil.getString(object, "name");
130+
if (policyName == null || policyName.isEmpty()) {
131+
throw new IllegalArgumentException("rule \"name\" is absent or empty");
132+
}
133+
List<Principal> principals = new ArrayList<>();
134+
Map<String, ?> source = JsonUtil.getObject(object, "source");
135+
if (source != null) {
136+
principals.add(parseSource(source));
137+
} else {
138+
principals.add(Principal.newBuilder().setAny(true).build());
139+
}
140+
List<Permission> permissions = new ArrayList<>();
141+
Map<String, ?> request = JsonUtil.getObject(object, "request");
142+
if (request != null) {
143+
permissions.add(parseRequest(request));
144+
} else {
145+
permissions.add(Permission.newBuilder().setAny(true).build());
146+
}
147+
Policy policy =
148+
Policy.newBuilder()
149+
.addAllPermissions(permissions)
150+
.addAllPrincipals(principals)
151+
.build();
152+
policies.put(name + "_" + policyName, policy);
153+
}
154+
return policies;
155+
}
156+
157+
/**
158+
* Translates a gRPC authorization policy in JSON string to Envoy RBAC policies.
159+
* On success, will return one of the following -
160+
* 1. One allow RBAC policy or,
161+
* 2. Two RBAC policies, deny policy followed by allow policy.
162+
* If the policy cannot be parsed or is invalid, an exception will be thrown.
163+
*/
164+
public static List<RBAC> translate(String authorizationPolicy)
165+
throws IllegalArgumentException, IOException {
166+
Object jsonObject = JsonParser.parse(authorizationPolicy);
167+
if (!(jsonObject instanceof Map)) {
168+
throw new IllegalArgumentException(
169+
"Authorization policy should be a JSON object. Found: "
170+
+ (jsonObject == null ? null : jsonObject.getClass()));
171+
}
172+
@SuppressWarnings("unchecked")
173+
Map<String, ?> json = (Map<String, ?>)jsonObject;
174+
String name = JsonUtil.getString(json, "name");
175+
if (name == null || name.isEmpty()) {
176+
throw new IllegalArgumentException("\"name\" is absent or empty");
177+
}
178+
List<RBAC> rbacs = new ArrayList<>();
179+
List<Map<String, ?>> objects = JsonUtil.getListOfObjects(json, "deny_rules");
180+
if (objects != null && !objects.isEmpty()) {
181+
rbacs.add(
182+
RBAC.newBuilder()
183+
.setAction(Action.DENY)
184+
.putAllPolicies(parseRules(objects, name))
185+
.build());
186+
}
187+
objects = JsonUtil.getListOfObjects(json, "allow_rules");
188+
if (objects == null || objects.isEmpty()) {
189+
throw new IllegalArgumentException("\"allow_rules\" is absent");
190+
}
191+
rbacs.add(
192+
RBAC.newBuilder()
193+
.setAction(Action.ALLOW)
194+
.putAllPolicies(parseRules(objects, name))
195+
.build());
196+
return rbacs;
197+
}
198+
}

0 commit comments

Comments
 (0)