aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-06-24 10:22:17 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-06-24 10:22:17 +0200
commite940996f0f8cb8c6adb22c7d8780e364d65ecea2 (patch)
tree7cdc6db520293ea1623b07fd68eff6a595d0e0e3
parenta7edf6d1ba812f11b7711c4bdc6dc6d9af9c7906 (diff)
downloadguichet-e940996f0f8cb8c6adb22c7d8780e364d65ecea2.tar.gz
guichet-e940996f0f8cb8c6adb22c7d8780e364d65ecea2.zip
generate a per-website dedicated key
-rw-r--r--garage.go15
-rw-r--r--login.go6
-rw-r--r--website.go131
-rw-r--r--webui_website.go2
4 files changed, 131 insertions, 23 deletions
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
+ }
+
+ dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
+ if err != nil {
+ return nil, err
}
- return NewWebsiteView(binfo, nil)
+ 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")
}