diff options
author | Quentin <quentin@dufour.io> | 2024-06-24 10:15:56 +0000 |
---|---|---|
committer | Quentin <quentin@dufour.io> | 2024-06-24 10:15:56 +0000 |
commit | 9917429da3b06462969b41d511ad4daf27eaf197 (patch) | |
tree | 1a27c40a1ff9123b5552769fcf3cbb543d8dc959 /website.go | |
parent | e7e05ed929c92c2b9d193f8193878c1a8a74c43c (diff) | |
parent | bc7bc61f7449b1f41ed9eb46388ab0c149856f96 (diff) | |
download | guichet-main.tar.gz guichet-main.zip |
Merge pull request 'per-bucket keys' (#68) from feat-per-bucket-key into mainmain
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/guichet/pulls/68
Diffstat (limited to 'website.go')
-rw-r--r-- | website.go | 234 |
1 files changed, 209 insertions, 25 deletions
@@ -3,6 +3,7 @@ package main import ( "fmt" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "log" "sort" "strings" ) @@ -18,7 +19,9 @@ var ( 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, this is an internal error") + 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 { @@ -49,8 +52,18 @@ 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 @@ -77,33 +90,154 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { maxW := user.Quota.WebsiteCount quota := NewQuotaStat(int64(len(wlist)), maxW, true) - return &WebsiteController{user, idx, wlist, quota}, nil + return &WebsiteController{user, keyInfo, idx, wlist, quota}, nil } -type WebsiteDescribe struct { - AccessKeyId string `json:"access_key_id"` - SecretAccessKey string `json:"secret_access_key"` - AllowedWebsites *QuotaStat `json:"quota_website_count"` - BurstBucketQuotaSize string `json:"burst_bucket_quota_size"` - Websites []*WebsiteId `json:"vhosts"` +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) Describe() (*WebsiteDescribe, error) { - s3key, err := w.User.S3KeyInfo() +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 nil, err + 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{ - *s3key.AccessKeyId, - *s3key.SecretAccessKey, + w.User.Username, &w.WebsiteCount, w.User.Quota.WebsiteSizeBurstedPretty(), - r}, nil + r, + }, nil } func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { @@ -117,7 +251,12 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { return nil, ErrFetchBucketInfo } - return NewWebsiteView(binfo), nil + 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) { @@ -161,7 +300,19 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV } } - return NewWebsiteView(binfo), nil + 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) { @@ -173,21 +324,24 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { 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) + 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() @@ -200,7 +354,13 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { return nil, ErrCantConfigureBucket } - return NewWebsiteView(binfo), nil + // 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 { @@ -213,6 +373,7 @@ func (w *WebsiteController) Delete(pretty string) error { return ErrWebsiteNotFound } + // Error checking binfo, err := grgGetBucket(website.Internal) if err != nil { return ErrFetchBucketInfo @@ -226,26 +387,49 @@ func (w *WebsiteController) Delete(pretty string) error { 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"` - Size QuotaStat `json:"quota_size"` - Files QuotaStat `json:"quota_files"` + 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) *WebsiteView { +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, size, objects} + return &WebsiteView{ + wid, + *s3key.AccessKeyId, + *s3key.SecretAccessKey.Get(), + size, + objects, + }, nil } type WebsitePatch struct { - Size *int64 `json:"quota_size"` - Vhost *string `json:"vhost"` + Size *int64 `json:"quota_size"` + Vhost *string `json:"vhost"` + RotateKey *bool `json:"rotate_key"` } |