package ldap import ( "crypto/rand" "encoding/base64" "fmt" "log" "git.nixc.us/a250/ss-atlas/internal/config" goldap "github.com/go-ldap/ldap/v3" ) type Client struct { cfg *config.Config } type ProvisionResult struct { Username string Password string IsNew bool } func New(cfg *config.Config) *Client { return &Client{cfg: cfg} } func (c *Client) connect() (*goldap.Conn, error) { conn, err := goldap.DialURL(c.cfg.LDAPUrl) if err != nil { return nil, fmt.Errorf("ldap dial: %w", err) } if err := conn.Bind(c.cfg.LDAPAdminDN, c.cfg.LDAPAdminPassword); err != nil { conn.Close() return nil, fmt.Errorf("ldap bind: %w", err) } return conn, nil } func (c *Client) ProvisionUser(username, email, stripeCustomerID string) (*ProvisionResult, error) { conn, err := c.connect() if err != nil { return nil, err } defer conn.Close() exists, err := c.userExists(conn, username) if err != nil { return nil, err } if exists { log.Printf("ldap user %s already exists", username) return &ProvisionResult{Username: username, IsNew: false}, nil } password := generatePassword() userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) addReq := goldap.NewAddRequest(userDN, nil) addReq.Attribute("objectClass", []string{"inetOrgPerson"}) addReq.Attribute("cn", []string{username}) addReq.Attribute("sn", []string{username}) addReq.Attribute("uid", []string{username}) addReq.Attribute("mail", []string{email}) addReq.Attribute("userPassword", []string{password}) addReq.Attribute("description", []string{stripeCustomerID}) if err := conn.Add(addReq); err != nil { return nil, fmt.Errorf("ldap add user %s: %w", username, err) } log.Printf("created ldap user %s (%s)", username, email) return &ProvisionResult{Username: username, Password: password, IsNew: true}, nil } func (c *Client) EnsureUser(username, email, stripeCustomerID string) error { _, err := c.ProvisionUser(username, email, stripeCustomerID) return err } func (c *Client) AddToGroup(username, groupName string) error { conn, err := c.connect() if err != nil { return err } defer conn.Close() groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN) userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) modReq := goldap.NewModifyRequest(groupDN, nil) modReq.Add("member", []string{userDN}) if err := conn.Modify(modReq); err != nil { return fmt.Errorf("ldap add %s to group %s: %w", username, groupName, err) } log.Printf("added %s to group %s", username, groupName) return nil } func (c *Client) RemoveFromGroup(username, groupName string) error { conn, err := c.connect() if err != nil { return err } defer conn.Close() groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN) userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) modReq := goldap.NewModifyRequest(groupDN, nil) modReq.Delete("member", []string{userDN}) if err := conn.Modify(modReq); err != nil { return fmt.Errorf("ldap remove %s from group %s: %w", username, groupName, err) } log.Printf("removed %s from group %s", username, groupName) return nil } func (c *Client) IsInGroup(username, groupName string) (bool, error) { conn, err := c.connect() if err != nil { return false, err } defer conn.Close() groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN) userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) searchReq := goldap.NewSearchRequest( groupDN, goldap.ScopeBaseObject, goldap.NeverDerefAliases, 1, 0, false, fmt.Sprintf("(member=%s)", goldap.EscapeFilter(userDN)), []string{"cn"}, nil, ) result, err := conn.Search(searchReq) if err != nil { return false, nil } return len(result.Entries) > 0, nil } func (c *Client) FindUserByDescription(stripeCustomerID string) (string, error) { conn, err := c.connect() if err != nil { return "", err } defer conn.Close() searchReq := goldap.NewSearchRequest( fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN), goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 1, 0, false, fmt.Sprintf("(description=%s)", goldap.EscapeFilter(stripeCustomerID)), []string{"uid"}, nil, ) result, err := conn.Search(searchReq) if err != nil { return "", fmt.Errorf("ldap search by description: %w", err) } if len(result.Entries) == 0 { return "", fmt.Errorf("no user found with stripe customer %s", stripeCustomerID) } return result.Entries[0].GetAttributeValue("uid"), nil } func (c *Client) userExists(conn *goldap.Conn, username string) (bool, error) { searchReq := goldap.NewSearchRequest( fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN), goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 1, 0, false, fmt.Sprintf("(uid=%s)", goldap.EscapeFilter(username)), []string{"uid"}, nil, ) result, err := conn.Search(searchReq) if err != nil { return false, fmt.Errorf("ldap search: %w", err) } return len(result.Entries) > 0, nil } func generatePassword() string { b := make([]byte, 18) if _, err := rand.Read(b); err != nil { panic("crypto/rand failed: " + err.Error()) } return base64.URLEncoding.EncodeToString(b) }