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"}} -
- - - -Identifiant de clé | -{{ .S3KeyInfo.AccessKeyId }} | -
---|---|
Clé secrète | -Cliquer pour afficher la clé secrète | -
Région | -garage | -
Endpoint URL | -https://garage.deuxfleurs.fr | -
Type d'URL | -DNS et chemin (préférer chemin) | -
Signature | -Version 4 | -
Configurer votre logiciel :
- -Créez un fichier nommé ~/.awsrc
:
-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 --
Ensuite vous pouvez utiliser awscli :
--source ~/.awsrc -aws s3 ls -aws s3 ls s3://my-bucket -aws s3 cp /tmp/a.txt s3://my-bucket -... --
Vous pouvez configurer Minio CLI avec cette commande :
--mc alias set \ - garage \ - https://garage.deuxfleurs.fr \ - {{ .S3KeyInfo.AccessKeyId }} \ - {{ .S3KeyInfo.SecretAccessKey }} \ - --api S3v4 --
Et ensuite pour utiliser Minio CLI avec :
--mc ls garage/ -mc cp /tmp/a.txt garage/my-bucket/a.txt -... --
Dans votre fichier config.toml
, rajoutez :
-[[deployment.targets]] - URL = "s3://bucket?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true®ion=garage" --
Assurez-vous d'avoir un fichier dans lequel les variables AWS_ACCESS_KEY_ID
et AWS_SECRET_ACCESS_KEY
sont définies,
- ici on suppose que vous avez suivi les instructions de l'outil awscli (ci-dessus) et que vous avez un fichier ~/.awsrc
qui défini ces variables.
- Ensuite :
-source ~/.awsrc -hugo deploy --
Nom d'utilisateur-ice | -{{ .Login.Info.Username }} | -
---|---|
Mot de passe | -(votre mot de passe guichet) | -
Hôte | -sftp://bagage.deuxfleurs.fr | -
Port | -2222 | -
Configurer votre logiciel :
- -Un exemple avec SCP :
--scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Info.Username }}@bagage.deuxfleurs.fr:mon_bucket/ --
@@ -59,23 +61,295 @@ {{ end }}
+ 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 CNAME garage.deuxfleurs.fr
ou ALIAS garage.deuxfleurs.fr
auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).
Identifiant de clé | +{{ .View.AccessKeyId }} | +
---|---|
Clé secrète | ++ [Afficher la clé secrète] + + | +
Région | +garage | +
Endpoint URL | +https://garage.deuxfleurs.fr | +
Type d'URL | +DNS et chemin (préférer chemin) | +
Signature | +Version 4 | +
Configurer votre logiciel :
+ +Lancez la commande :
+aws --profile {{ .View.Name.Pretty }} configure+ +
Entrez les informations suivantes quand elles vous sont demandées :
+Finalisez la configuration :
+aws --profile {{ .View.Name.Pretty }} configure set endpoint_url https://garage.deuxfleurs.fr+
Pour déployer votre dossier local public
lancez :
+aws --profile {{ .View.Name.Pretty }} s3 sync ./public s3://{{ .View.Name.Pretty }} ++
Vous pouvez configurer Minio CLI avec cette commande :
++mc alias set \ + {{ .View.Name.Pretty }} \ + https://garage.deuxfleurs.fr \ + {{ .View.AccessKeyId }} \ + [Afficher la clé secrète] \ + --api S3v4 ++
Et ensuite copiez votre site web avec la sous-commande mirror de Minio CLI :
++mc mirror --overwrite ./public/ {{ .View.Name.Pretty }}/ ++
Créez un fichier nommé .deployment.secrets
(ne commitez pas ce fichier dans votre dépôt !) :
+export AWS_ACCESS_KEY_ID={{ .View.AccessKeyId }} +export AWS_SECRET_ACCESS_KEY=[Afficher la clé secrète] ++
Dans votre fichier de configuration Hugo config.toml
(que vous pouvez commiter), rajoutez :
+[[deployment.targets]] + URL = "s3://bucket?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true®ion=garage" ++ +
Pour déployer, sourcez le fichier de configuration et laissez hugo faire :
++source .deployment.secrets +hugo deploy ++
Nom d'utilisateur-ice | +{{ .Describe.Username }} | +
---|---|
Mot de passe | +(votre mot de passe guichet) | +
Hôte | +sftp://sftp.deuxfleurs.fr | +
Port | +2222 | +
Configurez votre logiciel :
+ +Déployer le dossier local public sur le site web {{ .View.Name.Pretty }} :
++scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Describe.Username }}@sftp.deuxfleurs.fr:{{ .View.Name.Pretty }}/ ++
Dans la barre de connexion rapide du haut, entrez :
+Cliquez ensuite sur Connexion rapide
+Nom d'utilisateur-ice | +{{ .Describe.Username }} | +
---|---|
Mot de passe | +(votre mot de passe guichet) | +
Hôte | +https://bagage.deuxfleurs.fr ou davs://bagage.deuxfleurs.fr | +
Port | +443 (par défaut) | +
Configurez votre logiciel :
+ +Vous pouvez naviguer dans vos fichiers via l'explorateur web. + Utilisez simplement vos identifiants Guichet, l'explorateur est préconfiguré.
+ + + 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 CNAME garage.deuxfleurs.fr
ou ALIAS garage.deuxfleurs.fr
auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).