Skip to content

Commit e5fd16a

Browse files
feat: improve group search with exact name matching
- Updated GetGroupByName to use search parameter instead of name filter due to Authentik API limitations - Added URL encoding for search terms to handle spaces and special characters - Implemented client-side filtering to ensure exact case-sensitive name matches - Enhanced error handling with detailed API error messages - Added documentation explaining the rationale for using search parameter - Improved response body handling to read content only once for both success
1 parent 7deb9d0 commit e5fd16a

1 file changed

Lines changed: 31 additions & 8 deletions

File tree

pkg/authentik/groups.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"net/http"
12+
"net/url"
1213
)
1314

1415
// GroupRequest represents the request body for creating a group
@@ -73,11 +74,16 @@ func (c *APIClient) CreateGroup(ctx context.Context, name string, attributes map
7374
return &group, nil
7475
}
7576

76-
// GetGroupByName retrieves a group by name
77+
// GetGroupByName retrieves a group by name using search parameter and client-side filtering.
78+
// RATIONALE: Authentik API doesn't support exact filtering by name parameter (?name=).
79+
// Using ?search= and client-side filtering ensures exact case-sensitive match.
7780
func (c *APIClient) GetGroupByName(ctx context.Context, name string) (*GroupResponse, error) {
78-
url := fmt.Sprintf("%s/api/v3/core/groups/?name=%s", c.BaseURL, name)
81+
// Use search parameter instead of name filter (Authentik API limitation)
82+
// URL-encode the search term to handle spaces and special characters
83+
encodedName := url.QueryEscape(name)
84+
apiURL := fmt.Sprintf("%s/api/v3/core/groups/?search=%s", c.BaseURL, encodedName)
7985

80-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
86+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
8187
if err != nil {
8288
return nil, fmt.Errorf("failed to create request: %w", err)
8389
}
@@ -91,24 +97,41 @@ func (c *APIClient) GetGroupByName(ctx context.Context, name string) (*GroupResp
9197
}
9298
defer func() { _ = resp.Body.Close() }()
9399

100+
// Read body for both error and success cases
101+
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
102+
if readErr != nil {
103+
return nil, fmt.Errorf("failed to read response body: %w", readErr)
104+
}
105+
94106
if resp.StatusCode != http.StatusOK {
95-
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
107+
// Try to parse error detail from response
108+
var apiError struct {
109+
Detail string `json:"detail"`
110+
}
111+
if json.Unmarshal(body, &apiError) == nil && apiError.Detail != "" {
112+
return nil, fmt.Errorf("group fetch failed with status %d: %s", resp.StatusCode, apiError.Detail)
113+
}
96114
return nil, fmt.Errorf("group fetch failed with status %d: %s", resp.StatusCode, string(body))
97115
}
98116

99117
var result struct {
100118
Results []GroupResponse `json:"results"`
101119
}
102120

103-
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
121+
if err := json.Unmarshal(body, &result); err != nil {
104122
return nil, fmt.Errorf("failed to decode group response: %w", err)
105123
}
106124

107-
if len(result.Results) == 0 {
108-
return nil, fmt.Errorf("group not found: %s", name)
125+
// Filter results for exact case-sensitive match
126+
// (search may return partial matches)
127+
for i := range result.Results {
128+
if result.Results[i].Name == name {
129+
return &result.Results[i], nil
130+
}
109131
}
110132

111-
return &result.Results[0], nil
133+
// Group not found
134+
return nil, fmt.Errorf("group not found: %s", name)
112135
}
113136

114137
// ListGroups lists all groups, optionally filtered by prefix

0 commit comments

Comments
 (0)