123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- /*
- * 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())
- }
|