generate a per-website dedicated key
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed

This commit is contained in:
Quentin 2024-06-24 10:22:17 +02:00
parent a7edf6d1ba
commit e940996f0f
Signed by: quentin
GPG key ID: E9602264D639FF68
4 changed files with 131 additions and 23 deletions

View file

@ -44,6 +44,17 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
return resp, nil 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) { func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
client, ctx := gadmin() client, ctx := gadmin()
@ -59,14 +70,14 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
return binfo, nil 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() client, ctx := gadmin()
// Allow user's key // Allow user's key
ar := garage.AllowBucketKeyRequest{ ar := garage.AllowBucketKeyRequest{
BucketId: bid, BucketId: bid,
AccessKeyId: gkey, AccessKeyId: gkey,
Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), Permissions: *garage.NewAllowBucketKeyRequestPermissions(read, write, owner),
} }
binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
if err != nil { if err != nil {

View file

@ -143,6 +143,7 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
// --- Logged User --- // --- Logged User ---
type LoggedUser struct { type LoggedUser struct {
Username string
Login *LoginStatus Login *LoginStatus
Entry *ldap.Entry Entry *ldap.Entry
Capabilities *Capabilities Capabilities *Capabilities
@ -186,7 +187,9 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
} }
entry := sr.Entries[0] entry := sr.Entries[0]
username := login.Info.Username
lu := &LoggedUser{ lu := &LoggedUser{
Username: username,
Login: login, Login: login,
Entry: entry, Entry: entry,
Capabilities: NewCapabilities(login, entry), Capabilities: NewCapabilities(login, entry),
@ -204,6 +207,7 @@ func (lu *LoggedUser) WelcomeName() string {
} }
return ret return ret
} }
func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) { func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
var err error var err error
var keyPair *garage.KeyInfo var keyPair *garage.KeyInfo
@ -212,7 +216,7 @@ func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
keyID := lu.Entry.GetAttributeValue("garage_s3_access_key") keyID := lu.Entry.GetAttributeValue("garage_s3_access_key")
if keyID == "" { if keyID == "" {
// If there is no S3Key in LDAP, generate it... // If there is no S3Key in LDAP, generate it...
keyPair, err = grgCreateKey(lu.Login.Info.Username) keyPair, err = grgCreateKey(lu.Username)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
"log"
"sort" "sort"
"strings" "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") 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") 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") 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 { type WebsiteId struct {
@ -50,8 +52,17 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
return NewWebsiteId(*binfo.Id, binfo.GlobalAliases) 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 { type WebsiteController struct {
User *LoggedUser User *LoggedUser
RootKey *garage.KeyInfo
WebsiteIdx map[string]*WebsiteId WebsiteIdx map[string]*WebsiteId
PrettyList []string PrettyList []string
WebsiteCount QuotaStat WebsiteCount QuotaStat
@ -78,15 +89,86 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
maxW := user.Quota.WebsiteCount maxW := user.Quota.WebsiteCount
quota := NewQuotaStat(int64(len(wlist)), maxW, true) 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 { func (w *WebsiteController) getDedicatedWebsiteKey(binfo *garage.BucketInfo) (*garage.KeyInfo, error) {
AllowedWebsites *QuotaStat `json:"quota_website_count"` // Check bucket info is not null
BurstBucketQuotaSize string `json:"burst_bucket_quota_size"` if binfo == nil {
Websites []*WebsiteId `json:"vhosts"` 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) { func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
r := make([]*WebsiteId, 0, len(w.PrettyList)) r := make([]*WebsiteId, 0, len(w.PrettyList))
for _, k := range w.PrettyList { for _, k := range w.PrettyList {
@ -111,9 +193,12 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
return nil, ErrFetchBucketInfo 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) { 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 { 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) { func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
@ -185,7 +275,7 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
return nil, err return nil, err
} }
binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId) binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId, true, true, true)
if err != nil { if err != nil {
return nil, ErrCantAllowKey return nil, ErrCantAllowKey
} }
@ -204,9 +294,12 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
} }
// Create a dedicated key // 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 { func (w *WebsiteController) Delete(pretty string) error {
@ -234,7 +327,7 @@ func (w *WebsiteController) Delete(pretty string) error {
} }
// Delete dedicated key // Delete dedicated key
// @TODO // @TODO call flush
// Actually delete bucket // Actually delete bucket
err = grgDeleteBucket(website.Internal) err = grgDeleteBucket(website.Internal)
@ -272,7 +365,7 @@ func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteVi
} }
type WebsitePatch struct { type WebsitePatch struct {
Size *int64 `json:"quota_size"` Size *int64 `json:"quota_size"`
Vhost *string `json:"vhost"` Vhost *string `json:"vhost"`
RotateKey *bool `json:"rotate_key"` RotateKey *bool `json:"rotate_key"`
} }

View file

@ -104,7 +104,7 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
} }
case "rotate_key": case "rotate_key":
do_action := true do_action := true
_, processErr = ctrl.Patch(bucketName, &WebsitePatch { RotateKey: &do_action }) _, processErr = ctrl.Patch(bucketName, &WebsitePatch{RotateKey: &do_action})
default: default:
processErr = fmt.Errorf("Unknown action") processErr = fmt.Errorf("Unknown action")
} }