aboutsummaryrefslogtreecommitdiff
path: root/website.go
diff options
context:
space:
mode:
authorQuentin <quentin@dufour.io>2024-06-24 10:15:56 +0000
committerQuentin <quentin@dufour.io>2024-06-24 10:15:56 +0000
commit9917429da3b06462969b41d511ad4daf27eaf197 (patch)
tree1a27c40a1ff9123b5552769fcf3cbb543d8dc959 /website.go
parente7e05ed929c92c2b9d193f8193878c1a8a74c43c (diff)
parentbc7bc61f7449b1f41ed9eb46388ab0c149856f96 (diff)
downloadguichet-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.go234
1 files changed, 209 insertions, 25 deletions
diff --git a/website.go b/website.go
index 74daf89..ed39d28 100644
--- a/website.go
+++ b/website.go
@@ -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"`
}