aboutsummaryrefslogblamecommitdiff
path: root/website.go
blob: ed39d28181e1717308ec78808246b2a8021fca7b (plain) (tree)
1
2
3
4
5
6
7
8



             
                                                                     
             

                 


     







                                                                                                                                                                     
                                                                                                                                     
                                                                                                                                                                         

                                                                                                                                                     
                                                                                                                              

 
                       




                                           
 
 













                                                                 
                                                         




                                                                      


                             
                                                           




                                                                          
                               
                                
                                    

                                          











                                                                         
                                              










                                                                         
                                                                        

 









































                                                                                                                                   





                                                                                                              
                                                                                       
                                                                          




























                                                                                                                                                                  

 


















































                                                                                                                                                                  
 
                                                                  

                                                     
                                              
         
 
                                
                                
                                
                                                        

                  












                                                                          



                                                            
 
                                                  

 














                                                                                             

                                                                               





                                             
                                               




                                                          











                                                                               
                                                       



                                                       




                                                            

         
                                                  










                                                                         
                        




                                               
                                            




                                        
                                                                                         



                                           
                    






                                                




                                                   
                                 



                                                            
 
                                                  

 









                                                         
                         






                                                    
         




                                                      
                               



                                               

                                 



                                               
                         




                                                             

 

                                                                                            
                                              




                                                




                                                                            
                            
                    

                                             
                     
                        
              


                          


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