From 4bc04f5b2730c909ae7447f15d07e14b4bdc8fe3 Mon Sep 17 00:00:00 2001 From: Chris Mann Date: Wed, 26 Jul 2023 08:31:43 +0200 Subject: [PATCH] Refactoring --- templates/admin.html | 68 ++ templates/admin/activate.html | 39 + templates/admin/create.html | 96 ++ templates/admin/groups.html | 40 + templates/admin/ldap.html | 239 +++++ templates/admin/mailing.html | 35 + templates/admin/mailing/list.html | 117 +++ templates/admin/users.html | 43 + templates/directory/results.html | 23 + templates/garage/key.html | 237 +++++ templates/garage/website/inspect.html | 61 ++ templates/garage/website/list.html | 41 + templates/garage/website/new.html | 66 ++ templates/passwd/lost.html | 40 + templates/passwd/lost_password_email.txt | 13 + templates/user.html | 86 ++ templates/user/code/invalid.html | 4 + templates/user/code/send.html | 66 ++ templates/user/mail.txt | 13 + templates/user/new.html | 131 +++ utils-config.go | 53 ++ utils-http.go | 40 + utils-ldap.go | 17 + utils-ssha.go | 10 + view-admin.go | 1021 ++++++++++++++++++++++ view-home.go | 40 + view-invite.go | 555 ++++++++++++ view-login.go | 128 +++ view-passwd.go | 169 ++++ view-profile.go | 219 +++++ 30 files changed, 3710 insertions(+) create mode 100644 templates/admin.html create mode 100644 templates/admin/activate.html create mode 100644 templates/admin/create.html create mode 100644 templates/admin/groups.html create mode 100644 templates/admin/ldap.html create mode 100644 templates/admin/mailing.html create mode 100644 templates/admin/mailing/list.html create mode 100644 templates/admin/users.html create mode 100644 templates/directory/results.html create mode 100644 templates/garage/key.html create mode 100644 templates/garage/website/inspect.html create mode 100644 templates/garage/website/list.html create mode 100644 templates/garage/website/new.html create mode 100644 templates/passwd/lost.html create mode 100644 templates/passwd/lost_password_email.txt create mode 100644 templates/user.html create mode 100644 templates/user/code/invalid.html create mode 100644 templates/user/code/send.html create mode 100644 templates/user/mail.txt create mode 100644 templates/user/new.html create mode 100644 utils-config.go create mode 100644 utils-http.go create mode 100644 utils-ldap.go create mode 100644 utils-ssha.go create mode 100644 view-admin.go create mode 100644 view-home.go create mode 100644 view-invite.go create mode 100644 view-login.go create mode 100644 view-passwd.go create mode 100644 view-profile.go diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..3130f02 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,68 @@ +{{define "title"}}Home{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+ Bienvenue, {{ .Login.Login.WelcomeName }} ! +
+ +
+
+
+ Mon compte +
+ +
+
+ +{{if .Common.CanInvite}} +
+
+ Outils +
+ +
+{{end}} + +{{if .Common.CanAdmin}} +
+
+ Administration +
+ +
+{{end}} + +{{end}} diff --git a/templates/admin/activate.html b/templates/admin/activate.html new file mode 100644 index 0000000..2c482f4 --- /dev/null +++ b/templates/admin/activate.html @@ -0,0 +1,39 @@ +{{define "title"}}Activer des utilisateurs |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} + + + + + + + + + + + + {{with $root := .}} + {{range $user := $root.Users}} + + + + + + + + + + {{end}} + {{end}} + +
LoginEmailNom d'ffichagePrénomNom de familledescription
+ + Activer + + + {{$user.GetAttributeValue "cn"}} + {{$user.GetAttributeValue "mail"}}{{$user.GetAttributeValue "displayName"}}{{$user.GetAttributeValue "givenName"}}{{$user.GetAttributeValue "sn"}}{{$user.GetAttributeValue "description"}}
+{{end}} \ No newline at end of file diff --git a/templates/admin/create.html b/templates/admin/create.html new file mode 100644 index 0000000..44a75a6 --- /dev/null +++ b/templates/admin/create.html @@ -0,0 +1,96 @@ +{{define "title"}}Nouvel objet |{{end}} + +{{define "admenu"}} +{{end}} + + +{{define "body"}} +
+

Créer un objet

+
+ +
+ +
+ +{{if .Common.Error}} +
Impossible de créer l'objet. +
{{ .Common.Error }}
+
+{{end}} + +
+ + {{if eq .Template "ml"}} +
+ + +
+ {{else}} +
+ + +
+ {{end}} +
+ + +
+ {{ if eq .Template "user" }} +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + {{ else }} +
+ + +
+ +
+ + +
+ + {{ end }} + +
+ + +
+ +
+ +{{end}} diff --git a/templates/admin/groups.html b/templates/admin/groups.html new file mode 100644 index 0000000..f5cf22a --- /dev/null +++ b/templates/admin/groups.html @@ -0,0 +1,40 @@ +{{define "title"}}Liste des groupes |{{end}} + +{{define "admenu"}} +{{end}} + + +{{define "body"}} + +
+

Liste des groupes

+ Nouveau groupe +
+ +
+ Les groupes servent uniquement à contrôler l'accès à différentes fonctionalités de Deuxfleurs. + Ce ne sont pas des mailing lists. +
+ + + + + + + + {{with $root := .}} + {{range $group := $root.Groups}} + + + + + {{end}} + {{end}} + +
IdentifiantNom complet
+ + {{$group.GetAttributeValue $root.GroupNameAttr}} + + {{$group.GetAttributeValue "description"}}
+ +{{end}} diff --git a/templates/admin/ldap.html b/templates/admin/ldap.html new file mode 100644 index 0000000..6154a8b --- /dev/null +++ b/templates/admin/ldap.html @@ -0,0 +1,239 @@ +{{define "title"}}Explorateur LDAP |{{end}} + +{{define "admenu"}} +{{end}} + + +{{define "body"}} + +
+

Explorateur LDAP

+
+ +
+ +
+ + + + {{range .ChildrenOU}} + + + + + {{end}} + {{range .ChildrenOther}} + + + + + {{end}} + +
+ + 🗀 {{.Identifier}} + + {{.Name}}
+ + {{.Identifier}} + + {{.Name}}
+ +{{if .CanAddChild}} +
+ +utilisateur + +groupe + +ou + +objet +
+
+{{end}} + +{{if .Common.Success}} +
Modification enregistrée.
+{{end}} +{{if .Common.Error}} +
+ Impossible d'effectuer la modification. +
{{.Error}}
+
+{{end}} + +
Attributs
+
+ {{range $key, $value := .Props}} + {{if $value.Editable}} +
+
{{$value.Name}}
+ +
+
+
+ + + +
+ +
+
+
+
+ +
+ {{if $value.Deletable}} +
+ + + +
+ {{end}} +
+
+ {{end}} + {{end}} + {{range $key, $value := .Props}} + {{if not $value.Editable}} +
+
{{$key}}
+
+ {{range $value.Values}} + {{if eq $key "creatorsname" "modifiersname" }} +
{{.}}
+ {{else}} +
{{.}}
+ {{end}} + {{end}} +
+
+ {{end}} + {{end}} +
+
+
+ + +
+
+
+ +
+ +
+
+
+
+
+
+ +{{if .HasMembers}} +
+
Membres
+
+ {{range .Members}} +
+
+ {{.Name}} +
+
+ {{.DN}} +
+
+
+ + + +
+
+
+ {{end}} +
+ + +
+
Ajouter au groupe : +
+
+ + + {{range .PossibleNewMembers}} + + {{end}} + +
+
+ +
+ +
+{{end}} + +{{if .HasGroups}} +
+
Membre de
+
+ {{range .Groups}} +
+
+ {{.Name}} +
+
+ {{.DN}} +
+
+
+ + + +
+
+
+ {{end}} +
+ +
+
Nouveau groupe : +
+
+ + + {{range .PossibleNewGroups}} + + {{end}} + +
+
+ +
+ +
+{{end}} + +{{if .CanDelete}} +
+
Supprimer l'objet
+
+ Attention, cette opération est irrévocable ! +
+
+
+ +
+ +
+
+
+{{end}} + +
+ +{{end}} diff --git a/templates/admin/mailing.html b/templates/admin/mailing.html new file mode 100644 index 0000000..269bea2 --- /dev/null +++ b/templates/admin/mailing.html @@ -0,0 +1,35 @@ +{{define "title"}}Mailing lists |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} + +
+

Mailing lists

+ Nouvelle mailing list + Menu principal +
+ + + + + + + + {{with $root := .}} + {{range $ml := $root.MailingLists}} + + + + + {{end}} + {{end}} + +
AdresseDescription
+ + {{$ml.GetAttributeValue $root.MailingNameAttr}} + + {{$ml.GetAttributeValue "description"}}
+ +{{end}} diff --git a/templates/admin/mailing/list.html b/templates/admin/mailing/list.html new file mode 100644 index 0000000..6d33161 --- /dev/null +++ b/templates/admin/mailing/list.html @@ -0,0 +1,117 @@ +{{define "title"}}ML {{.MailingList.GetAttributeValue .MailingNameAttr}} |{{end}} +{{define "admenu"}} +{{end}} +{{define "body"}} + +
+

ML {{.MailingList.GetAttributeValue .MailingNameAttr}} + Vue avancée +

+ Liste des ML +
+ +{{if .Common.Success}} +
Modification enregistrée.
+{{end}} +{{if .Common.Error}} +
+ Impossible d'effectuer la modification. +
{{.Error}}
+
+{{end}} + +{{with $desc := .MailingList.GetAttributeValue "description"}}{{if $desc}} +

{{$desc}}

+{{end}}{{end}} + + + + + + + + + {{with $root := .}} + {{range $member := $root.Members}} + + + + + + {{end}} + {{end}} + {{if not .Members}} + + {{end}} + +
AdresseNom
+ + {{$member.GetAttributeValue "mail"}} + + {{$member.GetAttributeValue "displayname"}} +
+ + + +
+
(aucun abonné)
+ +
+
Ajouter un destinataire
+ +
+
+ +
+
Utilisateur existant :
+
+ + + {{range .PossibleNewMembers}} + {{if .GetAttributeValue "mail"}} + + {{end}} + {{end}} + +
+
+ +
+
+
+ + {{if .AllowGuest}} +
+
OU
+
+ +
+ +
+
E-mail :
+
+ +
+
+
+
+
+
Nom (optionnel) :
+
+ +
+
+ +
+
+
+ + Si un utilisateur existe déjà avec l'email spécifiée, celui-ci sera ajouté à la liste. + Sinon, un utilisateur invité sera créé. + +
+
+ {{end}} +
+ +{{end}} diff --git a/templates/admin/users.html b/templates/admin/users.html new file mode 100644 index 0000000..356ea4b --- /dev/null +++ b/templates/admin/users.html @@ -0,0 +1,43 @@ +{{define "title"}}Liste des utilisateurs |{{end}} + +{{define "admenu"}} +{{end}} + + +{{define "body"}} + +
+

Liste des utilisateurs

+ Nouvel utilisateur +
+ + + + + + + + + + {{with $root := .}} + {{range $user := $root.Users}} + + + + + + + {{end}} + {{end}} + +
IdentifiantNom completEmail
+ + Dèsactiver + + + + {{$user.GetAttributeValue $root.UserNameAttr}} + + {{$user.GetAttributeValue "displayName"}}{{$user.GetAttributeValue "mail"}}
+ +{{end}} diff --git a/templates/directory/results.html b/templates/directory/results.html new file mode 100644 index 0000000..c7dd715 --- /dev/null +++ b/templates/directory/results.html @@ -0,0 +1,23 @@ +{{if .Results}} + {{range .Results}} +
+
+
+ {{if .ProfilePicture}} + + + + {{else}} + {{end}} +
+
+ {{.DisplayName}} + {{.Id}}@ +
+

{{.Description}}

+
+
+ {{end}} +{{else}} + Aucun résultat. +{{end}} diff --git a/templates/garage/key.html b/templates/garage/key.html new file mode 100644 index 0000000..b322ab3 --- /dev/null +++ b/templates/garage/key.html @@ -0,0 +1,237 @@ +{{define "title"}}Profile |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Mes identifiants

+ Mes sites webs + Menu principal +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Identifiant de clé{{ .Key.AccessKeyId }}
Clé secrète{{ .Key.SecretAccessKey }}
Régiongarage
Endpoint URLhttps://garage.resdigita.org
Type d'URLDNS et chemin (préférer chemin)
SignatureVersion 4
+ +

Configurer votre logiciel :

+ +
+
+
+

+ +

+
+
+
+

Créez un fichier nommé ~/.awsrc :

+
+export AWS_ACCESS_KEY_ID={{ .Key.AccessKeyId }}
+export AWS_SECRET_ACCESS_KEY={{ .Key.SecretAccessKey }}
+export AWS_DEFAULT_REGION='garage'
+
+function aws { command aws --endpoint-url https://garage.resdigita.org $@ ; }
+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.resdigita.org \
+  {{ .Key.AccessKeyId }} \
+  {{ .Key.SecretAccessKey }} \
+  --api S3v4
+                        
+

Et ensuite pour utiliser Minio CLI avec :

+
+mc ls garage/
+mc cp /tmp/a.txt garage/my-bucket/a.txt
+...
+                        
+
+
+
+ +
+
+

+ +

+
+ +
+
+ Reportez vous au guide +
+
+
+ +
+
+

+ +

+
+
+
+

Dans votre fichier config.toml, rajoutez :

+
+[[deployment.targets]]
+ URL = "s3://bucket?endpoint=garage.resdigita.org&s3ForcePathStyle=true&region=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
+                        
+
+
+
+ +
+
+

+ +

+
+
+
+ Bientôt... +
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
Nom d'utilisateur-ice{{ .Login.Status.Info.Username }}
Mot de passe(votre mot de passe guichet)
Hôtesftp://bagage.resdigita.org
Port2222
+

Configurer votre logiciel :

+ +
+
+
+

+ +

+
+
+
+

Un exemple avec SCP :

+
+scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Status.Info.Username }}@bagage.resdigita.org:mon_bucket/
+                        
+
+
+
+
+
+

+ +

+
+
+
+ Bientôt +
+
+
+
+ +
+
+ +{{end}} diff --git a/templates/garage/website/inspect.html b/templates/garage/website/inspect.html new file mode 100644 index 0000000..3b5c8b5 --- /dev/null +++ b/templates/garage/website/inspect.html @@ -0,0 +1,61 @@ +{{define "title"}}Inspecter le site web |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Inspecter le site web

+ Mes identifiants + Nouveau site web + Mes sites webs +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ .Bucket.Id }}
URLs + {{ range $alias := .Bucket.GlobalAliases }} + {{ if contains $alias "." }} + https://{{ $alias }} + {{ else }} + https://{{ $alias }}.web.resdigita.org + {{ end }} + {{ end }} +
Document d'index {{ .IndexDoc }}
Document d'erreur{{ .Common.ErrorDoc }}
Nombre de fichiers{{ .Bucket.Objects }} / {{ .MaxObjects }}
Espace utilisé{{ .Bucket.Bytes }} / {{ .MaxSize }} octets
+ +

Configurer le nom de domaine

+ +{{ range $alias := .Bucket.GlobalAliases }} +{{ if contains $alias "." }} +

Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée CNAME garage.resdigita.org ou ALIAS garage.resdigita.org auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).

+{{ else }} +

Le nom de domaine https://{{ $alias }}.web.resdigita.org est fourni par Deuxfleurs, il n'y a pas de configuration à faire.

+{{ end }} +{{ end }} + +{{end}} diff --git a/templates/garage/website/list.html b/templates/garage/website/list.html new file mode 100644 index 0000000..eb36afa --- /dev/null +++ b/templates/garage/website/list.html @@ -0,0 +1,41 @@ +{{define "title"}}Sites webs |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} + + + + + + + + + + {{ range $buck := .Key.Buckets }} + {{ if $buck.GlobalAliases }} + + + + + {{ end }} + {{ end }} + +
IDURLs
+ {{$buck.Id}} + + {{ range $alias := $buck.GlobalAliases }} + {{ if contains $alias "." }} + https://{{ $alias }} + {{ else }} + https://{{ $alias }}.web.resdigita.org + {{ end }} + {{ end }} +
+{{end}} diff --git a/templates/garage/website/new.html b/templates/garage/website/new.html new file mode 100644 index 0000000..06b87c8 --- /dev/null +++ b/templates/garage/website/new.html @@ -0,0 +1,66 @@ +{{define "title"}}Créer un site web |{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Créer un site web

+ Mes identifiants + Mes sites webs +
+ + + +
+
+
+
+
+ + +
+
+ + +
+
+
+

La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.

+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+
+

Vous devez éditer votre zone DNS, souvent gérée par votre bureau d'enregistrement, comme Gandi, pour la faire pointer vers Deuxfleurs. Si vous utilisez un sous domaine (eg. site.exemple.com), une entrée CNAME est appropriée :

+
site   CNAME    3600    garage.resdigita.org.
+

Si vous utilisez la racine de votre nom de domaine (eg. exemple.com, aussi appelée APEX), la solution dépend de votre fournisseur DNS, il vous faudra au choix une entrée ALIAS ou CNAME en fonction de ce que votre fournisseur supporte :

+
@      ALIAS    3600    garage.resdigita.org.
+

La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.

+
+
+ + + +
+
+{{end}} diff --git a/templates/passwd/lost.html b/templates/passwd/lost.html new file mode 100644 index 0000000..6efdebe --- /dev/null +++ b/templates/passwd/lost.html @@ -0,0 +1,40 @@ +{{define "title"}}G Pas (Je n'ai pas mon mal de passe){{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +

G Pas

+ +

Refaire son mot de passe

+ +{{if .Common.ErrorMessage}} +
Impossible +
{{ .Common.ErrorMessage }}
+
+{{end}} +{{if .Common.Success}} +
+ Email envoyé au courriel de secours. +
+{{end}} + +

Merci de renseigner au moins un des champs ci-dessous.

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +{{end}} \ No newline at end of file diff --git a/templates/passwd/lost_password_email.txt b/templates/passwd/lost_password_email.txt new file mode 100644 index 0000000..1888c74 --- /dev/null +++ b/templates/passwd/lost_password_email.txt @@ -0,0 +1,13 @@ +From: {{.From}} +To: {{.To}} +Subject: Code d'invitation GVoisin.com +Content-type: text/plain; charset=utf-8 + +Une refonte de mot de passe avait été demandé sur GVoisin.com + +Pour créer votre compte, rendez-vous à l'adresse suivante: + +{{.WebBaseAddress}}/passwd/lost/{{.Code}} + +À bientôt sur GVoisin.com ! + diff --git a/templates/user.html b/templates/user.html new file mode 100644 index 0000000..b033688 --- /dev/null +++ b/templates/user.html @@ -0,0 +1,86 @@ +{{define "title"}}Profile{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Modifier mon profil

+
+ + {{if .Common.ErrorMessage}} +
Impossible d'effectuer la modification. +
{{ .Common.ErrorMessage }}
+
+ {{end}} + {{if .Common.Success}} +
+ Profil enregistré. +
+ {{end}} +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +{{/* + + + +*/}} +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +{{end}} diff --git a/templates/user/code/invalid.html b/templates/user/code/invalid.html new file mode 100644 index 0000000..fdb60de --- /dev/null +++ b/templates/user/code/invalid.html @@ -0,0 +1,4 @@ +{{define "title"}}Créer un compte |{{end}} + +{{define "admenu"}} +{{end}} diff --git a/templates/user/code/send.html b/templates/user/code/send.html new file mode 100644 index 0000000..04d2c8c --- /dev/null +++ b/templates/user/code/send.html @@ -0,0 +1,66 @@ +{{define "title"}}Envoyer un code d'invitation{{end}} +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Envoyer un code d'invitation

+ Retour +
+ + {{if .Common.ErrorMessage}} +
Impossible de génerer ou d'envoyer le code. +
{{ .Common.ErrorMessage }}
+
+ {{end}} + {{if .Common.Success}} +
+ {{if .CodeSentTo}} + Un code d'invitation a bien été envoyé à {{ .CodeSentTo }}. + {{end}} + {{if .CodeDisplay}} + Lien d'invitation : + +

+ {{.WebBaseAddress}}/invitation/{{.CodeDisplay}} +

+ {{end}} +
+ {{else}} +
+ Choisissez une option: + +
+
+
+ +
+
+ + +
+ {{if .Common.ErrorInvalidEmail}} +
+ Addresse mail invalide. +
+ {{end}} + +
+
+
+ +
+
+ +
+ +
+ +
+
+ {{end}} +{{end}} diff --git a/templates/user/mail.txt b/templates/user/mail.txt new file mode 100644 index 0000000..6058937 --- /dev/null +++ b/templates/user/mail.txt @@ -0,0 +1,13 @@ +From: {{.From}} +To: {{.To}} +Subject: Code d'invitation GVoisin.com +Content-type: text/plain; charset=utf-8 + +Vous avez été invité à créer un compte sur GVoisin.com par {{.InviteFrom}} :) + +Pour créer votre compte, rendez-vous à l'addresse suivante: + +{{.WebBaseAddress}}/invitation/{{.Code}} + +À bientôt sur GVoisin.com ! + diff --git a/templates/user/new.html b/templates/user/new.html new file mode 100644 index 0000000..3889e1f --- /dev/null +++ b/templates/user/new.html @@ -0,0 +1,131 @@ +{{define "title"}}Créer un compte{{end}} + +{{define "admenu"}} +{{end}} + +{{define "body"}} +
+

Création d'un nouveau compte

+
+{{if .Common.ErrorMessage}} +
Impossible de créer le compte. +
{{ .Common.ErrorMessage }}
+
+{{end}} +{{if .Common.WarningMessage}} +
Des erreurs se sont produites, le compte pourrait ne pas être totalement + fonctionnel. +
{{ .Common.WarningMessage }}
+
+{{end}} +{{if .Common.Success}} +
+ Le compe a été créé ! + Rendez-vous sur la page d'accueil pour vous connecter avec ce nouveau compte. +
+{{else}} +
+
+ + +
+
+ + +
+
+ + + + Le courriel de l'utilisateur. + +
+
+ + +
+
+ + + + Votre identifiant doit être en minuscule. + +
+
+
+ {{if .ErrorInvalidUsername}} +
+ Nom d'utilisateur invalide. Ne peut contenir que les caractères suivants : chiffres, lettres minuscules, point, + tiret bas (_) et tiret du milieu (-). +
+ {{end}} + {{if .ErrorUsernameTaken}} +
+ Ce nom d'utilisateur est déjà pris. +
+ {{end}} +
+ + + + Le courriel et login interne. + +
+

Utiliser ce mot de passe : {{ .SuggestPW }}

+
+ + + + La seule contrainte est que votre mot de passe doit faire au moins 8 caractères. Utilisez chiffres, majuscules, et + caractères spéciaux sans modération ! + +
+ {{if .ErrorPasswordTooShort}} +
+ Le mot de passe choisi est trop court (minimum 8 caractères). +
+ {{end}} +
+ + +
+ {{if .ErrorPasswordMismatch}} +
+ Les deux mots de passe entrés ne correspondent pas. +
+ {{end}} + +
+ + +{{end}} +{{end}} \ No newline at end of file diff --git a/utils-config.go b/utils-config.go new file mode 100644 index 0000000..ebbde9d --- /dev/null +++ b/utils-config.go @@ -0,0 +1,53 @@ +/* +config handles reading the config.json file at the root and processing the settings +*/ +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + "os" +) + +var configFlag = flag.String("config", "./config.json", "Configuration file path") + +var config *ConfigFile + +func readConfig() ConfigFile { + // Default configuration values for certain fields + config_file := ConfigFile{ + HttpBindAddr: ":9991", + LdapServerAddr: "ldap://127.0.0.1:389", + + UserNameAttr: "uid", + GroupNameAttr: "gid", + + InvitationNameAttr: "cn", + InvitedAutoGroups: []string{}, + + Org: "ResDigita", + } + + _, err := os.Stat(*configFlag) + if os.IsNotExist(err) { + log.Fatalf("Could not find Guichet configuration file at %s. Please create this file, for exemple starting with config.json.exemple and customizing it for your deployment.", *configFlag) + } + + if err != nil { + log.Fatal(err) + } + + bytes, err := ioutil.ReadFile(*configFlag) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(bytes, &config_file) + if err != nil { + log.Fatal(err) + } + + return config_file +} diff --git a/utils-http.go b/utils-http.go new file mode 100644 index 0000000..6cae84e --- /dev/null +++ b/utils-http.go @@ -0,0 +1,40 @@ +/* +http-utils provide utility functions that interact with http +*/ + +package main + +import ( + "crypto/tls" + "net" + "net/http" + + "github.com/go-ldap/ldap/v3" +) + +func logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + }) +} + +func ldapOpen(w http.ResponseWriter) (*ldap.Conn, error) { + if config.LdapTLS { + tlsConf := &tls.Config{ + ServerName: config.LdapServerAddr, + InsecureSkipVerify: true, + } + return ldap.DialTLS("tcp", net.JoinHostPort(config.LdapServerAddr, "636"), tlsConf) + } else { + return ldap.DialURL("ldap://" + config.LdapServerAddr) + } + + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // log.Printf(fmt.Sprintf("27: %v %v", err, l)) + // return nil + // } + + // return l +} diff --git a/utils-ldap.go b/utils-ldap.go new file mode 100644 index 0000000..6ca43d0 --- /dev/null +++ b/utils-ldap.go @@ -0,0 +1,17 @@ +/* +Utilities related to LDAP +*/ +package main + +import ( + "github.com/go-ldap/ldap/v3" +) + +func replaceIfContent(modifReq *ldap.ModifyRequest, key string, value string, previousValue string) error { + if value != "" { + modifReq.Replace(key, []string{value}) + } else if previousValue != "" { + modifReq.Delete(key, []string{previousValue}) + } + return nil +} diff --git a/utils-ssha.go b/utils-ssha.go new file mode 100644 index 0000000..a198d27 --- /dev/null +++ b/utils-ssha.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/jsimonetti/pwscheme/ssha512" +) + +// Encode encodes the []byte of raw password +func SSHAEncode(rawPassPhrase string) (string, error) { + return ssha512.Generate(rawPassPhrase, 16) +} diff --git a/view-admin.go b/view-admin.go new file mode 100644 index 0000000..6ff4f53 --- /dev/null +++ b/view-admin.go @@ -0,0 +1,1021 @@ +package main + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strings" + + "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" +) + +func checkAdminLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { + login := checkLogin(w, r) + if login == nil { + return nil + } + + if !login.Common.CanAdmin { + http.Error(w, "Not authorized to perform administrative operations.", http.StatusUnauthorized) + return nil + } + return login +} + +func (d EntryList) Len() int { + return len(d) +} + +func (d EntryList) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +func (d EntryList) Less(i, j int) bool { + return d[i].DN < d[j].DN +} + +func handleAdminActivateUsers(w http.ResponseWriter, r *http.Request) { + templateAdminActivateUsers := getTemplate("admin/activate.html") + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.InvitationBaseDN, + ldap.ScopeSingleLevel, + ldap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(&(objectClass=organizationalPerson))"), + []string{"cn", "displayName", "givenName", "sn", "mail", "uid"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminUsersTplData{ + Login: NestedLoginTplData{ + Login: login, + }, + UserNameAttr: config.UserNameAttr, + UserBaseDN: config.UserBaseDN, + Users: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: true, + LoggedIn: true, + }, + } + templateAdminActivateUsers.Execute(w, data) + +} + +func handleAdminActivateUser(w http.ResponseWriter, r *http.Request) { + cn := mux.Vars(r)["cn"] + login := checkAdminLogin(w, r) + if login == nil { + return + } + modifyRequest := *ldap.NewModifyDNRequest("cn="+cn+","+config.InvitationBaseDN, "cn="+cn, true, config.UserBaseDN) + err := login.conn.ModifyDN(&modifyRequest) + if err != nil { + return + } + http.Redirect(w, r, "/admin/activate", http.StatusFound) +} + +func handleAdminUnactivateUser(w http.ResponseWriter, r *http.Request) { + cn := mux.Vars(r)["cn"] + login := checkAdminLogin(w, r) + if login == nil { + return + } + modifyRequest := *ldap.NewModifyDNRequest("cn="+cn+","+config.UserBaseDN, "cn="+cn, true, config.InvitationBaseDN) + err := login.conn.ModifyDN(&modifyRequest) + if err != nil { + return + } + http.Redirect(w, r, "/admin/users", http.StatusFound) +} + +func handleAdminUsers(w http.ResponseWriter, r *http.Request) { + templateAdminUsers := getTemplate("admin/users.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=organizationalPerson))"), + []string{config.UserNameAttr, "dn", "displayName", "givenName", "sn", "mail", "uid", "cn"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminUsersTplData{ + Login: NestedLoginTplData{Login: login}, + UserNameAttr: config.UserNameAttr, + UserBaseDN: config.UserBaseDN, + Users: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.Users) + + // addNewUser(NewUser{ + // DN: "cn=newuser@lesgv.com,ou=newusers,dc=resdigita,dc=org", + // CN: "newuser@lesgv.com", + // GivenName: "New", + // SN: "User", + // DisplayName: "New User", + // Mail: "newuser@lesgv.com", + // }, config, login) + + templateAdminUsers.Execute(w, data) +} + +func handleAdminGroups(w http.ResponseWriter, r *http.Request) { + templateAdminGroups := getTemplate("admin/groups.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames))"), + []string{config.GroupNameAttr, "dn", "description"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminGroupsTplData{ + Login: NestedLoginTplData{ + Login: login}, + GroupNameAttr: config.GroupNameAttr, + GroupBaseDN: config.GroupBaseDN, + Groups: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.Groups) + + templateAdminGroups.Execute(w, data) +} + +func handleAdminMailing(w http.ResponseWriter, r *http.Request) { + templateAdminMailing := getTemplate("admin/mailing.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + searchRequest := ldap.NewSearchRequest( + config.MailingBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames))"), + []string{config.MailingNameAttr, "dn", "description"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := &AdminMailingTplData{ + Login: NestedLoginTplData{ + Login: login}, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + MailingLists: EntryList(sr.Entries), + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: false}, + } + sort.Sort(data.MailingLists) + + templateAdminMailing.Execute(w, data) +} + +func handleAdminMailingList(w http.ResponseWriter, r *http.Request) { + templateAdminMailingList := getTemplate("admin/mailing/list.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + id := mux.Vars(r)["id"] + dn := fmt.Sprintf("%s=%s,%s", config.MailingNameAttr, id, config.MailingBaseDN) + + // handle modifications + dError := "" + dSuccess := false + + if r.Method == "POST" { + r.ParseForm() + action := strings.Join(r.Form["action"], "") + if action == "add-member" { + member := strings.Join(r.Form["member"], "") + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{member}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("198: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "add-external" { + mail := strings.Join(r.Form["mail"], "") + sn := strings.Join(r.Form["sn"], "") + givenname := strings.Join(r.Form["givenname"], "") + member := strings.Join(r.Form["member"], "") + displayname := strings.Join(r.Form["displayname"], "") + + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", mail), + []string{"dn", "displayname", "mail"}, + nil) + sr, err := login.conn.Search(searchRequest) + if err != nil { + dError = err.Error() + } else { + if len(sr.Entries) == 0 { + if config.MailingGuestsBaseDN != "" { + guestDn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, mail, config.MailingGuestsBaseDN) + req := ldap.NewAddRequest(guestDn, nil) + //req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"}) + req.Attribute("objectclass", []string{"inetOrgPerson"}) + req.Attribute("mail", []string{fmt.Sprintf("%s", mail)}) + if givenname != "" { + req.Attribute("givenname", []string{givenname}) + } + if member != "" { + req.Attribute("member", []string{member}) + } + if displayname != "" { + req.Attribute("displayname", []string{displayname}) + } + if sn != "" { + req.Attribute("sn", []string{sn}) + } + // log.Printf(fmt.Sprintf("226: %v",req)) + err := login.conn.Add(req) + if err != nil { + dError = err.Error() + } else { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{guestDn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("249: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } else { + dError = "Adding guest users not supported, the user must already have an LDAP account." + } + } else if len(sr.Entries) == 1 { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add("member", []string{sr.Entries[0].DN}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("264: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else { + dError = fmt.Sprintf("Multiple users exist with email address %s", mail) + } + } + } else if action == "delete-member" { + member := strings.Join(r.Form["member"], "") + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Delete("member", []string{member}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("280: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } + + // Retrieve mailing list + searchRequest := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=groupOfNames)"), + []string{"dn", config.MailingNameAttr, "member", "description"}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(sr.Entries) != 1 { + http.Error(w, fmt.Sprintf("Object not found: %s", dn), http.StatusNotFound) + return + } + + ml := sr.Entries[0] + + memberDns := make(map[string]bool) + for _, attr := range ml.Attributes { + if attr.Name == "member" { + for _, v := range attr.Values { + memberDns[v] = true + } + } + } + + // Retrieve list of current and possible new members + members := []*ldap.Entry{} + possibleNewMembers := []*ldap.Entry{} + + searchRequest = ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectClass=organizationalPerson)"), + []string{"dn", "displayname", "mail"}, + nil) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, ent := range sr.Entries { + if _, ok := memberDns[ent.DN]; ok { + members = append(members, ent) + } else { + possibleNewMembers = append(possibleNewMembers, ent) + } + } + + data := &AdminMailingListTplData{ + Login: NestedLoginTplData{ + Login: login, + }, + MailingNameAttr: config.MailingNameAttr, + MailingBaseDN: config.MailingBaseDN, + + MailingList: ml, + Members: members, + PossibleNewMembers: possibleNewMembers, + AllowGuest: config.MailingGuestsBaseDN != "", + Common: NestedCommonTplData{ + CanAdmin: true, + Error: dError, + Success: dSuccess, + LoggedIn: true}, + } + sort.Sort(data.Members) + sort.Sort(data.PossibleNewMembers) + + templateAdminMailingList.Execute(w, data) +} + +// =================================================== +// LDAP EXPLORER +// =================================================== + +func handleAdminLDAP(w http.ResponseWriter, r *http.Request) { + templateAdminLDAP := getTemplate("admin/ldap.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + dn := mux.Vars(r)["dn"] + + dError := "" + dSuccess := false + + // Build path + path := []PathItem{ + PathItem{ + DN: config.BaseDN, + Identifier: config.BaseDN, + Active: dn == config.BaseDN, + }, + } + // log.Printf(fmt.Sprintf("434: %v",path)) + + len_base_dn := len(strings.Split(config.BaseDN, ",")) + dn_split := strings.Split(dn, ",") + dn_last_attr := strings.Split(dn_split[0], "=")[0] + for i := len_base_dn + 1; i <= len(dn_split); i++ { + path = append(path, PathItem{ + DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","), + Identifier: dn_split[len(dn_split)-i], + Active: i == len(dn_split), + }) + } + // log.Printf(fmt.Sprintf("446: %v",path)) + + // Handle modification operation + if r.Method == "POST" { + r.ParseForm() + action := strings.Join(r.Form["action"], "") + if action == "modify" { + attr := strings.Join(r.Form["attr"], "") + values := strings.Split(strings.Join(r.Form["values"], ""), "\n") + values_filtered := []string{} + for _, v := range values { + v2 := strings.TrimSpace(v) + if v2 != "" { + values_filtered = append(values_filtered, v2) + } + } + + if len(values_filtered) == 0 { + dError = "Refusing to delete attribute." + } else { + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Replace(attr, values_filtered) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("468: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } + } else if action == "add" { + attr := strings.Join(r.Form["attr"], "") + values := strings.Split(strings.Join(r.Form["values"], ""), "\n") + values_filtered := []string{} + for _, v := range values { + v2 := strings.TrimSpace(v) + if v2 != "" { + values_filtered = append(values_filtered, v2) + } + } + + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Add(attr, values_filtered) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("490: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete" { + attr := strings.Join(r.Form["attr"], "") + + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Replace(attr, []string{}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("503: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-from-group" { + group := strings.Join(r.Form["group"], "") + modify_request := ldap.NewModifyRequest(group, nil) + modify_request.Delete("member", []string{dn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("515: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "add-to-group" { + group := strings.Join(r.Form["group"], "") + modify_request := ldap.NewModifyRequest(group, nil) + modify_request.Add("member", []string{dn}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("527: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-member" { + member := strings.Join(r.Form["member"], "") + modify_request := ldap.NewModifyRequest(dn, nil) + modify_request.Delete("member", []string{member}) + + err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("539: %v",modify_request)) + if err != nil { + dError = err.Error() + } else { + dSuccess = true + } + } else if action == "delete-object" { + del_request := ldap.NewDelRequest(dn, nil) + err := login.conn.Del(del_request) + if err != nil { + dError = err.Error() + } else { + http.Redirect(w, r, "/admin/ldap/"+strings.Join(dn_split[1:], ","), http.StatusFound) + return + } + } + } + + // Get object and parse it + searchRequest := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{}, + nil) + + sr, err := login.conn.Search(searchRequest) + // log.Printf(fmt.Sprintf("569: %v",searchRequest)) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(sr.Entries) != 1 { + http.Error(w, fmt.Sprintf("Object not found: %s", dn), http.StatusNotFound) + return + } + + object := sr.Entries[0] + + // Read object properties and prepare appropriate form fields + props := make(map[string]*PropValues) + for _, attr := range object.Attributes { + name_lower := strings.ToLower(attr.Name) + if name_lower != dn_last_attr { + if existing, ok := props[name_lower]; ok { + existing.Values = append(existing.Values, attr.Values...) + } else { + editable := true + for _, restricted := range []string{ + "creatorsname", "modifiersname", "createtimestamp", + "modifytimestamp", "entryuuid", + } { + if strings.EqualFold(attr.Name, restricted) { + editable = false + break + } + } + deletable := true + for _, restricted := range []string{"objectclass", "structuralobjectclass"} { + if strings.EqualFold(attr.Name, restricted) { + deletable = false + break + } + } + props[name_lower] = &PropValues{ + Name: attr.Name, + Values: attr.Values, + Editable: editable, + Deletable: deletable, + } + } + } + } + + // Check objectclass to determine object type + objectClass := []string{} + if val, ok := props["objectclass"]; ok { + objectClass = val.Values + } + hasMembers, hasGroups, isOrganization := false, false, false + for _, oc := range objectClass { + if strings.EqualFold(oc, "organizationalPerson") || strings.EqualFold(oc, "person") || strings.EqualFold(oc, "inetOrgPerson") { + hasGroups = true + } + if strings.EqualFold(oc, "groupOfNames") { + hasMembers = true + } + if strings.EqualFold(oc, "organization") { + isOrganization = true + } + } + + // Parse member list and prepare form section + members_dn := []string{} + if mp, ok := props["member"]; ok { + members_dn = mp.Values + delete(props, "member") + } + + members := []EntryName{} + possibleNewMembers := []EntryName{} + if len(members_dn) > 0 || hasMembers { + // Lookup all existing users in the server + // to know the DN -> display name correspondance + searchRequest = ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectClass=organizationalPerson)"), + []string{"dn", "displayname", "description"}, + nil) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + userMap := make(map[string]string) + for _, ent := range sr.Entries { + userMap[ent.DN] = ent.GetAttributeValue("displayname") + if userMap[ent.DN] == "" { + userMap[ent.DN] = ent.GetAttributeValue("description") + } + } + + // Select members with their name and remove them from map + for _, memdn := range members_dn { + members = append(members, EntryName{ + DN: memdn, + Name: userMap[memdn], + }) + delete(userMap, memdn) + } + + // Create list of members that can be added + for dn, name := range userMap { + entry := EntryName{ + DN: dn, + Name: name, + } + if entry.Name == "" { + entry.Name = entry.DN + } + possibleNewMembers = append(possibleNewMembers, entry) + } + } + + // // Parse group list and prepare form section + // groups_dn := []string{} + // if gp, ok := props["memberof"]; ok { + // groups_dn = gp.Values + // delete(props, "memberof") + // } + + groups := []EntryName{} + possibleNewGroups := []EntryName{} + searchRequest = ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames)(member=%s))", dn), + []string{"dn", "displayName", "cn", "description"}, + nil) + // log.Printf(fmt.Sprintf("708: %v",searchRequest)) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // log.Printf(fmt.Sprintf("714: %v",sr.Entries)) + for _, ent := range sr.Entries { + groups = append(groups, EntryName{ + DN: ent.DN, + Name: ent.GetAttributeValue("cn"), + }) + } + searchRequest = ldap.NewSearchRequest( + config.GroupBaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=groupOfNames)(!(member=%s)))", dn), + []string{"dn", "displayName", "cn", "description"}, + nil) + // log.Printf(fmt.Sprintf("724: %v",searchRequest)) + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // log.Printf(fmt.Sprintf("714: %v",sr.Entries)) + for _, ent := range sr.Entries { + possibleNewGroups = append(possibleNewGroups, EntryName{ + DN: ent.DN, + Name: ent.GetAttributeValue("cn"), + }) + } + + // possibleNewGroup.DN = ent.GetAttributeValue("dn") + // possibleNewGroup.Name = ent.GetAttributeValue("cn") + // // log.Printf(fmt.Sprintf("725: %v %v",dn, ent.GetAttributeValue("member"))) + // for _, member := range ent .GetAttributeValue("member") { + // // // log.Printf(fmt.Sprintf("725: %v %v",dn, member)) + // if ent.GetAttributeValue("member") == dn { + // groups = append(groups,possibleNewGroup,) + // possibleNewGroup.DN = "" + // possibleNewGroup.Name = "" + // } + // // } + // if possibleNewGroup.DN != "" { + // possibleNewGroups = append(possibleNewGroups,possibleNewGroup,) + // possibleNewGroup = EntryName{} + // } + + // groupMap[.DN] = ent.GetAttributeValue("displayName") + // if groupMap[.DN] == "" { + // groupMap[.DN] = ent.GetAttributeValue("cn") + // } + // if groupMap[.DN] == "" { + // groupMap[.DN] = ent.GetAttributeValue("description") + // } + // } + + // // Calculate list of current groups + // // log.Printf(fmt.Sprintf("%v",groups_dn)) + // for _, grpdn := range groups_dn { + // // log.Printf(fmt.Sprintf("%v",grpdn)) + // groups = append(groups, EntryName{ + // DN: grpdn, + // Name: groupMap[grpdn], + // }) + // delete(groupMap, grpdn) + // } + + // // Calculate list of possible new groups + // for dn, name := range groupMap { + // entry := EntryName{ + // DN: dn, + // Name: name, + // } + // if entry.Name == "" { + // entry.Name = entry.DN + // } + // possibleNewGroups = append(possibleNewGroups, entry) + // } + // } + + // Get children + searchRequest = ldap.NewSearchRequest( + dn, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{"dn", "displayname", "description"}, + nil) + + sr, err = login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + sort.Sort(EntryList(sr.Entries)) + + childrenOU := []Child{} + childrenOther := []Child{} + for _, item := range sr.Entries { + name := item.GetAttributeValue("displayname") + if name == "" { + name = item.GetAttributeValue("description") + } + child := Child{ + DN: item.DN, + Identifier: strings.Split(item.DN, ",")[0], + Name: name, + } + if strings.HasPrefix(item.DN, "ou=") { + childrenOU = append(childrenOU, child) + } else { + childrenOther = append(childrenOther, child) + } + } + + // Run template, finally! + templateAdminLDAP.Execute(w, &AdminLDAPTplData{ + DN: dn, + + Path: path, + ChildrenOU: childrenOU, + ChildrenOther: childrenOther, + Props: props, + CanAddChild: dn_last_attr == "ou" || isOrganization, + CanDelete: dn != config.BaseDN && len(childrenOU) == 0 && len(childrenOther) == 0, + + HasMembers: len(members) > 0 || hasMembers, + Members: members, + PossibleNewMembers: possibleNewMembers, + HasGroups: len(groups) > 0 || hasGroups, + Groups: groups, + PossibleNewGroups: possibleNewGroups, + + Common: NestedCommonTplData{ + CanAdmin: true, + LoggedIn: true, + Error: dError, + Success: dSuccess, + }, + }) +} + +func handleAdminCreate(w http.ResponseWriter, r *http.Request) { + templateAdminCreate := getTemplate("admin/create.html") + + login := checkAdminLogin(w, r) + if login == nil { + return + } + + template := mux.Vars(r)["template"] + super_dn := mux.Vars(r)["super_dn"] + + // Check that base DN exists + searchRequest := ldap.NewSearchRequest( + super_dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{}, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(sr.Entries) != 1 { + http.Error(w, fmt.Sprintf("Parent object %s does not exist", super_dn), http.StatusNotFound) + return + } + + // Build path + path := []PathItem{ + PathItem{ + DN: config.BaseDN, + Identifier: config.BaseDN, + }, + } + + len_base_dn := len(strings.Split(config.BaseDN, ",")) + dn_split := strings.Split(super_dn, ",") + for i := len_base_dn + 1; i <= len(dn_split); i++ { + path = append(path, PathItem{ + DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","), + Identifier: dn_split[len(dn_split)-i], + }) + } + + // Handle data + data := &CreateData{ + SuperDN: super_dn, + Path: path, + } + data.Template = template + if template == "user" { + data.IdType = config.UserNameAttr + data.StructuralObjectClass = "inetOrgPerson" + data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop" + } else if template == "group" || template == "ml" { + data.IdType = config.UserNameAttr + data.StructuralObjectClass = "groupOfNames" + data.ObjectClass = "groupOfNames\ntop" + data.Member = "cn=sogo@resdigita.org,ou=users,dc=resdigita,dc=org" + } else if template == "ou" { + data.IdType = "ou" + data.StructuralObjectClass = "organizationalUnit" + data.ObjectClass = "organizationalUnit\ntop" + } else { + data.IdType = "cn" + data.ObjectClass = "top" + data.Template = "" + } + + if r.Method == "POST" { + r.ParseForm() + if data.Template == "" { + data.IdType = strings.TrimSpace(strings.Join(r.Form["idtype"], "")) + data.StructuralObjectClass = strings.TrimSpace(strings.Join(r.Form["soc"], "")) + data.ObjectClass = strings.Join(r.Form["oc"], "") + } + data.IdValue = strings.TrimSpace(strings.Join(r.Form["idvalue"], "")) + data.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], "")) + data.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], "")) + data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], "")) + data.Member = strings.TrimSpace(strings.Join(r.Form["member"], "")) + data.Description = strings.TrimSpace(strings.Join(r.Form["description"], "")) + data.SN = strings.TrimSpace(strings.Join(r.Form["sn"], "")) + + object_class := []string{} + for _, oc := range strings.Split(data.ObjectClass, "\n") { + x := strings.TrimSpace(oc) + if x != "" { + object_class = append(object_class, x) + } + } + + if len(object_class) == 0 { + data.Common.Error = "No object class specified" + } else if match, err := regexp.MatchString("^[a-z]+$", data.IdType); err != nil || !match { + data.Common.Error = "Invalid identifier type" + } else if len(data.IdValue) == 0 { + data.Common.Error = "No identifier specified" + } else { + newUser := User{ + DN: data.IdType + "=" + data.IdValue + "," + super_dn, + } + // dn := data.IdType + "=" + data.IdValue + "," + super_dn + // req := ldap.NewAddRequest(dn, nil) + // req.Attribute("objectclass", object_class) + // req.Attribute("mail", []string{data.IdValue}) + /* + if data.StructuralObjectClass != "" { + req.Attribute("structuralobjectclass", []string{data.StructuralObjectClass}) + } + */ + if data.Mail != "" { + newUser.Mail = data.Mail + // req.Attribute("mail", []string{data.Mail}) + } + if data.IdType == "cn" { + newUser.CN = data.IdValue + } else if data.IdType == "mail" { + newUser.Mail = data.IdValue + } else if data.IdType == "uid" { + newUser.UID = data.IdValue + } + + if data.DisplayName != "" { + newUser.DisplayName = data.DisplayName + // req.Attribute("displayname", []string{data.DisplayName}) + } + if data.GivenName != "" { + newUser.GivenName = data.GivenName + // req.Attribute("givenname", []string{data.GivenName}) + } + + // if data.Member != "" { + // req.Attribute("member", []string{data.Member}) + // } + if data.SN != "" { + newUser.SN = data.SN + // req.Attribute("sn", []string{data.SN}) + } + if data.Description != "" { + newUser.Description = data.Description + // req.Attribute("description", []string{data.Description}) + } + + add(newUser, config, login.conn) + + // err := login.conn.Add(req) + // // log.Printf(fmt.Sprintf("899: %v",err)) + // // log.Printf(fmt.Sprintf("899: %v",req)) + // // log.Printf(fmt.Sprintf("899: %v",data)) + // if err != nil { + // data.Common.Error = err.Error() + // } else { + if template == "ml" { + http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound) + } else { + http.Redirect(w, r, "/admin/ldap/"+newUser.DN, http.StatusFound) + } + // } + } + } + data.Common.CanAdmin = true + + templateAdminCreate.Execute(w, data) +} diff --git a/view-home.go b/view-home.go new file mode 100644 index 0000000..d895793 --- /dev/null +++ b/view-home.go @@ -0,0 +1,40 @@ +/* +home show the home page +*/ + +package main + +import "net/http" + +func handleHome(w http.ResponseWriter, r *http.Request) { + templateHome := getTemplate("home.html") + + login := checkLogin(w, r) + if login == nil { + status := handleLogin(w, r) + if status == nil { + return + } + login = checkLogin(w, r) + } + + can_admin := false + if login != nil { + can_admin = login.Common.CanAdmin + } + + data := HomePageData{ + Login: NestedLoginTplData{ + Login: login, + }, + BaseDN: config.BaseDN, + Org: config.Org, + Common: NestedCommonTplData{ + CanAdmin: can_admin, + CanInvite: true, + LoggedIn: true, + }, + } + templateHome.Execute(w, data) + +} diff --git a/view-invite.go b/view-invite.go new file mode 100644 index 0000000..733f026 --- /dev/null +++ b/view-invite.go @@ -0,0 +1,555 @@ +package main + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "html/template" + "log" + "net/http" + "regexp" + "strings" + + // "github.com/emersion/go-sasl" + // "github.com/emersion/go-smtp" + "github.com/go-ldap/ldap/v3" + "github.com/gorilla/mux" + "golang.org/x/crypto/argon2" +) + +var EMAIL_REGEXP = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { + + login := checkLogin(w, r) + if login == nil { + return nil + } + + // if !login.CanInvite { + // http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized) + // return nil + // } + + return login +} + +// New account creation directly from interface + +func openNewUserLdap(config *ConfigFile) (*ldap.Conn, error) { + l, err := openLdap(config) + if err != nil { + log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v %v", err, l)) + log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v", config)) + // data.Common.ErrorMessage = err.Error() + } + err = l.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", err)) + log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserDN)) + log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserPassword)) + log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config)) + // data.Common.ErrorMessage = err.Error() + } + return l, err +} + +func handleLostPassword(w http.ResponseWriter, r *http.Request) { + templateLostPasswordPage := getTemplate("passwd/lost.html") + if checkLogin(w, r) != nil { + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + } + + data := PasswordLostData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: false}, + } + + if r.Method == "POST" { + r.ParseForm() + data.Username = strings.TrimSpace(strings.Join(r.Form["username"], "")) + data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], "")) + data.OtherMailbox = strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")) + user := User{ + CN: strings.TrimSpace(strings.Join(r.Form["username"], "")), + UID: strings.TrimSpace(strings.Join(r.Form["username"], "")), + Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")), + OtherMailbox: strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")), + } + ldapConn, err := openNewUserLdap(config) + if err != nil { + log.Printf(fmt.Sprintf("handleLostPassword 99 : %v %v", err, ldapConn)) + data.Common.ErrorMessage = err.Error() + } + err = passwordLost(user, config, ldapConn) + if err != nil { + log.Printf(fmt.Sprintf("handleLostPassword 104 : %v %v", err, ldapConn)) + data.Common.ErrorMessage = err.Error() + } else { + err = ldapConn.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf(fmt.Sprintf("handleLostPassword 109 : %v %v", err, ldapConn)) + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + } + data.Common.CanAdmin = false + templateLostPasswordPage.Execute(w, data) +} + +func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) { + l, err := ldapOpen(w) + if err != nil { + log.Printf(fmt.Sprintf("58: %v %v", err, l)) + } + // l.Bind(config.NewUserDN, config.NewUserPassword) + + // login := checkInviterLogin(w, r) + // if login == nil { + // return + // } + // l, _ := ldap.DialURL(config.LdapServerAddr) + // l.Bind(config.NewUserDN, config.NewUserPassword) + + // loginInfo, err := doLogin(w, r, "testuser", config.NewUserDN, config.NewUserPassword) + + // if err != nil { + // log.Printf(fmt.Sprintf("58: %v %v", err, l)) + // } + + // l := ldapOpen(w) + if l == nil { + return + } + + err = l.Bind(config.NewUserDN, config.NewUserPassword) + if err != nil { + log.Printf(fmt.Sprintf("58: %v %v", err, l)) + } + handleNewAccount(w, r, l, config.NewUserDN) +} + +// New account creation using code + +func handleInvitationCode(w http.ResponseWriter, r *http.Request) { + code := mux.Vars(r)["code"] + code_id, code_pw := readCode(code) + + // log.Printf(code_pw) + + login := checkLogin(w, r) + + // l := ldapOpen(w) + // if l == nil { + // return + // } + + inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN + err := login.conn.Bind(inviteDn, code_pw) + if err != nil { + templateInviteInvalidCode := getTemplate("user/code/invalid.html") + templateInviteInvalidCode.Execute(w, nil) + return + } + + sReq := ldap.NewSearchRequest( + inviteDn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(objectclass=*)"), + []string{"dn", "creatorsname"}, + nil) + sr, err := login.conn.Search(sReq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(sr.Entries) != 1 { + http.Error(w, fmt.Sprintf("Expected 1 entry, got %d", len(sr.Entries)), http.StatusInternalServerError) + return + } + + invitedBy := sr.Entries[0].GetAttributeValue("creatorsname") + + if handleNewAccount(w, r, login.conn, invitedBy) { + del_req := ldap.NewDelRequest(inviteDn, nil) + err = login.conn.Del(del_req) + if err != nil { + log.Printf("Could not delete invitation %s: %s", inviteDn, err) + } + } +} + +// Common functions for new account + +func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invitedBy string) bool { + templateInviteNewAccount := getTemplate("user/new.html") + + data := &NewAccountData{} + + if r.Method == "POST" { + r.ParseForm() + + newUser := User{} + // login := checkLogin(w, r) + + // newUser.Mail = fmt.Sprintf("%s@%s", strings.TrimSpace(strings.Join(r.Form["username"], "")), "lesgv.com") + newUser.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], "")) + newUser.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], "")) + newUser.SN = strings.TrimSpace(strings.Join(r.Form["surname"], "")) + newUser.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], "")) + newUser.UID = strings.TrimSpace(strings.Join(r.Form["otheremail"], "")) + newUser.CN = strings.TrimSpace(strings.Join(r.Form["username"], "")) + newUser.DN = "cn=" + strings.TrimSpace(strings.Join(r.Form["username"], "")) + "," + config.InvitationBaseDN + + password1 := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + + if password1 != password2 { + data.Common.Success = false + data.ErrorPasswordMismatch = true + } else { + newUser.Password = password2 + l.Bind(config.NewUserDN, config.NewUserPassword) + err := add(newUser, config, l) + if err != nil { + data.Common.Success = false + data.Common.ErrorMessage = err.Error() + } + http.Redirect(w, r, "/admin/activate", http.StatusFound) + } + + // tryCreateAccount(l, data, password1, password2, invitedBy) + + } else { + data.SuggestPW = fmt.Sprintf("%s", suggestPassword()) + } + data.Common.CanAdmin = false + data.Common.LoggedIn = false + + templateInviteNewAccount.Execute(w, data) + return data.Common.Success +} + +func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 string, invitedBy string) { + checkFailed := false + + // Check if username is correct + if match, err := regexp.MatchString("^[a-z0-9._-]+$", data.Username); !(err == nil && match) { + data.ErrorInvalidUsername = true + checkFailed = true + } + + // Check if user exists + userDn := config.UserNameAttr + "=" + data.Username + "," + config.UserBaseDN + searchRq := ldap.NewSearchRequest( + userDn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, + "(objectclass=*)", + []string{"dn"}, + nil) + + sr, err := l.Search(searchRq) + if err != nil { + data.Common.ErrorMessage = err.Error() + checkFailed = true + } + + if len(sr.Entries) > 0 { + data.ErrorUsernameTaken = true + checkFailed = true + } + + // Check that password is long enough + if len(pass1) < 8 { + data.ErrorPasswordTooShort = true + checkFailed = true + } + + if pass1 != pass2 { + data.ErrorPasswordMismatch = true + checkFailed = true + } + + if checkFailed { + return + } + + // Actually create user + req := ldap.NewAddRequest(userDn, nil) + req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"}) + req.Attribute("structuralobjectclass", []string{"inetOrgPerson"}) + pw, err := SSHAEncode(pass1) + if err != nil { + data.Common.ErrorMessage = err.Error() + return + } + req.Attribute("userpassword", []string{pw}) + req.Attribute("invitedby", []string{invitedBy}) + if len(data.DisplayName) > 0 { + req.Attribute("displayname", []string{data.DisplayName}) + } + if len(data.GivenName) > 0 { + req.Attribute("givenname", []string{data.GivenName}) + } + if len(data.Surname) > 0 { + req.Attribute("sn", []string{data.Surname}) + } + if len(config.InvitedMailFormat) > 0 { + email := strings.ReplaceAll(config.InvitedMailFormat, "{}", data.Username) + req.Attribute("mail", []string{email}) + } + + err = l.Add(req) + if err != nil { + data.Common.ErrorMessage = err.Error() + return + } + + for _, group := range config.InvitedAutoGroups { + req := ldap.NewModifyRequest(group, nil) + req.Add("member", []string{userDn}) + err = l.Modify(req) + if err != nil { + data.Common.WarningMessage += fmt.Sprintf("Cannot add to %s: %s\n", group, err.Error()) + } + } + + data.Common.Success = true +} + +// ---- Code generation ---- + +func handleInviteSendCode(w http.ResponseWriter, r *http.Request) { + templateInviteSendCode := getTemplate("user/code/send.html") + + login := checkInviterLogin(w, r) + if login == nil { + return + } + + // carLicense + + if r.Method == "POST" { + r.ParseForm() + data := &SendCodeData{ + WebBaseAddress: config.WebAddress, + } + + // modify_request := ldap.NewModifyRequest(login.UserEntry.DN, nil) + // // choice := strings.Join(r.Form["choice"], "") + // // sendto := strings.Join(r.Form["sendto"], "") + code, code_id, code_pw := genCode() + log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw)) + // // Create invitation object in database + // modify_request.Add("carLicense", []string{fmt.Sprintf("%s,%s,%s",code, code_id, code_pw)}) + // err := login.conn.Modify(modify_request) + // if err != nil { + // data.Common.ErrorMessage = err.Error() + // // return + // } else { + // data.Common.Success = true + // data.CodeDisplay = code + // } + log.Printf(fmt.Sprintf("279: %v %v %v", code, code_id, code_pw)) + addReq := ldap.NewAddRequest("documentIdentifier="+code_id+","+config.InvitationBaseDN, nil) + addReq.Attribute("objectClass", []string{"top", "document", "simpleSecurityObject"}) + addReq.Attribute("cn", []string{code}) + addReq.Attribute("userPassword", []string{code_pw}) + addReq.Attribute("documentIdentifier", []string{code_id}) + log.Printf(fmt.Sprintf("285: %v %v %v", code, code_id, code_pw)) + log.Printf(fmt.Sprintf("286: %v", addReq)) + err := login.conn.Add(addReq) + if err != nil { + data.Common.ErrorMessage = err.Error() + // return + } else { + data.Common.Success = true + data.CodeDisplay = code + } + data.Common.CanAdmin = login.Common.CanAdmin + + templateInviteSendCode.Execute(w, data) + + // if choice == "display" || choice == "send" { + // log.Printf(fmt.Sprintf("260: %v %v %v %v", login, choice, sendto, data)) + // trySendCode(login, choice, sendto, data) + // } + } + +} + +func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) { + log.Printf(fmt.Sprintf("269: %v %v %v %v", login, choice, sendto, data)) + // Generate code + code, code_id, code_pw := genCode() + log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw)) + // Create invitation object in database + + // len_base_dn := len(strings.Split(config.BaseDN, ",")) + // dn_split := strings.Split(super_dn, ",") + // for i := len_base_dn + 1; i <= len(dn_split); i++ { + // path = append(path, PathItem{ + // DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","), + // Identifier: dn_split[len(dn_split)-i], + // }) + // } + // data := &SendCodeData{ + // WebBaseAddress: config.WebAddress, + // } + // // Handle data + // data := &CreateData{ + // SuperDN: super_dn, + // Path: path, + // } + // data.IdType = config.UserNameAttr + // data.StructuralObjectClass = "inetOrgPerson" + // data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop" + // data.IdValue = strings.TrimSpace(strings.Join(r.Form["idvalue"], "")) + // data.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], "")) + // data.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], "")) + // data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], "")) + // data.Member = strings.TrimSpace(strings.Join(r.Form["member"], "")) + // data.Description = strings.TrimSpace(strings.Join(r.Form["description"], "")) + // data.SN = strings.TrimSpace(strings.Join(r.Form["sn"], "")) + // object_class := []string{} + // for _, oc := range strings.Split(data.ObjectClass, "\n") { + // x := strings.TrimSpace(oc) + // if x != "" { + // object_class = append(object_class, x) + // } + // } + // dn := data.IdType + "=" + data.IdValue + "," + super_dn + // req := ldap.NewAddRequest(dn, nil) + // req.Attribute("objectclass", object_class) + // // req.Attribute("mail", []string{data.IdValue}) + // /* + // if data.StructuralObjectClass != "" { + // req.Attribute("structuralobjectclass", []string{data.StructuralObjectClass}) + // } + // */ + // if data.DisplayName != "" { + // req.Attribute("displayname", []string{data.DisplayName}) + // } + // if data.GivenName != "" { + // req.Attribute("givenname", []string{data.GivenName}) + // } + // if data.Mail != "" { + // req.Attribute("mail", []string{data.Mail}) + // } + // if data.Member != "" { + // req.Attribute("member", []string{data.Member}) + // } + // if data.SN != "" { + // req.Attribute("sn", []string{data.SN}) + // } + // if data.Description != "" { + // req.Attribute("description", []string{data.Description}) + // } + // err := login.conn.Add(req) + // // log.Printf(fmt.Sprintf("899: %v",err)) + // // log.Printf(fmt.Sprintf("899: %v",req)) + // // log.Printf(fmt.Sprintf("899: %v",data)) + // if err != nil { + // data.Common.Error = err.Error() + // } else { + // if template == "ml" { + // http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound) + // } else { + // http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound) + // } + // } + + // inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN + // req := ldap.NewAddRequest(inviteDn, nil) + // pw, err := SSHAEncode(code_pw) + // if err != nil { + // data.Common.ErrorMessage = err.Error() + // return + // } + // req.Attribute("employeeNumber", []string{pw}) + // req.Attribute("objectclass", []string{"top", "invitationCode"}) + + // err = login.conn.Add(req) + // if err != nil { + // log.Printf(fmt.Sprintf("286: %v", req)) + // data.Common.ErrorMessage = err.Error() + // return + // } + + // If we want to display it, do so + if choice == "display" { + data.Common.Success = true + data.CodeDisplay = code + return + } + + // Otherwise, we are sending a mail + if !EMAIL_REGEXP.MatchString(sendto) { + data.ErrorInvalidEmail = true + return + } + + templateMail := template.Must(template.ParseFiles(templatePath + "/invite_mail.txt")) + buf := bytes.NewBuffer([]byte{}) + templateMail.Execute(buf, &CodeMailFields{ + To: sendto, + From: config.MailFrom, + InviteFrom: login.WelcomeName(), + Code: code, + WebBaseAddress: config.WebAddress, + }) + + log.Printf("Sending mail to: %s", sendto) + // var auth sasl.Client = nil + // if config.SMTPUsername != "" { + // auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword) + // } + // err = smtp.SendMail(config.SMTPServer, auth, config.MailFrom, []string{sendto}, buf) + // if err != nil { + // data.Common.ErrorMessage = err.Error() + // return + // } + // log.Printf("Mail sent.") + + data.Common.Success = true + data.CodeSentTo = sendto +} + +func genCode() (code string, code_id string, code_pw string) { + random := make([]byte, 32) + n, err := rand.Read(random) + if err != nil || n != 32 { + log.Fatalf("Could not generate random bytes: %s", err) + } + + a := binary.BigEndian.Uint32(random[0:4]) + b := binary.BigEndian.Uint32(random[4:8]) + c := binary.BigEndian.Uint32(random[8:12]) + + code = fmt.Sprintf("%03d-%03d-%03d", a%1000, b%1000, c%1000) + code_id, code_pw = readCode(code) + log.Printf(fmt.Sprintf("342: %v %v %v", code, code_id, code_pw)) + return code, code_id, code_pw +} + +func readCode(code string) (code_id string, code_pw string) { + // Strip everything that is not a digit + code_digits := "" + for _, c := range code { + if c >= '0' && c <= '9' { + code_digits = code_digits + string(c) + } + } + + id_hash := argon2.IDKey([]byte(code_digits), []byte("Guichet ID"), 2, 64*1024, 4, 32) + pw_hash := argon2.IDKey([]byte(code_digits), []byte("Guichet PW"), 2, 64*1024, 4, 32) + + code_id = hex.EncodeToString(id_hash[:8]) + code_pw = hex.EncodeToString(pw_hash[:16]) + return code_id, code_pw +} diff --git a/view-login.go b/view-login.go new file mode 100644 index 0000000..f53b294 --- /dev/null +++ b/view-login.go @@ -0,0 +1,128 @@ +/* +login handles login and current-user verification +*/ + +package main + +import ( + "fmt" + "log" + "net/http" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +func (login *LoginStatus) WelcomeName() string { + ret := login.UserEntry.GetAttributeValue("givenName") + if ret == "" { + ret = login.UserEntry.GetAttributeValue("displayName") + } + if ret == "" { + ret = login.Info.Username + } + return ret +} + +func handleLogout(w http.ResponseWriter, r *http.Request) { + + err := logout(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusFound) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { + templateLogin := getTemplate("login.html") + + if r.Method == "POST" { + // log.Printf("%v", "Parsing Form handleLogin") + r.ParseForm() + + username := strings.Join(r.Form["username"], "") + password := strings.Join(r.Form["password"], "") + user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN) + + // log.Printf("%v", user_dn) + // log.Printf("%v", username) + + if strings.EqualFold(username, config.AdminAccount) { + user_dn = username + } + loginInfo, err := doLogin(w, r, username, user_dn, password) + // log.Printf("%v", loginInfo) + if err != nil { + data := &LoginFormData{ + Username: username, + Common: NestedCommonTplData{ + CanAdmin: false, + CanInvite: true, + LoggedIn: false, + }, + } + if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) { + data.WrongPass = true + } else if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { + data.WrongUser = true + } else { + log.Printf("%v", err) + log.Printf("%v", user_dn) + log.Printf("%v", username) + data.Common.ErrorMessage = err.Error() + } + templateLogin.Execute(w, data) + } + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return loginInfo + + } else if r.Method == "GET" { + templateLogin.Execute(w, LoginFormData{ + Common: NestedCommonTplData{ + CanAdmin: false, + CanInvite: true, + LoggedIn: false}}) + return nil + } else { + http.Error(w, "Unsupported method", http.StatusBadRequest) + return nil + } +} + +func doLogin(w http.ResponseWriter, r *http.Request, username string, user_dn string, password string) (*LoginInfo, error) { + l, _ := ldapOpen(w) + + err := l.Bind(user_dn, password) + if err != nil { + log.Printf("doLogin : %v", err) + log.Printf("doLogin : %v", user_dn) + return nil, err + } + + // Successfully logged in, save it to session + session, err := store.Get(r, SESSION_NAME) + if err != nil { + session, _ = store.New(r, SESSION_NAME) + } + + session.Values["login_username"] = username + session.Values["login_password"] = password + session.Values["login_dn"] = user_dn + + err = session.Save(r, w) + if err != nil { + log.Printf("doLogin Session Save: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return nil, err + } + + LoginInfo := LoginInfo{ + DN: user_dn, + Username: username, + Password: password, + } + + return &LoginInfo, nil +} diff --git a/view-passwd.go b/view-passwd.go new file mode 100644 index 0000000..415ddab --- /dev/null +++ b/view-passwd.go @@ -0,0 +1,169 @@ +/* +gpas is GVoisin password reset +*/ + +package main + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "log" + + // "github.com/emersion/go-sasl" + // "github.com/emersion/go-smtp" + "net/smtp" + + "github.com/go-ldap/ldap/v3" + // "strings" + b64 "encoding/base64" +) + +// type InvitationAccount struct { +// UID string +// Password string +// BaseDN string +// } + +// var EMAIL_REGEXP := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +func passwordLost(user User, config *ConfigFile, ldapConn *ldap.Conn) error { + if user.CN == "" && user.Mail == "" && user.OtherMailbox == "" { + return errors.New("Il n'y a pas de quoi identifier l'utilisateur") + } + searchFilter := "(|" + if user.CN != "" { + searchFilter += "(cn=" + user.UID + ")" + } + if user.Mail != "" { + searchFilter += "(mail=" + user.Mail + ")" + } + if user.OtherMailbox != "" { + searchFilter += "(carLicense=" + user.OtherMailbox + ")" + } + searchFilter += ")" + searchReq := ldap.NewSearchRequest(config.UserBaseDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, searchFilter, []string{"cn", "uid", "mail", "carLicense", "sn", "displayName", "givenName"}, nil) + searchRes, err := ldapConn.Search(searchReq) + if err != nil { + log.Printf(fmt.Sprintf("passwordLost 49 : %v %v", err, ldapConn)) + log.Printf(fmt.Sprintf("passwordLost 50 : %v", searchReq)) + log.Printf(fmt.Sprintf("passwordLost 51: %v", user)) + return err + } + if len(searchRes.Entries) == 0 { + log.Printf("Il n'y a pas d'utilisateur qui correspond %v", searchReq) + return errors.New("Il n'y a pas d'utilisateur qui correspond") + } + // log.Printf(fmt.Sprintf("passwordLost 58 : %v", user)) + // log.Printf(fmt.Sprintf("passwordLost 59 : %v", searchRes.Entries[0])) + // log.Printf(fmt.Sprintf("passwordLost 60 : %v", searchRes.Entries[0].GetAttributeValue("cn"))) + // log.Printf(fmt.Sprintf("passwordLost 61 : %v", searchRes.Entries[0].GetAttributeValue("uid"))) + // log.Printf(fmt.Sprintf("passwordLost 62 : %v", searchRes.Entries[0].GetAttributeValue("mail"))) + // log.Printf(fmt.Sprintf("passwordLost 63 : %v", searchRes.Entries[0].GetAttributeValue("carLicense"))) + // Préparation du courriel à envoyer + user.Password = suggestPassword() + code := b64.URLEncoding.EncodeToString([]byte(user.UID + ";" + user.Password)) + user.DN = "uid=" + searchRes.Entries[0].GetAttributeValue("cn") + ",ou=invitations,dc=resdigita,dc=org" + user.UID = searchRes.Entries[0].GetAttributeValue("cn") + user.CN = searchRes.Entries[0].GetAttributeValue("cn") + user.Mail = searchRes.Entries[0].GetAttributeValue("mail") + user.OtherMailbox = searchRes.Entries[0].GetAttributeValue("carLicense") + /* Check for outstanding invitation */ + searchReq = ldap.NewSearchRequest(config.InvitationBaseDN, ldap.ScopeBaseObject, + ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil) + searchRes, err = ldapConn.Search(searchReq) + if err != nil { + log.Printf(fmt.Sprintf("passwordLost (Check existing invitation) : %v", err)) + log.Printf(fmt.Sprintf("passwordLost (Check existing invitation) : %v", user)) + return err + } + if len(searchRes.Entries) == 0 { + /* Add the invitation */ + addReq := ldap.NewAddRequest( + user.DN, + nil) + addReq.Attribute("objectClass", []string{"top", "account", "simpleSecurityObject"}) + addReq.Attribute("uid", []string{user.UID}) + addReq.Attribute("userPassword", []string{"absdefghi"}) + addReq.Attribute("seeAlso", []string{config.UserNameAttr + "=" + user.UID + "," + config.UserBaseDN}) + err = ldapConn.Add(addReq) + if err != nil { + log.Printf(fmt.Sprintf("passwordLost 83 : %v", err)) + log.Printf(fmt.Sprintf("passwordLost 84 : %v", user)) + // log.Printf(fmt.Sprintf("passwordLost 85 : %v", searchRes.Entries[0])) + // For some reason I get here even if the entry exists already + return err + } + } + err = passwd(user, config, ldapConn) + if err != nil { + log.Printf(fmt.Sprintf("passwordLost 90 : %v", err)) + log.Printf(fmt.Sprintf("passwordLost 91 : %v", user)) + log.Printf(fmt.Sprintf("passwordLost 92 : %v", searchRes.Entries[0])) + return err + } + templateMail := template.Must(template.ParseFiles(templatePath + "/lost_password_email.txt")) + buf := bytes.NewBuffer([]byte{}) + templateMail.Execute(buf, &CodeMailFields{ + To: user.OtherMailbox, + From: config.MailFrom, + InviteFrom: user.UID, + Code: code, + WebBaseAddress: config.WebAddress, + }) + // message := []byte("Hi " + user.OtherMailbox) + log.Printf("Sending mail to: %s", user.OtherMailbox) + // var auth sasl.Client = nil + // if config.SMTPUsername != "" { + // auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword) + // } + message := buf.Bytes() + auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer) + log.Printf("auth: %v", auth) + err = smtp.SendMail(config.SMTPServer+":587", auth, config.SMTPUsername, []string{user.OtherMailbox}, message) + if err != nil { + log.Printf("email send error %v", err) + return err + } + log.Printf("Mail sent.") + return nil +} + +func passwordFound(user User, config *ConfigFile, ldapConn *ldap.Conn) (string, error) { + l, err := openLdap(config) + if err != nil { + log.Printf("passwordFound %v", err) + log.Printf("passwordFound Config : %v", config) + return "", err + } + if user.DN == "" && user.UID != "" { + user.DN = "uid=" + user.UID + ",ou=invitations,dc=resdigita,dc=org" + } + err = l.Bind(user.DN, user.Password) + if err != nil { + log.Printf("passwordFound %v", err) + log.Printf("passwordFound %v", user.DN) + log.Printf("passwordFound %v", user.UID) + return "", err + } + searchReq := ldap.NewSearchRequest(user.DN, ldap.ScopeBaseObject, + ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil) + var searchRes *ldap.SearchResult + searchRes, err = ldapConn.Search(searchReq) + if err != nil { + log.Printf("passwordFound %v", err) + log.Printf("passwordFound %v", searchReq) + log.Printf("passwordFound %v", ldapConn) + log.Printf("passwordFound %v", searchRes) + return "", err + } + if len(searchRes.Entries) == 0 { + log.Printf("passwordFound %v", err) + log.Printf("passwordFound %v", searchReq) + log.Printf("passwordFound %v", ldapConn) + log.Printf("passwordFound %v", searchRes) + return "", err + } + return searchRes.Entries[0].GetAttributeValue("seeAlso"), err +} diff --git a/view-profile.go b/view-profile.go new file mode 100644 index 0000000..67c9145 --- /dev/null +++ b/view-profile.go @@ -0,0 +1,219 @@ +package main + +import ( + b64 "encoding/base64" + "fmt" + "log" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handleProfile(w http.ResponseWriter, r *http.Request) { + templateProfile := getTemplate("profile.html") + + login := checkLogin(w, r) + if login == nil { + templatePasswd := getTemplate("passwd.html") + templatePasswd.Execute(w, PasswdTplData{ + + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: false}, + }) + return + } + + data := &ProfileTplData{ + Login: NestedLoginTplData{ + Status: login, + Login: login, + }, + Common: NestedCommonTplData{ + CanAdmin: login.Common.CanAdmin, + LoggedIn: true, + ErrorMessage: "", + Success: false, + }, + } + + data.Mail = login.UserEntry.GetAttributeValue("mail") + data.DisplayName = login.UserEntry.GetAttributeValue("displayName") + data.GivenName = login.UserEntry.GetAttributeValue("givenName") + data.Surname = login.UserEntry.GetAttributeValue("sn") + // data.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) + data.Description = login.UserEntry.GetAttributeValue("description") + //data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) + + if r.Method == "POST" { + //5MB maximum size files + r.ParseMultipartForm(5 << 20) + user := User{ + DN: login.Info.DN, + // CN: , + GivenName: strings.TrimSpace(strings.Join(r.Form["given_name"], "")), + DisplayName: strings.TrimSpace(strings.Join(r.Form["display_name"], "")), + Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")), + SN: strings.TrimSpace(strings.Join(r.Form["surname"], "")), + //UID: , + Description: strings.TrimSpace(strings.Join(r.Form["description"], "")), + // Password: , + } + + if user.DisplayName != "" { + err := modify(user, config, login.conn) + if err != nil { + data.Common.ErrorMessage = "handleProfile : " + err.Error() + } else { + data.Common.Success = true + } + } + findUser, err := get(user, config, login.conn) + if err != nil { + data.Common.ErrorMessage = "handleProfile : " + err.Error() + } + data.DisplayName = findUser.DisplayName + data.GivenName = findUser.GivenName + data.Surname = findUser.SN + data.Description = findUser.Description + data.Mail = findUser.Mail + data.Common.LoggedIn = false + + /* + visible := strings.TrimSpace(strings.Join(r.Form["visibility"], "")) + if visible != "" { + visible = "on" + } else { + visible = "off" + } + data.Visibility = visible + */ + /* + profilePicture, err := uploadProfilePicture(w, r, login) + if err != nil { + data.Common.ErrorMessage = err.Error() + } + if profilePicture != "" { + data.ProfilePicture = profilePicture + } + */ + + //modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{data.Visibility}) + //modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{"on"}) + //if data.ProfilePicture != "" { + // modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture}) + // } + + // err := login.conn.Modify(modify_request) + // log.Printf(fmt.Sprintf("Profile:079: %v",modify_request)) + // log.Printf(fmt.Sprintf("Profile:079: %v",err)) + // log.Printf(fmt.Sprintf("Profile:079: %v",data)) + // if err != nil { + // data.Common.ErrorMessage = err.Error() + // } else { + // data.Common.Success = true + // } + + } + + templateProfile.Execute(w, data) +} + +func handleFoundPassword(w http.ResponseWriter, r *http.Request) { + templateFoundPasswordPage := getTemplate("passwd.html") + data := PasswdTplData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: false}, + } + code := mux.Vars(r)["code"] + // code = strings.TrimSpace(strings.Join([]string{code}, "")) + newCode, _ := b64.URLEncoding.DecodeString(code) + ldapConn, err := openNewUserLdap(config) + if err != nil { + log.Printf(fmt.Sprint("handleFoundPassword / openNewUserLdap / %v", err)) + data.Common.ErrorMessage = err.Error() + } + codeArray := strings.Split(string(newCode), ";") + user := User{ + UID: codeArray[0], + Password: codeArray[1], + DN: "uid=" + codeArray[0] + ",ou=invitations,dc=resdigita,dc=org", + } + user.SeeAlso, err = passwordFound(user, config, ldapConn) + if err != nil { + log.Printf("handleFoundPassword / passwordFound %v", err) + log.Printf("handleFoundPassword / passwordFound %v", err) + data.Common.ErrorMessage = err.Error() + } + if r.Method == "POST" { + r.ParseForm() + + password := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + + if len(password) < 8 { + data.TooShortError = true + } else if password2 != password { + data.NoMatchError = true + } else { + err := passwd(User{ + DN: user.SeeAlso, + Password: password, + }, config, ldapConn) + if err != nil { + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + } + data.Common.CanAdmin = false + templateFoundPasswordPage.Execute(w, data) +} + +func handlePasswd(w http.ResponseWriter, r *http.Request) { + templatePasswd := getTemplate("passwd.html") + data := &PasswdTplData{ + Common: NestedCommonTplData{ + CanAdmin: false, + LoggedIn: true, + ErrorMessage: "", + Success: false, + }, + } + + login := checkLogin(w, r) + if login == nil { + templatePasswd.Execute(w, data) + return + } + data.Login.Status = login + data.Common.CanAdmin = login.Common.CanAdmin + + if r.Method == "POST" { + r.ParseForm() + + password := strings.Join(r.Form["password"], "") + password2 := strings.Join(r.Form["password2"], "") + + if len(password) < 8 { + data.TooShortError = true + } else if password2 != password { + data.NoMatchError = true + } else { + err := passwd(User{ + DN: login.Info.DN, + Password: password, + }, config, login.conn) + if err != nil { + data.Common.ErrorMessage = err.Error() + } else { + data.Common.Success = true + } + } + } + data.Common.CanAdmin = false + templatePasswd.Execute(w, data) +}