Implement basic PIM management
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
This commit is contained in:
parent
5dd6419d67
commit
1a9d750de7
8 changed files with 395 additions and 5 deletions
27
garage.go
27
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()
|
||||
|
||||
|
|
21
login.go
21
login.go
|
@ -111,12 +111,19 @@ func NewLdapCon() (*ldap.Conn, error) {
|
|||
type Capabilities struct {
|
||||
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)
|
||||
|
||||
|
|
3
main.go
3
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)
|
||||
|
|
211
pim_ctrl.go
Normal file
211
pim_ctrl.go
Normal file
|
@ -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
|
46
pim_http.go
Normal file
46
pim_http.go
Normal file
|
@ -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)
|
||||
}
|
54
quotas.go
54
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"`
|
||||
|
|
|
@ -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">
|
||||
|
|
16
templates/pim_inspect.html
Normal file
16
templates/pim_inspect.html
Normal file
|
@ -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}}
|
Loading…
Reference in a new issue