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 }