Commit 81e0d78d authored by Leonard Techel's avatar Leonard Techel
Browse files

Implement basic LDAP login

+ Encourage future use of i18n by doing it up from the first code commit
+ Basic template structure including flashes/…
+ Working flash errors

+ Reuse LDAP authenticator from prior experiments
+ Implement authenticator using an interface so we can easily implement
  e.g. a database authenticator later on
parent 4d54cdaf
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
[*.{js,json}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
conf/app.json
package authprovider
type User struct {
Id string
Name string
Mail string
Lang string
}
type AuthProvider interface {
Authenticate(username string, password string) (user User, err error)
Register(username string, password string) (user User, err error)
SetPassword(username string, password string) (err error)
UpdateUser(username string, data User) (err error)
}
package authprovider
import (
"fmt"
"github.com/go-ldap/ldap"
"regexp"
)
type LdapProviderConfig struct {
Host string `json:"host"`
Base string `json:"base"`
RootUser string `json:"root_user"`
RootPass string `json:"root_pass"`
Filter string `json:"filter"`
}
type LdapProvider struct {
cfg LdapProviderConfig
conn *ldap.Conn
}
func CreateLdapProvider(cfg LdapProviderConfig) (lp *LdapProvider, err error) {
lp = &LdapProvider{cfg: cfg}
return
}
func (lp *LdapProvider) connect() (conn *ldap.Conn, err error) {
conn, err = ldap.Dial("tcp", lp.cfg.Host)
return
}
// Do not forget to call connect() before!
func (lp *LdapProvider) bindRoot(conn *ldap.Conn) (err error) {
err = conn.Bind(lp.cfg.RootUser, lp.cfg.RootPass)
return
}
func (lp *LdapProvider) getUserByUID(conn *ldap.Conn, uid string) (entry *ldap.Entry, err error) {
q := ldap.NewSearchRequest(
lp.cfg.Base,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(lp.cfg.Filter, ldap.EscapeFilter(uid)),
[]string{"dn", "displayName", "mail", "preferredLanguage"},
nil,
)
result, err := conn.Search(q)
if err != nil {
return
}
if len(result.Entries) != 1 {
err = fmt.Errorf("User %s not found!", uid)
return
}
entry = result.Entries[0]
return
}
func (lp *LdapProvider) Authenticate(username string, password string) (user User, err error) {
conn, err := lp.connect()
if err != nil {
return
}
defer conn.Close()
// 1. Bind as root
err = lp.bindRoot(conn)
if err != nil {
return
}
// 2. Get DN
entry, err := lp.getUserByUID(conn, username)
if err != nil {
return
}
// 3. Try to bind as the user
err = conn.Bind(entry.DN, password)
if err != nil {
return
}
// 4. Create User data
lang := regexp.MustCompile("[a-zA-Z]+").FindString(entry.GetAttributeValue("preferredLanguage"))
user = User{
Id: username,
Name: entry.GetAttributeValue("displayName"),
Mail: entry.GetAttributeValue("mail"),
Lang: lang,
}
return
}
func (lp *LdapProvider) Register(username string, password string) (user User, err error) {
return
}
func (lp *LdapProvider) SetPassword(username string, password string) (err error) {
conn, err := lp.connect()
if err != nil {
return
}
defer conn.Close()
// 1. Bind as root
err = lp.bindRoot(conn)
if err != nil {
return
}
// 2. Get DN
entry, err := lp.getUserByUID(conn, username)
if err != nil {
return
}
// 3. Set new password
passwordModifyRequest := ldap.NewPasswordModifyRequest(entry.DN, "", password)
_, err = conn.PasswordModify(passwordModifyRequest)
if err != nil {
return
}
return
}
func (lp *LdapProvider) UpdateUser(username string, data User) (err error) {
conn, err := lp.connect()
if err != nil {
return
}
defer conn.Close()
// 1. Bind as root
err = lp.bindRoot(conn)
if err != nil {
return
}
// 2. Get DN
entry, err := lp.getUserByUID(conn, username)
if err != nil {
return
}
// 3. Set new data
modify := ldap.NewModifyRequest(entry.DN)
modify.Replace("displayName", []string{data.Name})
modify.Replace("mail", []string{data.Mail})
modify.Replace("preferredLanguage", []string{data.Lang})
err = conn.Modify(modify)
if err != nil {
return
}
return
}
{
"http_listen": ":8080",
"ldap": {
"host": "127.0.0.1:389",
"base": "dc=example,dc=org",
"filter": "(&(objectClass=organizationalPerson)(uid=%s))",
"root_user": "cn=root,dc=example,dc=org",
"root_pass": "changeme"
}
}
[user]
sign_in = Anmelden
username = Benutzername
username_example = max.muster
password = Passwort
not_authenticated = Du bist nicht angemeldet!
invalid_credentials = Benutzername und/oder Passwort sind falsch!
logout_success = Erfolgreich abgemeldet
welcome = Moin, %s
logout = Abmelden
[user]
sign_in = Sign in
username = Username
username_example = john.doe
password = Password
not_authenticated = Not logged in!
invalid_credentials = Wrong username/password combination!
logout_success = Successfully logged out.
welcome = Heya, %s
logout = Logout
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
)
var (
configPath = flag.String("c", "conf/app.json", "Config file path")
)
func parseConfig(filePath string) (cfg UCPConfig, err error) {
file, err := ioutil.ReadFile(filePath)
if err != nil {
return
}
err = json.Unmarshal(file, &cfg)
if err != nil {
return
}
return
}
func main() {
flag.Parse()
cfg, err := parseConfig(*configPath)
if err != nil {
log.Fatal(err)
}
CreateUCP(cfg)
}
{{define "flashes"}}
{{if .Flash.SuccessMsg}}
<div class="alert alert-success">{{.Flash.SuccessMsg}}</div>
{{end}}
{{if .Flash.ErrorMsg}}
<div class="alert alert-danger">{{.Flash.ErrorMsg}}</div>
{{end}}
{{if .Flash.InfoMsg}}
<div class="alert alert-info">{{.Flash.InfoMsg}}</div>
{{end}}
{{if .Flash.WarningMsg}}
<div class="alert alert-warning">{{.Flash.WarningMsg}}</div>
{{end}}
{{end}}
{{define "footer"}}
<footer>
<a href="?lang={{.Lang}}" class="btn btn-link btn-xs"><strong>{{.LangName}}</strong></a>
{{range .RestLangs}}
<a href="?lang={{.Lang}}" class="btn btn-link btn-xs">{{.Name}}</a>
{{end}}
</footer>
</div>
</body>
</html>
{{end}}
{{define "header"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>UCP</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
{{end}}
{{template "header" .}}
<div class="page-header">
<h1>{{.i18n.Tr "user.welcome" .User.Name}}</h1>
</div>
<div class="row">
<div class="col-sm-4 col-sm-push-4">
{{template "flashes" .}}
<div class="well well-xs">
<form action="/user/logout" method="post">
<input type="hidden" name="_csrf" value="{{.csrf_token}}" />
<button type="submit" class="btn btn-default">{{.i18n.Tr "user.logout"}}</button>
</form>
</div>
</div>
</div>
{{template "footer" .}}
{{template "header" .}}
<div class="page-header">
<h1>{{.i18n.Tr "user.sign_in"}}</h1>
</div>
<div class="row">
<div class="col-sm-4 col-sm-push-4">
<div class="well well-xs">
{{template "flashes" .}}
<form action="/user/login" method="post">
<div class="form-group">
<label for="username">{{.i18n.Tr "user.username"}}</label>
<input type="text" name="username" id="username" placeholder="{{.i18n.Tr "user.username_example"}}" class="form-control" autofocus />
</div>
<div class="form-group">
<label for="password">{{.i18n.Tr "user.password"}}</label>
<input type="password" name="password" id="password" placeholder="{{.i18n.Tr "user.password"}}" class="form-control" />
</div>
<input type="hidden" name="_csrf" value="{{.csrf_token}}" />
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-log-in"></span> {{.i18n.Tr "user.sign_in"}}</button>
</form>
</div>
</div>
</div>
{{template "footer" .}}
package main
import (
"github.com/go-macaron/binding"
"github.com/go-macaron/csrf"
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"gopkg.in/macaron.v1"
"net/http"
"github.com/barnslig/ucp/authprovider"
)
type UCPConfig struct {
HttpListen string `json:"http_listen"`
LDAP authprovider.LdapProviderConfig `json:"ldap"`
}
type UCP struct {
cfg UCPConfig
app *macaron.Macaron
}
func CreateUCP(cfg UCPConfig) (ucp *UCP) {
m := macaron.Classic()
ucp = &UCP{
cfg: cfg,
app: m,
}
m.Use(i18n.I18n(i18n.Options{
Langs: []string{"en-US", "de-DE"},
Names: []string{"English", "Deutsch"},
}))
m.Use(macaron.Renderer())
m.Use(session.Sessioner())
m.Use(csrf.Csrfer())
m.Use(func(ctx *macaron.Context, x csrf.CSRF) {
ctx.Data["csrf_token"] = x.GetToken()
})
m.Use(func(ctx *macaron.Context) {
auth, err := authprovider.CreateLdapProvider(ucp.cfg.LDAP)
if err != nil {
panic(err)
}
ctx.MapTo(auth, (*authprovider.AuthProvider)(nil))
})
m.Group("/user", func() {
m.Get("/", UserGet)
m.Combo("/login").
Get(UserGetLogin).
Post(csrf.Validate, binding.BindIgnErr(UserLoginForm{}), UserPostLogin)
m.Post("/logout", csrf.Validate, UserPostLogout)
})
panic(http.ListenAndServe(ucp.cfg.HttpListen, m))
return
}
package main
import (
"github.com/go-macaron/binding"
"github.com/go-macaron/session"
"gopkg.in/macaron.v1"
"github.com/barnslig/ucp/authprovider"
)
type UserLoginForm struct {
Username string `binding:"Required"`
Password string `binding:"Required"`
}
func UserGet(ctx *macaron.Context, sess session.Store, f *session.Flash) {
if sess.Get("user") == nil {
f.Error(ctx.Tr("user.not_authenticated"))
ctx.Redirect("/user/login")
return
}
ctx.Data["User"] = sess.Get("user").(authprovider.User)
ctx.HTML(200, "user/index")
}
func UserGetLogin(ctx *macaron.Context, sess session.Store) {
if sess.Get("user") != nil {
ctx.Redirect("/user")
return
}
ctx.HTML(200, "user/login")
}
func UserPostLogin(ctx *macaron.Context, sess session.Store, f *session.Flash, errs binding.Errors, postData UserLoginForm, auth authprovider.AuthProvider) {
if len(errs) > 0 {
f.Error(ctx.Tr("user.invalid_credentials"))
ctx.Redirect("/user/login")
return
}
user, err := auth.Authenticate(postData.Username, postData.Password)
if err != nil {
f.Error(ctx.Tr("user.invalid_credentials"))
ctx.Redirect("/user/login")
return
}
sess.Set("user", user)
ctx.Redirect("/user")
}
func UserPostLogout(ctx *macaron.Context, sess session.Store, f *session.Flash) {
sess.Set("user", nil)
f.Success(ctx.Tr("user.logout_success"))
ctx.Redirect("/user/login")
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment