Skip to content

Commit

Permalink
Merge pull request #310 from strangeman/ldap-auth
Browse files Browse the repository at this point in the history
Simple authorization via LDAP
  • Loading branch information
Echobob committed Apr 17, 2017
2 parents e484b8e + 68f5aec commit 7ea40d7
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 19 deletions.
145 changes: 129 additions & 16 deletions api/login.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,106 @@
package api

import (
"crypto/tls"
"database/sql"
"fmt"
"net/http"
"net/mail"
"strings"
"time"

log "github.com/Sirupsen/logrus"
database "github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/models"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gin-gonic/gin"
sq "github.com/masterminds/squirrel"
"golang.org/x/crypto/bcrypt"
"gopkg.in/ldap.v2"
)

func ldapAuthentication(auth, password string) (error, models.User) {

if util.Config.LdapEnable != true {
return fmt.Errorf("LDAP not configured"), models.User{}
}

bindusername := util.Config.LdapBindDN
bindpassword := util.Config.LdapBindPassword

l, err := ldap.Dial("tcp", util.Config.LdapServer)
if err != nil {
return err, models.User{}
}
defer l.Close()

// Reconnect with TLS if needed
if util.Config.LdapNeedTLS == true {
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return err, models.User{}
}
}

// First bind with a read only user
err = l.Bind(bindusername, bindpassword)
if err != nil {
return err, models.User{}
}

// Search for the given username
searchRequest := ldap.NewSearchRequest(
util.Config.LdapSearchDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(util.Config.LdapSearchFilter, auth),
[]string{util.Config.LdapMappings.DN},
nil,
)

sr, err := l.Search(searchRequest)
if err != nil {
return err, models.User{}
}

if len(sr.Entries) != 1 {
return fmt.Errorf("User does not exist or too many entries returned"), models.User{}
}

// Bind as the user to verify their password
userdn := sr.Entries[0].DN
err = l.Bind(userdn, password)
if err != nil {
return err, models.User{}
}

// Get user info and ensure authentication in case LDAP supports unauthenticated bind
searchRequest = ldap.NewSearchRequest(
util.Config.LdapSearchDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(util.Config.LdapSearchFilter, auth),
[]string{util.Config.LdapMappings.DN, util.Config.LdapMappings.Mail, util.Config.LdapMappings.Uid, util.Config.LdapMappings.CN},
nil,
)

sr, err = l.Search(searchRequest)
if err != nil {
return err, models.User{}
}

ldapUser := models.User{
Username: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.Uid),
Created: time.Now(),
Name: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.CN),
Email: sr.Entries[0].GetAttributeValue(util.Config.LdapMappings.Mail),
External: true,
Alert: false,
}

log.Info("User " + ldapUser.Name + " with email " + ldapUser.Email + " authorized via LDAP correctly")
return nil, ldapUser

}

func login(c *gin.Context) {
var login struct {
Auth string `json:"auth" binding:"required"`
Expand All @@ -27,31 +113,58 @@ func login(c *gin.Context) {

login.Auth = strings.ToLower(login.Auth)

q := sq.Select("*").
From("user")
ldapErr, ldapUser := ldapAuthentication(login.Auth, login.Password)

_, err := mail.ParseAddress(login.Auth)
if err == nil {
q = q.Where("email=?", login.Auth)
} else {
q = q.Where("username=?", login.Auth)
if util.Config.LdapEnable == true && ldapErr != nil {
log.Info(ldapErr.Error())
}

query, args, _ := q.ToSql()
q := sq.Select("*").
From("user")

var user models.User
if err := database.Mysql.SelectOne(&user, query, args...); err != nil {
if err == sql.ErrNoRows {
if ldapErr != nil {
// Perform normal authorization
_, err := mail.ParseAddress(login.Auth)
if err == nil {
q = q.Where("email=?", login.Auth)
} else {
q = q.Where("username=?", login.Auth)
}

query, args, _ := q.ToSql()

if err := database.Mysql.SelectOne(&user, query, args...); err != nil {
if err == sql.ErrNoRows {
c.AbortWithStatus(400)
return
}

panic(err)
}

if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(login.Password)); err != nil {
c.AbortWithStatus(400)
return
}
} else {
// Check if that user already exist in database
q = q.Where("username=? and external=true", ldapUser.Username)

query, args, _ := q.ToSql()

if err := database.Mysql.SelectOne(&user, query, args...); err != nil {
if err == sql.ErrNoRows {
//Create new user
user = ldapUser
if err := database.Mysql.Insert(&user); err != nil {
panic(err)
}
} else if err != nil {
panic(err)
}

panic(err)
}

if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(login.Password)); err != nil {
c.AbortWithStatus(400)
return
}
}

session := models.Session{
Expand Down
13 changes: 13 additions & 0 deletions api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"time"

log "github.com/Sirupsen/logrus"
database "github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/models"
"github.com/ansible-semaphore/semaphore/util"
Expand Down Expand Up @@ -63,6 +64,12 @@ func updateUser(c *gin.Context) {
return
}

if oldUser.External == true && oldUser.Username != user.Username {
log.Warn("Username is not editable for external LDAP users")
c.AbortWithStatus(400)
return
}

if _, err := database.Mysql.Exec("update user set name=?, username=?, email=?, alert=? where id=?", user.Name, user.Username, user.Email, user.Alert, oldUser.ID); err != nil {
panic(err)
}
Expand All @@ -76,6 +83,12 @@ func updateUserPassword(c *gin.Context) {
Pwd string `json:"password"`
}

if user.External == true {
log.Warn("Password is not editable for external LDAP users")
c.AbortWithStatus(400)
return
}

if err := c.Bind(&pwd); err != nil {
return
}
Expand Down
4 changes: 3 additions & 1 deletion db/migrations/v2.3.0.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
ALTER TABLE user ADD alert BOOLEAN NOT NULL AFTER password;
ALTER TABLE project ADD alert BOOLEAN NOT NULL AFTER name;
ALTER TABLE project ADD alert BOOLEAN NOT NULL AFTER name;

ALTER TABLE user ADD external BOOLEAN NOT NULL AFTER password;
1 change: 1 addition & 0 deletions models/User.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type User struct {
Name string `db:"name" json:"name" binding:"required"`
Email string `db:"email" json:"email" binding:"required"`
Password string `db:"password" json:"-"`
External bool `db:"external" json:"external"`
Alert bool `db:"alert" json:"alert"`
}

Expand Down
6 changes: 4 additions & 2 deletions public/html/users/user.pug
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
.col-sm-8: input.form-control(type="text" placeholder="Your name" ng-model="user.name")
.form-group
label.control-label.col-sm-4 Username
.col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username")
.col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username" ng-if="user.external==false")
.col-sm-8: input.form-control(type="text" placeholder="Username" ng-model="user.username" readonly="readonly" ng-if="user.external==true")
.form-group
label.control-label.col-sm-4 Email
.col-sm-8: input.form-control(type="email" placeholder="Email address" ng-model="user.email")
.form-group
label.control-label.col-sm-4 Password
.col-sm-8: input.form-control(type="password" placeholder="Enter new password" ng-model="user.password")
.col-sm-8: input.form-control(type="password" placeholder="Not editable for LDAP user" readonly="readonly" ng-model="user.password" ng-if="user.external==true")
.col-sm-8: input.form-control(type="password" placeholder="Enter new password" ng-model="user.password" ng-if="user.external==false")
.form-group
label.control-label.col-sm-4 Send alerts
.col-sm-8: input.checkbox-inline(type="checkbox" title="Send email alerts about failed tasks" ng-model="user.alert")
Expand Down
100 changes: 100 additions & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ type mySQLConfig struct {
DbName string `json:"name"`
}

type ldapMappings struct {
DN string `json:"dn"`
Mail string `json:"mail"`
Uid string `json:"uid"`
CN string `json:"cn"`
}

type configType struct {
MySQL mySQLConfig `json:"mysql"`
// Format `:port_num` eg, :3000
Expand All @@ -48,6 +55,16 @@ type configType struct {
//web host
WebHost string `json:"web_host"`

//ldap settings
LdapEnable bool `json:"ldap_enable"`
LdapBindDN string `json:"ldap_binddn"`
LdapBindPassword string `json:"ldap_bindpassword"`
LdapServer string `json:"ldap_server"`
LdapNeedTLS bool `json:"ldap_needtls"`
LdapSearchDN string `json:"ldap_searchdn"`
LdapSearchFilter string `json:"ldap_searchfilter"`
LdapMappings ldapMappings `json:"ldap_mappings"`

//telegram alerting
TelegramAlert bool `json:"telegram_alert"`
TelegramChat string `json:"telegram_chat"`
Expand Down Expand Up @@ -261,4 +278,87 @@ func (conf *configType) Scan() {
conf.TelegramAlert = false
}

var LdapAnswer string
fmt.Print(" > Enable LDAP authentication (y/n, default n): ")
fmt.Scanln(&LdapAnswer)
if LdapAnswer == "yes" || LdapAnswer == "y" {

conf.LdapEnable = true

fmt.Print(" > LDAP server host (default localhost:389): ")
fmt.Scanln(&conf.LdapServer)

if len(conf.LdapServer) == 0 {
conf.LdapServer = "localhost:389"
}

var LdapTLSAnswer string
fmt.Print(" > Enable LDAP TLS connection (y/n, default n): ")
fmt.Scanln(&LdapTLSAnswer)
if LdapTLSAnswer == "yes" || LdapTLSAnswer == "y" {
conf.LdapNeedTLS = true
} else {
conf.LdapNeedTLS = false
}

fmt.Print(" > LDAP DN for bind (default cn=user,ou=users,dc=example): ")
fmt.Scanln(&conf.LdapBindDN)

if len(conf.LdapBindDN) == 0 {
conf.LdapBindDN = "cn=user,ou=users,dc=example"
}

fmt.Print(" > Password for LDAP bind user (default pa55w0rd): ")
fmt.Scanln(&conf.LdapBindPassword)

if len(conf.LdapBindPassword) == 0 {
conf.LdapBindPassword = "pa55w0rd"
}

fmt.Print(" > LDAP DN for user search (default ou=users,dc=example): ")
fmt.Scanln(&conf.LdapSearchDN)

if len(conf.LdapSearchDN) == 0 {
conf.LdapSearchDN = "ou=users,dc=example"
}

fmt.Print(" > LDAP search filter (default (uid=" + "%" + "s)): ")
fmt.Scanln(&conf.LdapSearchFilter)

if len(conf.LdapSearchFilter) == 0 {
conf.LdapSearchFilter = "(uid=%s)"
}

fmt.Print(" > LDAP mapping for DN field (default dn): ")
fmt.Scanln(&conf.LdapMappings.DN)

if len(conf.LdapMappings.DN) == 0 {
conf.LdapMappings.DN = "dn"
}

fmt.Print(" > LDAP mapping for username field (default uid): ")
fmt.Scanln(&conf.LdapMappings.Uid)

if len(conf.LdapMappings.Uid) == 0 {
conf.LdapMappings.Uid = "uid"
}

fmt.Print(" > LDAP mapping for full name field (default cn): ")
fmt.Scanln(&conf.LdapMappings.CN)

if len(conf.LdapMappings.CN) == 0 {
conf.LdapMappings.CN = "cn"
}

fmt.Print(" > LDAP mapping for email field (default mail): ")
fmt.Scanln(&conf.LdapMappings.Mail)

if len(conf.LdapMappings.Mail) == 0 {
conf.LdapMappings.Mail = "mail"
}

} else {
conf.LdapEnable = false
}

}

0 comments on commit 7ea40d7

Please sign in to comment.