Compare commits
3 commits
main
...
feat/aerog
Author | SHA1 | Date | |
---|---|---|---|
aff7efd726 | |||
1a9d750de7 | |||
5dd6419d67 |
21 changed files with 936 additions and 759 deletions
|
@ -1,13 +1,26 @@
|
||||||
when:
|
---
|
||||||
event:
|
kind: pipeline
|
||||||
- push
|
name: default
|
||||||
- pull_request
|
|
||||||
- tag
|
node:
|
||||||
- cron
|
nix-daemon: 1
|
||||||
- manual
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: nixpkgs/nix:nixos-22.05
|
image: nixpkgs/nix:nixos-22.05
|
||||||
commands:
|
commands:
|
||||||
- nix build --extra-experimental-features nix-command --extra-experimental-features flakes .
|
- nix build --extra-experimental-features nix-command --extra-experimental-features flakes .
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- custom
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
- tag
|
||||||
|
- cron
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: 7fc9d4362327e769e2454229ceb91f587eb11755f1f96207f733e41cc650f693
|
||||||
|
|
||||||
|
...
|
|
@ -1,6 +1,6 @@
|
||||||
# Guichet
|
# Guichet
|
||||||
|
|
||||||
[![status-badge](https://woodpecker.deuxfleurs.fr/api/badges/37/status.svg)](https://woodpecker.deuxfleurs.fr/repos/37)
|
[![Build Status](https://drone.deuxfleurs.fr/api/badges/Deuxfleurs/guichet/status.svg?ref=refs/heads/main)](https://drone.deuxfleurs.fr/Deuxfleurs/guichet)
|
||||||
|
|
||||||
Guichet is a simple LDAP web interface for the following tasks:
|
Guichet is a simple LDAP web interface for the following tasks:
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"http_bind_addr": ":9991",
|
"http_bind_addr": "[::]:9991",
|
||||||
"ldap_server_addr": "ldap://127.0.0.1:389",
|
"ldap_server_addr": "ldap://localhost:389",
|
||||||
|
|
||||||
"base_dn": "dc=bottin,dc=eu",
|
"base_dn": "dc=bottin,dc=eu",
|
||||||
"user_base_dn": "ou=users,dc=bottin,dc=eu",
|
"user_base_dn": "ou=users,dc=bottin,dc=eu",
|
||||||
|
@ -11,9 +11,11 @@
|
||||||
"invitation_base_dn": "ou=invitations,dc=bottin,dc=eu",
|
"invitation_base_dn": "ou=invitations,dc=bottin,dc=eu",
|
||||||
"invitation_name_attr": "cn",
|
"invitation_name_attr": "cn",
|
||||||
"invited_mail_format": "{}@bottin.eu",
|
"invited_mail_format": "{}@bottin.eu",
|
||||||
"invited_auto_groups": [ ],
|
"invited_auto_groups": [
|
||||||
|
"cn=email,ou=groups,dc=bottin,dc=eu"
|
||||||
|
],
|
||||||
|
|
||||||
"web_address": "http://localhost:9991",
|
"web_address": "https://guichet.bottin.eu",
|
||||||
"mail_from": "welcome@bottin.eu",
|
"mail_from": "welcome@bottin.eu",
|
||||||
"smtp_server": "smtp.bottin.eu",
|
"smtp_server": "smtp.bottin.eu",
|
||||||
"smtp_username": "guichet",
|
"smtp_username": "guichet",
|
||||||
|
@ -24,12 +26,11 @@
|
||||||
"group_can_invite": "cn=admin,ou=groups,dc=bottin,dc=eu",
|
"group_can_invite": "cn=admin,ou=groups,dc=bottin,dc=eu",
|
||||||
|
|
||||||
"s3_admin_endpoint": "localhost:3903",
|
"s3_admin_endpoint": "localhost:3903",
|
||||||
"s3_admin_token": "GlXP43PWH3LuvEGSNxKYzZCyUss8VqZmarBU+HUlrxw=",
|
"s3_admin_token": "<change me>",
|
||||||
|
|
||||||
"s3_endpoint": "localhost:3900",
|
"s3_endpoint": "localhost:3900",
|
||||||
"s3_access_key": "",
|
"s3_access_key": "<change me>",
|
||||||
"s3_secret_key": "",
|
"s3_secret_key": "<change me>",
|
||||||
"s3_region": "garage",
|
"s3_region": "garage",
|
||||||
"s3_bucket": "bottin-pictures"
|
"s3_bucket": "bottin-pictures"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
flake.nix
11
flake.nix
|
@ -12,6 +12,9 @@
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
overlays = [
|
overlays = [
|
||||||
(import "${gomod2nix}/overlay.nix")
|
(import "${gomod2nix}/overlay.nix")
|
||||||
|
/*(self: super: {
|
||||||
|
gomod = super.callPackage "${gomod2nix}/builder/" { };
|
||||||
|
})*/
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
@ -35,16 +38,10 @@
|
||||||
platforms = platforms.linux;
|
platforms = platforms.linux;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
container = pkgs.dockerTools.buildImage {
|
container = pkgs.dockerTools.buildImage {
|
||||||
name = "dxflrs/guichet";
|
name = "dxflrs/guichet";
|
||||||
copyToRoot = pkgs.buildEnv {
|
|
||||||
name = "guichet-env";
|
|
||||||
paths = [ guichet pkgs.cacert ];
|
|
||||||
};
|
|
||||||
config = {
|
config = {
|
||||||
Entrypoint = "/bin/guichet";
|
Entrypoint = "${guichet}/bin/guichet";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
|
|
247
garage.go
247
garage.go
|
@ -4,7 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func gadmin() (*garage.APIClient, context.Context) {
|
func gadmin() (*garage.APIClient, context.Context) {
|
||||||
|
@ -23,9 +26,8 @@ func gadmin() (*garage.APIClient, context.Context) {
|
||||||
func grgCreateKey(name string) (*garage.KeyInfo, error) {
|
func grgCreateKey(name string) (*garage.KeyInfo, error) {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
kr := garage.NewAddKeyRequest()
|
kr := garage.AddKeyRequest{Name: &name}
|
||||||
kr.SetName(name)
|
resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(kr).Execute()
|
||||||
resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(*kr).Execute()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%+v\n", err)
|
fmt.Printf("%+v\n", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -36,7 +38,7 @@ func grgCreateKey(name string) (*garage.KeyInfo, error) {
|
||||||
func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
|
func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
resp, _, err := client.KeyApi.GetKey(ctx).Id(accessKey).ShowSecretKey("true").Execute()
|
resp, _, err := client.KeyApi.GetKey(ctx, accessKey).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%+v\n", err)
|
fmt.Printf("%+v\n", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -44,28 +46,6 @@ 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 grgDelKey(accessKey string) error {
|
|
||||||
client, ctx := gadmin()
|
|
||||||
|
|
||||||
_, err := client.KeyApi.DeleteKey(ctx).Id(accessKey).Execute()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%+v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
|
func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
|
@ -81,14 +61,41 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
|
||||||
return binfo, nil
|
return binfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func grgAllowKeyOnBucket(bid, gkey string, read, write, owner bool) (*garage.BucketInfo, error) {
|
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()
|
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(read, write, owner),
|
Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true),
|
||||||
}
|
}
|
||||||
binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
|
binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -111,7 +118,7 @@ func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess {
|
||||||
func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
|
func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
binfo, _, err := client.BucketApi.UpdateBucket(ctx).Id(bid).UpdateBucketRequest(*ur).Execute()
|
binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%+v\n", err)
|
fmt.Printf("%+v\n", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -168,7 +175,7 @@ func grgDelLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) {
|
||||||
func grgGetBucket(bid string) (*garage.BucketInfo, error) {
|
func grgGetBucket(bid string) (*garage.BucketInfo, error) {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
resp, _, err := client.BucketApi.GetBucketInfo(ctx).Id(bid).Execute()
|
resp, _, err := client.BucketApi.GetBucketInfo(ctx, bid).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -180,9 +187,187 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) {
|
||||||
func grgDeleteBucket(bid string) error {
|
func grgDeleteBucket(bid string) error {
|
||||||
client, ctx := gadmin()
|
client, ctx := gadmin()
|
||||||
|
|
||||||
_, err := client.BucketApi.DeleteBucket(ctx).Id(bid).Execute()
|
_, err := client.BucketApi.DeleteBucket(ctx, bid).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Start page rendering functions
|
||||||
|
|
||||||
|
func handleWebsiteConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := RequireUserHtml(w, r)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tKey := getTemplate("garage_key.html")
|
||||||
|
tKey.Execute(w, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebsiteList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := RequireUserHtml(w, r)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := NewWebsiteController(user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctrl.PrettyList) > 0 {
|
||||||
|
http.Redirect(w, r, "/website/inspect/"+ctrl.PrettyList[0], http.StatusFound)
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, "/website/new", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteNewTpl struct {
|
||||||
|
Ctrl *WebsiteController
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebsiteNew(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := RequireUserHtml(w, r)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := NewWebsiteController(user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := &WebsiteNewTpl{ctrl, nil}
|
||||||
|
|
||||||
|
tWebsiteNew := getTemplate("garage_website_new.html")
|
||||||
|
if r.Method == "POST" {
|
||||||
|
r.ParseForm()
|
||||||
|
|
||||||
|
bucket := strings.Join(r.Form["bucket"], "")
|
||||||
|
if bucket == "" {
|
||||||
|
bucket = strings.Join(r.Form["bucket2"], "")
|
||||||
|
}
|
||||||
|
|
||||||
|
view, err := ctrl.Create(bucket)
|
||||||
|
if err != nil {
|
||||||
|
tpl.Err = err
|
||||||
|
tWebsiteNew.Execute(w, tpl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tWebsiteNew.Execute(w, tpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteInspectTpl struct {
|
||||||
|
Describe *WebsiteDescribe
|
||||||
|
View *WebsiteView
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var processErr error
|
||||||
|
|
||||||
|
user := RequireUserHtml(w, r)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := NewWebsiteController(user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := mux.Vars(r)["bucket"]
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
r.ParseForm()
|
||||||
|
action := strings.Join(r.Form["action"], "")
|
||||||
|
switch action {
|
||||||
|
case "increase_quota":
|
||||||
|
_, processErr = ctrl.Patch(bucketName, &WebsitePatch{Size: &user.Quota.WebsiteSizeBursted})
|
||||||
|
case "delete_bucket":
|
||||||
|
processErr = ctrl.Delete(bucketName)
|
||||||
|
if processErr == nil {
|
||||||
|
http.Redirect(w, r, "/website", http.StatusFound)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
processErr = fmt.Errorf("Unknown action")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
view, err := ctrl.Inspect(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
describe, err := ctrl.Describe()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := &WebsiteInspectTpl{describe, view, processErr}
|
||||||
|
|
||||||
|
tWebsiteInspect := getTemplate("garage_website_inspect.html")
|
||||||
|
tWebsiteInspect.Execute(w, &tpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebsiteVhost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var processErr error
|
||||||
|
|
||||||
|
user := RequireUserHtml(w, r)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl, err := NewWebsiteController(user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := mux.Vars(r)["bucket"]
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
r.ParseForm()
|
||||||
|
|
||||||
|
bucket := strings.Join(r.Form["bucket"], "")
|
||||||
|
if bucket == "" {
|
||||||
|
bucket = strings.Join(r.Form["bucket2"], "")
|
||||||
|
}
|
||||||
|
|
||||||
|
view, processErr := ctrl.Patch(bucketName, &WebsitePatch{Vhost: &bucket})
|
||||||
|
if processErr == nil {
|
||||||
|
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view, err := ctrl.Inspect(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
describe, err := ctrl.Describe()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := &WebsiteInspectTpl{describe, view, processErr}
|
||||||
|
tWebsiteEdit := getTemplate("garage_website_edit.html")
|
||||||
|
tWebsiteEdit.Execute(w, &tpl)
|
||||||
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module git.deuxfleurs.fr/Deuxfleurs/guichet
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20231128153612-8b81fae65e5e
|
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9
|
||||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||||
github.com/emersion/go-smtp v0.12.1
|
github.com/emersion/go-smtp v0.12.1
|
||||||
github.com/go-ldap/ldap/v3 v3.1.6
|
github.com/go-ldap/ldap/v3 v3.1.6
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -33,8 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9 h1:ERg8KCpIKym98EOKa8Gq0NSBxsasD3sqb/R0gg1wOzU=
|
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9 h1:ERg8KCpIKym98EOKa8Gq0NSBxsasD3sqb/R0gg1wOzU=
|
||||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9/go.mod h1:TlSL6QVxozmdRaSgP6Akspi0HCJv4HAkkq3Dldru4GM=
|
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9/go.mod h1:TlSL6QVxozmdRaSgP6Akspi0HCJv4HAkkq3Dldru4GM=
|
||||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20231128153612-8b81fae65e5e h1:h89CAh0qmUcGJykss/utXIw+yRGa3Gr6VyrZ5ZWN0kY=
|
|
||||||
git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20231128153612-8b81fae65e5e/go.mod h1:TlSL6QVxozmdRaSgP6Akspi0HCJv4HAkkq3Dldru4GM=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
|
|
@ -2,8 +2,8 @@ schema = 3
|
||||||
|
|
||||||
[mod]
|
[mod]
|
||||||
[mod."git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"]
|
[mod."git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"]
|
||||||
version = "v0.0.0-20231128153612-8b81fae65e5e"
|
version = "v0.0.0-20230131081355-c965fe7f7dc9"
|
||||||
hash = "sha256-o9kbcJ25/cYYwWZz/LBF7ZDyW8bZAjdg5pPu0gvb5JQ="
|
hash = "sha256-qJN9yDMIh3xRk/3IYEWZca/biMVXXmDlPTzy0Cg11oc="
|
||||||
[mod."github.com/emersion/go-sasl"]
|
[mod."github.com/emersion/go-sasl"]
|
||||||
version = "v0.0.0-20191210011802-430746ea8b9b"
|
version = "v0.0.0-20191210011802-430746ea8b9b"
|
||||||
hash = "sha256-bADpAn0ZhlTTsEB3MsG8J31cQjTtHTzohX/wkL1aMIc="
|
hash = "sha256-bADpAn0ZhlTTsEB3MsG8J31cQjTtHTzohX/wkL1aMIc="
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"suffix": "dc=bottin,dc=eu",
|
"suffix": "dc=bottin,dc=eu",
|
||||||
"bind": "bottin:389",
|
"bind": "bottin:389",
|
||||||
"consul_host": "consul:8500",
|
"consul_host": "consul:8500",
|
||||||
"acl": [
|
"acl": [
|
||||||
"ANONYMOUS::bind:*,ou=users,dc=bottin,dc=eu:",
|
"ANONYMOUS::bind:*,ou=users,dc=bottin,dc=eu:",
|
||||||
"ANONYMOUS::bind:cn=admin,dc=bottin,dc=eu:",
|
"ANONYMOUS::bind:cn=admin,dc=bottin,dc=eu:",
|
||||||
|
"cn=admin,dc=bottin,dc=eu::bind read add modify delete:*:*",
|
||||||
"*,dc=bottin,dc=eu::read:*:* !userpassword",
|
"*,dc=bottin,dc=eu::read:*:* !userpassword",
|
||||||
"cn=admin,dc=bottin,dc=eu::read add modify delete:*:*",
|
"*:cn=admin,ou=groups,dc=bottin,dc=eu:bind read add modify delete:*:*",
|
||||||
"*:cn=admin,ou=groups,dc=bottin,dc=eu:read add modify delete:*:*",
|
|
||||||
|
|
||||||
"ANONYMOUS::bind:*,ou=invitations,dc=bottin,dc=eu:",
|
"ANONYMOUS::bind:*,ou=invitations,dc=bottin,dc=eu:",
|
||||||
"*,ou=invitations,dc=bottin,dc=eu::delete:SELF:*",
|
"*,ou=invitations,dc=bottin,dc=eu::delete:SELF:*",
|
||||||
|
@ -15,5 +15,5 @@
|
||||||
"*,ou=invitations,dc=bottin,dc=eu::modifyAdd:cn=email,ou=groups,dc=bottin,dc=eu:*",
|
"*,ou=invitations,dc=bottin,dc=eu::modifyAdd:cn=email,ou=groups,dc=bottin,dc=eu:*",
|
||||||
|
|
||||||
"*::read modify:SELF:*"
|
"*::read modify:SELF:*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
consul:
|
consul:
|
||||||
# sync with nixos stable packages assuming our stack is up to date
|
image: hashicorp/consul:1.16
|
||||||
# https://search.nixos.org/packages?channel=24.05&from=0&size=50&sort=relevance&type=packages&query=consul
|
|
||||||
image: hashicorp/consul:1.18
|
|
||||||
restart: "always"
|
restart: "always"
|
||||||
expose:
|
expose:
|
||||||
- 8500
|
- 8500
|
||||||
bottin:
|
bottin:
|
||||||
# sync with deuxfleurs/nixcfg/cluster/prod/app/core/deploy/bottin.hcl
|
|
||||||
# to ensure compatibility with prod
|
|
||||||
image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
|
image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
|
||||||
#command: "-config /etc/bottin.json"
|
#command: "-config /etc/bottin.json"
|
||||||
restart: "always"
|
restart: "always"
|
||||||
|
@ -19,9 +15,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- "./config/bottin.json:/config.json"
|
- "./config/bottin.json:/config.json"
|
||||||
garage:
|
garage:
|
||||||
# sync with deuxfleurs/nixcfg/cluster/prod/app/garage/deploy/garage.hcl
|
image: dxflrs/garage:v0.9.1
|
||||||
# to ensure compatibility with prod
|
|
||||||
image: superboum/garage:v1.0.0-rc1-hotfix-red-ftr-wquorum
|
|
||||||
ports:
|
ports:
|
||||||
- "3900:3900"
|
- "3900:3900"
|
||||||
- "3902:3902"
|
- "3902:3902"
|
||||||
|
|
36
login.go
36
login.go
|
@ -109,14 +109,21 @@ func NewLdapCon() (*ldap.Conn, error) {
|
||||||
|
|
||||||
// --- Capabilities ---
|
// --- Capabilities ---
|
||||||
type Capabilities struct {
|
type Capabilities struct {
|
||||||
CanAdmin bool
|
CanAdmin bool
|
||||||
CanInvite bool
|
CanInvite bool
|
||||||
|
CanUseEmail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
|
func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
|
||||||
// Initialize
|
// Initialize
|
||||||
canAdmin := false
|
canAdmin := false
|
||||||
canInvite := false
|
canInvite := false
|
||||||
|
canUseEmail := false
|
||||||
|
|
||||||
|
// Composable logic
|
||||||
|
hasAeroBucketId := false
|
||||||
|
hasAeroBucketName := false
|
||||||
|
hasAeroCryptoRoot := false
|
||||||
|
|
||||||
// Special case for the "admin" account that is de-facto admin
|
// Special case for the "admin" account that is de-facto admin
|
||||||
canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount)
|
canAdmin = strings.EqualFold(login.Info.DN(), config.AdminAccount)
|
||||||
|
@ -132,18 +139,27 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
|
||||||
canAdmin = true
|
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{
|
return &Capabilities{
|
||||||
CanAdmin: canAdmin,
|
CanAdmin: canAdmin,
|
||||||
CanInvite: canInvite,
|
CanInvite: canInvite,
|
||||||
|
CanUseEmail: canUseEmail,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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
|
||||||
|
@ -174,6 +190,10 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
|
||||||
FIELD_NAME_PROFILE_PICTURE,
|
FIELD_NAME_PROFILE_PICTURE,
|
||||||
FIELD_QUOTA_WEBSITE_SIZE_BURSTED,
|
FIELD_QUOTA_WEBSITE_SIZE_BURSTED,
|
||||||
FIELD_QUOTA_WEBSITE_COUNT,
|
FIELD_QUOTA_WEBSITE_COUNT,
|
||||||
|
FIELD_QUOTA_PIM_SIZE_BURSTED,
|
||||||
|
FIELD_AEROGRAMME_CRYPTOROOT,
|
||||||
|
FIELD_AEROGRAMME_BUCKET_ID,
|
||||||
|
FIELD_AEROGRAMME_BUCKET_NAME,
|
||||||
},
|
},
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
@ -187,9 +207,7 @@ 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),
|
||||||
|
@ -207,7 +225,9 @@ func (lu *LoggedUser) WelcomeName() string {
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
func (lu *LoggedUser) Email() string {
|
||||||
|
return lu.Entry.GetAttributeValue("mail")
|
||||||
|
}
|
||||||
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
|
||||||
|
@ -216,7 +236,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.Username)
|
keyPair, err = grgCreateKey(lu.Login.Info.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -225,7 +245,7 @@ func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
|
||||||
// @FIXME compatibility feature for bagage (SFTP+webdav)
|
// @FIXME compatibility feature for bagage (SFTP+webdav)
|
||||||
// you can remove it once bagage will be updated to fetch the key from garage directly
|
// you can remove it once bagage will be updated to fetch the key from garage directly
|
||||||
// or when bottin will be able to dynamically fetch it.
|
// or when bottin will be able to dynamically fetch it.
|
||||||
modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey.Get()})
|
modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey})
|
||||||
err = lu.Login.conn.Modify(modify_request)
|
err = lu.Login.conn.Modify(modify_request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
4
main.go
4
main.go
|
@ -159,9 +159,13 @@ func server(args []string) {
|
||||||
|
|
||||||
r.HandleFunc("/website", handleWebsiteList)
|
r.HandleFunc("/website", handleWebsiteList)
|
||||||
r.HandleFunc("/website/new", handleWebsiteNew)
|
r.HandleFunc("/website/new", handleWebsiteNew)
|
||||||
|
r.HandleFunc("/website/configure", handleWebsiteConfigure)
|
||||||
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
|
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
|
||||||
r.HandleFunc("/website/vhost/{bucket}", handleWebsiteVhost)
|
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/new_account", handleInviteNewAccount)
|
||||||
r.HandleFunc("/invite/send_code", handleInviteSendCode)
|
r.HandleFunc("/invite/send_code", handleInviteSendCode)
|
||||||
r.HandleFunc("/invitation/{code}", handleInvitationCode)
|
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
|
56
pim_http.go
Normal file
56
pim_http.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PimInspectView struct {
|
||||||
|
User *LoggedUser
|
||||||
|
Debug string
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
view := PimInspectView {
|
||||||
|
User: user,
|
||||||
|
Debug: string(pim_json),
|
||||||
|
}
|
||||||
|
|
||||||
|
tKey := getTemplate("pim_inspect.html")
|
||||||
|
tKey.Execute(w, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
"github.com/go-ldap/ldap/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Note: PIM = Personal Information Manager
|
||||||
const (
|
const (
|
||||||
// --- Default Quota Values ---
|
// --- Default Quota Values Websites ---
|
||||||
QUOTA_WEBSITE_SIZE_DEFAULT = 1024 * 1024 * 50 // 50MB
|
QUOTA_WEBSITE_SIZE_DEFAULT = 1024 * 1024 * 50 // 50MB
|
||||||
QUOTA_WEBSITE_SIZE_BURSTED = 1024 * 1024 * 200 // 200MB
|
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
|
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 ---
|
// --- Per-user overridable fields ---
|
||||||
FIELD_QUOTA_WEBSITE_SIZE_BURSTED = "quota_website_size_bursted"
|
FIELD_QUOTA_WEBSITE_SIZE_BURSTED = "quota_website_size_bursted"
|
||||||
FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count"
|
FIELD_QUOTA_WEBSITE_COUNT = "quota_website_count"
|
||||||
|
FIELD_QUOTA_PIM_SIZE_BURSTED = "quota_pim_size_bursted"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserQuota struct {
|
type UserQuota struct {
|
||||||
|
@ -26,6 +33,9 @@ type UserQuota struct {
|
||||||
WebsiteSizeDefault int64
|
WebsiteSizeDefault int64
|
||||||
WebsiteSizeBursted int64
|
WebsiteSizeBursted int64
|
||||||
WebsiteObjects int64
|
WebsiteObjects int64
|
||||||
|
PimSizeDefault int64
|
||||||
|
PimSizeBursted int64
|
||||||
|
PimObjects int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserQuota() *UserQuota {
|
func NewUserQuota() *UserQuota {
|
||||||
|
@ -34,6 +44,9 @@ func NewUserQuota() *UserQuota {
|
||||||
WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT,
|
WebsiteSizeDefault: QUOTA_WEBSITE_SIZE_DEFAULT,
|
||||||
WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED,
|
WebsiteSizeBursted: QUOTA_WEBSITE_SIZE_BURSTED,
|
||||||
WebsiteObjects: QUOTA_WEBSITE_OBJECTS,
|
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
|
quotas.WebsiteSizeBursted = q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q, err := entryToQuota(entry, FIELD_QUOTA_PIM_SIZE_BURSTED); err == nil {
|
||||||
|
quotas.PimSizeBursted = q
|
||||||
|
}
|
||||||
|
|
||||||
return quotas
|
return quotas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +95,16 @@ func (q *UserQuota) DefaultWebsiteQuota() *garage.UpdateBucketRequestQuotas {
|
||||||
return qr
|
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 {
|
func (q *UserQuota) WebsiteSizeAdjust(sz int64) int64 {
|
||||||
if sz < q.WebsiteSizeDefault {
|
if sz < q.WebsiteSizeDefault {
|
||||||
return q.WebsiteSizeDefault
|
return q.WebsiteSizeDefault
|
||||||
|
@ -100,6 +127,29 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string {
|
||||||
return prettyValue(q.WebsiteSizeBursted)
|
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
|
// --- A quota stat we can use
|
||||||
type QuotaStat struct {
|
type QuotaStat struct {
|
||||||
Current int64 `json:"current"`
|
Current int64 `json:"current"`
|
||||||
|
|
234
templates/garage_key.html
Normal file
234
templates/garage_key.html
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
{{define "title"}}Profile |{{end}}
|
||||||
|
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="d-flex">
|
||||||
|
<h4>Mes identifiants</h4>
|
||||||
|
<a class="ml-auto btn btn-link" href="/website">Mes sites webs</a>
|
||||||
|
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs" id="proto" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" id="s3-tab" data-toggle="tab" href="#s3" role="tab" aria-controls="s3" aria-selected="true">S3</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="sftp-tab" data-toggle="tab" href="#sftp" role="tab" aria-controls="sftp" aria-selected="false">SFTP</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="protocols">
|
||||||
|
<div class="tab-pane fade show active" id="s3" role="tabpanel" aria-labelledby="s3-tab">
|
||||||
|
<table class="table mt-4">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="col-md-2">Identifiant de clé</th>
|
||||||
|
<td>{{ .S3KeyInfo.AccessKeyId }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Clé secrète</th>
|
||||||
|
<td><a href="#" onclick="document.getElementById('secret_key').style.display='inline'; this.style.display='none'">Cliquer pour afficher la clé secrète</a><span id="secret_key" style="display: none">{{ .S3KeyInfo.SecretAccessKey }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Région</th>
|
||||||
|
<td>garage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Endpoint URL</th>
|
||||||
|
<td>https://garage.deuxfleurs.fr</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Type d'URL</th>
|
||||||
|
<td>DNS et chemin (préférer chemin)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Signature</th>
|
||||||
|
<td>Version 4</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Configurer votre logiciel :</p>
|
||||||
|
|
||||||
|
<div class="accordion" id="softconfig">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="awscli-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#awscli" aria-expanded="false" aria-controls="awscli">
|
||||||
|
awscli
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="awscli" class="collapse" aria-labelledby="awscli-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Créez un fichier nommé <code>~/.awsrc</code> :</p>
|
||||||
|
<pre>
|
||||||
|
export AWS_ACCESS_KEY_ID={{ .S3KeyInfo.AccessKeyId }}
|
||||||
|
export AWS_SECRET_ACCESS_KEY={{ .S3KeyInfo.SecretAccessKey }}
|
||||||
|
export AWS_DEFAULT_REGION='garage'
|
||||||
|
|
||||||
|
function aws { command aws --endpoint-url https://garage.deuxfleurs.fr $@ ; }
|
||||||
|
aws --version
|
||||||
|
</pre>
|
||||||
|
<p>Ensuite vous pouvez utiliser awscli :</p>
|
||||||
|
<pre>
|
||||||
|
source ~/.awsrc
|
||||||
|
aws s3 ls
|
||||||
|
aws s3 ls s3://my-bucket
|
||||||
|
aws s3 cp /tmp/a.txt s3://my-bucket
|
||||||
|
...
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="minio-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#minio" aria-expanded="true" aria-controls="minio">
|
||||||
|
Minio CLI
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="minio" class="collapse" aria-labelledby="minio-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Vous pouvez configurer Minio CLI avec cette commande :</p>
|
||||||
|
<pre>
|
||||||
|
mc alias set \
|
||||||
|
garage \
|
||||||
|
https://garage.deuxfleurs.fr \
|
||||||
|
{{ .S3KeyInfo.AccessKeyId }} \
|
||||||
|
{{ .S3KeyInfo.SecretAccessKey }} \
|
||||||
|
--api S3v4
|
||||||
|
</pre>
|
||||||
|
<p>Et ensuite pour utiliser Minio CLI avec :</p>
|
||||||
|
<pre>
|
||||||
|
mc ls garage/
|
||||||
|
mc cp /tmp/a.txt garage/my-bucket/a.txt
|
||||||
|
...
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="winscp-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#winscp" aria-expanded="true" aria-controls="winscp">
|
||||||
|
WinSCP
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="winscp" class="collapse" aria-labelledby="winscp-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
Reportez vous <a href="">au guide</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="hugo-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#hugo" aria-expanded="false" aria-controls="hugo">
|
||||||
|
Hugo
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="hugo" class="collapse" aria-labelledby="hugo-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Dans votre fichier <code>config.toml</code>, rajoutez :</p>
|
||||||
|
<pre>
|
||||||
|
[[deployment.targets]]
|
||||||
|
URL = "s3://bucket?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true&region=garage"
|
||||||
|
</pre>
|
||||||
|
<p>Assurez-vous d'avoir un fichier dans lequel les variables <code>AWS_ACCESS_KEY_ID</code> et <code>AWS_SECRET_ACCESS_KEY</code> sont définies,
|
||||||
|
ici on suppose que vous avez suivi les instructions de l'outil awscli (ci-dessus) et que vous avez un fichier <code>~/.awsrc</code> qui défini ces variables.
|
||||||
|
Ensuite : </p>
|
||||||
|
<pre>
|
||||||
|
source ~/.awsrc
|
||||||
|
hugo deploy
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="publii-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#publii" aria-expanded="false" aria-controls="publii">
|
||||||
|
Publii
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="publii" class="collapse" aria-labelledby="publii-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<em>Bientôt...</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sftp -->
|
||||||
|
<div class="tab-pane fade" id="sftp" role="tabpanel" aria-labelledby="sftp-tab">
|
||||||
|
<table class="table mt-4">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Nom d'utilisateur-ice</th>
|
||||||
|
<td>{{ .Login.Info.Username }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Mot de passe</th>
|
||||||
|
<td>(votre mot de passe guichet)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Hôte</th>
|
||||||
|
<td>sftp://bagage.deuxfleurs.fr</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Port</th>
|
||||||
|
<td>2222</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>Configurer votre logiciel :</p>
|
||||||
|
|
||||||
|
<div class="accordion" id="softconfig2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="filezilla-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
||||||
|
scp
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="filezilla" class="collapse show" aria-labelledby="filezilla-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Un exemple avec SCP :</p>
|
||||||
|
<pre>
|
||||||
|
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="filezilla-title">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
||||||
|
Filezilla
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="filezilla" class="collapse" aria-labelledby="filezilla-title" data-parent="#softconfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<em>Bientôt</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
|
@ -3,8 +3,8 @@
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<!--<h4>Inspecter les sites webs</h4>-->
|
<!--<h4>Inspecter les sites webs</h4>-->
|
||||||
<a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a>
|
<a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a>
|
||||||
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -15,9 +15,9 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div class="col-md-3 mt-3">
|
<div class="col-md-3 mt-3">
|
||||||
<a class="btn btn-primary btn-block" href="/website/new">
|
<a class="btn btn-primary btn-block" href="/website/new">
|
||||||
<svg id="i-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="18" height="18" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
|
<svg id="i-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="18" height="18" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
|
||||||
<path d="M16 2 L16 30 M2 16 L30 16" />
|
<path d="M16 2 L16 30 M2 16 L30 16" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ml-1">Nouveau site web</span>
|
<span class="ml-1">Nouveau site web</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -45,13 +45,11 @@
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<h2>{{ .View.Name.Url }}</h2>
|
<h2>{{ .View.Name.Url }}</h2>
|
||||||
|
|
||||||
<!-- QUOTAS -->
|
|
||||||
|
|
||||||
<h5 class="mt-3">Quotas</h5>
|
<h5 class="mt-3">Quotas</h5>
|
||||||
<div class="progress mt-3">
|
<div class="progress mt-3">
|
||||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ .View.Size.Current }}" aria-valuemin="0" aria-valuemax="{{ .View.Size.Max }}" style="width: {{ .View.Size.Percent }}%; min-width: 2em;">
|
<div class="progress-bar" role="progressbar" aria-valuenow="{{ .View.Size.Current }}" aria-valuemin="0" aria-valuemax="{{ .View.Size.Max }}" style="width: {{ .View.Size.Percent }}%; min-width: 2em;">
|
||||||
{{ .View.Size.Percent }}%
|
{{ .View.Size.Percent }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
|
@ -61,295 +59,23 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ACTIONS -->
|
|
||||||
<h5 class="mt-3">Actions</h5>
|
<h5 class="mt-3">Actions</h5>
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
<div class="btn-group" role="group" aria-label="Actions sur le site web">
|
<div class="btn-group" role="group" aria-label="Actions sur le site web">
|
||||||
<button class="btn btn-secondary" name="action" value="increase_quota">Augmenter le quota</button>
|
<button class="btn btn-secondary" name="action" value="increase_quota">Augmenter le quota</button>
|
||||||
<button class="btn btn-secondary" name="action" value="rotate_key">Rotation de la clé</button>
|
|
||||||
<a class="btn btn-secondary" href="/website/vhost/{{ .View.Name.Pretty }}">Changer le nom de domaine</a>
|
<a class="btn btn-secondary" href="/website/vhost/{{ .View.Name.Pretty }}">Changer le nom de domaine</a>
|
||||||
<button class="btn btn-danger" name="action" value="delete_bucket">Supprimer</button>
|
<button class="btn btn-danger" name="action" value="delete_bucket">Supprimer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<!-- INFO -->
|
{{ if .View.Name.Expanded }}
|
||||||
<h5 class="mt-3">Informations de connexion</h5>
|
<h5 class="mt-5">Vous ne savez pas comment configurer votre nom de domaine ?</h5>
|
||||||
<ul class="nav nav-tabs" id="proto" role="tablist">
|
<p> Le nom de domaine {{ .View.Name.Url }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
|
||||||
<li class="nav-item">
|
{{ end }}
|
||||||
<a class="nav-link active" id="s3-tab" data-toggle="tab" href="#s3" role="tab" aria-controls="s3" aria-selected="true">S3 (recommandé)</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" id="sftp-tab" data-toggle="tab" href="#sftp" role="tab" aria-controls="sftp" aria-selected="false">SFTP</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" id="dav-tab" data-toggle="tab" href="#dav" role="tab" aria-controls="dav" aria-selected="false">WebDAV</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="tab-content" id="protocols">
|
|
||||||
<div class="tab-pane fade show active" id="s3" role="tabpanel" aria-labelledby="s3-tab">
|
|
||||||
<table class="table mt-4">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row" class="col-md-2">Identifiant de clé</th>
|
|
||||||
<td>{{ .View.AccessKeyId }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Clé secrète</th>
|
|
||||||
<td>
|
|
||||||
<a href="#" onclick="document.getElementById('secret_key').style.display='inline'; this.style.display='none'">[Afficher la clé secrète]</a>
|
|
||||||
<span id="secret_key" style="display: none">{{ .View.SecretAccessKey }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Région</th>
|
|
||||||
<td>garage</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Endpoint URL</th>
|
|
||||||
<td>https://garage.deuxfleurs.fr</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Type d'URL</th>
|
|
||||||
<td>DNS et chemin (préférer chemin)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Signature</th>
|
|
||||||
<td>Version 4</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p>Configurer votre logiciel :</p>
|
|
||||||
|
|
||||||
<div class="accordion" id="softconfig">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="awscli-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#awscli" aria-expanded="false" aria-controls="awscli">
|
|
||||||
awscli (tout générateur de site statique)
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div id="awscli" class="collapse show" aria-labelledby="awscli-title" data-parent="#softconfig">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Lancez la commande :</p>
|
|
||||||
<pre>aws --profile {{ .View.Name.Pretty }} configure</pre>
|
|
||||||
|
|
||||||
<p>Entrez les informations suivantes quand elles vous sont demandées :</p>
|
|
||||||
<dl>
|
|
||||||
<dt>AWS Access Key ID [None]:</dt><dd>{{ .View.AccessKeyId }}</dd>
|
|
||||||
<dt>AWS Secret Access Key [None]:</dt><dd><a href="#" onclick="document.getElementById('aws_secret_key').style.display='inline'; this.style.display='none'">[Afficher la clé secrète]</a><span id="aws_secret_key" style="display: none">{{ .View.SecretAccessKey }}</span></dd>
|
|
||||||
<dt>Default region name [None]:</dt> <dd>garage</dd>
|
|
||||||
<dt>Default output format [None]:</dt> <dd>(laissez vide et appuyez sur entrée)</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<p>Finalisez la configuration :</p>
|
|
||||||
<pre>aws --profile {{ .View.Name.Pretty }} configure set endpoint_url https://garage.deuxfleurs.fr</pre>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Pour déployer votre dossier local <code>public</code> lancez :</p>
|
|
||||||
<pre>
|
|
||||||
aws --profile {{ .View.Name.Pretty }} s3 sync ./public s3://{{ .View.Name.Pretty }}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="minio-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#minio" aria-expanded="true" aria-controls="minio">
|
|
||||||
Minio CLI (tout générateur de site statique)
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="minio" class="collapse" aria-labelledby="minio-title" data-parent="#softconfig">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Vous pouvez configurer Minio CLI avec cette commande :</p>
|
|
||||||
<pre>
|
|
||||||
mc alias set \
|
|
||||||
{{ .View.Name.Pretty }} \
|
|
||||||
https://garage.deuxfleurs.fr \
|
|
||||||
{{ .View.AccessKeyId }} \
|
|
||||||
<a href="#" onclick="document.getElementById('minio_secret_key').style.display='inline'; this.style.display='none'">[Afficher la clé secrète]</a><span id="minio_secret_key" style="display: none">{{ .View.SecretAccessKey }}</span> \
|
|
||||||
--api S3v4
|
|
||||||
</pre>
|
|
||||||
<p>Et ensuite copiez votre site web avec la sous-commande mirror de Minio CLI :</p>
|
|
||||||
<pre>
|
|
||||||
mc mirror --overwrite ./public/ {{ .View.Name.Pretty }}/
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="hugo-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#hugo" aria-expanded="false" aria-controls="hugo">
|
|
||||||
Hugo
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div id="hugo" class="collapse" aria-labelledby="hugo-title" data-parent="#softconfig">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Créez un fichier nommé <code>.deployment.secrets</code> (ne commitez pas ce fichier dans votre dépôt !) :</p>
|
|
||||||
<pre>
|
|
||||||
export AWS_ACCESS_KEY_ID={{ .View.AccessKeyId }}
|
|
||||||
export AWS_SECRET_ACCESS_KEY=<a href="#" onclick="document.getElementById('ugo_secret_key').style.display='inline'; this.style.display='none'">[Afficher la clé secrète]</a><span id="hugo_secret_key" style="display: none">{{ .View.SecretAccessKey }}</span>
|
|
||||||
</pre>
|
|
||||||
<p>Dans votre fichier de configuration Hugo <code>config.toml</code> (que vous pouvez commiter), rajoutez :</p>
|
|
||||||
<pre>
|
|
||||||
[[deployment.targets]]
|
|
||||||
URL = "s3://bucket?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true&region=garage"
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>Pour déployer, sourcez le fichier de configuration et laissez hugo faire : </p>
|
|
||||||
<pre>
|
|
||||||
source .deployment.secrets
|
|
||||||
hugo deploy
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="sftp" role="tabpanel" aria-labelledby="sftp-tab">
|
|
||||||
<br>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
N'automatisez pas votre déploiement en SFTP car vous risqueriez de faire fuiter votre mot de passe.<br>
|
|
||||||
Pour toute forme d'automatisation, préférez le protocole S3.
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
L'algorithme de clé utilisé par le serveur est désactivé par défaut sur les clients SSH récents.<br>
|
|
||||||
Vous devez rajouter l'option -oHostKeyAlgorithms=+ssh-rsa pour vous connecter.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="table mt-4">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Nom d'utilisateur-ice</th>
|
|
||||||
<td>{{ .Describe.Username }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Mot de passe</th>
|
|
||||||
<td>(votre mot de passe guichet)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Hôte</th>
|
|
||||||
<td>sftp://sftp.deuxfleurs.fr</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Port</th>
|
|
||||||
<td>2222</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>Configurez votre logiciel :</p>
|
|
||||||
|
|
||||||
<div class="accordion" id="softconfig2">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="scp-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
|
||||||
scp
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div id="filezilla" class="collapse show" aria-labelledby="scp-title" data-parent="#softconfig2">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Déployer le dossier local <em>public</em> sur le site web {{ .View.Name.Pretty }} :</p>
|
|
||||||
<pre>
|
|
||||||
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Describe.Username }}@sftp.deuxfleurs.fr:{{ .View.Name.Pretty }}/
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="filezilla-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
|
|
||||||
Filezilla
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div id="filezilla" class="collapse" aria-labelledby="filezilla-title" data-parent="#softconfig2">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Dans la barre de connexion rapide du haut, entrez :</p>
|
|
||||||
<dl>
|
|
||||||
<dt>Hôte</dt> <dd>sftp://sftp.deuxfleurs.fr</dd>
|
|
||||||
<dt>Nom d'utilisateur</dt> <dd>{{ .Describe.Username }}</dd>
|
|
||||||
<dt>Mot de passe</dt> <dd>(votre mot de passe guichet)</dd>
|
|
||||||
<dt>Port</dt> <dd>2222</dd>
|
|
||||||
</dl>
|
|
||||||
<p>Cliquez ensuite sur <strong>Connexion rapide</strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="dav" role="tabpanel" aria-labelledby="dav-tab">
|
|
||||||
<br>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
N'automatisez pas votre déploiement en WebDAV car vous risqueriez de faire fuiter votre mot de passe.<br>
|
|
||||||
Pour toute forme d'automatisation, préférez le protocole S3.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="table mt-4">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Nom d'utilisateur-ice</th>
|
|
||||||
<td>{{ .Describe.Username }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Mot de passe</th>
|
|
||||||
<td>(votre mot de passe guichet)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Hôte</th>
|
|
||||||
<td>https://bagage.deuxfleurs.fr ou davs://bagage.deuxfleurs.fr</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Port</th>
|
|
||||||
<td>443 (par défaut)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>Configurez votre logiciel :</p>
|
|
||||||
|
|
||||||
<div class="accordion" id="softconfig3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header" id="drive-title">
|
|
||||||
<h2 class="mb-0">
|
|
||||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#drive" aria-expanded="false" aria-controls="filezilla">
|
|
||||||
Explorateur web
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div id="drive" class="collapse show" aria-labelledby="drive-title" data-parent="#softconfig3">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Vous pouvez naviguer dans vos fichiers via l'explorateur web.
|
|
||||||
Utilisez simplement vos identifiants Guichet, l'explorateur est préconfiguré.</p>
|
|
||||||
|
|
||||||
<p><a href="https://drive.deuxfleurs.fr">Accéder à l'explorateur</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ if .View.Name.Expanded }}
|
|
||||||
<h5 class="mt-5">Vous ne savez pas comment configurer votre nom de domaine ?</h5>
|
|
||||||
<p> Le nom de domaine {{ .View.Name.Url }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,31 @@
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Mes services
|
Mes publications sur la toile
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
|
<a class="list-group-item list-group-item-action" href="/website/configure">Mes identifiants</a>
|
||||||
<a class="list-group-item list-group-item-action" href="/website">Mes sites Web</a>
|
<a class="list-group-item list-group-item-action" href="/website">Mes sites Web</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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}}
|
{{if .User.Capabilities.CanInvite}}
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|
33
templates/pim_inspect.html
Normal file
33
templates/pim_inspect.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{{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>
|
||||||
|
--- login info ---
|
||||||
|
email: {{ .User.Email }}
|
||||||
|
username: {{ .User.Login.Info.Username }}
|
||||||
|
password: ********
|
||||||
|
|
||||||
|
--- autodiscovery ---
|
||||||
|
Implemented: RFC6186 DNS SRV autodiscovery + Thunderbird Autoconfig
|
||||||
|
Not implemented: Microsoft Autodiscover + Apple Mobileconfig
|
||||||
|
|
||||||
|
--- manual configuration ---
|
||||||
|
IMAP: imap.saint-ex.deuxfleurs.org:993 (TLS)
|
||||||
|
SMTP: smtp.saint-ex.deuxfleurs.org:465 (TLS)
|
||||||
|
|
||||||
|
--- dump PIM controller ---
|
||||||
|
{{ .Debug }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
238
website.go
238
website.go
|
@ -3,7 +3,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -19,9 +18,7 @@ var (
|
||||||
ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket")
|
ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket")
|
||||||
ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket")
|
ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket")
|
||||||
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, this is 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 {
|
||||||
|
@ -52,18 +49,8 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
|
||||||
return NewWebsiteId(*binfo.Id, binfo.GlobalAliases)
|
return NewWebsiteId(*binfo.Id, binfo.GlobalAliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----
|
|
||||||
|
|
||||||
type WebsiteDescribe struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
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
|
||||||
|
@ -90,154 +77,33 @@ 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, keyInfo, idx, wlist, quota}, nil
|
return &WebsiteController{user, idx, wlist, quota}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) getDedicatedWebsiteKey(binfo *garage.BucketInfo) (*garage.KeyInfo, error) {
|
type WebsiteDescribe struct {
|
||||||
// Check bucket info is not null
|
AccessKeyId string `json:"access_key_id"`
|
||||||
if binfo == nil {
|
SecretAccessKey string `json:"secret_access_key"`
|
||||||
return nil, ErrFetchBucketInfo
|
AllowedWebsites *QuotaStat `json:"quota_website_count"`
|
||||||
}
|
BurstBucketQuotaSize string `json:"burst_bucket_quota_size"`
|
||||||
|
Websites []*WebsiteId `json:"vhosts"`
|
||||||
// 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 key name is *exactly* the one we requested
|
|
||||||
if *keyInfo.Name != dedicatedKeyName {
|
|
||||||
log.Printf("Expected key: %s, got %s. Invariant violated.\n", dedicatedKeyName, *keyInfo.Name)
|
|
||||||
return nil, ErrDedicatedKeyInvariant
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the dedicated key does not contain any other bucket than this one
|
|
||||||
// and report if 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebsiteController) flushDedicatedWebsiteKey(binfo *garage.BucketInfo) error {
|
|
||||||
// Check bucket info is not null
|
|
||||||
if binfo == nil {
|
|
||||||
return 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 ErrDedicatedKeyInvariant
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the string template by concatening the username and the bucket identifier
|
|
||||||
dedicatedKeyName := fmt.Sprintf("%s:web:%s", w.User.Username, *binfo.Id)
|
|
||||||
|
|
||||||
// Fetch the dedicated key
|
|
||||||
keyInfo, err := grgSearchKey(dedicatedKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the key name is *exactly* the one we requested
|
|
||||||
if *keyInfo.Name != dedicatedKeyName {
|
|
||||||
log.Printf("Expected key: %s, got %s. Invariant violated.\n", dedicatedKeyName, *keyInfo.Name)
|
|
||||||
return ErrDedicatedKeyInvariant
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the dedicated key contains no other bucket than this one
|
|
||||||
// (can also be empty, useful to heal a partially created key)
|
|
||||||
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 ErrDedicatedKeyInvariant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally delete this key
|
|
||||||
err = grgDelKey(*keyInfo.AccessKeyId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("Deleted dedicated key %s", dedicatedKeyName)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
|
func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
|
||||||
|
s3key, err := w.User.S3KeyInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
r := make([]*WebsiteId, 0, len(w.PrettyList))
|
r := make([]*WebsiteId, 0, len(w.PrettyList))
|
||||||
for _, k := range w.PrettyList {
|
for _, k := range w.PrettyList {
|
||||||
r = append(r, w.WebsiteIdx[k])
|
r = append(r, w.WebsiteIdx[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
return &WebsiteDescribe{
|
return &WebsiteDescribe{
|
||||||
w.User.Username,
|
*s3key.AccessKeyId,
|
||||||
|
*s3key.SecretAccessKey,
|
||||||
&w.WebsiteCount,
|
&w.WebsiteCount,
|
||||||
w.User.Quota.WebsiteSizeBurstedPretty(),
|
w.User.Quota.WebsiteSizeBurstedPretty(),
|
||||||
r,
|
r}, nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
|
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
|
||||||
|
@ -251,12 +117,7 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
|
||||||
return nil, ErrFetchBucketInfo
|
return nil, ErrFetchBucketInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
|
return NewWebsiteView(binfo), nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewWebsiteView(binfo, dedicatedKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
|
func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
|
||||||
|
@ -300,19 +161,7 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if patch.RotateKey != nil && *patch.RotateKey {
|
return NewWebsiteView(binfo), nil
|
||||||
err = w.flushDedicatedWebsiteKey(binfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -324,24 +173,21 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
|
||||||
return nil, ErrWebsiteQuotaReached
|
return nil, ErrWebsiteQuotaReached
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
binfo, err := grgCreateBucket(pretty)
|
binfo, err := grgCreateBucket(pretty)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrCantCreateBucket
|
return nil, ErrCantCreateBucket
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow user's global key on bucket
|
|
||||||
s3key, err := w.User.S3KeyInfo()
|
s3key, err := w.User.S3KeyInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId, true, true, true)
|
binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrCantAllowKey
|
return nil, ErrCantAllowKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set quota
|
|
||||||
qr := w.User.Quota.DefaultWebsiteQuota()
|
qr := w.User.Quota.DefaultWebsiteQuota()
|
||||||
wr := allowWebsiteDefault()
|
wr := allowWebsiteDefault()
|
||||||
|
|
||||||
|
@ -354,13 +200,7 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
|
||||||
return nil, ErrCantConfigureBucket
|
return nil, ErrCantConfigureBucket
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a dedicated key
|
return NewWebsiteView(binfo), nil
|
||||||
dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewWebsiteView(binfo, dedicatedKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) Delete(pretty string) error {
|
func (w *WebsiteController) Delete(pretty string) error {
|
||||||
|
@ -373,7 +213,6 @@ func (w *WebsiteController) Delete(pretty string) error {
|
||||||
return ErrWebsiteNotFound
|
return ErrWebsiteNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error checking
|
|
||||||
binfo, err := grgGetBucket(website.Internal)
|
binfo, err := grgGetBucket(website.Internal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrFetchBucketInfo
|
return ErrFetchBucketInfo
|
||||||
|
@ -387,49 +226,26 @@ func (w *WebsiteController) Delete(pretty string) error {
|
||||||
return ErrBucketDeleteUnfinishedUpload
|
return ErrBucketDeleteUnfinishedUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete dedicated key
|
|
||||||
err = w.flushDedicatedWebsiteKey(binfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actually delete bucket
|
|
||||||
err = grgDeleteBucket(website.Internal)
|
err = grgDeleteBucket(website.Internal)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteView struct {
|
type WebsiteView struct {
|
||||||
Name *WebsiteId `json:"vhost"`
|
Name *WebsiteId `json:"vhost"`
|
||||||
AccessKeyId string `json:"access_key_id"`
|
Size QuotaStat `json:"quota_size"`
|
||||||
SecretAccessKey string `json:"secret_access_key"`
|
Files QuotaStat `json:"quota_files"`
|
||||||
Size QuotaStat `json:"quota_size"`
|
|
||||||
Files QuotaStat `json:"quota_files"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) {
|
func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
|
||||||
if binfo == nil {
|
|
||||||
return nil, ErrFetchBucketInfo
|
|
||||||
}
|
|
||||||
if s3key == nil {
|
|
||||||
return nil, ErrFetchDedicatedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
q := binfo.GetQuotas()
|
q := binfo.GetQuotas()
|
||||||
|
|
||||||
wid := NewWebsiteIdFromBucketInfo(binfo)
|
wid := NewWebsiteIdFromBucketInfo(binfo)
|
||||||
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, size, objects}
|
||||||
wid,
|
|
||||||
*s3key.AccessKeyId,
|
|
||||||
*s3key.SecretAccessKey.Get(),
|
|
||||||
size,
|
|
||||||
objects,
|
|
||||||
}, 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"`
|
|
||||||
}
|
}
|
||||||
|
|
178
webui_website.go
178
webui_website.go
|
@ -1,178 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- Start page rendering functions
|
|
||||||
func handleWebsiteList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := RequireUserHtml(w, r)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewWebsiteController(user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ctrl.PrettyList) > 0 {
|
|
||||||
http.Redirect(w, r, "/website/inspect/"+ctrl.PrettyList[0], http.StatusFound)
|
|
||||||
} else {
|
|
||||||
http.Redirect(w, r, "/website/new", http.StatusFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsiteNewTpl struct {
|
|
||||||
Ctrl *WebsiteController
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleWebsiteNew(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := RequireUserHtml(w, r)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewWebsiteController(user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := &WebsiteNewTpl{ctrl, nil}
|
|
||||||
|
|
||||||
tWebsiteNew := getTemplate("garage_website_new.html")
|
|
||||||
if r.Method == "POST" {
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
bucket := strings.Join(r.Form["bucket"], "")
|
|
||||||
if bucket == "" {
|
|
||||||
bucket = strings.Join(r.Form["bucket2"], "")
|
|
||||||
}
|
|
||||||
|
|
||||||
view, err := ctrl.Create(bucket)
|
|
||||||
if err != nil {
|
|
||||||
tpl.Err = err
|
|
||||||
tWebsiteNew.Execute(w, tpl)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tWebsiteNew.Execute(w, tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsiteInspectTpl struct {
|
|
||||||
Describe *WebsiteDescribe
|
|
||||||
View *WebsiteView
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var processErr error
|
|
||||||
|
|
||||||
user := RequireUserHtml(w, r)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewWebsiteController(user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketName := mux.Vars(r)["bucket"]
|
|
||||||
|
|
||||||
if r.Method == "POST" {
|
|
||||||
r.ParseForm()
|
|
||||||
action := strings.Join(r.Form["action"], "")
|
|
||||||
switch action {
|
|
||||||
case "increase_quota":
|
|
||||||
_, processErr = ctrl.Patch(bucketName, &WebsitePatch{Size: &user.Quota.WebsiteSizeBursted})
|
|
||||||
case "delete_bucket":
|
|
||||||
processErr = ctrl.Delete(bucketName)
|
|
||||||
if processErr == nil {
|
|
||||||
http.Redirect(w, r, "/website", http.StatusFound)
|
|
||||||
}
|
|
||||||
case "rotate_key":
|
|
||||||
do_action := true
|
|
||||||
_, processErr = ctrl.Patch(bucketName, &WebsitePatch{RotateKey: &do_action})
|
|
||||||
default:
|
|
||||||
processErr = fmt.Errorf("Unknown action")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
view, err := ctrl.Inspect(bucketName)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
describe, err := ctrl.Describe()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := &WebsiteInspectTpl{describe, view, processErr}
|
|
||||||
|
|
||||||
tWebsiteInspect := getTemplate("garage_website_inspect.html")
|
|
||||||
tWebsiteInspect.Execute(w, &tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleWebsiteVhost(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var processErr error
|
|
||||||
|
|
||||||
user := RequireUserHtml(w, r)
|
|
||||||
if user == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewWebsiteController(user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketName := mux.Vars(r)["bucket"]
|
|
||||||
|
|
||||||
if r.Method == "POST" {
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
bucket := strings.Join(r.Form["bucket"], "")
|
|
||||||
if bucket == "" {
|
|
||||||
bucket = strings.Join(r.Form["bucket2"], "")
|
|
||||||
}
|
|
||||||
|
|
||||||
view, processErr := ctrl.Patch(bucketName, &WebsitePatch{Vhost: &bucket})
|
|
||||||
if processErr == nil {
|
|
||||||
http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view, err := ctrl.Inspect(bucketName)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
describe, err := ctrl.Describe()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := &WebsiteInspectTpl{describe, view, processErr}
|
|
||||||
tWebsiteEdit := getTemplate("garage_website_edit.html")
|
|
||||||
tWebsiteEdit.Execute(w, &tpl)
|
|
||||||
}
|
|
Loading…
Reference in a new issue