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