aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-02-12 19:19:41 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-02-12 19:19:41 +0100
commit1a9d750de7bb30bea5dcc7cd14f3f1826ddf00f7 (patch)
treed8d17e30646e4cb2af3ea15a8c12ec60eb0e545c
parent5dd6419d676aa60ddde188a800d5084211e88d89 (diff)
downloadguichet-1a9d750de7bb30bea5dcc7cd14f3f1826ddf00f7.tar.gz
guichet-1a9d750de7bb30bea5dcc7cd14f3f1826ddf00f7.zip
Implement basic PIM management
-rw-r--r--garage.go27
-rw-r--r--login.go25
-rw-r--r--main.go3
-rw-r--r--pim_ctrl.go211
-rw-r--r--pim_http.go46
-rw-r--r--quotas.go54
-rw-r--r--templates/home.html18
-rw-r--r--templates/pim_inspect.html16
8 files changed, 395 insertions, 5 deletions
diff --git a/garage.go b/garage.go
index 7cd879b..0176382 100644
--- a/garage.go
+++ b/garage.go
@@ -61,6 +61,33 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
return binfo, nil
}
+func grgCreateLocalBucket(bucket, gkey string) (*garage.BucketInfo, error) {
+ client, ctx := gadmin()
+
+ is_true := true
+ is_false := false
+
+ la := garage.CreateBucketRequestLocalAlias {
+ AccessKeyId: &gkey,
+ Alias: &bucket,
+ Allow: &garage.CreateBucketRequestLocalAliasAllow {
+ Read: &is_true,
+ Write: &is_true,
+ Owner: &is_false,
+ },
+ }
+
+ br := garage.NewCreateBucketRequest()
+ br.SetLocalAlias(la)
+
+ binfo, _, err := client.BucketApi.CreateBucket(ctx).CreateBucketRequest(*br).Execute()
+ if err != nil {
+ fmt.Printf("%+v\n", err)
+ return nil, err
+ }
+ return binfo, nil
+}
+
func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) {
client, ctx := gadmin()
diff --git a/login.go b/login.go
index 277e3ae..62a72c1 100644
--- a/login.go
+++ b/login.go
@@ -109,14 +109,21 @@ func NewLdapCon() (*ldap.Conn, error) {
// --- Capabilities ---
type Capabilities struct {
- CanAdmin bool
- CanInvite bool
+ CanAdmin bool
+ CanInvite bool
+ CanUseEmail bool
}
func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
// Initialize
canAdmin := false
canInvite := false
+ canUseEmail := false
+
+ // Composable logic
+ hasAeroBucketId := false
+ hasAeroBucketName := false
+ hasAeroCryptoRoot := false
// Special case for the "admin" account that is de-facto admin
canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount)
@@ -132,12 +139,22 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
canAdmin = true
}
}
+ } else if strings.EqualFold(attr.Name, FIELD_AEROGRAMME_CRYPTOROOT) {
+ hasAeroCryptoRoot = true
+ } else if strings.EqualFold(attr.Name, FIELD_AEROGRAMME_BUCKET_ID) {
+ hasAeroBucketId = true
+ } else if strings.EqualFold(attr.Name, FIELD_AEROGRAMME_BUCKET_NAME) {
+ hasAeroBucketName = true
}
}
+ // Boolean logic
+ canUseEmail = hasAeroBucketId && hasAeroBucketName && hasAeroCryptoRoot
+
return &Capabilities{
CanAdmin: canAdmin,
CanInvite: canInvite,
+ CanUseEmail: canUseEmail,
}
}
@@ -173,6 +190,10 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
FIELD_NAME_PROFILE_PICTURE,
FIELD_QUOTA_WEBSITE_SIZE_BURSTED,
FIELD_QUOTA_WEBSITE_COUNT,
+ FIELD_QUOTA_PIM_SIZE_BURSTED,
+ FIELD_AEROGRAMME_CRYPTOROOT,
+ FIELD_AEROGRAMME_BUCKET_ID,
+ FIELD_AEROGRAMME_BUCKET_NAME,
},
nil)
diff --git a/main.go b/main.go
index 39c7f08..30fd42e 100644
--- a/main.go
+++ b/main.go
@@ -163,6 +163,9 @@ func server(args []string) {
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
r.HandleFunc("/website/vhost/{bucket}", handleWebsiteVhost)
+ r.HandleFunc("/pim/setup", handlePimSetup)
+ r.HandleFunc("/pim/inspect", handlePimInspect)
+
r.HandleFunc("/invite/new_account", handleInviteNewAccount)
r.HandleFunc("/invite/send_code", handleInviteSendCode)
r.HandleFunc("/invitation/{code}", handleInvitationCode)
diff --git a/pim_ctrl.go b/pim_ctrl.go
new file mode 100644
index 0000000..8589536
--- /dev/null
+++ b/pim_ctrl.go
@@ -0,0 +1,211 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os/exec"
+ "strings"
+ "slices"
+ garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
+ "github.com/go-ldap/ldap/v3"
+)
+
+const (
+ FIELD_AEROGRAMME_CRYPTOROOT = "aero_cryptoroot"
+ FIELD_AEROGRAMME_BUCKET_ID = "aero_bucket_id"
+ FIELD_AEROGRAMME_BUCKET_NAME = "aero_bucket"
+ LOCAL_ALIAS_NAME = "aerogramme"
+)
+
+
+var (
+ ErrPimBuilderDirty = fmt.Errorf("builder is dirty.")
+ ErrPimBucketLocalAliasNotFound = fmt.Errorf("local alias does not exist in garage or points to the wrong bucket.")
+ ErrPimBucketIdEmpty = fmt.Errorf("missing bucket ID in LDAP.")
+ ErrPimBucketNameEmpty = fmt.Errorf("missing bucket local garage alias in LDAP.")
+ ErrPimBucketInfoNotFetched = fmt.Errorf("bucket info has not been fetched.")
+ ErrPimCryptoRootEmpty = fmt.Errorf("missing cryptoroot in LDAP.")
+ ErrPimCantCreateBucket = fmt.Errorf("unable to create PIM bucket.")
+)
+
+type PimBuilder struct {
+ user *LoggedUser
+ cryptoroot string
+ bucketId string
+ bucketName string
+ bucketInfo *garage.BucketInfo
+ dirty bool
+ errors []error
+}
+
+func NewPimBuilder(user *LoggedUser) *PimBuilder {
+ return &PimBuilder {
+ user: user,
+ cryptoroot: user.Entry.GetAttributeValue(FIELD_AEROGRAMME_CRYPTOROOT),
+ bucketId: user.Entry.GetAttributeValue(FIELD_AEROGRAMME_BUCKET_ID),
+ bucketName: user.Entry.GetAttributeValue(FIELD_AEROGRAMME_BUCKET_NAME),
+ bucketInfo: nil,
+ dirty: false,
+ errors: make([]error, 0),
+ }
+}
+func (pm *PimBuilder) CheckCryptoRoot() *PimBuilder {
+ if pm.cryptoroot == "" {
+ cmd := exec.Command("./aerogramme", "tools", "crypto-root", "new-clear-text")
+ var out strings.Builder
+ cmd.Stdout = &out
+ err := cmd.Run()
+ if err != nil {
+ pm.errors = append(pm.errors, err)
+ return pm
+ }
+ pm.cryptoroot = out.String()
+ pm.dirty = true
+ }
+ return pm
+}
+
+func (pm *PimBuilder) CheckBucket() *PimBuilder {
+ keyInfo, err := pm.user.S3KeyInfo()
+ if err != nil {
+ pm.errors = append(pm.errors, err)
+ return pm
+ }
+
+ if pm.bucketId == "" {
+ candidateName := LOCAL_ALIAS_NAME
+ var bInfo *garage.BucketInfo
+ var err error
+
+ err = nil
+ for _, ext := range []string{"", "-1", "-2", "-3", "-4", "-5"} {
+ candidateName = LOCAL_ALIAS_NAME + ext
+ bInfo, err = grgCreateLocalBucket(candidateName, *keyInfo.AccessKeyId)
+ if err == nil {
+ break
+ }
+ }
+
+ if err != nil {
+ pm.errors = append(pm.errors, ErrPimCantCreateBucket)
+ return pm
+ }
+
+ qr := pm.user.Quota.DefaultPimQuota()
+ ur := garage.NewUpdateBucketRequest()
+ ur.SetQuotas(*qr)
+ bInfo, err = grgUpdateBucket(*bInfo.Id, ur)
+ if err != nil {
+ pm.errors = append(pm.errors, err)
+ return pm
+ }
+
+ pm.bucketId = *bInfo.Id
+ pm.bucketName = candidateName
+ pm.bucketInfo = bInfo
+ pm.dirty = true
+ } else {
+ binfo, err := grgGetBucket(pm.bucketId)
+ if err != nil {
+ pm.errors = append(pm.errors, err)
+ return pm
+ }
+ pm.bucketInfo = binfo
+
+ //@TODO find my key, check that pm.bucketName exists in bucketLocalAliases
+ nameFound := false
+ for _, k := range binfo.Keys {
+ if *k.AccessKeyId != *keyInfo.AccessKeyId {
+ // not my key
+ continue
+ }
+ if slices.Contains(k.BucketLocalAliases, pm.bucketName) {
+ nameFound = true
+ break
+ }
+ }
+ if !nameFound {
+ pm.errors = append(pm.errors, ErrPimBucketLocalAliasNotFound)
+ return pm
+ }
+ }
+
+ return pm
+}
+
+func (pm *PimBuilder) LdapUpdate() *PimBuilder {
+ if len(pm.errors) > 0 {
+ return pm
+ }
+
+ modify_request := ldap.NewModifyRequest(pm.user.Login.Info.DN(), nil)
+ modify_request.Replace(FIELD_AEROGRAMME_CRYPTOROOT, []string{pm.cryptoroot})
+ modify_request.Replace(FIELD_AEROGRAMME_BUCKET_NAME, []string{pm.bucketName})
+ modify_request.Replace(FIELD_AEROGRAMME_BUCKET_ID, []string{pm.bucketId})
+ err := pm.user.Login.conn.Modify(modify_request)
+ if err != nil {
+ pm.errors = append(pm.errors, err)
+ return pm
+ }
+
+ pm.dirty = false
+ return pm
+}
+
+func (pm *PimBuilder) Build() (*PimController, error) {
+ // checks
+ if pm.dirty {
+ pm.errors = append(pm.errors, ErrPimBuilderDirty)
+ }
+ if pm.bucketId == "" {
+ pm.errors = append(pm.errors, ErrPimBucketIdEmpty)
+ }
+ if pm.bucketName == "" {
+ pm.errors = append(pm.errors, ErrPimBucketNameEmpty)
+ }
+ if pm.bucketInfo == nil {
+ pm.errors = append(pm.errors, ErrPimBucketInfoNotFetched)
+ }
+ if pm.cryptoroot == "" {
+ pm.errors = append(pm.errors, ErrPimCryptoRootEmpty)
+ }
+ if len(pm.errors) > 0 {
+ err := errors.New("PIM Builder failed")
+ for _, iterErr := range pm.errors {
+ err = errors.Join(err, iterErr)
+ }
+ return nil, err
+ }
+
+ // quotas
+ q := pm.bucketInfo.GetQuotas()
+ size := NewQuotaStat(*pm.bucketInfo.Bytes, (&q).GetMaxSize(), true)
+ objects := NewQuotaStat(*pm.bucketInfo.Objects, (&q).GetMaxObjects(), false)
+
+ // final object
+ pim_ctl := &PimController {
+ BucketId: pm.bucketId,
+ BucketName: pm.bucketName,
+ Size: size,
+ Files: objects,
+ user: pm.user,
+ bucketInfo: pm.bucketInfo,
+ cryptoroot: pm.cryptoroot,
+
+ }
+
+ return pim_ctl, nil
+}
+
+// --- Controller ---
+type PimController struct {
+ BucketId string `json:"bucket_id"`
+ BucketName string `json:"bucket_name"`
+ Size QuotaStat `json:"quota_size"`
+ Files QuotaStat `json:"quota_files"`
+ user *LoggedUser
+ bucketInfo *garage.BucketInfo
+ cryptoroot string
+}
+
+//@FIXME Implement quota bursting
diff --git a/pim_http.go b/pim_http.go
new file mode 100644
index 0000000..e04c702
--- /dev/null
+++ b/pim_http.go
@@ -0,0 +1,46 @@
+package main
+
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+func handlePimInspect(w http.ResponseWriter, r *http.Request) {
+ user := RequireUserHtml(w, r)
+ if user == nil {
+ return
+ }
+
+ pim_ctl, err := NewPimBuilder(user).CheckCryptoRoot().CheckBucket().Build()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ pim_json, err := json.MarshalIndent(pim_ctl, "", " ")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ tKey := getTemplate("pim_inspect.html")
+ tKey.Execute(w, string(pim_json))
+}
+
+func handlePimSetup(w http.ResponseWriter, r *http.Request) {
+ user := RequireUserHtml(w, r)
+ if user == nil {
+ return
+ }
+
+ _, err := NewPimBuilder(user).CheckCryptoRoot().CheckBucket().LdapUpdate().Build()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ user.Capabilities.CanUseEmail = true
+
+
+ http.Redirect(w, r, "/pim/inspect", http.StatusFound)
+}
diff --git a/quotas.go b/quotas.go
index 894ea3c..c68f5de 100644
--- a/quotas.go
+++ b/quotas.go
@@ -9,16 +9,23 @@ import (
"github.com/go-ldap/ldap/v3"
)
+// Note: PIM = Personal Information Manager
const (
- // --- Default Quota Values ---
+ // --- Default Quota Values Websites ---
QUOTA_WEBSITE_SIZE_DEFAULT = 1024 * 1024 * 50 // 50MB
QUOTA_WEBSITE_SIZE_BURSTED = 1024 * 1024 * 200 // 200MB
- QUOTA_WEBSITE_OBJECTS = 10000 // 10k objects
+ QUOTA_WEBSITE_OBJECTS = 10_000 // 10k objects
QUOTA_WEBSITE_COUNT = 5 // 5 buckets
+ // --- Default Quota Values PIM ---
+ QUOTA_PIM_SIZE_DEFAULT = 1024 * 1024 * 100 // 100MB
+ QUOTA_PIM_SIZE_BURSTED = 1024 * 1024 * 500 // 500MB
+ QUOTA_PIM_OBJECTS = 100_000 // 100k objects
+
// --- Per-user overridable fields ---
FIELD_QUOTA_WEBSITE_SIZE_BURSTED = "quota_website_size_bursted"
FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count"
+ FIELD_QUOTA_PIM_SIZE_BURSTED = "quota_pim_size_bursted"
)
type UserQuota struct {
@@ -26,6 +33,9 @@ type UserQuota struct {
WebsiteSizeDefault int64
WebsiteSizeBursted int64
WebsiteObjects int64
+ PimSizeDefault int64
+ PimSizeBursted int64
+ PimObjects int64
}
func NewUserQuota() *UserQuota {
@@ -34,6 +44,9 @@ func NewUserQuota() *UserQuota {
WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT,
WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED,
WebsiteObjects: QUOTA_WEBSITE_OBJECTS,
+ PimSizeDefault: QUOTA_PIM_SIZE_DEFAULT,
+ PimSizeBursted: QUOTA_PIM_SIZE_BURSTED,
+ PimObjects: QUOTA_PIM_OBJECTS,
}
}
@@ -66,6 +79,10 @@ func NewUserQuotaFromEntry(entry *ldap.Entry) *UserQuota {
quotas.WebsiteSizeBursted = q
}
+ if q, err := entryToQuota(entry, FIELD_QUOTA_PIM_SIZE_BURSTED); err == nil {
+ quotas.PimSizeBursted = q
+ }
+
return quotas
}
@@ -78,6 +95,16 @@ func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas {
return qr
}
+func (q *UserQuota) DefaultPimQuota() *garage.UpdateBucketRequestQuotas {
+ qr := garage.NewUpdateBucketRequestQuotas()
+
+ qr.SetMaxSize(q.PimSizeDefault)
+ qr.SetMaxObjects(q.PimObjects)
+
+ return qr
+}
+
+// Website getters/setters
func (q *UserQuota) WebsiteSizeAdjust(sz int64) int64 {
if sz < q.WebsiteSizeDefault {
return q.WebsiteSizeDefault
@@ -100,6 +127,29 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string {
return prettyValue(q.WebsiteSizeBursted)
}
+// PIM getters/setters
+func (q *UserQuota) PimSizeAdjust(sz int64) int64 {
+ if sz < q.PimSizeDefault {
+ return q.PimSizeDefault
+ } else if sz > q.PimSizeBursted {
+ return q.PimSizeBursted
+ } else {
+ return sz
+ }
+}
+
+func (q *UserQuota) PimObjectAdjust(objs int64) int64 {
+ if objs > q.PimObjects || objs <= 0 {
+ return q.PimObjects
+ } else {
+ return objs
+ }
+}
+
+func (q *UserQuota) PimSizeBurstedPretty() string {
+ return prettyValue(q.PimSizeBursted)
+}
+
// --- A quota stat we can use
type QuotaStat struct {
Current int64 `json:"current"`
diff --git a/templates/home.html b/templates/home.html
index dd88d13..8e0a23c 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -24,7 +24,7 @@
<div class="mt-3">
<div class="card">
<div class="card-header">
- Mon espace sur la toile
+ Mes publications sur la toile
</div>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/website/configure">Mes identifiants</a>
@@ -33,6 +33,22 @@
</div>
</div>
+<div class="mt-3">
+ <div class="card">
+ <div class="card-header">
+ Mon espace personnel (email, calendrier, contacts, etc.)
+ </div>
+ <div class="list-group list-group-flush">
+{{if .User.Capabilities.CanUseEmail}}
+ <a class="list-group-item list-group-item-action disabled" href="#">Accéder à l'interface web</a>
+ <a class="list-group-item list-group-item-action" href="/pim/inspect">Voir les détails</a>
+{{ else }}
+ <a class="list-group-item list-group-item-action" href="/pim/setup">Créer mon espace</a>
+{{ end }}
+ </div>
+ </div>
+</div>
+
{{if .User.Capabilities.CanInvite}}
<div class="card mt-3">
<div class="card-header">
diff --git a/templates/pim_inspect.html b/templates/pim_inspect.html
new file mode 100644
index 0000000..7d86f9b
--- /dev/null
+++ b/templates/pim_inspect.html
@@ -0,0 +1,16 @@
+{{define "title"}}Configurer mConfigurer mon compte email |{{end}}
+
+{{define "body"}}
+<div class="d-flex">
+ <h4>Mon adresse email</h4>
+ <a class="ml-auto btn btn-info" href="/">Menu principal</a>
+</div>
+
+<div class="row">
+ <div class="col-md-12 mt-3">
+ <div class="alert alert-danger">PAGE DE DEBUG, NON CONFORME POUR UNE MISE EN PRODUCTION</div>
+ </div>
+ <pre>{{ . }}</pre>
+</div>
+
+{{end}}