| /* |
| * Copyright 2021 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 rbac |
| |
| import ( |
| "errors" |
| "fmt" |
| "net" |
| "regexp" |
| |
| v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" |
| v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" |
| v3route_componentspb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" |
| v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" |
| internalmatcher "google.golang.org/grpc/internal/xds/matcher" |
| ) |
| |
| // matcher is an interface that takes data about incoming RPC's and returns |
| // whether it matches with whatever matcher implements this interface. |
| type matcher interface { |
| match(data *rpcData) bool |
| } |
| |
| // policyMatcher helps determine whether an incoming RPC call matches a policy. |
| // A policy is a logical role (e.g. Service Admin), which is comprised of |
| // permissions and principals. A principal is an identity (or identities) for a |
| // downstream subject which are assigned the policy (role), and a permission is |
| // an action(s) that a principal(s) can take. A policy matches if both a |
| // permission and a principal match, which will be determined by the child or |
| // permissions and principal matchers. policyMatcher implements the matcher |
| // interface. |
| type policyMatcher struct { |
| permissions *orMatcher |
| principals *orMatcher |
| } |
| |
| func newPolicyMatcher(policy *v3rbacpb.Policy) (*policyMatcher, error) { |
| permissions, err := matchersFromPermissions(policy.Permissions) |
| if err != nil { |
| return nil, err |
| } |
| principals, err := matchersFromPrincipals(policy.Principals) |
| if err != nil { |
| return nil, err |
| } |
| return &policyMatcher{ |
| permissions: &orMatcher{matchers: permissions}, |
| principals: &orMatcher{matchers: principals}, |
| }, nil |
| } |
| |
| func (pm *policyMatcher) match(data *rpcData) bool { |
| // A policy matches if and only if at least one of its permissions match the |
| // action taking place AND at least one if its principals match the |
| // downstream peer. |
| return pm.permissions.match(data) && pm.principals.match(data) |
| } |
| |
| // matchersFromPermissions takes a list of permissions (can also be |
| // a single permission, e.g. from a not matcher which is logically !permission) |
| // and returns a list of matchers which correspond to that permission. This will |
| // be called in many instances throughout the initial construction of the RBAC |
| // engine from the AND and OR matchers and also from the NOT matcher. |
| func matchersFromPermissions(permissions []*v3rbacpb.Permission) ([]matcher, error) { |
| var matchers []matcher |
| for _, permission := range permissions { |
| switch permission.GetRule().(type) { |
| case *v3rbacpb.Permission_AndRules: |
| mList, err := matchersFromPermissions(permission.GetAndRules().Rules) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, &andMatcher{matchers: mList}) |
| case *v3rbacpb.Permission_OrRules: |
| mList, err := matchersFromPermissions(permission.GetOrRules().Rules) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, &orMatcher{matchers: mList}) |
| case *v3rbacpb.Permission_Any: |
| matchers = append(matchers, &alwaysMatcher{}) |
| case *v3rbacpb.Permission_Header: |
| m, err := newHeaderMatcher(permission.GetHeader()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Permission_UrlPath: |
| m, err := newURLPathMatcher(permission.GetUrlPath()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Permission_DestinationIp: |
| // Due to this being on server side, the destination IP is the local |
| // IP. |
| m, err := newLocalIPMatcher(permission.GetDestinationIp()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Permission_DestinationPort: |
| matchers = append(matchers, newPortMatcher(permission.GetDestinationPort())) |
| case *v3rbacpb.Permission_NotRule: |
| mList, err := matchersFromPermissions([]*v3rbacpb.Permission{{Rule: permission.GetNotRule().Rule}}) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, ¬Matcher{matcherToNot: mList[0]}) |
| case *v3rbacpb.Permission_Metadata: |
| // Never matches - so no-op if not inverted, always match if |
| // inverted. |
| if permission.GetMetadata().GetInvert() { // Test metadata being no-op and also metadata with invert always matching |
| matchers = append(matchers, &alwaysMatcher{}) |
| } |
| case *v3rbacpb.Permission_RequestedServerName: |
| // Not supported in gRPC RBAC currently - a permission typed as |
| // requested server name in the initial config will be a no-op. |
| } |
| } |
| return matchers, nil |
| } |
| |
| func matchersFromPrincipals(principals []*v3rbacpb.Principal) ([]matcher, error) { |
| var matchers []matcher |
| for _, principal := range principals { |
| switch principal.GetIdentifier().(type) { |
| case *v3rbacpb.Principal_AndIds: |
| mList, err := matchersFromPrincipals(principal.GetAndIds().Ids) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, &andMatcher{matchers: mList}) |
| case *v3rbacpb.Principal_OrIds: |
| mList, err := matchersFromPrincipals(principal.GetOrIds().Ids) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, &orMatcher{matchers: mList}) |
| case *v3rbacpb.Principal_Any: |
| matchers = append(matchers, &alwaysMatcher{}) |
| case *v3rbacpb.Principal_Authenticated_: |
| authenticatedMatcher, err := newAuthenticatedMatcher(principal.GetAuthenticated()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, authenticatedMatcher) |
| case *v3rbacpb.Principal_DirectRemoteIp: |
| m, err := newRemoteIPMatcher(principal.GetDirectRemoteIp()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Principal_Header: |
| // Do we need an error here? |
| m, err := newHeaderMatcher(principal.GetHeader()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Principal_UrlPath: |
| m, err := newURLPathMatcher(principal.GetUrlPath()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Principal_NotId: |
| mList, err := matchersFromPrincipals([]*v3rbacpb.Principal{{Identifier: principal.GetNotId().Identifier}}) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, ¬Matcher{matcherToNot: mList[0]}) |
| case *v3rbacpb.Principal_SourceIp: |
| // The source ip principal identifier is deprecated. Thus, a |
| // principal typed as a source ip in the identifier will be a no-op. |
| // The config should use DirectRemoteIp instead. |
| case *v3rbacpb.Principal_RemoteIp: |
| // RBAC in gRPC treats direct_remote_ip and remote_ip as logically |
| // equivalent, as per A41. |
| m, err := newRemoteIPMatcher(principal.GetRemoteIp()) |
| if err != nil { |
| return nil, err |
| } |
| matchers = append(matchers, m) |
| case *v3rbacpb.Principal_Metadata: |
| // Not supported in gRPC RBAC currently - a principal typed as |
| // Metadata in the initial config will be a no-op. |
| } |
| } |
| return matchers, nil |
| } |
| |
| // orMatcher is a matcher where it successfully matches if one of it's |
| // children successfully match. It also logically represents a principal or |
| // permission, but can also be it's own entity further down the tree of |
| // matchers. orMatcher implements the matcher interface. |
| type orMatcher struct { |
| matchers []matcher |
| } |
| |
| func (om *orMatcher) match(data *rpcData) bool { |
| // Range through child matchers and pass in data about incoming RPC, and |
| // only one child matcher has to match to be logically successful. |
| for _, m := range om.matchers { |
| if m.match(data) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // andMatcher is a matcher that is successful if every child matcher |
| // matches. andMatcher implements the matcher interface. |
| type andMatcher struct { |
| matchers []matcher |
| } |
| |
| func (am *andMatcher) match(data *rpcData) bool { |
| for _, m := range am.matchers { |
| if !m.match(data) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // alwaysMatcher is a matcher that will always match. This logically |
| // represents an any rule for a permission or a principal. alwaysMatcher |
| // implements the matcher interface. |
| type alwaysMatcher struct { |
| } |
| |
| func (am *alwaysMatcher) match(data *rpcData) bool { |
| return true |
| } |
| |
| // notMatcher is a matcher that nots an underlying matcher. notMatcher |
| // implements the matcher interface. |
| type notMatcher struct { |
| matcherToNot matcher |
| } |
| |
| func (nm *notMatcher) match(data *rpcData) bool { |
| return !nm.matcherToNot.match(data) |
| } |
| |
| // headerMatcher is a matcher that matches on incoming HTTP Headers present |
| // in the incoming RPC. headerMatcher implements the matcher interface. |
| type headerMatcher struct { |
| matcher internalmatcher.HeaderMatcher |
| } |
| |
| func newHeaderMatcher(headerMatcherConfig *v3route_componentspb.HeaderMatcher) (*headerMatcher, error) { |
| var m internalmatcher.HeaderMatcher |
| switch headerMatcherConfig.HeaderMatchSpecifier.(type) { |
| case *v3route_componentspb.HeaderMatcher_ExactMatch: |
| m = internalmatcher.NewHeaderExactMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetExactMatch(), headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_SafeRegexMatch: |
| regex, err := regexp.Compile(headerMatcherConfig.GetSafeRegexMatch().Regex) |
| if err != nil { |
| return nil, err |
| } |
| m = internalmatcher.NewHeaderRegexMatcher(headerMatcherConfig.Name, regex, headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_RangeMatch: |
| m = internalmatcher.NewHeaderRangeMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetRangeMatch().Start, headerMatcherConfig.GetRangeMatch().End, headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_PresentMatch: |
| m = internalmatcher.NewHeaderPresentMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetPresentMatch(), headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_PrefixMatch: |
| m = internalmatcher.NewHeaderPrefixMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetPrefixMatch(), headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_SuffixMatch: |
| m = internalmatcher.NewHeaderSuffixMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetSuffixMatch(), headerMatcherConfig.InvertMatch) |
| case *v3route_componentspb.HeaderMatcher_ContainsMatch: |
| m = internalmatcher.NewHeaderContainsMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetContainsMatch(), headerMatcherConfig.InvertMatch) |
| default: |
| return nil, errors.New("unknown header matcher type") |
| } |
| return &headerMatcher{matcher: m}, nil |
| } |
| |
| func (hm *headerMatcher) match(data *rpcData) bool { |
| return hm.matcher.Match(data.md) |
| } |
| |
| // urlPathMatcher matches on the URL Path of the incoming RPC. In gRPC, this |
| // logically maps to the full method name the RPC is calling on the server side. |
| // urlPathMatcher implements the matcher interface. |
| type urlPathMatcher struct { |
| stringMatcher internalmatcher.StringMatcher |
| } |
| |
| func newURLPathMatcher(pathMatcher *v3matcherpb.PathMatcher) (*urlPathMatcher, error) { |
| stringMatcher, err := internalmatcher.StringMatcherFromProto(pathMatcher.GetPath()) |
| if err != nil { |
| return nil, err |
| } |
| return &urlPathMatcher{stringMatcher: stringMatcher}, nil |
| } |
| |
| func (upm *urlPathMatcher) match(data *rpcData) bool { |
| return upm.stringMatcher.Match(data.fullMethod) |
| } |
| |
| // remoteIPMatcher and localIPMatcher both are matchers that match against |
| // a CIDR Range. Two different matchers are needed as the remote and destination |
| // ip addresses come from different parts of the data about incoming RPC's |
| // passed in. Matching a CIDR Range means to determine whether the IP Address |
| // falls within the CIDR Range or not. They both implement the matcher |
| // interface. |
| type remoteIPMatcher struct { |
| // ipNet represents the CidrRange that this matcher was configured with. |
| // This is what will remote and destination IP's will be matched against. |
| ipNet *net.IPNet |
| } |
| |
| func newRemoteIPMatcher(cidrRange *v3corepb.CidrRange) (*remoteIPMatcher, error) { |
| // Convert configuration to a cidrRangeString, as Go standard library has |
| // methods that parse cidr string. |
| cidrRangeString := fmt.Sprintf("%s/%d", cidrRange.AddressPrefix, cidrRange.PrefixLen.Value) |
| _, ipNet, err := net.ParseCIDR(cidrRangeString) |
| if err != nil { |
| return nil, err |
| } |
| return &remoteIPMatcher{ipNet: ipNet}, nil |
| } |
| |
| func (sim *remoteIPMatcher) match(data *rpcData) bool { |
| return sim.ipNet.Contains(net.IP(net.ParseIP(data.peerInfo.Addr.String()))) |
| } |
| |
| type localIPMatcher struct { |
| ipNet *net.IPNet |
| } |
| |
| func newLocalIPMatcher(cidrRange *v3corepb.CidrRange) (*localIPMatcher, error) { |
| cidrRangeString := fmt.Sprintf("%s/%d", cidrRange.AddressPrefix, cidrRange.PrefixLen.Value) |
| _, ipNet, err := net.ParseCIDR(cidrRangeString) |
| if err != nil { |
| return nil, err |
| } |
| return &localIPMatcher{ipNet: ipNet}, nil |
| } |
| |
| func (dim *localIPMatcher) match(data *rpcData) bool { |
| return dim.ipNet.Contains(net.IP(net.ParseIP(data.localAddr.String()))) |
| } |
| |
| // portMatcher matches on whether the destination port of the RPC matches the |
| // destination port this matcher was instantiated with. portMatcher |
| // implements the matcher interface. |
| type portMatcher struct { |
| destinationPort uint32 |
| } |
| |
| func newPortMatcher(destinationPort uint32) *portMatcher { |
| return &portMatcher{destinationPort: destinationPort} |
| } |
| |
| func (pm *portMatcher) match(data *rpcData) bool { |
| return data.destinationPort == pm.destinationPort |
| } |
| |
| // authenticatedMatcher matches on the name of the Principal. If set, the URI |
| // SAN or DNS SAN in that order is used from the certificate, otherwise the |
| // subject field is used. If unset, it applies to any user that is |
| // authenticated. authenticatedMatcher implements the matcher interface. |
| type authenticatedMatcher struct { |
| stringMatcher *internalmatcher.StringMatcher |
| } |
| |
| func newAuthenticatedMatcher(authenticatedMatcherConfig *v3rbacpb.Principal_Authenticated) (*authenticatedMatcher, error) { |
| // Represents this line in the RBAC documentation = "If unset, it applies to |
| // any user that is authenticated" (see package-level comments). |
| if authenticatedMatcherConfig.PrincipalName == nil { |
| return &authenticatedMatcher{}, nil |
| } |
| stringMatcher, err := internalmatcher.StringMatcherFromProto(authenticatedMatcherConfig.PrincipalName) |
| if err != nil { |
| return nil, err |
| } |
| return &authenticatedMatcher{stringMatcher: &stringMatcher}, nil |
| } |
| |
| func (am *authenticatedMatcher) match(data *rpcData) bool { |
| if data.authType != "tls" { |
| // Connection is not authenticated. |
| return false |
| } |
| if am.stringMatcher == nil { |
| // Allows any authenticated user. |
| return true |
| } |
| // "If there is no client certificate (thus no SAN nor Subject), check if "" |
| // (empty string) matches. If it matches, the principal_name is said to |
| // match" - A41 |
| if len(data.certs) == 0 { |
| return am.stringMatcher.Match("") |
| } |
| cert := data.certs[0] |
| // The order of matching as per the RBAC documentation (see package-level comments) |
| // is as follows: URI SANs, DNS SANs, and then subject name. |
| for _, uriSAN := range cert.URIs { |
| if am.stringMatcher.Match(uriSAN.String()) { |
| return true |
| } |
| } |
| for _, dnsSAN := range cert.DNSNames { |
| if am.stringMatcher.Match(dnsSAN) { |
| return true |
| } |
| } |
| return am.stringMatcher.Match(cert.Subject.String()) |
| } |