diff --git a/garage.go b/garage.go index 52a26b0..fabd6bf 100644 --- a/garage.go +++ b/garage.go @@ -44,6 +44,17 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) { return resp, nil } +func grgSearchKey(name string) (*garage.KeyInfo, error) { + client, ctx := gadmin() + + resp, _, err := client.KeyApi.GetKey(ctx).Search(name).ShowSecretKey("true").Execute() + if err != nil { + fmt.Printf("%+v\n", err) + return nil, err + } + return resp, nil +} + func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { client, ctx := gadmin() @@ -59,14 +70,14 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { return binfo, nil } -func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) { +func grgAllowKeyOnBucket(bid, gkey string, read, write, owner bool) (*garage.BucketInfo, error) { client, ctx := gadmin() // Allow user's key ar := garage.AllowBucketKeyRequest{ BucketId: bid, AccessKeyId: gkey, - Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), + Permissions: *garage.NewAllowBucketKeyRequestPermissions(read, write, owner), } binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() if err != nil { diff --git a/login.go b/login.go index 4bbcd65..a2c7d8f 100644 --- a/login.go +++ b/login.go @@ -143,6 +143,7 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities { // --- Logged User --- type LoggedUser struct { + Username string Login *LoginStatus Entry *ldap.Entry Capabilities *Capabilities @@ -186,7 +187,9 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) { } entry := sr.Entries[0] + username := login.Info.Username lu := &LoggedUser{ + Username: username, Login: login, Entry: entry, Capabilities: NewCapabilities(login, entry), @@ -204,6 +207,7 @@ func (lu *LoggedUser) WelcomeName() string { } return ret } + func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) { var err error var keyPair *garage.KeyInfo @@ -212,7 +216,7 @@ func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) { keyID := lu.Entry.GetAttributeValue("garage_s3_access_key") if keyID == "" { // If there is no S3Key in LDAP, generate it... - keyPair, err = grgCreateKey(lu.Login.Info.Username) + keyPair, err = grgCreateKey(lu.Username) if err != nil { return nil, err } diff --git a/website.go b/website.go index ae4ffff..e581780 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" ) @@ -20,6 +21,7 @@ var ( 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 { @@ -50,8 +52,17 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId { return NewWebsiteId(*binfo.Id, binfo.GlobalAliases) } +// ----- + +type WebsiteDescribe struct { + 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 @@ -78,15 +89,86 @@ 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 { - 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 dedicated key does not contain any other bucket than this one + // and that 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 } +//@TODO: flushDedicatedWebsiteKey() + func (w *WebsiteController) Describe() (*WebsiteDescribe, error) { r := make([]*WebsiteId, 0, len(w.PrettyList)) for _, k := range w.PrettyList { @@ -111,9 +193,12 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { return nil, ErrFetchBucketInfo } - // @TODO: fetch the associated key + dedicatedKey, err := w.getDedicatedWebsiteKey(binfo) + if err != nil { + return nil, err + } - return NewWebsiteView(binfo, nil) + return NewWebsiteView(binfo, dedicatedKey) } func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) { @@ -158,10 +243,15 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV } if patch.RotateKey != nil && *patch.RotateKey { - // @TODO: rotate key + // @TODO: rotate key by calling flush } - return NewWebsiteView(binfo, nil) + dedicatedKey, err := w.getDedicatedWebsiteKey(binfo) + if err != nil { + return nil, err + } + + return NewWebsiteView(binfo, dedicatedKey) } func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { @@ -185,7 +275,7 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { 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 } @@ -204,9 +294,12 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { } // Create a dedicated key - // @TODO + dedicatedKey, err := w.getDedicatedWebsiteKey(binfo) + if err != nil { + return nil, err + } - return NewWebsiteView(binfo, nil) + return NewWebsiteView(binfo, dedicatedKey) } func (w *WebsiteController) Delete(pretty string) error { @@ -234,7 +327,7 @@ func (w *WebsiteController) Delete(pretty string) error { } // Delete dedicated key - // @TODO + // @TODO call flush // Actually delete bucket err = grgDeleteBucket(website.Internal) @@ -251,7 +344,7 @@ type WebsiteView struct { func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) { if binfo == nil { - return nil, ErrFetchBucketInfo + return nil, ErrFetchBucketInfo } if s3key == nil { return nil, ErrFetchDedicatedKey @@ -263,16 +356,16 @@ func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteVi size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true) objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false) return &WebsiteView{ - wid, + wid, *s3key.AccessKeyId, *s3key.SecretAccessKey.Get(), - size, + size, objects, }, nil } type WebsitePatch struct { - Size *int64 `json:"quota_size"` - Vhost *string `json:"vhost"` - RotateKey *bool `json:"rotate_key"` + Size *int64 `json:"quota_size"` + Vhost *string `json:"vhost"` + RotateKey *bool `json:"rotate_key"` } diff --git a/webui_website.go b/webui_website.go index 9685374..642c837 100644 --- a/webui_website.go +++ b/webui_website.go @@ -104,7 +104,7 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { } case "rotate_key": do_action := true - _, processErr = ctrl.Patch(bucketName, &WebsitePatch { RotateKey: &do_action }) + _, processErr = ctrl.Patch(bucketName, &WebsitePatch{RotateKey: &do_action}) default: processErr = fmt.Errorf("Unknown action") }