This commit is contained in:
13
README.md
13
README.md
@@ -42,15 +42,20 @@ Example for generating API_SECRET and SECRETS_KEY is the following command on li
|
||||
|
||||
### LDAP specific configuration
|
||||
|
||||
Several environment variables are available to configure LDAP integration if required. If the LDAP_BIND_ADDRESS is specified, SMT will attempt to perform an LDAP search for the provided username if no matches to the locally configured users are found in the database.
|
||||
Several optional environment variables are available to configure LDAP integration if required. If these parameters are not specifed, LDAP integration will not be used.
|
||||
|
||||
If the LDAP_BIND_ADDRESS is specified, SMT will attempt to perform an LDAP search for the provided username if no matches to the locally configured users are found in the database. This search will utilise the provided credentials to perform the LDAP bind.
|
||||
|
||||
This lookup will utilise the sAMAccountName property of the user object in Active Directory. No other LDAP providers have been tested.
|
||||
|
||||
Upon successfully verifying the LDAP credentials, SMT will verify if any of the group memberships matches a role defined in the SMT database. If no match is found, the authentication will not succeed.
|
||||
|
||||
|Environment Variable Name| Description | Example | Default |
|
||||
|--|--|--|--|
|
||||
| LDAP_BIND_ADDRESS | If LDAP integration is needed, specify the LDAP Bind address. Only LDAPS on port 636 is supported. Do not specify port 636 in the bind address | dc.example.com | No default specified |
|
||||
| LDAP_BASE_DN | If LDAP integration is needed, specify the base DN to use when binding to AD | "CN=Users,DC=example,DC=com" | No default specified |
|
||||
| LDAP_TRUST_CERT_FILE | If LDAP integration is needed, specify filepath to PEM format public certificate of Certificate Authority signing LDAPS communications | caroot.pem | No default specified, must define this value |
|
||||
| LDAP_BIND_ADDRESS | Specify the LDAP Bind address. Only LDAPS on port 636 is supported. Do not specify port 636 in the bind address | dc.example.com | No default specified |
|
||||
| LDAP_BASE_DN | Specify the base DN to use when binding to AD | "CN=Users,DC=example,DC=com" | No default specified |
|
||||
| LDAP_TRUST_CERT_FILE | Specify filepath to PEM format public certificate of Certificate Authority signing LDAPS communications | caroot.pem | No default specified, must define this value |
|
||||
| LDAP_INSECURE_VALIDATION | Specify whether to skip certificate validation when connecting to LDAPS. Do not enable this in production | true | false |
|
||||
|
||||
## Systemd script
|
||||
|
||||
|
@@ -4,10 +4,12 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap"
|
||||
@@ -22,9 +24,12 @@ type LdapConfig struct {
|
||||
}
|
||||
|
||||
var systemCA *x509.CertPool
|
||||
var ldaps *ldap.Conn
|
||||
|
||||
// var ldaps *ldap.Conn
|
||||
var LdapServer string
|
||||
var CertLoaded bool
|
||||
var LdapEnabled bool
|
||||
var LdapInsecure bool = false
|
||||
var LdapBaseDn string
|
||||
var DefaultDomainSuffix string
|
||||
|
||||
@@ -118,8 +123,8 @@ func LdapSetup() bool {
|
||||
// Load LDAP certificate if necessary
|
||||
loadLdapCert()
|
||||
|
||||
ldapServer := os.Getenv("LDAP_BIND_ADDRESS")
|
||||
if ldapServer == "" {
|
||||
LdapServer = os.Getenv("LDAP_BIND_ADDRESS")
|
||||
if LdapServer == "" {
|
||||
log.Printf("VerifyLdapCreds no LDAP bind address supplied\n")
|
||||
return false
|
||||
} else {
|
||||
@@ -132,6 +137,14 @@ func LdapSetup() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
insecure := os.Getenv("LDAP_INSECURE_VALIDATION")
|
||||
if insecure != "" {
|
||||
LdapInsecure, err = strconv.ParseBool(insecure)
|
||||
if err != nil {
|
||||
log.Printf("LdapSetup could not convert environment variable LDAP_INSECURE_VALIDATION with value of '%s'\n", insecure)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up TLS to use our custom certificate authority passed in cli argument
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: systemCA,
|
||||
@@ -139,31 +152,49 @@ func LdapSetup() bool {
|
||||
}
|
||||
|
||||
// Add port if not specified in .env file
|
||||
if !(strings.HasSuffix(ldapServer, ":636")) {
|
||||
ldapServer = fmt.Sprintf("%s:636", ldapServer)
|
||||
log.Printf("VerifyLdapCreds updated ldapServer string '%s'\n", ldapServer)
|
||||
if !(strings.HasSuffix(LdapServer, ":636")) {
|
||||
LdapServer = fmt.Sprintf("%s:636", LdapServer)
|
||||
log.Printf("VerifyLdapCreds updated ldapServer string '%s'\n", LdapServer)
|
||||
}
|
||||
|
||||
// try connecting to AD via TLS and our custom certificate authority
|
||||
ldaps, err = ldap.DialTLS("tcp", ldapServer, tlsConfig)
|
||||
ldaps, err := ldap.DialTLS("tcp", LdapServer, tlsConfig)
|
||||
if err != nil {
|
||||
log.Printf("VerifyLdapCreds error connecting to LDAP bind address '%s' : '%s'\n", ldapServer, err)
|
||||
log.Printf("VerifyLdapCreds error connecting to LDAP bind address '%s' : '%s'\n", LdapServer, err)
|
||||
return false
|
||||
}
|
||||
|
||||
//defer ldaps.Close()
|
||||
|
||||
LdapEnabled = true
|
||||
|
||||
namingContext := LookupNamingContext()
|
||||
namingContext := LookupNamingContext(ldaps)
|
||||
if namingContext != "" {
|
||||
DefaultDomainSuffix = DomainSuffixFromNamingContext(namingContext)
|
||||
}
|
||||
|
||||
ldaps.Close()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func LookupNamingContext() string {
|
||||
// LdapConnect sets up the connection to LDAP to be used by other functions
|
||||
func ldapConnect() *ldap.Conn {
|
||||
|
||||
// Set up TLS to use our custom certificate authority passed in cli argument
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: systemCA,
|
||||
InsecureSkipVerify: LdapInsecure,
|
||||
}
|
||||
|
||||
ldaps, err := ldap.DialTLS("tcp", LdapServer, tlsConfig)
|
||||
if err != nil {
|
||||
log.Printf("VerifyLdapCreds error connecting to LDAP bind address '%s' : '%s'\n", LdapServer, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ldaps
|
||||
}
|
||||
|
||||
func LookupNamingContext(ldaps *ldap.Conn) string {
|
||||
// Retrieve the defaultNamingContext
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
"",
|
||||
@@ -194,10 +225,47 @@ func LookupNamingContext() string {
|
||||
return defaultNamingContext
|
||||
}
|
||||
|
||||
func GetLdapGroupMembership(username string, password string) ([]string, error) {
|
||||
var err error
|
||||
username = CheckUsername(username)
|
||||
|
||||
ldaps := ldapConnect()
|
||||
defer ldaps.Close()
|
||||
|
||||
// try an authenticated bind to AD to verify credentials
|
||||
log.Printf("Attempting LDAP bind with user '%s' and password length '%d'\n", username, len(password))
|
||||
err = ldaps.Bind(username, password)
|
||||
if err != nil {
|
||||
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
|
||||
errString := "invalid user credentials"
|
||||
log.Print(errString)
|
||||
return nil, errors.New(errString)
|
||||
} else {
|
||||
errString := fmt.Sprintf("VerifyLdapCreds error binding to LDAP with supplied credentials : '%s'\n", err)
|
||||
log.Print(errString)
|
||||
return nil, errors.New(errString)
|
||||
}
|
||||
} else {
|
||||
log.Printf("VerifyLdapCreds successfully bound to LDAP\n")
|
||||
}
|
||||
|
||||
groups, err := GetGroupsOfUser(username, LdapBaseDn, ldaps)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("VerifyLdapCreds group search error : '%s'\n", err)
|
||||
log.Print(errString)
|
||||
return nil, errors.New(errString)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func VerifyLdapCreds(username string, password string) bool {
|
||||
var err error
|
||||
username = CheckUsername(username)
|
||||
|
||||
ldaps := ldapConnect()
|
||||
|
||||
// try an authenticated bind to AD to verify credentials
|
||||
log.Printf("Attempting LDAP bind with user '%s' and password length '%d'\n", username, len(password))
|
||||
err = ldaps.Bind(username, password)
|
||||
|
@@ -36,8 +36,8 @@ const createUsers string = `
|
||||
RoleId INTEGER,
|
||||
UserName VARCHAR,
|
||||
Password VARCHAR,
|
||||
LdapUser BOOLEAN,
|
||||
LdapDN VARCHAR,
|
||||
LdapUser BOOLEAN DEFAULT 0,
|
||||
LdapDN VARCHAR DEFAULT '',
|
||||
FOREIGN KEY (RoleId) REFERENCES roles(RoleId)
|
||||
);
|
||||
`
|
||||
@@ -138,7 +138,7 @@ func CreateTables() {
|
||||
cryptText, _ := bcrypt.GenerateFromPassword([]byte(initialPassword), bcrypt.DefaultCost)
|
||||
initialPassword = string(cryptText)
|
||||
}
|
||||
if _, err = db.Exec("INSERT INTO users VALUES(1, 1, 'Administrator', ?);", initialPassword); err != nil {
|
||||
if _, err = db.Exec("INSERT INTO users (RoleId, UserName, Password, LdapUser) VALUES(1, 1, 'Administrator', ?, 0);", initialPassword); err != nil {
|
||||
log.Printf("Error adding initial admin role : '%s'", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -183,13 +183,13 @@ func CreateTables() {
|
||||
ldapUserCheck, _ := CheckColumnExists("users", "LdapUser")
|
||||
if !ldapUserCheck {
|
||||
log.Printf("CreateTables creating ldap columns in user table")
|
||||
_, err := db.Exec("ALTER TABLE users ADD COLUMN LdapUser BOOLEAN;")
|
||||
_, err := db.Exec("ALTER TABLE users ADD COLUMN LdapUser BOOLEAN DEFAULT 0;")
|
||||
if err != nil {
|
||||
log.Printf("Error altering users table to add LdapUser column : '%s'\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = db.Exec("ALTER TABLE users ADD COLUMN LdapDN VARCHAR;")
|
||||
_, err = db.Exec("ALTER TABLE users ADD COLUMN LdapDN VARCHAR DEFAULT '';")
|
||||
if err != nil {
|
||||
log.Printf("Error altering users table to add LdapDN column : '%s'\n", err)
|
||||
os.Exit(1)
|
||||
|
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"smt/utils/token"
|
||||
@@ -35,7 +36,7 @@ func (u *User) SaveUser() (*User, error) {
|
||||
_, err = GetUserByName(u.UserName)
|
||||
if err != nil && err.Error() == "user not found" {
|
||||
log.Printf("SaveUser confirmed no existing user, continuing with creation of user '%s'\n", u.UserName)
|
||||
result, err := db.NamedExec((`INSERT INTO users (RoleId, UserName, Password) VALUES (:RoleId, :UserName, :Password)`), u)
|
||||
result, err := db.NamedExec((`INSERT INTO users (RoleId, UserName, Password, LdapUser, LdapDn) VALUES (:RoleId, :UserName, :Password, :LdapUser, :LdapDN)`), u)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SaveUser error executing sql record : '%s'\n", err)
|
||||
@@ -95,11 +96,20 @@ func LoginCheck(username string, password string) (string, error) {
|
||||
if err == sql.ErrNoRows {
|
||||
// check LDAP if enabled
|
||||
if LdapEnabled {
|
||||
//check, err := LdapLoginCheck(username, password)
|
||||
check := VerifyLdapCreds(username, password)
|
||||
if check {
|
||||
u.UserId = StoreLdapUser(username)
|
||||
ldapUser, err := LdapLoginCheck(username, password)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("LoginCheck erro checking LDAP for user : '%s'\n", err)
|
||||
log.Print(errString)
|
||||
return "", errors.New(errString)
|
||||
|
||||
}
|
||||
|
||||
if ldapUser == (User{}) {
|
||||
errString := fmt.Sprintf("LoginCheck user not found in LDAP : '%s'\n", err)
|
||||
log.Print(errString)
|
||||
return "", errors.New(errString)
|
||||
}
|
||||
|
||||
} else {
|
||||
return "", errors.New("specified user not found in database")
|
||||
}
|
||||
@@ -110,9 +120,6 @@ func LoginCheck(username string, password string) (string, error) {
|
||||
log.Printf("LoginCheck retrieved user '%v' from database\n", u)
|
||||
}
|
||||
|
||||
// TODO : attempt ldap bind
|
||||
//VerifyLdapCreds(username, password)
|
||||
|
||||
err = VerifyPassword(password, u.Password)
|
||||
|
||||
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
@@ -133,6 +140,46 @@ func LoginCheck(username string, password string) (string, error) {
|
||||
|
||||
}
|
||||
|
||||
func LdapLoginCheck(username string, password string) (User, error) {
|
||||
var u User
|
||||
|
||||
// try to get LDAP group membership
|
||||
groups, err := GetLdapGroupMembership(username, password)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid user credentials" {
|
||||
return u, nil
|
||||
} else {
|
||||
return u, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compare all roles against the list of user's group membership
|
||||
roleList, err := QueryRoles()
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
|
||||
matchFound := false
|
||||
for _, role := range roleList {
|
||||
for _, group := range groups {
|
||||
if role.LdapGroup == group {
|
||||
log.Printf("Found match, user is allowed role ID '%d'\n", role.RoleId)
|
||||
matchFound = true
|
||||
break
|
||||
} else {
|
||||
log.Printf("Role '%s' with LDAP group '%s' not match user group '%s'\n", role.RoleName, role.LdapGroup, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchFound {
|
||||
// If we found a match, then store user with appropriate role ID
|
||||
u.UserId = StoreLdapUser(username)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// StoreLdapUser creates a user record in the database and returns the corresponding userId
|
||||
func StoreLdapUser(username string) int {
|
||||
|
||||
@@ -180,7 +227,7 @@ func GetUserRoleByID(uid uint) (UserRole, error) {
|
||||
|
||||
// Query database for matching user object
|
||||
log.Printf("GetUserRoleByID querying for userid '%d'\n", uid)
|
||||
err := db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", uid).StructScan(&ur)
|
||||
err := db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, users.LdapUser, users.LdapDN, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", uid).StructScan(&ur)
|
||||
if err != nil {
|
||||
log.Printf("GetUserRoleByID received error when querying database : '%s'\n", err)
|
||||
return ur, errors.New("GetUserRoleByID user not found")
|
||||
@@ -202,7 +249,7 @@ func GetUserRoleFromToken(c *gin.Context) (UserRole, error) {
|
||||
|
||||
// Query database for matching user object
|
||||
log.Printf("GetUserRoleFromToken querying for userid '%d'\n", user_id)
|
||||
err = db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", user_id).StructScan(&ur)
|
||||
err = db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, users.LdapUser, users.LdapDN, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", user_id).StructScan(&ur)
|
||||
if err != nil {
|
||||
log.Printf("GetUserRoleFromToken received error when querying database : '%s'\n", err)
|
||||
return ur, errors.New("GetUserRoleFromToken user not found")
|
||||
|
Reference in New Issue
Block a user