github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/config.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package event
    19  
    20  import (
    21  	"encoding/xml"
    22  	"errors"
    23  	"io"
    24  	"reflect"
    25  	"strings"
    26  	"unicode/utf8"
    27  
    28  	"github.com/minio/minio-go/v7/pkg/set"
    29  )
    30  
    31  // ValidateFilterRuleValue - checks if given value is filter rule value or not.
    32  func ValidateFilterRuleValue(value string) error {
    33  	for _, segment := range strings.Split(value, "/") {
    34  		if segment == "." || segment == ".." {
    35  			return &ErrInvalidFilterValue{value}
    36  		}
    37  	}
    38  
    39  	if len(value) <= 1024 && utf8.ValidString(value) && !strings.Contains(value, `\`) {
    40  		return nil
    41  	}
    42  
    43  	return &ErrInvalidFilterValue{value}
    44  }
    45  
    46  // FilterRule - represents elements inside <FilterRule>...</FilterRule>
    47  type FilterRule struct {
    48  	Name  string `xml:"Name"`
    49  	Value string `xml:"Value"`
    50  }
    51  
    52  func (filter FilterRule) isEmpty() bool {
    53  	return filter.Name == "" && filter.Value == ""
    54  }
    55  
    56  // MarshalXML implements a custom marshaller to support `omitempty` feature.
    57  func (filter FilterRule) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
    58  	if filter.isEmpty() {
    59  		return nil
    60  	}
    61  	type filterRuleWrapper FilterRule
    62  	return e.EncodeElement(filterRuleWrapper(filter), start)
    63  }
    64  
    65  // UnmarshalXML - decodes XML data.
    66  func (filter *FilterRule) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    67  	// Make subtype to avoid recursive UnmarshalXML().
    68  	type filterRule FilterRule
    69  	rule := filterRule{}
    70  	if err := d.DecodeElement(&rule, &start); err != nil {
    71  		return err
    72  	}
    73  
    74  	if rule.Name != "prefix" && rule.Name != "suffix" {
    75  		return &ErrInvalidFilterName{rule.Name}
    76  	}
    77  
    78  	if err := ValidateFilterRuleValue(filter.Value); err != nil {
    79  		return err
    80  	}
    81  
    82  	*filter = FilterRule(rule)
    83  
    84  	return nil
    85  }
    86  
    87  // FilterRuleList - represents multiple <FilterRule>...</FilterRule>
    88  type FilterRuleList struct {
    89  	Rules []FilterRule `xml:"FilterRule,omitempty"`
    90  }
    91  
    92  // UnmarshalXML - decodes XML data.
    93  func (ruleList *FilterRuleList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    94  	// Make subtype to avoid recursive UnmarshalXML().
    95  	type filterRuleList FilterRuleList
    96  	rules := filterRuleList{}
    97  	if err := d.DecodeElement(&rules, &start); err != nil {
    98  		return err
    99  	}
   100  
   101  	// FilterRuleList must have only one prefix and/or suffix.
   102  	nameSet := set.NewStringSet()
   103  	for _, rule := range rules.Rules {
   104  		if nameSet.Contains(rule.Name) {
   105  			if rule.Name == "prefix" {
   106  				return &ErrFilterNamePrefix{}
   107  			}
   108  
   109  			return &ErrFilterNameSuffix{}
   110  		}
   111  
   112  		nameSet.Add(rule.Name)
   113  	}
   114  
   115  	*ruleList = FilterRuleList(rules)
   116  	return nil
   117  }
   118  
   119  func (ruleList FilterRuleList) isEmpty() bool {
   120  	return len(ruleList.Rules) == 0
   121  }
   122  
   123  // Pattern - returns pattern using prefix and suffix values.
   124  func (ruleList FilterRuleList) Pattern() string {
   125  	var prefix string
   126  	var suffix string
   127  
   128  	for _, rule := range ruleList.Rules {
   129  		switch rule.Name {
   130  		case "prefix":
   131  			prefix = rule.Value
   132  		case "suffix":
   133  			suffix = rule.Value
   134  		}
   135  	}
   136  
   137  	return NewPattern(prefix, suffix)
   138  }
   139  
   140  // S3Key - represents elements inside <S3Key>...</S3Key>
   141  type S3Key struct {
   142  	RuleList FilterRuleList `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
   143  }
   144  
   145  // MarshalXML implements a custom marshaller to support `omitempty` feature.
   146  func (s3Key S3Key) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
   147  	if s3Key.RuleList.isEmpty() {
   148  		return nil
   149  	}
   150  	type s3KeyWrapper S3Key
   151  	return e.EncodeElement(s3KeyWrapper(s3Key), start)
   152  }
   153  
   154  // common - represents common elements inside <QueueConfiguration>, <CloudFunctionConfiguration>
   155  // and <TopicConfiguration>
   156  type common struct {
   157  	ID     string `xml:"Id" json:"Id"`
   158  	Filter S3Key  `xml:"Filter" json:"Filter"`
   159  	Events []Name `xml:"Event" json:"Event"`
   160  }
   161  
   162  // Queue - represents elements inside <QueueConfiguration>
   163  type Queue struct {
   164  	common
   165  	ARN ARN `xml:"Queue"`
   166  }
   167  
   168  // UnmarshalXML - decodes XML data.
   169  func (q *Queue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
   170  	// Make subtype to avoid recursive UnmarshalXML().
   171  	type queue Queue
   172  	parsedQueue := queue{}
   173  	if err := d.DecodeElement(&parsedQueue, &start); err != nil {
   174  		return err
   175  	}
   176  
   177  	if len(parsedQueue.Events) == 0 {
   178  		return errors.New("missing event name(s)")
   179  	}
   180  
   181  	eventStringSet := set.NewStringSet()
   182  	for _, eventName := range parsedQueue.Events {
   183  		if eventStringSet.Contains(eventName.String()) {
   184  			return &ErrDuplicateEventName{eventName}
   185  		}
   186  
   187  		eventStringSet.Add(eventName.String())
   188  	}
   189  
   190  	*q = Queue(parsedQueue)
   191  
   192  	return nil
   193  }
   194  
   195  // Validate - checks whether queue has valid values or not.
   196  func (q Queue) Validate(region string, targetList *TargetList) error {
   197  	if q.ARN.region == "" {
   198  		if !targetList.Exists(q.ARN.TargetID) {
   199  			return &ErrARNNotFound{q.ARN}
   200  		}
   201  		return nil
   202  	}
   203  
   204  	if region != "" && q.ARN.region != region {
   205  		return &ErrUnknownRegion{q.ARN.region}
   206  	}
   207  
   208  	if !targetList.Exists(q.ARN.TargetID) {
   209  		return &ErrARNNotFound{q.ARN}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  // SetRegion - sets region value to queue's ARN.
   216  func (q *Queue) SetRegion(region string) {
   217  	q.ARN.region = region
   218  }
   219  
   220  // ToRulesMap - converts Queue to RulesMap
   221  func (q Queue) ToRulesMap() RulesMap {
   222  	pattern := q.Filter.RuleList.Pattern()
   223  	return NewRulesMap(q.Events, pattern, q.ARN.TargetID)
   224  }
   225  
   226  // Unused.  Available for completion.
   227  type lambda struct {
   228  	ARN string `xml:"CloudFunction"`
   229  }
   230  
   231  // Unused. Available for completion.
   232  type topic struct {
   233  	ARN string `xml:"Topic" json:"Topic"`
   234  }
   235  
   236  // Config - notification configuration described in
   237  // http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
   238  type Config struct {
   239  	XMLNS      string   `xml:"xmlns,attr,omitempty"`
   240  	XMLName    xml.Name `xml:"NotificationConfiguration"`
   241  	QueueList  []Queue  `xml:"QueueConfiguration,omitempty"`
   242  	LambdaList []lambda `xml:"CloudFunctionConfiguration,omitempty"`
   243  	TopicList  []topic  `xml:"TopicConfiguration,omitempty"`
   244  }
   245  
   246  // UnmarshalXML - decodes XML data.
   247  func (conf *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
   248  	// Make subtype to avoid recursive UnmarshalXML().
   249  	type config Config
   250  	parsedConfig := config{}
   251  	if err := d.DecodeElement(&parsedConfig, &start); err != nil {
   252  		return err
   253  	}
   254  
   255  	// Empty queue list means user wants to delete the notification configuration.
   256  	if len(parsedConfig.QueueList) > 0 {
   257  		for i, q1 := range parsedConfig.QueueList[:len(parsedConfig.QueueList)-1] {
   258  			for _, q2 := range parsedConfig.QueueList[i+1:] {
   259  				// Removes the region from ARN if server region is not set
   260  				if q2.ARN.region != "" && q1.ARN.region == "" {
   261  					q2.ARN.region = ""
   262  				}
   263  				if reflect.DeepEqual(q1, q2) {
   264  					return &ErrDuplicateQueueConfiguration{q1}
   265  				}
   266  			}
   267  		}
   268  	}
   269  
   270  	if len(parsedConfig.LambdaList) > 0 || len(parsedConfig.TopicList) > 0 {
   271  		return &ErrUnsupportedConfiguration{}
   272  	}
   273  
   274  	*conf = Config(parsedConfig)
   275  
   276  	return nil
   277  }
   278  
   279  // Validate - checks whether config has valid values or not.
   280  func (conf Config) Validate(region string, targetList *TargetList) error {
   281  	for _, queue := range conf.QueueList {
   282  		if err := queue.Validate(region, targetList); err != nil {
   283  			return err
   284  		}
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // SetRegion - sets region to all queue configuration.
   291  func (conf *Config) SetRegion(region string) {
   292  	for i := range conf.QueueList {
   293  		conf.QueueList[i].SetRegion(region)
   294  	}
   295  }
   296  
   297  // ToRulesMap - converts all queue configuration to RulesMap.
   298  func (conf *Config) ToRulesMap() RulesMap {
   299  	rulesMap := make(RulesMap)
   300  
   301  	for _, queue := range conf.QueueList {
   302  		rulesMap.Add(queue.ToRulesMap())
   303  	}
   304  
   305  	return rulesMap
   306  }
   307  
   308  // ParseConfig - parses data in reader to notification configuration.
   309  func ParseConfig(reader io.Reader, region string, targetList *TargetList) (*Config, error) {
   310  	var config Config
   311  
   312  	if err := xml.NewDecoder(reader).Decode(&config); err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	if err := config.Validate(region, targetList); err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	config.SetRegion(region)
   321  	// If xml namespace is empty, set a default value before returning.
   322  	if config.XMLNS == "" {
   323  		config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
   324  	}
   325  	return &config, nil
   326  }