package main import ( "fmt" "log" "os" "os/signal" "syscall" "strings" "encoding/json" "encoding/base64" "math/rand" consul "github.com/hashicorp/consul/api" ldap "github.com/vjeantet/ldapserver" message "github.com/vjeantet/goldap/message" ) func dnToConsul(dn string) string { rdns := strings.Split(dn, ",") // Reverse rdns for i, j := 0, len(rdns)-1; i < j; i, j = i+1, j-1 { rdns[i], rdns[j] = rdns[j], rdns[i] } return strings.Join(rdns, "/") } type DNComponent struct { Type string Value string } func parseDN(dn string) ([]DNComponent, error) { rdns := strings.Split(dn, ",") ret := []DNComponent{} for _, rdn := range rdns { splits := strings.Split(rdn, "=") if len(splits) != 2 { return nil, fmt.Errorf("Wrong DN component: %s (expected type=value)", rdn) } ret = append(ret, DNComponent{ Type: splits[0], Value: splits[1], }) } return ret, nil } type Config struct { Suffix string } type Server struct { config Config kv *consul.KV } type Attributes map[string]interface{} func main() { //ldap logger ldap.Logger = log.New(os.Stdout, "[server] ", log.LstdFlags) // Connect to Consul client, err := consul.NewClient(consul.DefaultConfig()) if err != nil { panic(err) } kv := client.KV() // TODO read config from somewhere config := Config { Suffix: "dc=gobottin,dc=eu", } gobottin := Server{config: config, kv: kv} err = gobottin.init() if err != nil { panic(err) } //Create a new LDAP Server ldapserver := ldap.NewServer() routes := ldap.NewRouteMux() routes.Bind(gobottin.handleBind) ldapserver.Handle(routes) // listen on 10389 go ldapserver.ListenAndServe("127.0.0.1:10389") // 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 { pair, _, err := server.kv.Get(dnToConsul(server.config.Suffix) + "/attribute=objectClass", nil) if err != nil { return err } if pair != nil { return nil } base_attributes := Attributes{ "objectClass": []string{"top", "dcObject", "organization"}, "structuralObjectClass": "Organization", } suffix_dn, err := parseDN(server.config.Suffix) if err != nil { return err } base_attributes[suffix_dn[0].Type] = 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 := Attributes{ "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, "description": "LDAP administrator", "cn": "admin", "userpassword": admin_pass_hash, "structuralObjectClass": "organizationalRole", "permissions": []string{"read", "write"}, } err = server.addElements(admin_dn, admin_attributes) if err != nil { return err } log.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 Attributes) error { prefix := dnToConsul(dn) for k, v := range attrs { json, err := json.Marshal(v) if err != nil { return err } pair := &consul.KVPair{Key: prefix + "/attribute=" + k, Value: json} _, err = server.kv.Put(pair, nil) if err != nil { return err } } return nil } func (server *Server) handleBind(w ldap.ResponseWriter, m *ldap.Message) { r := m.GetBindRequest() result_code, err := server.handleBindInternal(w, r) res := ldap.NewBindResponse(result_code) if err != nil { res.SetDiagnosticMessage(err.Error()) log.Printf("Failed bind for %s: %s", string(r.Name()), err.Error()) } w.Write(res) } func (server *Server) handleBindInternal(w ldap.ResponseWriter, r message.BindRequest) (int, error) { pair, _, err := server.kv.Get(dnToConsul(string(r.Name())) + "/attribute=userpassword", nil) if err != nil { return ldap.LDAPResultOperationsError, err } if pair == nil { return ldap.LDAPResultNoSuchObject, nil } hash := "" err = json.Unmarshal(pair.Value, &hash) if err != nil { return ldap.LDAPResultOperationsError, err } valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) if valid { return ldap.LDAPResultSuccess, nil } else { return ldap.LDAPResultInvalidCredentials, nil } }