forked from Deuxfleurs/bottin
Alex Auvolat
f8c726dcda
After an object has been updated, membership information must be propagated to other object. Such operations may fail when calling consul but if they do we don't return fail immediatly returning an error code any more. Instead we just print all the errors to our logs and try to process the remaining updates.
377 lines
8.6 KiB
Go
377 lines
8.6 KiB
Go
package main
|
|
|
|
// @FIXME: Proper handling of various upper/lower case combinations
|
|
// @FIXME: Implement missing search filters (in applyFilter)
|
|
// @FIXME: Add an initial prefix to the consul key value
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
ldap "./ldapserver"
|
|
consul "github.com/hashicorp/consul/api"
|
|
message "github.com/vjeantet/goldap/message"
|
|
)
|
|
|
|
const DEBUG = false
|
|
|
|
const ATTR_USERPASSWORD = "userpassword"
|
|
const ATTR_MEMBER = "member"
|
|
const ATTR_MEMBEROF = "memberof"
|
|
const ATTR_ENTRYUUID = "entryuuid"
|
|
const ATTR_CREATORSNAME = "creatorsname"
|
|
const ATTR_CREATETIMESTAMP = "createtimestamp"
|
|
const ATTR_MODIFIERSNAME = "modifiersname"
|
|
const ATTR_MODIFYTIMESTAMP = "modifytimestamp"
|
|
|
|
type ConfigFile struct {
|
|
Suffix string `json:"suffix"`
|
|
BindAddress string `json:"bind_address"`
|
|
ConsulHost string `json:"consul_host"`
|
|
Acl []string `json:"acl"`
|
|
SSLCertFile string `json:"ssl_cert_file"`
|
|
SSLKeyFile string `json:"ssl_key_file"`
|
|
SSLServerName string `json:"ssl_server_name"`
|
|
}
|
|
|
|
type Config struct {
|
|
Suffix string
|
|
BindAddress string
|
|
ConsulHost string
|
|
|
|
Acl ACL
|
|
|
|
TlsConfig *tls.Config
|
|
}
|
|
|
|
type Server struct {
|
|
logger *log.Logger
|
|
config Config
|
|
kv *consul.KV
|
|
}
|
|
|
|
type State struct {
|
|
login Login
|
|
}
|
|
|
|
type Entry map[string][]string
|
|
|
|
var configFlag = flag.String("config", "./config.json", "Configuration file path")
|
|
|
|
func readConfig() Config {
|
|
config_file := ConfigFile{
|
|
BindAddress: "0.0.0.0:389",
|
|
}
|
|
|
|
bytes, err := ioutil.ReadFile(*configFlag)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = json.Unmarshal(bytes, &config_file)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
acl, err := ParseACL(config_file.Acl)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ret := Config{
|
|
Suffix: config_file.Suffix,
|
|
BindAddress: config_file.BindAddress,
|
|
ConsulHost: config_file.ConsulHost,
|
|
Acl: acl,
|
|
}
|
|
|
|
if config_file.SSLCertFile != "" && config_file.SSLKeyFile != "" && config_file.SSLServerName != "" {
|
|
cert_txt, err := ioutil.ReadFile(config_file.SSLCertFile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
key_txt, err := ioutil.ReadFile(config_file.SSLKeyFile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
cert, err := tls.X509KeyPair(cert_txt, key_txt)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ret.TlsConfig = &tls.Config{
|
|
MinVersion: tls.VersionSSL30,
|
|
MaxVersion: tls.VersionTLS12,
|
|
Certificates: []tls.Certificate{cert},
|
|
ServerName: config_file.SSLServerName,
|
|
}
|
|
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
ldap.Logger = log.New(os.Stdout, "[ldapserver] ", log.LstdFlags)
|
|
|
|
config := readConfig()
|
|
|
|
// 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 {
|
|
panic(err)
|
|
}
|
|
kv := consul_client.KV()
|
|
|
|
// Create gobottin server
|
|
gobottin := Server{
|
|
logger: log.New(os.Stdout, "[gobottin] ", log.LstdFlags),
|
|
config: config,
|
|
kv: kv,
|
|
}
|
|
err = gobottin.init()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
//Create a new LDAP Server
|
|
ldapserver := ldap.NewServer()
|
|
ldapserver.NewUserState = func() ldap.UserState {
|
|
return &State{
|
|
login: Login{
|
|
user: "ANONYMOUS",
|
|
groups: []string{},
|
|
},
|
|
}
|
|
}
|
|
|
|
routes := ldap.NewRouteMux()
|
|
routes.Bind(gobottin.handleBind)
|
|
routes.Search(gobottin.handleSearch)
|
|
routes.Add(gobottin.handleAdd)
|
|
routes.Compare(gobottin.handleCompare)
|
|
routes.Delete(gobottin.handleDelete)
|
|
routes.Modify(gobottin.handleModify)
|
|
ldapserver.Handle(routes)
|
|
|
|
if config.TlsConfig != nil {
|
|
secureConn := func(s *ldap.Server) {
|
|
s.Listener = tls.NewListener(s.Listener, config.TlsConfig)
|
|
}
|
|
go ldapserver.ListenAndServe(config.BindAddress, secureConn)
|
|
} else {
|
|
go ldapserver.ListenAndServe(config.BindAddress)
|
|
}
|
|
|
|
// 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)
|
|
|
|
ldapserver.Stop()
|
|
}
|
|
|
|
func (server *Server) init() error {
|
|
path, err := dnToConsul(server.config.Suffix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pair, _, err := server.kv.Get(path+"/attribute=objectClass", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pair != nil {
|
|
return nil
|
|
}
|
|
|
|
base_attributes := Entry{
|
|
"objectClass": []string{"top", "dcObject", "organization"},
|
|
"structuralObjectClass": []string{"Organization"},
|
|
}
|
|
suffix_dn, err := parseDN(server.config.Suffix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
base_attributes[suffix_dn[0].Type] = []string{suffix_dn[0].Value}
|
|
|
|
err = server.addElements(server.config.Suffix, base_attributes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
admin_pass := make([]byte, 8)
|
|
rand.Read(admin_pass)
|
|
admin_pass_str := base64.RawURLEncoding.EncodeToString(admin_pass)
|
|
admin_pass_hash := SSHAEncode([]byte(admin_pass_str))
|
|
|
|
admin_dn := "cn=admin," + server.config.Suffix
|
|
admin_attributes := Entry{
|
|
"objectClass": []string{"simpleSecurityObject", "organizationalRole"},
|
|
"description": []string{"LDAP administrator"},
|
|
"cn": []string{"admin"},
|
|
ATTR_USERPASSWORD: []string{admin_pass_hash},
|
|
"structuralObjectClass": []string{"organizationalRole"},
|
|
"permissions": []string{"read", "write"},
|
|
}
|
|
|
|
err = server.addElements(admin_dn, admin_attributes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
server.logger.Printf(
|
|
"It seems to be a new installation, we created a default user for you:\n\n dn: %s\n password: %s\n\nWe didn't use true random, you should replace it as soon as possible.",
|
|
admin_dn,
|
|
admin_pass_str,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (server *Server) addElements(dn string, attrs Entry) error {
|
|
prefix, err := dnToConsul(dn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
path := prefix + "/attribute=" + k
|
|
if len(v) == 0 {
|
|
// If we have zero values, delete associated k/v pair
|
|
_, err := server.kv.Delete(path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
json, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pair := &consul.KVPair{Key: path, Value: json}
|
|
_, err = server.kv.Put(pair, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (server *Server) getAttribute(dn string, attr string) ([]string, error) {
|
|
path, err := dnToConsul(dn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pair, _, err := server.kv.Get(path+"/attribute="+attr, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if pair == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return parseValue(pair.Value)
|
|
}
|
|
|
|
func (server *Server) objectExists(dn string) (bool, error) {
|
|
prefix, err := dnToConsul(dn)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
data, _, err := server.kv.List(prefix+"/", nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return len(data) > 0, nil
|
|
}
|
|
|
|
func (server *Server) checkSuffix(dn string, allow_extend bool) (string, error) {
|
|
suffix := server.config.Suffix
|
|
if len(dn) < len(suffix) {
|
|
if dn != suffix[-len(dn):] || !allow_extend {
|
|
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
|
|
}
|
|
}
|
|
|
|
func (server *Server) handleBind(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) {
|
|
state := s.(*State)
|
|
r := m.GetBindRequest()
|
|
|
|
result_code, err := server.handleBindInternal(state, &r)
|
|
|
|
res := ldap.NewBindResponse(result_code)
|
|
if err != nil {
|
|
res.SetDiagnosticMessage(err.Error())
|
|
server.logger.Printf("Failed bind for %s: %s", string(r.Name()), err.Error())
|
|
}
|
|
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)
|
|
}
|
|
w.Write(res)
|
|
}
|
|
|
|
func (server *Server) handleBindInternal(state *State, r *message.BindRequest) (int, error) {
|
|
// Check permissions
|
|
if !server.config.Acl.Check(&state.login, "bind", string(r.Name()), []string{}) {
|
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
|
}
|
|
|
|
// Try to retrieve password and check for match
|
|
passwd, err := server.getAttribute(string(r.Name()), ATTR_USERPASSWORD)
|
|
if err != nil {
|
|
return ldap.LDAPResultOperationsError, err
|
|
}
|
|
if passwd == nil {
|
|
return ldap.LDAPResultNoSuchObject, nil
|
|
}
|
|
|
|
for _, hash := range passwd {
|
|
valid := SSHAMatches(hash, []byte(r.AuthenticationSimple()))
|
|
if valid {
|
|
groups, err := server.getAttribute(string(r.Name()), ATTR_MEMBEROF)
|
|
if err != nil {
|
|
return ldap.LDAPResultOperationsError, err
|
|
}
|
|
state.login = Login{
|
|
user: string(r.Name()),
|
|
groups: groups,
|
|
}
|
|
return ldap.LDAPResultSuccess, nil
|
|
}
|
|
}
|
|
return ldap.LDAPResultInvalidCredentials, nil
|
|
}
|
|
|
|
|