From 379826551155b52c36c4ecb04c3be1ceed10f5e6 Mon Sep 17 00:00:00 2001 From: Leopere Date: Wed, 4 Mar 2026 20:18:04 -0500 Subject: [PATCH] Ensure customers group exists (EnsureGroup), move group logic to groups.go; improve subscription error log Made-with: Cursor --- .../internal/handlers/subscription.go | 2 +- docker/ss-atlas/internal/ldap/client.go | 81 ------------ docker/ss-atlas/internal/ldap/groups.go | 122 ++++++++++++++++++ 3 files changed, 123 insertions(+), 82 deletions(-) create mode 100644 docker/ss-atlas/internal/ldap/groups.go diff --git a/docker/ss-atlas/internal/handlers/subscription.go b/docker/ss-atlas/internal/handlers/subscription.go index 4706753..7d73809 100644 --- a/docker/ss-atlas/internal/handlers/subscription.go +++ b/docker/ss-atlas/internal/handlers/subscription.go @@ -151,7 +151,7 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) { // Grant active subscription: add to customers group so dashboard shows subscribed. if err := a.ldap.AddToGroup(result.Username, "customers"); err != nil { - log.Printf("ldap add to customers failed for %s: %v", result.Username, err) + log.Printf("ldap add to customers failed for %s: %v (create group 'customers' in LLDAP admin if missing)", result.Username, err) } inGroup, _ := a.ldap.IsInGroup(result.Username, "customers") diff --git a/docker/ss-atlas/internal/ldap/client.go b/docker/ss-atlas/internal/ldap/client.go index add9360..1207a43 100644 --- a/docker/ss-atlas/internal/ldap/client.go +++ b/docker/ss-atlas/internal/ldap/client.go @@ -117,62 +117,6 @@ func (c *Client) EnsureUser(username, email, stripeCustomerID, phone string) err return err } -func (c *Client) AddToGroup(username, groupName string) error { - groupID, err := c.getGroupID(groupName) - if err != nil { - return fmt.Errorf("resolve group %s: %w", groupName, err) - } - - query := `mutation($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } }` - _, err = c.gql.exec(query, map[string]any{"userId": username, "groupId": groupID}) - if err != nil { - return fmt.Errorf("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 { - groupID, err := c.getGroupID(groupName) - if err != nil { - return fmt.Errorf("resolve group %s: %w", groupName, err) - } - - query := `mutation($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } }` - _, err = c.gql.exec(query, map[string]any{"userId": username, "groupId": groupID}) - if err != nil { - return fmt.Errorf("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) { - query := `query($userId: String!) { user(userId: $userId) { groups { displayName } } }` - data, err := c.gql.exec(query, map[string]any{"userId": username}) - if err != nil { - return false, err - } - - var result struct { - User struct { - Groups []struct { - DisplayName string `json:"displayName"` - } `json:"groups"` - } `json:"user"` - } - if err := json.Unmarshal(data, &result); err != nil { - return false, err - } - - for _, g := range result.User.Groups { - if g.DisplayName == groupName { - return true, nil - } - } - return false, nil -} - func (c *Client) SetStripeCustomerID(username, customerID string) error { query := `mutation($userId: String!, $attrs: [AttributeValueInput!]!) { updateUser(user: { id: $userId, insertAttributes: $attrs }) { ok } @@ -327,31 +271,6 @@ func (c *Client) CountCustomers() (int, error) { return n, nil } -func (c *Client) getGroupID(groupName string) (int, error) { - query := `query { groups { id displayName } }` - data, err := c.gql.exec(query, nil) - if err != nil { - return 0, err - } - - var result struct { - Groups []struct { - ID int `json:"id"` - DisplayName string `json:"displayName"` - } `json:"groups"` - } - if err := json.Unmarshal(data, &result); err != nil { - return 0, err - } - - for _, g := range result.Groups { - if g.DisplayName == groupName { - return g.ID, nil - } - } - return 0, fmt.Errorf("group %s not found", groupName) -} - func (c *Client) userExists(conn *goldap.Conn, username string) (bool, error) { searchReq := goldap.NewSearchRequest( fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN), diff --git a/docker/ss-atlas/internal/ldap/groups.go b/docker/ss-atlas/internal/ldap/groups.go new file mode 100644 index 0000000..6d92ec5 --- /dev/null +++ b/docker/ss-atlas/internal/ldap/groups.go @@ -0,0 +1,122 @@ +package ldap + +import ( + "encoding/json" + "fmt" + "log" +) + +func (c *Client) getGroupID(groupName string) (int, error) { + query := `query { groups { id displayName } }` + data, err := c.gql.exec(query, nil) + if err != nil { + return 0, err + } + + var result struct { + Groups []struct { + ID int `json:"id"` + DisplayName string `json:"displayName"` + } `json:"groups"` + } + if err := json.Unmarshal(data, &result); err != nil { + return 0, err + } + + for _, g := range result.Groups { + if g.DisplayName == groupName { + return g.ID, nil + } + } + return 0, fmt.Errorf("group %s not found", groupName) +} + +// EnsureGroup creates the group in LLDAP if it does not exist (e.g. "customers"). +// Idempotent: no-op if group already exists. +func (c *Client) EnsureGroup(displayName string) error { + _, err := c.getGroupID(displayName) + if err == nil { + return nil + } + // Group missing: try to create via GraphQL (LLDAP mutation). + query := `mutation($displayName: String!) { createGroup(displayName: $displayName) { id } }` + data, err := c.gql.exec(query, map[string]any{"displayName": displayName}) + if err != nil { + log.Printf("lldap createGroup %q failed: %v (create group manually in LLDAP admin if needed)", displayName, err) + return err + } + var out struct { + CreateGroup struct { + ID int `json:"id"` + } `json:"createGroup"` + } + if err := json.Unmarshal(data, &out); err != nil { + log.Printf("lldap createGroup %q response unmarshal: %v", displayName, err) + return err + } + if out.CreateGroup.ID == 0 { + log.Printf("lldap createGroup %q returned no id", displayName) + return fmt.Errorf("createGroup %s: no id in response", displayName) + } + log.Printf("lldap created group %q (id=%d)", displayName, out.CreateGroup.ID) + return nil +} + +func (c *Client) AddToGroup(username, groupName string) error { + if err := c.EnsureGroup(groupName); err != nil { + return fmt.Errorf("ensure group %s: %w", groupName, err) + } + groupID, err := c.getGroupID(groupName) + if err != nil { + return fmt.Errorf("resolve group %s: %w", groupName, err) + } + + query := `mutation($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } }` + _, err = c.gql.exec(query, map[string]any{"userId": username, "groupId": groupID}) + if err != nil { + return fmt.Errorf("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 { + groupID, err := c.getGroupID(groupName) + if err != nil { + return fmt.Errorf("resolve group %s: %w", groupName, err) + } + + query := `mutation($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } }` + _, err = c.gql.exec(query, map[string]any{"userId": username, "groupId": groupID}) + if err != nil { + return fmt.Errorf("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) { + query := `query($userId: String!) { user(userId: $userId) { groups { displayName } } }` + data, err := c.gql.exec(query, map[string]any{"userId": username}) + if err != nil { + return false, err + } + + var result struct { + User struct { + Groups []struct { + DisplayName string `json:"displayName"` + } `json:"groups"` + } `json:"user"` + } + if err := json.Unmarshal(data, &result); err != nil { + return false, err + } + + for _, g := range result.User.Groups { + if g.DisplayName == groupName { + return true, nil + } + } + return false, nil +}