github.com/looshlee/beatles@v0.0.0-20220727174639-742810ab631c/pkg/kafka/policy.go (about)

     1  // Copyright 2017 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kafka
    16  
    17  import (
    18  	"github.com/cilium/cilium/pkg/flowdebug"
    19  	"github.com/cilium/cilium/pkg/policy/api"
    20  
    21  	"github.com/optiopay/kafka/proto"
    22  	"github.com/sirupsen/logrus"
    23  )
    24  
    25  // isTopicAPIKey returns true if kind is apiKey message type which contains a
    26  // topic in its request.
    27  func isTopicAPIKey(kind int16) bool {
    28  	switch kind {
    29  	case api.ProduceKey,
    30  		api.FetchKey,
    31  		api.OffsetsKey,
    32  		api.MetadataKey,
    33  		api.LeaderAndIsr,
    34  		api.StopReplica,
    35  		api.UpdateMetadata,
    36  		api.OffsetCommitKey,
    37  		api.OffsetFetchKey,
    38  		api.CreateTopicsKey,
    39  		api.DeleteTopicsKey,
    40  		api.DeleteRecordsKey,
    41  		api.OffsetForLeaderEpochKey,
    42  		api.AddPartitionsToTxnKey,
    43  		api.WriteTxnMarkersKey,
    44  		api.TxnOffsetCommitKey,
    45  		api.AlterReplicaLogDirsKey,
    46  		api.DescribeLogDirsKey,
    47  		api.CreatePartitionsKey:
    48  
    49  		return true
    50  	}
    51  	return false
    52  }
    53  
    54  func matchNonTopicRequests(req *RequestMessage, rule api.PortRuleKafka) bool {
    55  	// matchNonTopicRequests() is called when
    56  	// the kafka parser was not able to parse beyond the generic header.
    57  	// This could be due to 2 sceanrios:
    58  	// 1. It was a non-topic request
    59  	// 2. The parser could not parse further even if there was a topic present.
    60  	// For scenario 2, if topic is present, we need to return
    61  	// false since topic can never be associated with this request kind.
    62  	if rule.Topic != "" && isTopicAPIKey(req.kind) {
    63  		return false
    64  	}
    65  	// TODO add functionality for parsing clientID GH-3097
    66  	//if rule.ClientID != "" && rule.ClientID != req.GetClientID() {
    67  	//	return false
    68  	//}
    69  	return true
    70  }
    71  
    72  func matchProduceReq(req *proto.ProduceReq, rule api.PortRuleKafka) bool {
    73  	if req == nil {
    74  		return false
    75  	}
    76  
    77  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
    78  		return false
    79  	}
    80  
    81  	return true
    82  }
    83  
    84  func matchFetchReq(req *proto.FetchReq, rule api.PortRuleKafka) bool {
    85  	if req == nil {
    86  		return false
    87  	}
    88  
    89  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
    90  		return false
    91  	}
    92  
    93  	return true
    94  }
    95  
    96  func matchOffsetReq(req *proto.OffsetReq, rule api.PortRuleKafka) bool {
    97  	if req == nil {
    98  		return false
    99  	}
   100  
   101  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
   102  		return false
   103  	}
   104  
   105  	return true
   106  }
   107  
   108  func matchMetadataReq(req *proto.MetadataReq, rule api.PortRuleKafka) bool {
   109  	if req == nil {
   110  		return false
   111  	}
   112  
   113  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
   114  		return false
   115  	}
   116  
   117  	return true
   118  }
   119  
   120  func matchOffsetCommitReq(req *proto.OffsetCommitReq, rule api.PortRuleKafka) bool {
   121  	if req == nil {
   122  		return false
   123  	}
   124  
   125  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
   126  		return false
   127  	}
   128  
   129  	return true
   130  }
   131  
   132  func matchOffsetFetchReq(req *proto.OffsetFetchReq, rule api.PortRuleKafka) bool {
   133  	if req == nil {
   134  		return false
   135  	}
   136  
   137  	if rule.ClientID != "" && rule.ClientID != req.ClientID {
   138  		return false
   139  	}
   140  
   141  	return true
   142  }
   143  
   144  func (req *RequestMessage) ruleMatches(rule api.PortRuleKafka) bool {
   145  	if req == nil {
   146  		return false
   147  	}
   148  
   149  	flowdebug.Log(log.WithFields(logrus.Fields{
   150  		fieldRequest: req.String(),
   151  		fieldRule:    rule,
   152  	}), "Matching Kafka rule")
   153  
   154  	if !rule.CheckAPIKeyRole(req.kind) {
   155  		return false
   156  	}
   157  
   158  	apiVersion, isWildcard := rule.GetAPIVersion()
   159  	if !isWildcard && apiVersion != req.version {
   160  		return false
   161  	}
   162  
   163  	// If the rule contains no additional conditionals, it is not required
   164  	// to match into the request specific fields.
   165  	if rule.Topic == "" && rule.ClientID == "" {
   166  		return true
   167  	}
   168  
   169  	switch val := req.request.(type) {
   170  	case *proto.ProduceReq:
   171  		return matchProduceReq(val, rule)
   172  	case *proto.FetchReq:
   173  		return matchFetchReq(val, rule)
   174  	case *proto.OffsetReq:
   175  		return matchOffsetReq(val, rule)
   176  	case *proto.MetadataReq:
   177  		return matchMetadataReq(val, rule)
   178  	case *proto.OffsetCommitReq:
   179  		return matchOffsetCommitReq(val, rule)
   180  	case *proto.OffsetFetchReq:
   181  		return matchOffsetFetchReq(val, rule)
   182  	case *proto.ConsumerMetadataReq:
   183  		return true
   184  	case nil:
   185  		// This is the case when requests like
   186  		// heartbeat,findcordinator, et al
   187  		// are specified. They are not
   188  		// associated with a topic, but we should
   189  		// still check for ClientID present in request header.
   190  		return matchNonTopicRequests(req, rule)
   191  	default:
   192  		// If all conditions have been met, allow the request
   193  		return true
   194  	}
   195  }
   196  
   197  // MatchesRule validates the Kafka request message against the provided list of
   198  // rules. The function will return true if the policy allows the message,
   199  // otherwise false is returned.
   200  func (req *RequestMessage) MatchesRule(rules []api.PortRuleKafka) bool {
   201  	topics := req.GetTopics()
   202  	// Maintain a map of all topics in the request.
   203  	// We should allow the request only if all topics are
   204  	// allowed by the list of rules.
   205  	reqTopicsMap := make(map[string]bool, len(topics))
   206  	for _, topic := range topics {
   207  		reqTopicsMap[topic] = true
   208  	}
   209  
   210  	for _, rule := range rules {
   211  		if rule.Topic == "" || len(topics) == 0 {
   212  			if req.ruleMatches(rule) {
   213  				return true
   214  			}
   215  		} else if reqTopicsMap[rule.Topic] {
   216  			if req.ruleMatches(rule) {
   217  				delete(reqTopicsMap, rule.Topic)
   218  				if len(reqTopicsMap) == 0 {
   219  					return true
   220  				}
   221  			}
   222  		}
   223  	}
   224  	return false
   225  }