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"` }