1/*
2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
3 * Copyright 2020 MinIO, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package notification
19
20import (
21	"encoding/xml"
22	"errors"
23	"fmt"
24
25	"github.com/minio/minio-go/v7/pkg/set"
26)
27
28// EventType is a S3 notification event associated to the bucket notification configuration
29type EventType string
30
31// The role of all event types are described in :
32// 	http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations
33const (
34	ObjectCreatedAll                     EventType = "s3:ObjectCreated:*"
35	ObjectCreatedPut                               = "s3:ObjectCreated:Put"
36	ObjectCreatedPost                              = "s3:ObjectCreated:Post"
37	ObjectCreatedCopy                              = "s3:ObjectCreated:Copy"
38	ObjectCreatedCompleteMultipartUpload           = "s3:ObjectCreated:CompleteMultipartUpload"
39	ObjectAccessedGet                              = "s3:ObjectAccessed:Get"
40	ObjectAccessedHead                             = "s3:ObjectAccessed:Head"
41	ObjectAccessedAll                              = "s3:ObjectAccessed:*"
42	ObjectRemovedAll                               = "s3:ObjectRemoved:*"
43	ObjectRemovedDelete                            = "s3:ObjectRemoved:Delete"
44	ObjectRemovedDeleteMarkerCreated               = "s3:ObjectRemoved:DeleteMarkerCreated"
45	ObjectReducedRedundancyLostObject              = "s3:ReducedRedundancyLostObject"
46	BucketCreatedAll                               = "s3:BucketCreated:*"
47	BucketRemovedAll                               = "s3:BucketRemoved:*"
48)
49
50// FilterRule - child of S3Key, a tag in the notification xml which
51// carries suffix/prefix filters
52type FilterRule struct {
53	Name  string `xml:"Name"`
54	Value string `xml:"Value"`
55}
56
57// S3Key - child of Filter, a tag in the notification xml which
58// carries suffix/prefix filters
59type S3Key struct {
60	FilterRules []FilterRule `xml:"FilterRule,omitempty"`
61}
62
63// Filter - a tag in the notification xml structure which carries
64// suffix/prefix filters
65type Filter struct {
66	S3Key S3Key `xml:"S3Key,omitempty"`
67}
68
69// Arn - holds ARN information that will be sent to the web service,
70// ARN desciption can be found in http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
71type Arn struct {
72	Partition string
73	Service   string
74	Region    string
75	AccountID string
76	Resource  string
77}
78
79// NewArn creates new ARN based on the given partition, service, region, account id and resource
80func NewArn(partition, service, region, accountID, resource string) Arn {
81	return Arn{Partition: partition,
82		Service:   service,
83		Region:    region,
84		AccountID: accountID,
85		Resource:  resource}
86}
87
88// String returns the string format of the ARN
89func (arn Arn) String() string {
90	return "arn:" + arn.Partition + ":" + arn.Service + ":" + arn.Region + ":" + arn.AccountID + ":" + arn.Resource
91}
92
93// Config - represents one single notification configuration
94// such as topic, queue or lambda configuration.
95type Config struct {
96	ID     string      `xml:"Id,omitempty"`
97	Arn    Arn         `xml:"-"`
98	Events []EventType `xml:"Event"`
99	Filter *Filter     `xml:"Filter,omitempty"`
100}
101
102// NewConfig creates one notification config and sets the given ARN
103func NewConfig(arn Arn) Config {
104	return Config{Arn: arn, Filter: &Filter{}}
105}
106
107// AddEvents adds one event to the current notification config
108func (t *Config) AddEvents(events ...EventType) {
109	t.Events = append(t.Events, events...)
110}
111
112// AddFilterSuffix sets the suffix configuration to the current notification config
113func (t *Config) AddFilterSuffix(suffix string) {
114	if t.Filter == nil {
115		t.Filter = &Filter{}
116	}
117	newFilterRule := FilterRule{Name: "suffix", Value: suffix}
118	// Replace any suffix rule if existing and add to the list otherwise
119	for index := range t.Filter.S3Key.FilterRules {
120		if t.Filter.S3Key.FilterRules[index].Name == "suffix" {
121			t.Filter.S3Key.FilterRules[index] = newFilterRule
122			return
123		}
124	}
125	t.Filter.S3Key.FilterRules = append(t.Filter.S3Key.FilterRules, newFilterRule)
126}
127
128// AddFilterPrefix sets the prefix configuration to the current notification config
129func (t *Config) AddFilterPrefix(prefix string) {
130	if t.Filter == nil {
131		t.Filter = &Filter{}
132	}
133	newFilterRule := FilterRule{Name: "prefix", Value: prefix}
134	// Replace any prefix rule if existing and add to the list otherwise
135	for index := range t.Filter.S3Key.FilterRules {
136		if t.Filter.S3Key.FilterRules[index].Name == "prefix" {
137			t.Filter.S3Key.FilterRules[index] = newFilterRule
138			return
139		}
140	}
141	t.Filter.S3Key.FilterRules = append(t.Filter.S3Key.FilterRules, newFilterRule)
142}
143
144// EqualEventTypeList tells whether a and b contain the same events
145func EqualEventTypeList(a, b []EventType) bool {
146	if len(a) != len(b) {
147		return false
148	}
149	setA := set.NewStringSet()
150	for _, i := range a {
151		setA.Add(string(i))
152	}
153
154	setB := set.NewStringSet()
155	for _, i := range b {
156		setB.Add(string(i))
157	}
158
159	return setA.Difference(setB).IsEmpty()
160}
161
162// EqualFilterRuleList tells whether a and b contain the same filters
163func EqualFilterRuleList(a, b []FilterRule) bool {
164	if len(a) != len(b) {
165		return false
166	}
167
168	setA := set.NewStringSet()
169	for _, i := range a {
170		setA.Add(fmt.Sprintf("%s-%s", i.Name, i.Value))
171	}
172
173	setB := set.NewStringSet()
174	for _, i := range b {
175		setB.Add(fmt.Sprintf("%s-%s", i.Name, i.Value))
176	}
177
178	return setA.Difference(setB).IsEmpty()
179}
180
181// Equal returns whether this `Config` is equal to another defined by the passed parameters
182func (t *Config) Equal(events []EventType, prefix, suffix string) bool {
183	if t == nil {
184		return false
185	}
186
187	// Compare events
188	passEvents := EqualEventTypeList(t.Events, events)
189
190	// Compare filters
191	var newFilterRules []FilterRule
192	if prefix != "" {
193		newFilterRules = append(newFilterRules, FilterRule{Name: "prefix", Value: prefix})
194	}
195	if suffix != "" {
196		newFilterRules = append(newFilterRules, FilterRule{Name: "suffix", Value: suffix})
197	}
198
199	var currentFilterRules []FilterRule
200	if t.Filter != nil {
201		currentFilterRules = t.Filter.S3Key.FilterRules
202	}
203
204	passFilters := EqualFilterRuleList(currentFilterRules, newFilterRules)
205	return passEvents && passFilters
206}
207
208// TopicConfig carries one single topic notification configuration
209type TopicConfig struct {
210	Config
211	Topic string `xml:"Topic"`
212}
213
214// QueueConfig carries one single queue notification configuration
215type QueueConfig struct {
216	Config
217	Queue string `xml:"Queue"`
218}
219
220// LambdaConfig carries one single cloudfunction notification configuration
221type LambdaConfig struct {
222	Config
223	Lambda string `xml:"CloudFunction"`
224}
225
226// Configuration - the struct that represents the whole XML to be sent to the web service
227type Configuration struct {
228	XMLName       xml.Name       `xml:"NotificationConfiguration"`
229	LambdaConfigs []LambdaConfig `xml:"CloudFunctionConfiguration"`
230	TopicConfigs  []TopicConfig  `xml:"TopicConfiguration"`
231	QueueConfigs  []QueueConfig  `xml:"QueueConfiguration"`
232}
233
234// AddTopic adds a given topic config to the general bucket notification config
235func (b *Configuration) AddTopic(topicConfig Config) bool {
236	newTopicConfig := TopicConfig{Config: topicConfig, Topic: topicConfig.Arn.String()}
237	for _, n := range b.TopicConfigs {
238		// If new config matches existing one
239		if n.Topic == newTopicConfig.Arn.String() && newTopicConfig.Filter == n.Filter {
240
241			existingConfig := set.NewStringSet()
242			for _, v := range n.Events {
243				existingConfig.Add(string(v))
244			}
245
246			newConfig := set.NewStringSet()
247			for _, v := range topicConfig.Events {
248				newConfig.Add(string(v))
249			}
250
251			if !newConfig.Intersection(existingConfig).IsEmpty() {
252				return false
253			}
254		}
255	}
256	b.TopicConfigs = append(b.TopicConfigs, newTopicConfig)
257	return true
258}
259
260// AddQueue adds a given queue config to the general bucket notification config
261func (b *Configuration) AddQueue(queueConfig Config) bool {
262	newQueueConfig := QueueConfig{Config: queueConfig, Queue: queueConfig.Arn.String()}
263	for _, n := range b.QueueConfigs {
264		if n.Queue == newQueueConfig.Arn.String() && newQueueConfig.Filter == n.Filter {
265
266			existingConfig := set.NewStringSet()
267			for _, v := range n.Events {
268				existingConfig.Add(string(v))
269			}
270
271			newConfig := set.NewStringSet()
272			for _, v := range queueConfig.Events {
273				newConfig.Add(string(v))
274			}
275
276			if !newConfig.Intersection(existingConfig).IsEmpty() {
277				return false
278			}
279		}
280	}
281	b.QueueConfigs = append(b.QueueConfigs, newQueueConfig)
282	return true
283}
284
285// AddLambda adds a given lambda config to the general bucket notification config
286func (b *Configuration) AddLambda(lambdaConfig Config) bool {
287	newLambdaConfig := LambdaConfig{Config: lambdaConfig, Lambda: lambdaConfig.Arn.String()}
288	for _, n := range b.LambdaConfigs {
289		if n.Lambda == newLambdaConfig.Arn.String() && newLambdaConfig.Filter == n.Filter {
290
291			existingConfig := set.NewStringSet()
292			for _, v := range n.Events {
293				existingConfig.Add(string(v))
294			}
295
296			newConfig := set.NewStringSet()
297			for _, v := range lambdaConfig.Events {
298				newConfig.Add(string(v))
299			}
300
301			if !newConfig.Intersection(existingConfig).IsEmpty() {
302				return false
303			}
304		}
305	}
306	b.LambdaConfigs = append(b.LambdaConfigs, newLambdaConfig)
307	return true
308}
309
310// RemoveTopicByArn removes all topic configurations that match the exact specified ARN
311func (b *Configuration) RemoveTopicByArn(arn Arn) {
312	var topics []TopicConfig
313	for _, topic := range b.TopicConfigs {
314		if topic.Topic != arn.String() {
315			topics = append(topics, topic)
316		}
317	}
318	b.TopicConfigs = topics
319}
320
321// ErrNoConfigMatch is returned when a notification configuration (sqs,sns,lambda) is not found when trying to delete
322var ErrNoConfigMatch = errors.New("no notification configuration matched")
323
324// RemoveTopicByArnEventsPrefixSuffix removes a topic configuration that match the exact specified ARN, events, prefix and suffix
325func (b *Configuration) RemoveTopicByArnEventsPrefixSuffix(arn Arn, events []EventType, prefix, suffix string) error {
326	removeIndex := -1
327	for i, v := range b.TopicConfigs {
328		// if it matches events and filters, mark the index for deletion
329		if v.Topic == arn.String() && v.Config.Equal(events, prefix, suffix) {
330			removeIndex = i
331			break // since we have at most one matching config
332		}
333	}
334	if removeIndex >= 0 {
335		b.TopicConfigs = append(b.TopicConfigs[:removeIndex], b.TopicConfigs[removeIndex+1:]...)
336		return nil
337	}
338	return ErrNoConfigMatch
339}
340
341// RemoveQueueByArn removes all queue configurations that match the exact specified ARN
342func (b *Configuration) RemoveQueueByArn(arn Arn) {
343	var queues []QueueConfig
344	for _, queue := range b.QueueConfigs {
345		if queue.Queue != arn.String() {
346			queues = append(queues, queue)
347		}
348	}
349	b.QueueConfigs = queues
350}
351
352// RemoveQueueByArnEventsPrefixSuffix removes a queue configuration that match the exact specified ARN, events, prefix and suffix
353func (b *Configuration) RemoveQueueByArnEventsPrefixSuffix(arn Arn, events []EventType, prefix, suffix string) error {
354	removeIndex := -1
355	for i, v := range b.QueueConfigs {
356		// if it matches events and filters, mark the index for deletion
357		if v.Queue == arn.String() && v.Config.Equal(events, prefix, suffix) {
358			removeIndex = i
359			break // since we have at most one matching config
360		}
361	}
362	if removeIndex >= 0 {
363		b.QueueConfigs = append(b.QueueConfigs[:removeIndex], b.QueueConfigs[removeIndex+1:]...)
364		return nil
365	}
366	return ErrNoConfigMatch
367}
368
369// RemoveLambdaByArn removes all lambda configurations that match the exact specified ARN
370func (b *Configuration) RemoveLambdaByArn(arn Arn) {
371	var lambdas []LambdaConfig
372	for _, lambda := range b.LambdaConfigs {
373		if lambda.Lambda != arn.String() {
374			lambdas = append(lambdas, lambda)
375		}
376	}
377	b.LambdaConfigs = lambdas
378}
379
380// RemoveLambdaByArnEventsPrefixSuffix removes a topic configuration that match the exact specified ARN, events, prefix and suffix
381func (b *Configuration) RemoveLambdaByArnEventsPrefixSuffix(arn Arn, events []EventType, prefix, suffix string) error {
382	removeIndex := -1
383	for i, v := range b.LambdaConfigs {
384		// if it matches events and filters, mark the index for deletion
385		if v.Lambda == arn.String() && v.Config.Equal(events, prefix, suffix) {
386			removeIndex = i
387			break // since we have at most one matching config
388		}
389	}
390	if removeIndex >= 0 {
391		b.LambdaConfigs = append(b.LambdaConfigs[:removeIndex], b.LambdaConfigs[removeIndex+1:]...)
392		return nil
393	}
394	return ErrNoConfigMatch
395}
396