aboutsummaryrefslogtreecommitdiff
path: root/website.go
blob: ed39d28181e1717308ec78808246b2a8021fca7b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
package main

import (
	"fmt"
	garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
	"log"
	"sort"
	"strings"
)

var (
	ErrWebsiteNotFound              = fmt.Errorf("Website not found")
	ErrFetchBucketInfo              = fmt.Errorf("Failed to fetch bucket information")
	ErrWebsiteQuotaReached          = fmt.Errorf("Can't create additional websites, quota reached")
	ErrEmptyBucketName              = fmt.Errorf("You can't create a website with an empty name")
	ErrCantCreateBucket             = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character")
	ErrCantAllowKey                 = fmt.Errorf("Can't allow given key on the target bucket")
	ErrCantConfigureBucket          = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)")
	ErrBucketDeleteNotEmpty         = fmt.Errorf("You must remove all the files before deleting a bucket")
	ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket")
	ErrCantChangeVhost              = fmt.Errorf("Can't change the vhost to the desired value. Maybe it's already used by someone else or an internal error occured")
	ErrCantRemoveOldVhost           = fmt.Errorf("The new vhost is bound to the bucket but the old one can't be removed, it's an internal error")
	ErrFetchDedicatedKey            = fmt.Errorf("Bucket has no dedicated key while it's required, it's an internal error")
	ErrDedicatedKeyInvariant        = fmt.Errorf("A security invariant on the dedicated key has been violated, aborting.")
)

type WebsiteId struct {
	Pretty   string   `json:"name"`
	Internal string   `json:"-"`
	Alt      []string `json:"alt_name"`
	Expanded bool     `json:"expanded"`
	Url      string   `json:"domain"`
}

func NewWebsiteId(id string, aliases []string) *WebsiteId {
	pretty := id
	var alt []string
	if len(aliases) > 0 {
		pretty = aliases[0]
		alt = aliases[1:]
	}
	expanded := strings.Contains(pretty, ".")

	url := pretty
	if !expanded {
		url = fmt.Sprintf("%s.web.deuxfleurs.fr", pretty)
	}

	return &WebsiteId{pretty, id, alt, expanded, url}
}
func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
	return NewWebsiteId(*binfo.Id, binfo.GlobalAliases)
}

// -----

type WebsiteDescribe struct {
	Username             string       `json:"username"`
	AllowedWebsites      *QuotaStat   `json:"quota_website_count"`
	BurstBucketQuotaSize string       `json:"burst_bucket_quota_size"`
	Websites             []*WebsiteId `json:"vhosts"`
}

type WebsiteController struct {
	User         *LoggedUser
	RootKey      *garage.KeyInfo
	WebsiteIdx   map[string]*WebsiteId
	PrettyList   []string
	WebsiteCount QuotaStat
}

func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
	idx := map[string]*WebsiteId{}
	var wlist []string

	keyInfo, err := user.S3KeyInfo()
	if err != nil {
		return nil, err
	}

	for _, bckt := range keyInfo.Buckets {
		if len(bckt.GlobalAliases) > 0 {
			wid := NewWebsiteId(*bckt.Id, bckt.GlobalAliases)
			idx[wid.Pretty] = wid
			wlist = append(wlist, wid.Pretty)
		}
	}
	sort.Strings(wlist)

	maxW := user.Quota.WebsiteCount
	quota := NewQuotaStat(int64(len(wlist)), maxW, true)

	return &WebsiteController{user, keyInfo, idx, wlist, quota}, nil
}

func (w *WebsiteController) getDedicatedWebsiteKey(binfo *garage.BucketInfo) (*garage.KeyInfo, error) {
	// Check bucket info is not null
	if binfo == nil {
		return nil, ErrFetchBucketInfo
	}

	// Check the bucket is owned by the user's root key
	usersRootKeyFound := false
	for _, bucketKeyInfo := range binfo.Keys {
		if *bucketKeyInfo.AccessKeyId == *w.RootKey.AccessKeyId && *bucketKeyInfo.Permissions.Owner {
			usersRootKeyFound = true
			break
		}
	}
	if !usersRootKeyFound {
		log.Printf("%s is not an owner of bucket %s. Invariant violated.\n", w.User.Username, *binfo.Id)
		return nil, ErrDedicatedKeyInvariant
	}

	// Check that username does not contain a ":" (should not be possible due to the invitation regex)
	// We do this check as ":" is used as a separator
	if strings.Contains(w.User.Username, ":") || w.User.Username == "" || *binfo.Id == "" {
		log.Printf("Username (%s) or bucket identifier (%s) is invalid. Invariant violated.\n", w.User.Username, *binfo.Id)
		return nil, ErrDedicatedKeyInvariant
	}

	// Build the string template by concatening the username and the bucket identifier
	dedicatedKeyName := fmt.Sprintf("%s:web:%s", w.User.Username, *binfo.Id)

	// Try to fetch the dedicated key
	keyInfo, err := grgSearchKey(dedicatedKeyName)
	if err != nil {
		// On error, try to create it.
		// @FIXME we should try to create only on 404 Not Found errors
		keyInfo, err = grgCreateKey(dedicatedKeyName)
		if err != nil {
			// On error again, abort
			return nil, err
		}
		log.Printf("Created dedicated key %s\n", dedicatedKeyName)
	}

	// Check that the key name is *exactly* the one we requested
	if *keyInfo.Name != dedicatedKeyName {
		log.Printf("Expected key: %s, got %s. Invariant violated.\n", dedicatedKeyName, *keyInfo.Name)
		return nil, ErrDedicatedKeyInvariant
	}

	// Check that the dedicated key does not contain any other bucket than this one
	// and report if this bucket key is found with correct permissions
	permissionsOk := false
	for _, buck := range keyInfo.Buckets {
		if *buck.Id != *binfo.Id {
			log.Printf("Key %s is used on bucket %s while it should be exclusive to %s. Invariant violated.\n", dedicatedKeyName, *buck.Id, *binfo.Id)
			return nil, ErrDedicatedKeyInvariant
		}
		if *buck.Id == *binfo.Id && *buck.Permissions.Read && *buck.Permissions.Write {
			permissionsOk = true
		}
	}

	// Allow this bucket on the key if it's not already the case
	// (will be executed when 1) key is first created and 2) as an healing mechanism)
	if !permissionsOk {
		binfo, err = grgAllowKeyOnBucket(*binfo.Id, *keyInfo.AccessKeyId, true, true, false)
		if err != nil {
			return nil, err
		}
		log.Printf("Key %s was not properly allowed on bucket %s, fixing permissions. Intended behavior.", dedicatedKeyName, *binfo.Id)

		// Refresh the key to have an object with proper permissions
		keyInfo, err = grgGetKey(*keyInfo.AccessKeyId)
		if err != nil {
			return nil, err
		}
	}

	// Return the key
	return keyInfo, nil
}

func (w *WebsiteController) flushDedicatedWebsiteKey(binfo *garage.BucketInfo) error {
	// Check bucket info is not null
	if binfo == nil {
		return ErrFetchBucketInfo
	}

	// Check the bucket is owned by the user's root key
	usersRootKeyFound := false
	for _, bucketKeyInfo := range binfo.Keys {
		if *bucketKeyInfo.AccessKeyId == *w.RootKey.AccessKeyId && *bucketKeyInfo.Permissions.Owner {
			usersRootKeyFound = true
			break
		}
	}
	if !usersRootKeyFound {
		log.Printf("%s is not an owner of bucket %s. Invariant violated.\n", w.User.Username, *binfo.Id)
		return ErrDedicatedKeyInvariant
	}

	// Build the string template by concatening the username and the bucket identifier
	dedicatedKeyName := fmt.Sprintf("%s:web:%s", w.User.Username, *binfo.Id)

	// Fetch the dedicated key
	keyInfo, err := grgSearchKey(dedicatedKeyName)
	if err != nil {
		return err
	}

	// Check that the key name is *exactly* the one we requested
	if *keyInfo.Name != dedicatedKeyName {
		log.Printf("Expected key: %s, got %s. Invariant violated.\n", dedicatedKeyName, *keyInfo.Name)
		return ErrDedicatedKeyInvariant
	}

	// Check that the dedicated key contains no other bucket than this one
	// (can also be empty, useful to heal a partially created key)
	for _, buck := range keyInfo.Buckets {
		if *buck.Id != *binfo.Id {
			log.Printf("Key %s is used on bucket %s while it should be exclusive to %s. Invariant violated.\n", dedicatedKeyName, *buck.Id, *binfo.Id)
			return ErrDedicatedKeyInvariant
		}
	}

	// Finally delete this key
	err = grgDelKey(*keyInfo.AccessKeyId)
	if err != nil {
		return err
	}
	log.Printf("Deleted dedicated key %s", dedicatedKeyName)
	return nil
}

func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
	r := make([]*WebsiteId, 0, len(w.PrettyList))
	for _, k := range w.PrettyList {
		r = append(r, w.WebsiteIdx[k])
	}

	return &WebsiteDescribe{
		w.User.Username,
		&w.WebsiteCount,
		w.User.Quota.WebsiteSizeBurstedPretty(),
		r,
	}, nil
}

func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
	website, ok := w.WebsiteIdx[pretty]
	if !ok {
		return nil, ErrWebsiteNotFound
	}

	binfo, err := grgGetBucket(website.Internal)
	if err != nil {
		return nil, ErrFetchBucketInfo
	}

	dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
	if err != nil {
		return nil, err
	}

	return NewWebsiteView(binfo, dedicatedKey)
}

func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
	website, ok := w.WebsiteIdx[pretty]
	if !ok {
		return nil, ErrWebsiteNotFound
	}

	binfo, err := grgGetBucket(website.Internal)
	if err != nil {
		return nil, ErrFetchBucketInfo
	}

	// Patch the max size
	urQuota := garage.NewUpdateBucketRequestQuotas()
	urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize()))
	urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects()))
	if patch.Size != nil {
		urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size))
	}

	// Build the update
	ur := garage.NewUpdateBucketRequest()
	ur.SetQuotas(*urQuota)

	// Call garage "update bucket" function
	binfo, err = grgUpdateBucket(website.Internal, ur)
	if err != nil {
		return nil, ErrCantConfigureBucket
	}

	// Update the alias if the vhost field is set and different
	if patch.Vhost != nil && *patch.Vhost != "" && *patch.Vhost != pretty {
		binfo, err = grgAddGlobalAlias(website.Internal, *patch.Vhost)
		if err != nil {
			return nil, ErrCantChangeVhost
		}
		binfo, err = grgDelGlobalAlias(website.Internal, pretty)
		if err != nil {
			return nil, ErrCantRemoveOldVhost
		}
	}

	if patch.RotateKey != nil && *patch.RotateKey {
		err = w.flushDedicatedWebsiteKey(binfo)
		if err != nil {
			return nil, err
		}
	}

	dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
	if err != nil {
		return nil, err
	}

	return NewWebsiteView(binfo, dedicatedKey)
}

func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
	if pretty == "" {
		return nil, ErrEmptyBucketName
	}

	if w.WebsiteCount.IsFull() {
		return nil, ErrWebsiteQuotaReached
	}

	// Create bucket
	binfo, err := grgCreateBucket(pretty)
	if err != nil {
		return nil, ErrCantCreateBucket
	}

	// Allow user's global key on bucket
	s3key, err := w.User.S3KeyInfo()
	if err != nil {
		return nil, err
	}

	binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId, true, true, true)
	if err != nil {
		return nil, ErrCantAllowKey
	}

	// Set quota
	qr := w.User.Quota.DefaultWebsiteQuota()
	wr := allowWebsiteDefault()

	ur := garage.NewUpdateBucketRequest()
	ur.SetWebsiteAccess(*wr)
	ur.SetQuotas(*qr)

	binfo, err = grgUpdateBucket(*binfo.Id, ur)
	if err != nil {
		return nil, ErrCantConfigureBucket
	}

	// Create a dedicated key
	dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
	if err != nil {
		return nil, err
	}

	return NewWebsiteView(binfo, dedicatedKey)
}

func (w *WebsiteController) Delete(pretty string) error {
	if pretty == "" {
		return ErrEmptyBucketName
	}

	website, ok := w.WebsiteIdx[pretty]
	if !ok {
		return ErrWebsiteNotFound
	}

	// Error checking
	binfo, err := grgGetBucket(website.Internal)
	if err != nil {
		return ErrFetchBucketInfo
	}

	if *binfo.Objects > int64(0) {
		return ErrBucketDeleteNotEmpty
	}

	if *binfo.UnfinishedUploads > int32(0) {
		return ErrBucketDeleteUnfinishedUpload
	}

	// Delete dedicated key
	err = w.flushDedicatedWebsiteKey(binfo)
	if err != nil {
		return err
	}

	// Actually delete bucket
	err = grgDeleteBucket(website.Internal)
	return err
}

type WebsiteView struct {
	Name            *WebsiteId `json:"vhost"`
	AccessKeyId     string     `json:"access_key_id"`
	SecretAccessKey string     `json:"secret_access_key"`
	Size            QuotaStat  `json:"quota_size"`
	Files           QuotaStat  `json:"quota_files"`
}

func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) {
	if binfo == nil {
		return nil, ErrFetchBucketInfo
	}
	if s3key == nil {
		return nil, ErrFetchDedicatedKey
	}

	q := binfo.GetQuotas()

	wid := NewWebsiteIdFromBucketInfo(binfo)
	size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
	objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
	return &WebsiteView{
		wid,
		*s3key.AccessKeyId,
		*s3key.SecretAccessKey.Get(),
		size,
		objects,
	}, nil
}

type WebsitePatch struct {
	Size      *int64  `json:"quota_size"`
	Vhost     *string `json:"vhost"`
	RotateKey *bool   `json:"rotate_key"`
}