xref: /aosp_15_r20/external/grpc-grpc-java/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
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 
getStringMatcher(String value)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 
parseSource(Map<String, ?> source)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 
parseHeader(Map<String, ?> header)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 
parseRequest(Map<String, ?> request)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 
parseRules( List<Map<String, ?>> objects, String name)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   */
translate(String authorizationPolicy)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 }
199