aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.json.example18
-rw-r--r--flake.nix11
-rw-r--r--garage.go220
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--gomod2nix.toml4
-rw-r--r--integration/docker-compose.yml10
-rw-r--r--login.go8
-rw-r--r--main.go1
-rw-r--r--templates/garage_key.html234
-rw-r--r--templates/garage_website_inspect.html296
-rw-r--r--templates/home.html3
-rw-r--r--website.go234
-rw-r--r--webui_website.go178
14 files changed, 738 insertions, 483 deletions
diff --git a/config.json.example b/config.json.example
index 1760685..2d40aac 100644
--- a/config.json.example
+++ b/config.json.example
@@ -10,25 +10,23 @@
"invitation_base_dn": "ou=invitations,dc=bottin,dc=eu",
"invitation_name_attr": "cn",
- "invited_mail_format": "{}@example.com",
- "invited_auto_groups": [
- "cn=email,ou=groups,dc=bottin,dc=eu"
- ],
+ "invited_mail_format": "{}@bottin.eu",
+ "invited_auto_groups": [ ],
- "web_address": "http://guichet.localhost:9991",
- "mail_from": "welcome@example.com",
- "smtp_server": "smtp.example.com",
+ "web_address": "http://localhost:9991",
+ "mail_from": "welcome@bottin.eu",
+ "smtp_server": "smtp.bottin.eu",
"smtp_username": "guichet",
"smtp_password": "",
"admin_account": "cn=admin,dc=bottin,dc=eu",
- "group_can_admin": "gid=admin,ou=groups,dc=bottin,dc=eu",
- "group_can_invite": "",
+ "group_can_admin": "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_token": "GlXP43PWH3LuvEGSNxKYzZCyUss8VqZmarBU+HUlrxw=",
- "s3_endpoint": "localhost",
+ "s3_endpoint": "localhost:3900",
"s3_access_key": "",
"s3_secret_key": "",
"s3_region": "garage",
diff --git a/flake.nix b/flake.nix
index 5d69f9f..3b6ad3b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -12,9 +12,6 @@
system = "x86_64-linux";
overlays = [
(import "${gomod2nix}/overlay.nix")
- /*(self: super: {
- gomod = super.callPackage "${gomod2nix}/builder/" { };
- })*/
];
};
src = ./.;
@@ -38,10 +35,16 @@
platforms = platforms.linux;
};
};
+
+
container = pkgs.dockerTools.buildImage {
name = "dxflrs/guichet";
+ copyToRoot = pkgs.buildEnv {
+ name = "guichet-env";
+ paths = [ guichet pkgs.cacert ];
+ };
config = {
- Entrypoint = "${guichet}/bin/guichet";
+ Entrypoint = "/bin/guichet";
};
};
in {
diff --git a/garage.go b/garage.go
index 7cd879b..44b8dae 100644
--- a/garage.go
+++ b/garage.go
@@ -4,10 +4,7 @@ import (
"context"
"fmt"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
- "github.com/gorilla/mux"
"log"
- "net/http"
- "strings"
)
func gadmin() (*garage.APIClient, context.Context) {
@@ -26,8 +23,9 @@ func gadmin() (*garage.APIClient, context.Context) {
func grgCreateKey(name string) (*garage.KeyInfo, error) {
client, ctx := gadmin()
- kr := garage.AddKeyRequest{Name: &name}
- resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(kr).Execute()
+ kr := garage.NewAddKeyRequest()
+ kr.SetName(name)
+ resp, _, err := client.KeyApi.AddKey(ctx).AddKeyRequest(*kr).Execute()
if err != nil {
fmt.Printf("%+v\n", err)
return nil, err
@@ -38,7 +36,7 @@ func grgCreateKey(name string) (*garage.KeyInfo, error) {
func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
client, ctx := gadmin()
- resp, _, err := client.KeyApi.GetKey(ctx, accessKey).Execute()
+ resp, _, err := client.KeyApi.GetKey(ctx).Id(accessKey).ShowSecretKey("true").Execute()
if err != nil {
fmt.Printf("%+v\n", err)
return nil, err
@@ -46,6 +44,28 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) {
return resp, nil
}
+func grgSearchKey(name string) (*garage.KeyInfo, error) {
+ client, ctx := gadmin()
+
+ resp, _, err := client.KeyApi.GetKey(ctx).Search(name).ShowSecretKey("true").Execute()
+ if err != nil {
+ fmt.Printf("%+v\n", err)
+ return nil, err
+ }
+ return resp, nil
+}
+
+func 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) {
client, ctx := gadmin()
@@ -61,14 +81,14 @@ func grgCreateBucket(bucket string) (*garage.BucketInfo, error) {
return binfo, nil
}
-func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) {
+func grgAllowKeyOnBucket(bid, gkey string, read, write, owner bool) (*garage.BucketInfo, error) {
client, ctx := gadmin()
// Allow user's key
ar := garage.AllowBucketKeyRequest{
BucketId: bid,
AccessKeyId: gkey,
- Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true),
+ Permissions: *garage.NewAllowBucketKeyRequestPermissions(read, write, owner),
}
binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute()
if err != nil {
@@ -91,7 +111,7 @@ func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess {
func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) {
client, ctx := gadmin()
- binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute()
+ binfo, _, err := client.BucketApi.UpdateBucket(ctx).Id(bid).UpdateBucketRequest(*ur).Execute()
if err != nil {
fmt.Printf("%+v\n", err)
return nil, err
@@ -148,7 +168,7 @@ func grgDelLocalAlias(bid, key, alias string) (*garage.BucketInfo, error) {
func grgGetBucket(bid string) (*garage.BucketInfo, error) {
client, ctx := gadmin()
- resp, _, err := client.BucketApi.GetBucketInfo(ctx, bid).Execute()
+ resp, _, err := client.BucketApi.GetBucketInfo(ctx).Id(bid).Execute()
if err != nil {
log.Println(err)
return nil, err
@@ -160,187 +180,9 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) {
func grgDeleteBucket(bid string) error {
client, ctx := gadmin()
- _, err := client.BucketApi.DeleteBucket(ctx, bid).Execute()
+ _, err := client.BucketApi.DeleteBucket(ctx).Id(bid).Execute()
if err != nil {
log.Println(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)
-}
diff --git a/go.mod b/go.mod
index 56bd9f6..86ed878 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module git.deuxfleurs.fr/Deuxfleurs/guichet
go 1.18
require (
- git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20230131081355-c965fe7f7dc9
+ git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20231128153612-8b81fae65e5e
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-smtp v0.12.1
github.com/go-ldap/ldap/v3 v3.1.6
diff --git a/go.sum b/go.sum
index ae748fd..6543905 100644
--- a/go.sum
+++ b/go.sum
@@ -33,6 +33,8 @@ 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=
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-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/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=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index 4f10838..21b9dbc 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -2,8 +2,8 @@ schema = 3
[mod]
[mod."git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"]
- version = "v0.0.0-20230131081355-c965fe7f7dc9"
- hash = "sha256-qJN9yDMIh3xRk/3IYEWZca/biMVXXmDlPTzy0Cg11oc="
+ version = "v0.0.0-20231128153612-8b81fae65e5e"
+ hash = "sha256-o9kbcJ25/cYYwWZz/LBF7ZDyW8bZAjdg5pPu0gvb5JQ="
[mod."github.com/emersion/go-sasl"]
version = "v0.0.0-20191210011802-430746ea8b9b"
hash = "sha256-bADpAn0ZhlTTsEB3MsG8J31cQjTtHTzohX/wkL1aMIc="
diff --git a/integration/docker-compose.yml b/integration/docker-compose.yml
index ec855db..e44a723 100644
--- a/integration/docker-compose.yml
+++ b/integration/docker-compose.yml
@@ -1,11 +1,15 @@
version: '3'
services:
consul:
- image: hashicorp/consul:1.16
+ # sync with nixos stable packages assuming our stack is up to date
+ # 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"
expose:
- 8500
bottin:
+ # sync with deuxfleurs/nixcfg/cluster/prod/app/core/deploy/bottin.hcl
+ # to ensure compatibility with prod
image: dxflrs/bottin:7h18i30cckckaahv87d3c86pn4a7q41z
#command: "-config /etc/bottin.json"
restart: "always"
@@ -15,7 +19,9 @@ services:
volumes:
- "./config/bottin.json:/config.json"
garage:
- image: dxflrs/garage:v0.8.2
+ # sync with deuxfleurs/nixcfg/cluster/prod/app/garage/deploy/garage.hcl
+ # to ensure compatibility with prod
+ image: superboum/garage:v1.0.0-rc1-hotfix-red-ftr-wquorum
ports:
- "3900:3900"
- "3902:3902"
diff --git a/login.go b/login.go
index 277e3ae..a2c7d8f 100644
--- a/login.go
+++ b/login.go
@@ -143,6 +143,7 @@ func NewCapabilities(login *LoginStatus, entry *ldap.Entry) *Capabilities {
// --- Logged User ---
type LoggedUser struct {
+ Username string
Login *LoginStatus
Entry *ldap.Entry
Capabilities *Capabilities
@@ -186,7 +187,9 @@ func NewLoggedUser(login *LoginStatus) (*LoggedUser, error) {
}
entry := sr.Entries[0]
+ username := login.Info.Username
lu := &LoggedUser{
+ Username: username,
Login: login,
Entry: entry,
Capabilities: NewCapabilities(login, entry),
@@ -204,6 +207,7 @@ func (lu *LoggedUser) WelcomeName() string {
}
return ret
}
+
func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
var err error
var keyPair *garage.KeyInfo
@@ -212,7 +216,7 @@ func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
keyID := lu.Entry.GetAttributeValue("garage_s3_access_key")
if keyID == "" {
// If there is no S3Key in LDAP, generate it...
- keyPair, err = grgCreateKey(lu.Login.Info.Username)
+ keyPair, err = grgCreateKey(lu.Username)
if err != nil {
return nil, err
}
@@ -221,7 +225,7 @@ func (lu *LoggedUser) S3KeyInfo() (*garage.KeyInfo, error) {
// @FIXME compatibility feature for bagage (SFTP+webdav)
// 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.
- modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey})
+ modify_request.Replace("garage_s3_secret_key", []string{*keyPair.SecretAccessKey.Get()})
err = lu.Login.conn.Modify(modify_request)
if err != nil {
return nil, err
diff --git a/main.go b/main.go
index 39c7f08..e1b0eb8 100644
--- a/main.go
+++ b/main.go
@@ -159,7 +159,6 @@ func server(args []string) {
r.HandleFunc("/website", handleWebsiteList)
r.HandleFunc("/website/new", handleWebsiteNew)
- r.HandleFunc("/website/configure", handleWebsiteConfigure)
r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect)
r.HandleFunc("/website/vhost/{bucket}", handleWebsiteVhost)
diff --git a/templates/garage_key.html b/templates/garage_key.html
deleted file mode 100644
index cf56822..0000000
--- a/templates/garage_key.html
+++ /dev/null
@@ -1,234 +0,0 @@
-{{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&amp;s3ForcePathStyle=true&amp;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}}
diff --git a/templates/garage_website_inspect.html b/templates/garage_website_inspect.html
index a8f463d..6182f0d 100644
--- a/templates/garage_website_inspect.html
+++ b/templates/garage_website_inspect.html
@@ -3,8 +3,8 @@
{{define "body"}}
<div class="d-flex">
<!--<h4>Inspecter les sites webs</h4>-->
- <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-auto btn btn-link" href="/website/configure">Mes identifiants</a>
+ <a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div>
<div class="row">
@@ -15,9 +15,9 @@
{{ end }}
<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">
- <path d="M16 2 L16 30 M2 16 L30 16" />
+ <path d="M16 2 L16 30 M2 16 L30 16" />
</svg>
<span class="ml-1">Nouveau site web</span>
</a>
@@ -45,11 +45,13 @@
<div class="col-md-9">
<h2>{{ .View.Name.Url }}</h2>
+ <!-- QUOTAS -->
+
<h5 class="mt-3">Quotas</h5>
<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;">
{{ .View.Size.Percent }}%
- </div>
+ </div>
</div>
<p class="text-center">
@@ -59,23 +61,295 @@
{{ end }}
</p>
+ <!-- ACTIONS -->
<h5 class="mt-3">Actions</h5>
<form action="" method="post">
<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>
- <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>
</form>
- {{ 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 }}
+ <!-- INFO -->
+ <h5 class="mt-3">Informations de connexion</h5>
+ <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 (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&amp;s3ForcePathStyle=true&amp;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>
+
+ {{ 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>
{{ end }}
diff --git a/templates/home.html b/templates/home.html
index dd88d13..3210a13 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -24,10 +24,9 @@
<div class="mt-3">
<div class="card">
<div class="card-header">
- Mon espace sur la toile
+ Mes services
</div>
<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>
</div>
</div>
diff --git a/website.go b/website.go
index 74daf89..ed39d28 100644
--- a/website.go
+++ b/website.go
@@ -3,6 +3,7 @@ package main
import (
"fmt"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
+ "log"
"sort"
"strings"
)
@@ -18,7 +19,9 @@ var (
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")
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, this is an internal error")
+ ErrCantRemoveOldVhost = fmt.Errorf("The new vhost is bound to the bucket but the old one can't be removed, it's an internal error")
+ ErrFetchDedicatedKey = fmt.Errorf("Bucket has no dedicated key while it's required, it's an internal error")
+ ErrDedicatedKeyInvariant = fmt.Errorf("A security invariant on the dedicated key has been violated, aborting.")
)
type WebsiteId struct {
@@ -49,8 +52,18 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId {
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 {
User *LoggedUser
+ RootKey *garage.KeyInfo
WebsiteIdx map[string]*WebsiteId
PrettyList []string
WebsiteCount QuotaStat
@@ -77,33 +90,154 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
maxW := user.Quota.WebsiteCount
quota := NewQuotaStat(int64(len(wlist)), maxW, true)
- return &WebsiteController{user, idx, wlist, quota}, nil
+ return &WebsiteController{user, keyInfo, idx, wlist, quota}, nil
}
-type WebsiteDescribe struct {
- AccessKeyId string `json:"access_key_id"`
- SecretAccessKey string `json:"secret_access_key"`
- AllowedWebsites *QuotaStat `json:"quota_website_count"`
- BurstBucketQuotaSize string `json:"burst_bucket_quota_size"`
- Websites []*WebsiteId `json:"vhosts"`
+func (w *WebsiteController) getDedicatedWebsiteKey(binfo *garage.BucketInfo) (*garage.KeyInfo, error) {
+ // Check bucket info is not null
+ if binfo == nil {
+ return nil, ErrFetchBucketInfo
+ }
+
+ // Check the bucket is owned by the user's root key
+ usersRootKeyFound := false
+ for _, bucketKeyInfo := range binfo.Keys {
+ if *bucketKeyInfo.AccessKeyId == *w.RootKey.AccessKeyId && *bucketKeyInfo.Permissions.Owner {
+ usersRootKeyFound = true
+ break
+ }
+ }
+ if !usersRootKeyFound {
+ log.Printf("%s is not an owner of bucket %s. Invariant violated.\n", w.User.Username, *binfo.Id)
+ return nil, ErrDedicatedKeyInvariant
+ }
+
+ // Check that username does not contain a ":" (should not be possible due to the invitation regex)
+ // We do this check as ":" is used as a separator
+ if strings.Contains(w.User.Username, ":") || w.User.Username == "" || *binfo.Id == "" {
+ log.Printf("Username (%s) or bucket identifier (%s) is invalid. Invariant violated.\n", w.User.Username, *binfo.Id)
+ return nil, ErrDedicatedKeyInvariant
+ }
+
+ // Build the string template by concatening the username and the bucket identifier
+ dedicatedKeyName := fmt.Sprintf("%s:web:%s", w.User.Username, *binfo.Id)
+
+ // Try to fetch the dedicated key
+ keyInfo, err := grgSearchKey(dedicatedKeyName)
+ if err != nil {
+ // On error, try to create it.
+ // @FIXME we should try to create only on 404 Not Found errors
+ keyInfo, err = grgCreateKey(dedicatedKeyName)
+ if err != nil {
+ // On error again, abort
+ return nil, err
+ }
+ log.Printf("Created dedicated key %s\n", dedicatedKeyName)
+ }
+
+ // Check that the 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) Describe() (*WebsiteDescribe, error) {
- s3key, err := w.User.S3KeyInfo()
+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 nil, err
+ 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) {
r := make([]*WebsiteId, 0, len(w.PrettyList))
for _, k := range w.PrettyList {
r = append(r, w.WebsiteIdx[k])
}
+
return &WebsiteDescribe{
- *s3key.AccessKeyId,
- *s3key.SecretAccessKey,
+ w.User.Username,
&w.WebsiteCount,
w.User.Quota.WebsiteSizeBurstedPretty(),
- r}, nil
+ r,
+ }, nil
}
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
@@ -117,7 +251,12 @@ func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
return nil, ErrFetchBucketInfo
}
- return NewWebsiteView(binfo), nil
+ dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewWebsiteView(binfo, dedicatedKey)
}
func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
@@ -161,7 +300,19 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV
}
}
- return NewWebsiteView(binfo), nil
+ if patch.RotateKey != nil && *patch.RotateKey {
+ 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) {
@@ -173,21 +324,24 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
return nil, ErrWebsiteQuotaReached
}
+ // Create bucket
binfo, err := grgCreateBucket(pretty)
if err != nil {
return nil, ErrCantCreateBucket
}
+ // Allow user's global key on bucket
s3key, err := w.User.S3KeyInfo()
if err != nil {
return nil, err
}
- binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId)
+ binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId, true, true, true)
if err != nil {
return nil, ErrCantAllowKey
}
+ // Set quota
qr := w.User.Quota.DefaultWebsiteQuota()
wr := allowWebsiteDefault()
@@ -200,7 +354,13 @@ func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) {
return nil, ErrCantConfigureBucket
}
- return NewWebsiteView(binfo), nil
+ // Create a dedicated key
+ dedicatedKey, err := w.getDedicatedWebsiteKey(binfo)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewWebsiteView(binfo, dedicatedKey)
}
func (w *WebsiteController) Delete(pretty string) error {
@@ -213,6 +373,7 @@ func (w *WebsiteController) Delete(pretty string) error {
return ErrWebsiteNotFound
}
+ // Error checking
binfo, err := grgGetBucket(website.Internal)
if err != nil {
return ErrFetchBucketInfo
@@ -226,26 +387,49 @@ func (w *WebsiteController) Delete(pretty string) error {
return ErrBucketDeleteUnfinishedUpload
}
+ // Delete dedicated key
+ err = w.flushDedicatedWebsiteKey(binfo)
+ if err != nil {
+ return err
+ }
+
+ // Actually delete bucket
err = grgDeleteBucket(website.Internal)
return err
}
type WebsiteView struct {
- Name *WebsiteId `json:"vhost"`
- Size QuotaStat `json:"quota_size"`
- Files QuotaStat `json:"quota_files"`
+ Name *WebsiteId `json:"vhost"`
+ AccessKeyId string `json:"access_key_id"`
+ SecretAccessKey string `json:"secret_access_key"`
+ Size QuotaStat `json:"quota_size"`
+ Files QuotaStat `json:"quota_files"`
}
-func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
+func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) {
+ if binfo == nil {
+ return nil, ErrFetchBucketInfo
+ }
+ if s3key == nil {
+ return nil, ErrFetchDedicatedKey
+ }
+
q := binfo.GetQuotas()
wid := NewWebsiteIdFromBucketInfo(binfo)
size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true)
objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false)
- return &WebsiteView{wid, size, objects}
+ return &WebsiteView{
+ wid,
+ *s3key.AccessKeyId,
+ *s3key.SecretAccessKey.Get(),
+ size,
+ objects,
+ }, nil
}
type WebsitePatch struct {
- Size *int64 `json:"quota_size"`
- Vhost *string `json:"vhost"`
+ Size *int64 `json:"quota_size"`
+ Vhost *string `json:"vhost"`
+ RotateKey *bool `json:"rotate_key"`
}
diff --git a/webui_website.go b/webui_website.go
new file mode 100644
index 0000000..642c837
--- /dev/null
+++ b/webui_website.go
@@ -0,0 +1,178 @@
+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)
+}