progress on ldap
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-01-05 09:48:48 +11:00
parent fa4f896093
commit cb7376eeeb
5 changed files with 152 additions and 31 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -7,5 +7,6 @@ BIND_PORT=8443
LDAP_BIND_ADDRESS=adcp12.cdc.home
LDAP_BASE_DN=CN=Users,DC=cdc,DC=home
LDAP_TRUST_CERT_FILE=
LDAP_INSECURE_VALIDATION=true
TLS_KEY_FILE=key.pem
TLS_CERT_FILE=cert.pem