google.golang.org/grpc@v1.62.1/authz/rbac_translator.go (about) 1 /* 2 * Copyright 2021 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 authz exposes methods to manage authorization within gRPC. 18 // 19 // # Experimental 20 // 21 // Notice: This package is EXPERIMENTAL and may be changed or removed 22 // in a later release. 23 package authz 24 25 import ( 26 "bytes" 27 "encoding/json" 28 "fmt" 29 "strings" 30 31 v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1" 32 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 33 v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" 34 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 35 v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" 36 "google.golang.org/protobuf/types/known/anypb" 37 "google.golang.org/protobuf/types/known/structpb" 38 ) 39 40 // This is used when converting a custom config from raw JSON to a TypedStruct 41 // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>" 42 const typeURLPrefix = "grpc.authz.audit_logging/" 43 44 type header struct { 45 Key string 46 Values []string 47 } 48 49 type peer struct { 50 Principals []string 51 } 52 53 type request struct { 54 Paths []string 55 Headers []header 56 } 57 58 type rule struct { 59 Name string 60 Source peer 61 Request request 62 } 63 64 type auditLogger struct { 65 Name string `json:"name"` 66 Config *structpb.Struct `json:"config"` 67 IsOptional bool `json:"is_optional"` 68 } 69 70 type auditLoggingOptions struct { 71 AuditCondition string `json:"audit_condition"` 72 AuditLoggers []*auditLogger `json:"audit_loggers"` 73 } 74 75 // Represents the SDK authorization policy provided by user. 76 type authorizationPolicy struct { 77 Name string 78 DenyRules []rule `json:"deny_rules"` 79 AllowRules []rule `json:"allow_rules"` 80 AuditLoggingOptions auditLoggingOptions `json:"audit_logging_options"` 81 } 82 83 func principalOr(principals []*v3rbacpb.Principal) *v3rbacpb.Principal { 84 return &v3rbacpb.Principal{ 85 Identifier: &v3rbacpb.Principal_OrIds{ 86 OrIds: &v3rbacpb.Principal_Set{ 87 Ids: principals, 88 }, 89 }, 90 } 91 } 92 93 func permissionOr(permission []*v3rbacpb.Permission) *v3rbacpb.Permission { 94 return &v3rbacpb.Permission{ 95 Rule: &v3rbacpb.Permission_OrRules{ 96 OrRules: &v3rbacpb.Permission_Set{ 97 Rules: permission, 98 }, 99 }, 100 } 101 } 102 103 func permissionAnd(permission []*v3rbacpb.Permission) *v3rbacpb.Permission { 104 return &v3rbacpb.Permission{ 105 Rule: &v3rbacpb.Permission_AndRules{ 106 AndRules: &v3rbacpb.Permission_Set{ 107 Rules: permission, 108 }, 109 }, 110 } 111 } 112 113 func getStringMatcher(value string) *v3matcherpb.StringMatcher { 114 switch { 115 case value == "*": 116 return &v3matcherpb.StringMatcher{ 117 MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{ 118 SafeRegex: &v3matcherpb.RegexMatcher{Regex: ".+"}}, 119 } 120 case strings.HasSuffix(value, "*"): 121 prefix := strings.TrimSuffix(value, "*") 122 return &v3matcherpb.StringMatcher{ 123 MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: prefix}, 124 } 125 case strings.HasPrefix(value, "*"): 126 suffix := strings.TrimPrefix(value, "*") 127 return &v3matcherpb.StringMatcher{ 128 MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: suffix}, 129 } 130 default: 131 return &v3matcherpb.StringMatcher{ 132 MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: value}, 133 } 134 } 135 } 136 137 func getHeaderMatcher(key, value string) *v3routepb.HeaderMatcher { 138 switch { 139 case value == "*": 140 return &v3routepb.HeaderMatcher{ 141 Name: key, 142 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{ 143 SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: ".+"}}, 144 } 145 case strings.HasSuffix(value, "*"): 146 prefix := strings.TrimSuffix(value, "*") 147 return &v3routepb.HeaderMatcher{ 148 Name: key, 149 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: prefix}, 150 } 151 case strings.HasPrefix(value, "*"): 152 suffix := strings.TrimPrefix(value, "*") 153 return &v3routepb.HeaderMatcher{ 154 Name: key, 155 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: suffix}, 156 } 157 default: 158 return &v3routepb.HeaderMatcher{ 159 Name: key, 160 HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: value}, 161 } 162 } 163 } 164 165 func parsePrincipalNames(principalNames []string) []*v3rbacpb.Principal { 166 ps := make([]*v3rbacpb.Principal, 0, len(principalNames)) 167 for _, principalName := range principalNames { 168 newPrincipalName := &v3rbacpb.Principal{ 169 Identifier: &v3rbacpb.Principal_Authenticated_{ 170 Authenticated: &v3rbacpb.Principal_Authenticated{ 171 PrincipalName: getStringMatcher(principalName), 172 }, 173 }} 174 ps = append(ps, newPrincipalName) 175 } 176 return ps 177 } 178 179 func parsePeer(source peer) *v3rbacpb.Principal { 180 if len(source.Principals) == 0 { 181 return &v3rbacpb.Principal{ 182 Identifier: &v3rbacpb.Principal_Any{ 183 Any: true, 184 }, 185 } 186 } 187 return principalOr(parsePrincipalNames(source.Principals)) 188 } 189 190 func parsePaths(paths []string) []*v3rbacpb.Permission { 191 ps := make([]*v3rbacpb.Permission, 0, len(paths)) 192 for _, path := range paths { 193 newPath := &v3rbacpb.Permission{ 194 Rule: &v3rbacpb.Permission_UrlPath{ 195 UrlPath: &v3matcherpb.PathMatcher{ 196 Rule: &v3matcherpb.PathMatcher_Path{Path: getStringMatcher(path)}}}} 197 ps = append(ps, newPath) 198 } 199 return ps 200 } 201 202 func parseHeaderValues(key string, values []string) []*v3rbacpb.Permission { 203 vs := make([]*v3rbacpb.Permission, 0, len(values)) 204 for _, value := range values { 205 newHeader := &v3rbacpb.Permission{ 206 Rule: &v3rbacpb.Permission_Header{ 207 Header: getHeaderMatcher(key, value)}} 208 vs = append(vs, newHeader) 209 } 210 return vs 211 } 212 213 var unsupportedHeaders = map[string]bool{ 214 "host": true, 215 "connection": true, 216 "keep-alive": true, 217 "proxy-authenticate": true, 218 "proxy-authorization": true, 219 "te": true, 220 "trailer": true, 221 "transfer-encoding": true, 222 "upgrade": true, 223 } 224 225 func unsupportedHeader(key string) bool { 226 return key[0] == ':' || strings.HasPrefix(key, "grpc-") || unsupportedHeaders[key] 227 } 228 229 func parseHeaders(headers []header) ([]*v3rbacpb.Permission, error) { 230 hs := make([]*v3rbacpb.Permission, 0, len(headers)) 231 for i, header := range headers { 232 if header.Key == "" { 233 return nil, fmt.Errorf(`"headers" %d: "key" is not present`, i) 234 } 235 header.Key = strings.ToLower(header.Key) 236 if unsupportedHeader(header.Key) { 237 return nil, fmt.Errorf(`"headers" %d: unsupported "key" %s`, i, header.Key) 238 } 239 if len(header.Values) == 0 { 240 return nil, fmt.Errorf(`"headers" %d: "values" is not present`, i) 241 } 242 values := parseHeaderValues(header.Key, header.Values) 243 hs = append(hs, permissionOr(values)) 244 } 245 return hs, nil 246 } 247 248 func parseRequest(request request) (*v3rbacpb.Permission, error) { 249 var and []*v3rbacpb.Permission 250 if len(request.Paths) > 0 { 251 and = append(and, permissionOr(parsePaths(request.Paths))) 252 } 253 if len(request.Headers) > 0 { 254 headers, err := parseHeaders(request.Headers) 255 if err != nil { 256 return nil, err 257 } 258 and = append(and, permissionAnd(headers)) 259 } 260 if len(and) > 0 { 261 return permissionAnd(and), nil 262 } 263 return &v3rbacpb.Permission{ 264 Rule: &v3rbacpb.Permission_Any{ 265 Any: true, 266 }, 267 }, nil 268 } 269 270 func parseRules(rules []rule, prefixName string) (map[string]*v3rbacpb.Policy, error) { 271 policies := make(map[string]*v3rbacpb.Policy) 272 for i, rule := range rules { 273 if rule.Name == "" { 274 return policies, fmt.Errorf(`%d: "name" is not present`, i) 275 } 276 permission, err := parseRequest(rule.Request) 277 if err != nil { 278 return nil, fmt.Errorf("%d: %v", i, err) 279 } 280 policyName := prefixName + "_" + rule.Name 281 policies[policyName] = &v3rbacpb.Policy{ 282 Principals: []*v3rbacpb.Principal{parsePeer(rule.Source)}, 283 Permissions: []*v3rbacpb.Permission{permission}, 284 } 285 } 286 return policies, nil 287 } 288 289 // Parse auditLoggingOptions to the associated RBAC protos. The single 290 // auditLoggingOptions results in two different parsed protos, one for the allow 291 // policy and one for the deny policy 292 func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggingOptions, deny *v3rbacpb.RBAC_AuditLoggingOptions, err error) { 293 allow = &v3rbacpb.RBAC_AuditLoggingOptions{} 294 deny = &v3rbacpb.RBAC_AuditLoggingOptions{} 295 296 if options.AuditCondition != "" { 297 rbacCondition, ok := v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition_value[options.AuditCondition] 298 if !ok { 299 return nil, nil, fmt.Errorf("failed to parse AuditCondition %v. Allowed values {NONE, ON_DENY, ON_ALLOW, ON_DENY_AND_ALLOW}", options.AuditCondition) 300 } 301 allow.AuditCondition = v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition) 302 deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition)) 303 } 304 305 for i, config := range options.AuditLoggers { 306 if config.Name == "" { 307 return nil, nil, fmt.Errorf("missing required field: name in audit_logging_options.audit_loggers[%v]", i) 308 } 309 if config.Config == nil { 310 config.Config = &structpb.Struct{} 311 } 312 typedStruct := &v1xdsudpatypepb.TypedStruct{ 313 TypeUrl: typeURLPrefix + config.Name, 314 Value: config.Config, 315 } 316 customConfig, err := anypb.New(typedStruct) 317 if err != nil { 318 return nil, nil, fmt.Errorf("error parsing custom audit logger config: %v", err) 319 } 320 321 logger := &v3corepb.TypedExtensionConfig{Name: config.Name, TypedConfig: customConfig} 322 rbacConfig := v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{ 323 IsOptional: config.IsOptional, 324 AuditLogger: logger, 325 } 326 allow.LoggerConfigs = append(allow.LoggerConfigs, &rbacConfig) 327 deny.LoggerConfigs = append(deny.LoggerConfigs, &rbacConfig) 328 } 329 330 return allow, deny, nil 331 } 332 333 // Maps the AuditCondition coming from AuditLoggingOptions to the proper 334 // condition for the deny policy RBAC proto 335 func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition) v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition { 336 // Mapping the overall policy AuditCondition to what it must be for the Deny and Allow RBAC 337 // See gRPC A59 for details - https://github.com/grpc/proposal/pull/346/files 338 // |Authorization Policy |DENY RBAC |ALLOW RBAC | 339 // |----------------------|-------------------|---------------------| 340 // |NONE |NONE |NONE | 341 // |ON_DENY |ON_DENY |ON_DENY | 342 // |ON_ALLOW |NONE |ON_ALLOW | 343 // |ON_DENY_AND_ALLOW |ON_DENY |ON_DENY_AND_ALLOW | 344 switch condition { 345 case v3rbacpb.RBAC_AuditLoggingOptions_NONE: 346 return v3rbacpb.RBAC_AuditLoggingOptions_NONE 347 case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY: 348 return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY 349 case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW: 350 return v3rbacpb.RBAC_AuditLoggingOptions_NONE 351 case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW: 352 return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY 353 default: 354 return v3rbacpb.RBAC_AuditLoggingOptions_NONE 355 } 356 } 357 358 // translatePolicy translates SDK authorization policy in JSON format to two 359 // Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC 360 // allow policy. Also returns the overall policy name. If the input policy 361 // cannot be parsed or is invalid, an error will be returned. 362 func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, string, error) { 363 policy := &authorizationPolicy{} 364 d := json.NewDecoder(bytes.NewReader([]byte(policyStr))) 365 d.DisallowUnknownFields() 366 if err := d.Decode(policy); err != nil { 367 return nil, "", fmt.Errorf("failed to unmarshal policy: %v", err) 368 } 369 if policy.Name == "" { 370 return nil, "", fmt.Errorf(`"name" is not present`) 371 } 372 if len(policy.AllowRules) == 0 { 373 return nil, "", fmt.Errorf(`"allow_rules" is not present`) 374 } 375 allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos() 376 if err != nil { 377 return nil, "", err 378 } 379 rbacs := make([]*v3rbacpb.RBAC, 0, 2) 380 if len(policy.DenyRules) > 0 { 381 denyPolicies, err := parseRules(policy.DenyRules, policy.Name) 382 if err != nil { 383 return nil, "", fmt.Errorf(`"deny_rules" %v`, err) 384 } 385 denyRBAC := &v3rbacpb.RBAC{ 386 Action: v3rbacpb.RBAC_DENY, 387 Policies: denyPolicies, 388 AuditLoggingOptions: denyLogger, 389 } 390 rbacs = append(rbacs, denyRBAC) 391 } 392 allowPolicies, err := parseRules(policy.AllowRules, policy.Name) 393 if err != nil { 394 return nil, "", fmt.Errorf(`"allow_rules" %v`, err) 395 } 396 allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies, AuditLoggingOptions: allowLogger} 397 return append(rbacs, allowRBAC), policy.Name, nil 398 }