generate a per-website dedicated key
This commit is contained in:
parent
a7edf6d1ba
commit
e940996f0f
4 changed files with 131 additions and 23 deletions
15
garage.go
15
garage.go
|
@ -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 {
|
||||||
|
|
6
login.go
6
login.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
131
website.go
131
website.go
|
@ -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)
|
||||||
|
@ -251,7 +344,7 @@ type WebsiteView struct {
|
||||||
|
|
||||||
func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) {
|
func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) {
|
||||||
if binfo == nil {
|
if binfo == nil {
|
||||||
return nil, ErrFetchBucketInfo
|
return nil, ErrFetchBucketInfo
|
||||||
}
|
}
|
||||||
if s3key == nil {
|
if s3key == nil {
|
||||||
return nil, ErrFetchDedicatedKey
|
return nil, ErrFetchDedicatedKey
|
||||||
|
@ -263,16 +356,16 @@ func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteVi
|
||||||
size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
|
size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
|
||||||
objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
|
objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
|
||||||
return &WebsiteView{
|
return &WebsiteView{
|
||||||
wid,
|
wid,
|
||||||
*s3key.AccessKeyId,
|
*s3key.AccessKeyId,
|
||||||
*s3key.SecretAccessKey.Get(),
|
*s3key.SecretAccessKey.Get(),
|
||||||
size,
|
size,
|
||||||
objects,
|
objects,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue