2020-01-19 11:49:49 +00:00
package main
import (
2020-02-01 10:32:50 +00:00
"crypto/rand"
2020-01-26 18:27:17 +00:00
"crypto/tls"
2020-01-19 12:00:53 +00:00
"encoding/base64"
"encoding/json"
2020-01-26 18:27:17 +00:00
"flag"
2020-01-19 11:49:49 +00:00
"fmt"
2020-01-26 18:27:17 +00:00
"io/ioutil"
2020-01-19 11:49:49 +00:00
"os"
"os/signal"
2020-01-19 12:00:53 +00:00
"syscall"
2020-01-19 11:49:49 +00:00
2020-02-02 13:32:37 +00:00
ldap "bottin/ldapserver"
2020-01-19 11:49:49 +00:00
consul "github.com/hashicorp/consul/api"
2021-07-07 16:26:02 +00:00
message "bottin/goldap"
2020-02-01 14:05:44 +00:00
log "github.com/sirupsen/logrus"
2020-01-19 11:49:49 +00:00
)
2020-02-15 11:04:06 +00:00
// System managed attributes (cannot be changed by user, see checkRestrictedAttr)
2020-01-26 20:22:51 +00:00
const ATTR_MEMBEROF = "memberof"
const ATTR_ENTRYUUID = "entryuuid"
const ATTR_CREATORSNAME = "creatorsname"
const ATTR_CREATETIMESTAMP = "createtimestamp"
const ATTR_MODIFIERSNAME = "modifiersname"
const ATTR_MODIFYTIMESTAMP = "modifytimestamp"
2020-02-15 11:04:06 +00:00
// Attributes that we are interested in at various points
2020-02-14 20:23:01 +00:00
const ATTR_OBJECTCLASS = "objectclass"
2020-02-15 11:04:06 +00:00
const ATTR_MEMBER = "member"
const ATTR_USERPASSWORD = "userpassword"
2020-01-26 20:22:51 +00:00
2020-01-26 18:27:17 +00:00
type ConfigFile struct {
2021-03-09 17:24:30 +00:00
Suffix string ` json:"suffix" `
Bind string ` json:"bind" `
BindSecure string ` json:"bind_secure" `
LogLevel string ` json:"log_level" `
ConsulHost string ` json:"consul_host" `
ConsulConsistent bool ` json:"consul_force_consistency" `
Acl [ ] string ` json:"acl" `
TLSCertFile string ` json:"tls_cert_file" `
TLSKeyFile string ` json:"tls_key_file" `
TLSServerName string ` json:"tls_server_name" `
2020-01-26 18:27:17 +00:00
}
2020-01-19 11:49:49 +00:00
type Config struct {
2020-01-27 15:32:39 +00:00
Suffix string
Bind string
BindSecure string
2020-02-01 14:05:44 +00:00
LogLevel log . Level
2020-01-26 18:27:17 +00:00
2021-03-09 17:24:30 +00:00
ConsulHost string
ConsulConsistent bool
2020-01-26 18:27:17 +00:00
Acl ACL
2020-01-27 15:32:39 +00:00
TLSConfig * tls . Config
2020-01-19 11:49:49 +00:00
}
type Server struct {
2020-01-26 18:47:38 +00:00
logger * log . Logger
2020-01-19 11:49:49 +00:00
config Config
2021-03-09 17:24:30 +00:00
kv * consul . KV
readOpts consul . QueryOptions
2020-01-19 12:00:53 +00:00
}
type State struct {
2020-01-26 17:42:04 +00:00
login Login
2020-01-19 11:49:49 +00:00
}
2020-01-26 18:27:17 +00:00
var configFlag = flag . String ( "config" , "./config.json" , "Configuration file path" )
2020-02-13 13:41:33 +00:00
var resyncFlag = flag . Bool ( "resync" , false , "Check and re-synchronize memberOf values before launch" )
2020-01-19 11:49:49 +00:00
2020-02-01 14:05:44 +00:00
func readConfig ( logger * log . Logger ) Config {
2020-01-26 18:27:17 +00:00
config_file := ConfigFile {
2020-01-27 15:32:39 +00:00
Bind : "0.0.0.0:389" ,
BindSecure : "0.0.0.0:636" ,
2020-01-26 18:27:17 +00:00
}
bytes , err := ioutil . ReadFile ( * configFlag )
2020-01-19 11:49:49 +00:00
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-19 11:49:49 +00:00
}
2020-01-26 18:27:17 +00:00
err = json . Unmarshal ( bytes , & config_file )
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-26 17:42:04 +00:00
}
2020-01-26 18:27:17 +00:00
acl , err := ParseACL ( config_file . Acl )
2020-01-26 17:42:04 +00:00
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
}
log_level := log . InfoLevel
if config_file . LogLevel != "" {
log_level , err = log . ParseLevel ( config_file . LogLevel )
if err != nil {
logger . Fatal ( err )
}
2020-01-26 17:42:04 +00:00
}
2020-01-26 18:27:17 +00:00
ret := Config {
2020-01-27 15:32:39 +00:00
Suffix : config_file . Suffix ,
Bind : config_file . Bind ,
BindSecure : config_file . BindSecure ,
2020-02-01 14:05:44 +00:00
LogLevel : log_level ,
2021-03-09 17:24:30 +00:00
ConsulHost : config_file . ConsulHost ,
ConsulConsistent : config_file . ConsulConsistent ,
Acl : acl ,
2020-01-26 18:27:17 +00:00
}
2020-01-27 15:08:35 +00:00
if config_file . TLSCertFile != "" && config_file . TLSKeyFile != "" && config_file . TLSServerName != "" {
cert_txt , err := ioutil . ReadFile ( config_file . TLSCertFile )
2020-01-26 18:27:17 +00:00
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-26 18:27:17 +00:00
}
2020-01-27 15:08:35 +00:00
key_txt , err := ioutil . ReadFile ( config_file . TLSKeyFile )
2020-01-26 18:27:17 +00:00
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-26 18:27:17 +00:00
}
cert , err := tls . X509KeyPair ( cert_txt , key_txt )
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-26 18:27:17 +00:00
}
2020-01-27 15:08:35 +00:00
ret . TLSConfig = & tls . Config {
MinVersion : tls . VersionTLS10 ,
2020-01-26 18:27:17 +00:00
MaxVersion : tls . VersionTLS12 ,
Certificates : [ ] tls . Certificate { cert } ,
2020-01-27 15:08:35 +00:00
ServerName : config_file . TLSServerName ,
2020-01-26 18:27:17 +00:00
}
}
return ret
}
func main ( ) {
2020-01-26 20:03:18 +00:00
flag . Parse ( )
2020-02-01 14:05:44 +00:00
logger := log . New ( )
logger . SetOutput ( os . Stdout )
logger . SetFormatter ( & log . TextFormatter { } )
config := readConfig ( logger )
if log_level := os . Getenv ( "BOTTIN_LOG_LEVEL" ) ; log_level != "" {
level , err := log . ParseLevel ( log_level )
if err != nil {
logger . Fatal ( err )
}
logger . SetLevel ( level )
} else {
logger . SetLevel ( config . LogLevel )
}
2020-01-26 18:27:17 +00:00
2020-02-01 14:05:44 +00:00
ldap . Logger = logger
2020-01-26 18:27:17 +00:00
// Connect to Consul
consul_config := consul . DefaultConfig ( )
if config . ConsulHost != "" {
consul_config . Address = config . ConsulHost
}
consul_client , err := consul . NewClient ( consul_config )
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-19 11:49:49 +00:00
}
2021-03-09 17:24:30 +00:00
2020-01-26 18:27:17 +00:00
kv := consul_client . KV ( )
2021-03-09 17:24:30 +00:00
readOpts := consul . QueryOptions { }
if config . ConsulConsistent {
logger . Info ( "Using consistent reads on Consul database, this may lead to performance degradation. Set \"consul_force_consistency\": false in your config file if you have performance issues." )
readOpts . RequireConsistent = true
} else {
readOpts . AllowStale = true
}
2020-01-19 11:49:49 +00:00
2020-01-31 21:15:40 +00:00
// Create bottin server
bottin := Server {
2021-03-09 17:24:30 +00:00
logger : logger ,
config : config ,
kv : kv ,
readOpts : readOpts ,
2020-01-26 18:47:38 +00:00
}
2020-01-31 21:15:40 +00:00
err = bottin . init ( )
2020-01-19 11:49:49 +00:00
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-19 11:49:49 +00:00
}
2020-02-13 13:41:33 +00:00
if * resyncFlag {
err = bottin . memberOfResync ( )
if err != nil {
logger . Fatal ( err )
}
}
2020-01-27 15:32:39 +00:00
// Create routes
2020-01-19 11:49:49 +00:00
routes := ldap . NewRouteMux ( )
2020-01-27 15:08:35 +00:00
2020-01-31 21:15:40 +00:00
routes . Bind ( bottin . handleBind )
routes . Search ( bottin . handleSearch )
routes . Add ( bottin . handleAdd )
routes . Compare ( bottin . handleCompare )
routes . Delete ( bottin . handleDelete )
routes . Modify ( bottin . handleModify )
2020-01-27 15:08:35 +00:00
2020-01-27 15:32:39 +00:00
if config . TLSConfig != nil {
2020-01-31 21:15:40 +00:00
routes . Extended ( bottin . handleStartTLS ) .
2020-01-27 15:08:35 +00:00
RequestName ( ldap . NoticeOfStartTLS ) . Label ( "StartTLS" )
}
2020-01-27 15:32:39 +00:00
// Create LDAP servers
var ldapServer , ldapServerSecure * ldap . Server = nil , nil
// Bind on standard LDAP port without TLS
if config . Bind != "" {
ldapServer = ldap . NewServer ( )
ldapServer . Handle ( routes )
2020-01-31 21:15:40 +00:00
ldapServer . NewUserState = bottin . newUserState
2020-01-27 15:32:39 +00:00
go func ( ) {
err := ldapServer . ListenAndServe ( config . Bind )
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-27 15:32:39 +00:00
}
} ( )
}
// Bind on LDAP secure port with TLS
if config . BindSecure != "" {
if config . TLSConfig != nil {
ldapServerSecure := ldap . NewServer ( )
ldapServerSecure . Handle ( routes )
2020-01-31 21:15:40 +00:00
ldapServerSecure . NewUserState = bottin . newUserState
2020-01-27 15:32:39 +00:00
secureConn := func ( s * ldap . Server ) {
s . Listener = tls . NewListener ( s . Listener , config . TLSConfig )
}
go func ( ) {
err := ldapServerSecure . ListenAndServe ( config . BindSecure , secureConn )
if err != nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( err )
2020-01-27 15:32:39 +00:00
}
} ( )
} else {
2020-02-01 14:05:44 +00:00
logger . Warnf ( "Warning: no valid TLS configuration was provided, not binding on %s" , config . BindSecure )
2020-01-27 15:32:39 +00:00
}
}
2020-01-19 11:49:49 +00:00
2020-01-27 15:32:39 +00:00
if ldapServer == nil && ldapServerSecure == nil {
2020-02-01 14:05:44 +00:00
logger . Fatal ( "Not doing anything." )
2020-01-27 15:32:39 +00:00
}
2020-01-27 15:08:35 +00:00
2020-01-27 15:32:39 +00:00
// When CTRL+C, SIGINT and SIGTERM signal occurs
// Then stop server gracefully
ch := make ( chan os . Signal )
signal . Notify ( ch , syscall . SIGINT , syscall . SIGTERM )
<- ch
close ( ch )
if ldapServer != nil {
ldapServer . Stop ( )
2020-01-27 15:08:35 +00:00
}
2020-01-27 15:32:39 +00:00
if ldapServerSecure != nil {
ldapServerSecure . Stop ( )
}
}
func ( server * Server ) newUserState ( ) ldap . UserState {
return & State {
login : Login {
user : "ANONYMOUS" ,
groups : [ ] string { } ,
} ,
2020-01-26 18:27:17 +00:00
}
2020-01-19 11:49:49 +00:00
}
func ( server * Server ) init ( ) error {
2020-02-15 11:04:06 +00:00
// Check that suffix is in canonical format in config file
suffix_canonical , err := server . checkDN ( server . config . Suffix , false )
2020-01-19 21:27:54 +00:00
if err != nil {
return err
}
2020-02-15 11:04:06 +00:00
if suffix_canonical != server . config . Suffix {
return fmt . Errorf ( "Please write suffix in canonical format: %s" , suffix_canonical )
}
2020-01-19 21:27:54 +00:00
2020-02-15 11:04:06 +00:00
// Check that root object exists.
// If it does, we're done. Otherwise, we have some initialization to do.
exists , err := server . objectExists ( server . config . Suffix )
2020-01-19 11:49:49 +00:00
if err != nil {
return err
}
2020-02-15 11:04:06 +00:00
if exists {
2020-01-19 11:49:49 +00:00
return nil
}
2020-02-15 11:04:06 +00:00
// We have to initialize the server.
// Create a root object and an admin object.
2020-01-19 16:55:25 +00:00
base_attributes := Entry {
2020-02-14 20:26:43 +00:00
ATTR_OBJECTCLASS : [ ] string { "top" , "dcObject" , "organization" } ,
"structuralobjectclass" : [ ] string { "organization" } ,
2020-01-26 22:12:00 +00:00
ATTR_CREATORSNAME : [ ] string { server . config . Suffix } ,
ATTR_CREATETIMESTAMP : [ ] string { genTimestamp ( ) } ,
ATTR_ENTRYUUID : [ ] string { genUuid ( ) } ,
2020-01-19 11:49:49 +00:00
}
suffix_dn , err := parseDN ( server . config . Suffix )
if err != nil {
return err
}
2020-01-19 17:24:21 +00:00
base_attributes [ suffix_dn [ 0 ] . Type ] = [ ] string { suffix_dn [ 0 ] . Value }
2020-01-19 11:49:49 +00:00
2020-11-13 11:55:32 +00:00
err = server . putAttributes ( server . config . Suffix , base_attributes )
2020-01-19 11:49:49 +00:00
if err != nil {
return err
}
2021-07-09 13:24:33 +00:00
admin_pass_str , environnement_variable_exist := os . LookupEnv ( "BOTTIN_DEFAULT_ADMIN_PW" )
if ! environnement_variable_exist {
admin_pass := make ( [ ] byte , 8 )
_ , err = rand . Read ( admin_pass )
if err != nil {
return err
}
admin_pass_str = base64 . RawURLEncoding . EncodeToString ( admin_pass )
} else {
server . logger . Printf ( "It seems that exists a password in environnement variable" )
2020-01-27 16:01:32 +00:00
}
2021-07-09 13:24:33 +00:00
2020-01-19 11:49:49 +00:00
admin_pass_hash := SSHAEncode ( [ ] byte ( admin_pass_str ) )
admin_dn := "cn=admin," + server . config . Suffix
2020-01-19 16:55:25 +00:00
admin_attributes := Entry {
2020-02-14 20:26:43 +00:00
ATTR_OBJECTCLASS : [ ] string { "simpleSecurityObject" , "organizationalRole" } ,
2020-02-10 10:37:39 +00:00
"displayname" : [ ] string { "LDAP administrator" } ,
"description" : [ ] string { "Administrator account automatically created by Bottin" } ,
2020-01-19 17:24:21 +00:00
"cn" : [ ] string { "admin" } ,
2020-02-14 20:26:43 +00:00
"structuralobjectclass" : [ ] string { "organizationalRole" } ,
2020-01-26 22:12:00 +00:00
ATTR_USERPASSWORD : [ ] string { admin_pass_hash } ,
ATTR_CREATORSNAME : [ ] string { server . config . Suffix } ,
ATTR_CREATETIMESTAMP : [ ] string { genTimestamp ( ) } ,
ATTR_ENTRYUUID : [ ] string { genUuid ( ) } ,
2020-01-19 11:49:49 +00:00
}
2020-11-13 11:55:32 +00:00
err = server . putAttributes ( admin_dn , admin_attributes )
2020-01-19 11:49:49 +00:00
if err != nil {
return err
}
2020-01-26 18:47:38 +00:00
server . logger . Printf (
2020-01-27 16:01:32 +00:00
"It seems to be a new installation, we created a default user for you:\n\n dn: %s\n password: %s\n\nWe recommend replacing it as soon as possible." ,
2020-01-19 11:49:49 +00:00
admin_dn ,
admin_pass_str ,
)
return nil
}
2020-02-02 12:51:47 +00:00
func ( server * Server ) checkDN ( dn string , allow_extend bool ) ( string , error ) {
2020-02-15 11:04:06 +00:00
// 1. Canonicalize: remove spaces between things and put all in lower case
2020-02-02 12:51:47 +00:00
dn , err := canonicalDN ( dn )
if err != nil {
return "" , err
}
// 2. Check suffix (add it if allow_extend is set)
2020-01-19 18:19:34 +00:00
suffix := server . config . Suffix
if len ( dn ) < len ( suffix ) {
2020-01-27 15:39:08 +00:00
if dn != suffix [ len ( suffix ) - len ( dn ) : ] || ! allow_extend {
2020-01-19 18:19:34 +00:00
return suffix , fmt . Errorf (
"Only handling stuff under DN %s" , suffix )
}
return suffix , nil
} else {
if dn [ len ( dn ) - len ( suffix ) : ] != suffix {
return suffix , fmt . Errorf (
"Only handling stuff under DN %s" , suffix )
}
return dn , nil
}
}
2020-01-27 15:08:35 +00:00
func ( server * Server ) handleStartTLS ( s ldap . UserState , w ldap . ResponseWriter , m * ldap . Message ) {
tlsConn := tls . Server ( m . Client . GetConn ( ) , server . config . TLSConfig )
res := ldap . NewExtendedResponse ( ldap . LDAPResultSuccess )
res . SetResponseName ( ldap . NoticeOfStartTLS )
w . Write ( res )
if err := tlsConn . Handshake ( ) ; err != nil {
2020-02-01 14:05:44 +00:00
server . logger . Printf ( "StartTLS Handshake error %v" , err )
2020-01-27 15:08:35 +00:00
res . SetDiagnosticMessage ( fmt . Sprintf ( "StartTLS Handshake error : \"%s\"" , err . Error ( ) ) )
res . SetResultCode ( ldap . LDAPResultOperationsError )
w . Write ( res )
return
}
m . Client . SetConn ( tlsConn )
}
2020-01-19 12:00:53 +00:00
func ( server * Server ) handleBind ( s ldap . UserState , w ldap . ResponseWriter , m * ldap . Message ) {
state := s . ( * State )
2020-01-19 11:49:49 +00:00
r := m . GetBindRequest ( )
2020-01-19 17:24:21 +00:00
result_code , err := server . handleBindInternal ( state , & r )
2020-01-19 11:49:49 +00:00
res := ldap . NewBindResponse ( result_code )
if err != nil {
res . SetDiagnosticMessage ( err . Error ( ) )
2020-01-26 18:47:38 +00:00
}
if result_code == ldap . LDAPResultSuccess {
server . logger . Printf ( "Successfully bound to %s" , string ( r . Name ( ) ) )
} else {
server . logger . Printf ( "Failed to bind to %s (%s)" , string ( r . Name ( ) ) , err )
2020-01-19 11:49:49 +00:00
}
w . Write ( res )
}
2020-01-19 17:24:21 +00:00
func ( server * Server ) handleBindInternal ( state * State , r * message . BindRequest ) ( int , error ) {
2020-01-26 17:42:04 +00:00
// Check permissions
if ! server . config . Acl . Check ( & state . login , "bind" , string ( r . Name ( ) ) , [ ] string { } ) {
2020-01-27 23:52:30 +00:00
return ldap . LDAPResultInsufficientAccessRights , fmt . Errorf ( "Insufficient access rights for %#v" , state . login )
2020-01-26 17:42:04 +00:00
}
// Try to retrieve password and check for match
2020-01-26 20:22:51 +00:00
passwd , err := server . getAttribute ( string ( r . Name ( ) ) , ATTR_USERPASSWORD )
2020-01-19 11:49:49 +00:00
if err != nil {
return ldap . LDAPResultOperationsError , err
}
2020-01-19 18:10:38 +00:00
for _ , hash := range passwd {
valid := SSHAMatches ( hash , [ ] byte ( r . AuthenticationSimple ( ) ) )
if valid {
2020-01-26 20:22:51 +00:00
groups , err := server . getAttribute ( string ( r . Name ( ) ) , ATTR_MEMBEROF )
2020-01-26 17:42:04 +00:00
if err != nil {
return ldap . LDAPResultOperationsError , err
}
state . login = Login {
user : string ( r . Name ( ) ) ,
groups : groups ,
}
2020-01-19 18:10:38 +00:00
return ldap . LDAPResultSuccess , nil
}
2020-01-19 11:49:49 +00:00
}
2020-01-27 23:52:30 +00:00
return ldap . LDAPResultInvalidCredentials , fmt . Errorf ( "No password match" )
2020-01-19 11:49:49 +00:00
}