Skip to content

feat(RHOAIENG-50753): add LDAP user and group autocomplete for workspace sharing#805

Open
mprpic wants to merge 1 commit intomainfrom
ldap-autocomplete
Open

feat(RHOAIENG-50753): add LDAP user and group autocomplete for workspace sharing#805
mprpic wants to merge 1 commit intomainfrom
ldap-autocomplete

Conversation

@mprpic
Copy link
Contributor

@mprpic mprpic commented Mar 4, 2026

Integrate LDAP to provide autocomplete when sharing workspaces. The backend queries LDAP with substring matching on uid, givenName, and sn for users, and prefix matching on cn for groups. Results are cached in-memory with a 5-minute TTL. The frontend replaces the plain text input with an autocomplete dropdown that debounces queries and supports keyboard navigation. LDAP is optional and gracefully degrades to manual input when unconfigured or unreachable.

@github-actions

This comment has been minimized.

@mprpic mprpic force-pushed the ldap-autocomplete branch from a316335 to 18b453a Compare March 5, 2026 02:00
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

Ambient Code Platform

Kubernetes-native AI automation platform that orchestrates agentic sessions through containerized microservices. Built with Go (backend, operator), NextJS + Shadcn (frontend), Python (runner), and Kubernetes CRDs.

Technical artifacts still use "vteam" for backward compatibility.

Structure

  • components/backend/ - Go REST API (Gin), manages K8s Custom Resources with multi-tenant project isolation
  • components/frontend/ - NextJS web UI for session management and monitoring
  • components/operator/ - Go Kubernetes controller, watches CRDs and creates Jobs
  • components/runners/ambient-runner/ - Python runner executing Claude Code CLI in Job pods
  • components/ambient-cli/ - Go CLI (acpctl), manages agentic sessions from the command line
  • components/public-api/ - Stateless HTTP gateway, proxies to backend (no direct K8s access)
  • components/manifests/ - Kustomize-based deployment manifests and overlays
  • e2e/ - Cypress end-to-end tests
  • docs/ - Astro Starlight documentation site

Key Files

  • CRD definitions: components/manifests/base/crds/agenticsessions-crd.yaml, projectsettings-crd.yaml
  • Session lifecycle: components/backend/handlers/sessions.go, components/operator/internal/handlers/sessions.go
  • Auth & RBAC middleware: components/backend/handlers/middleware.go
  • K8s client init: components/operator/internal/config/config.go
  • Runner entry point: components/runners/ambient-runner/main.py
  • Route registration: components/backend/routes.go
  • Frontend API layer: components/frontend/src/services/api/, src/services/queries/

Session Flow

User Creates Session → Backend Creates CR → Operator Spawns Job →
Pod Runs Claude CLI → Results Stored in CR → UI Displays Progress

Commands

make build-all                # Build all container images
make deploy                   # Deploy to cluster
make test                     # Run tests
make lint                     # Lint code
make kind-up                  # Start local Kind cluster
make test-e2e-local           # Run E2E tests against Kind

Per-Component

# Backend / Operator (Go)
cd components/backend && gofmt -l . && go vet ./... && golangci-lint run
cd components/operator && gofmt -l . && go vet ./... && golangci-lint run

# Frontend
cd components/frontend && npm run build  # Must pass with 0 errors, 0 warnings

# Runner (Python)
cd components/runners/ambient-runner && uv venv && uv pip install -e .

# Docs
cd docs && npm run dev  # http://localhost:4321

Critical Context

  • User token auth required: All user-facing API ops use GetK8sClientsForRequest(c), never the backend service account
  • OwnerReferences on all child resources: Jobs, Secrets, PVCs must have controller owner refs
  • No panic() in production: Return explicit fmt.Errorf with context
  • No any types in frontend: Use proper types, unknown, or generic constraints
  • Conventional commits: Squashed on merge to main

Pre-commit Hooks

The project uses the pre-commit framework to run linters locally before every commit. Configuration lives in .pre-commit-config.yaml.

Install

make setup-hooks

What Runs

On every git commit:

Hook Scope
trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files, check-merge-conflict, detect-private-key All files
ruff-format, ruff (check + fix) Python (runners, scripts)
gofmt, go vet, golangci-lint Go (backend, operator, public-api — per-module)
eslint Frontend TypeScript/JavaScript
branch-protection Blocks commits to main/master/production

On every git push:

Hook Scope
push-protection Blocks pushes to main/master/production

Run Manually

make lint                                    # All hooks, all files
pre-commit run gofmt-check --all-files       # Single hook
pre-commit run --files path/to/file.go       # Single file

Skip Hooks

git commit --no-verify    # Skip pre-commit hooks
git push --no-verify      # Skip pre-push hooks

Notes

  • Go and ESLint wrappers (scripts/pre-commit/) skip gracefully if the toolchain is not installed
  • tsc --noEmit and npm run build are not included (slow; CI gates on them)
  • Branch/push protection scripts remain in scripts/git-hooks/ and are invoked by pre-commit

More Info

See BOOKMARKS.md for architecture decisions, development context, code patterns, and component-specific guides.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

Claude Code Review

Summary

This PR adds LDAP-backed user and group autocomplete to the workspace sharing flow. The feature is well-scoped, optional-by-default (graceful degradation when unconfigured), and includes solid test coverage. The LDAP client uses proper filter escaping and input sanitization. A few issues need attention before merge: all three new handlers use the wrong variable for the auth nil check (deviating from the mandatory project pattern), and the frontend builds a custom combobox from scratch when a Shadcn equivalent exists.


Issues by Severity

Blocker Issues

None.


Critical Issues

1. Auth nil check uses wrong client variable in all 3 handlers

Files: components/backend/handlers/ldap.go lines 69-70, 99-100, 129-130

All three handlers discard reqK8s with _ and check k8sDyn (the dynamic client) for nil:

_, k8sDyn := GetK8sClientsForRequest(c)
if k8sDyn == nil {

The documented mandatory pattern across the entire codebase (backend-development.md, k8s-client-usage.md, security-standards.md) is to check the typed K8s client (reqK8s):

reqK8s, _ := GetK8sClientsForRequest(c)
if reqK8s == nil {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"})
    c.Abort()
    return
}

While functionally equivalent today (both return nil together), this breaks the established convention, hides the unused reqK8s, and creates a maintenance hazard if GetK8sClientsForRequest behavior ever diverges. All three handlers (SearchLDAPUsers, SearchLDAPGroups, GetLDAPUser) need this fix.

Violates: backend-development.md Critical Rules - Authentication and Authorization; k8s-client-usage.md Pattern 1.


Major Issues

2. Custom combobox dropdown built from scratch instead of Shadcn Command

File: components/frontend/src/components/workspace-sections/_components/ldap-autocomplete.tsx

The component renders a bare <ul> styled with Shadcn design tokens to simulate a dropdown. Shadcn ships a Command component (built on CMDK) designed exactly for async-populated autocomplete patterns, already available in the project's UI kit.

Per frontend-development.md Shadcn UI Components Only rule: FORBIDDEN: Creating custom UI components from scratch for buttons, inputs, dialogs, etc.

The Shadcn Popover + Command + CommandInput + CommandList pattern is the correct implementation path. It also provides accessibility (ARIA roles, keyboard navigation) for free rather than re-implementing manually.

3. New LDAP connection opened per search, no pooling

File: components/backend/ldap/client.go - SearchUsers, SearchGroups, GetUser

Each search dials a fresh TCP+TLS connection and closes it immediately via defer conn.Close(). Every cache miss creates a new LDAP connection. A user typing 5 characters generates up to 4 cold-connection searches (after the first 2-char trigger). At scale this adds meaningful latency and load on the LDAP server. Recommend persisting a connection with reconnect-on-error, or at minimum documenting this as a known limitation.

4. LDAP_GROUP_BASE_DN env var not mapped in deployment manifest

File: components/manifests/base/backend-deployment.yaml

main.go reads os.Getenv("LDAP_GROUP_BASE_DN") but the deployment manifest only maps LDAP_URL, LDAP_BASE_DN, and LDAP_SKIP_TLS_VERIFY from the ldap-config ConfigMap. Operators who need a non-default group base DN cannot configure it without manually patching the manifest.


Minor Issues

5. k8sDyn obtained but never used for K8s operations

File: components/backend/handlers/ldap.go lines 69, 99, 129

The dynamic client is obtained and only checked for nil, never used for actual K8s operations. Once Critical issue 1 is fixed, simplify to reqK8s, _ := GetK8sClientsForRequest(c).

6. Component exceeds 200-line guideline

File: components/frontend/src/components/workspace-sections/_components/ldap-autocomplete.tsx (252 lines)

frontend-development.md sets a 200-line limit. Much of the excess comes from duplicated render blocks for user vs group modes. Extracting a shared ResultItem sub-component would reduce size and eliminate the duplication.

7. Trivial JSDoc comments on self-evident functions

Files: components/frontend/src/services/api/ldap.ts, components/frontend/src/services/queries/use-ldap.ts

Comments like /** Search LDAP users by query (min 2 chars) */ add no information beyond the function name. Per CLAUDE.md: only add comments where logic is not self-evident.


Positive Highlights

  • Injection prevention is solid: sanitizeQuery strips newlines and carriage returns to block log injection, and goldap.EscapeFilter() is applied to every query before it enters an LDAP filter. The %q format verb in log lines ensures special chars are Go-quoted, not literal.
  • TLS hardened correctly: MinVersion: tls.VersionTLS12 is set, and InsecureSkipVerify has a proper nolint:gosec annotation noting it is dev-only and env-var-controlled.
  • Cache is thread-safe: sync.Map is the right primitive for concurrent read-heavy access, and TTL expiry is correctly lazy-evaluated on read.
  • Comprehensive unit tests: 9 test cases cover filter generation (special chars, multi-word, asterisk), DN parsing, query sanitization (newline stripping, truncation), cache TTL expiry, and short-query early-exit. Exactly the coverage this new LDAP package warrants.
  • React Query hooks follow the pattern precisely: enabled: query.length >= 2, staleTime set, hierarchical key factory (ldapKeys.userSearch(query)) correctly includes the query in the cache key with no collision risk.
  • Graceful degradation is well-implemented: optional: true on all ConfigMap refs, LDAPClient == nil guards in every handler, frontend falls back silently to no suggestions.
  • UX improvement in sharing-section.tsx: Clearing subjectName on subject-type switch prevents stale values carrying over between user/group modes.

Recommendations

  1. (Critical) Fix all three handlers: reqK8s, _ := GetK8sClientsForRequest(c) and check reqK8s == nil.
  2. (Major) Replace the custom <ul> dropdown with Shadcn Popover + Command + CommandList.
  3. (Major) Add LDAP_GROUP_BASE_DN to backend-deployment.yaml env mappings and ConfigMap specs.
  4. (Major) Document the per-request connection model as a known limitation, or add connection pooling.
  5. (Minor) Remove trivial JSDoc comments from ldap.ts and use-ldap.ts.
  6. (Minor) Extract a shared ResultItem component to bring ldap-autocomplete.tsx under 200 lines.

…ace sharing

Integrate LDAP to provide autocomplete when sharing workspaces. The
backend queries LDAP with substring matching on uid, givenName, and sn
for users, and prefix matching on cn for groups. Results are cached
in-memory with a 5-minute TTL. The frontend replaces the plain text
input with an autocomplete dropdown that debounces queries and supports
keyboard navigation. LDAP is optional and gracefully degrades to manual
input when unconfigured or unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Martin Prpič <mprpic@redhat.com>
@mprpic mprpic force-pushed the ldap-autocomplete branch from 18b453a to 134aa71 Compare March 5, 2026 02:33
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

Claude Code Review

Summary

PR #805 adds LDAP-backed user and group autocomplete for the workspace sharing flow. The implementation is well-structured: the Go backend adds a clean ldap package with in-memory caching, proper input sanitization, test coverage, and graceful degradation when LDAP is unconfigured. The frontend follows the React Query + API-layer separation pattern correctly. However, one critical security misconfiguration in the production manifest must be resolved before merge, and a few quality issues are worth addressing.

Issues by Severity

Blocker Issues

1. TLS verification disabled in production manifest

  • File: components/manifests/overlays/production/ldap-config.yaml:9
  • Problem: LDAP_SKIP_TLS_VERIFY: "true" is set in the production overlay. This disables TLS certificate verification for all LDAP connections in production, leaving LDAP traffic (including user/group enumeration) vulnerable to MITM interception.
  • Standard violated: security-standards.md — secure connections; the code itself carries the //nolint:gosec // controlled by LDAP_SKIP_TLS_VERIFY env var for dev comment, explicitly calling this a dev-only flag.
  • Fix: Remove LDAP_SKIP_TLS_VERIFY from the production ConfigMap entirely (or set it to "false"). If the production LDAP server requires it, that must be resolved at the infra level, not by disabling cert verification.
# components/manifests/overlays/production/ldap-config.yaml
data:
  LDAP_URL: "ldaps://ldap.corp.redhat.com"
  LDAP_BASE_DN: "ou=users,dc=redhat,dc=com"
  # LDAP_SKIP_TLS_VERIFY omitted — defaults to false

Critical Issues

None.


Major Issues

1. LDAP query error state not surfaced in the autocomplete component

  • File: components/frontend/src/components/workspace-sections/_components/ldap-autocomplete.tsx:54-58
  • Problem: The error value from useLDAPUserSearch / useLDAPGroupSearch is never destructured or consumed. When LDAP is unreachable (network error, 503), the component shows "No results found" — indistinguishable from a legitimate empty result. Users will think their search input is simply wrong rather than knowing the service is unavailable.
  • Standard violated: frontend-development.md pre-commit checklist — "Loading and error states handled".
  • Fix: Destructure error and render a distinct error message:
const { data: users, isLoading: isLoadingUsers, error: usersError } = useLDAPUserSearch(...)
const { data: groups, isLoading: isLoadingGroups, error: groupsError } = useLDAPGroupSearch(...)
const queryError = mode === 'user' ? usersError : groupsError;

// In the dropdown:
{queryError && (
  <li className="px-3 py-2 text-sm text-destructive">
    Search unavailable — enter name manually
  </li>
)}

2. Component exceeds 200-line limit

  • File: components/frontend/src/components/workspace-sections/_components/ldap-autocomplete.tsx (252 lines)
  • Problem: The frontend guidelines cap components at 200 lines. The excess is primarily the duplicated user/group <li> rendering blocks (~50 lines of near-identical JSX).
  • Standard violated: frontend-development.md — "Components under 200 lines".
  • Fix: Extract a AutocompleteItem sub-component for the <li> items or factor out the rendering into a helper to bring the component under the limit.

Minor Issues

1. Unbounded cache growth in sync.Map

  • File: components/backend/ldap/client.go:225
  • Problem: Expired entries are only removed on access (cacheGet deletes stale entries). Keys that are queried once and never again accumulate indefinitely. Under high query diversity (many unique strings), memory grows without bound.
  • Suggestion: Add a periodic background sweep (e.g., a goroutine that ranges over the map every cacheTTL interval and deletes expired entries), or use a bounded LRU cache.

2. Dead code: useLDAPUser hook and getUser API function

  • Files: components/frontend/src/services/queries/use-ldap.ts:1436, components/frontend/src/services/api/ldap.ts:1372
  • Problem: useLDAPUser and ldapApi.getUser are added but never called anywhere in this PR. Unused exports in a public query index create confusion about the API surface.
  • Suggestion: Remove them and add when actually needed, or leave a comment linking to the planned consumer.

3. GetLDAPUser returns 404 when LDAP is unconfigured — inconsistent with search endpoints

  • File: components/backend/handlers/ldap.go:142-145
  • Problem: When LDAPClient == nil, SearchLDAPUsers and SearchLDAPGroups return 200 {} (empty results), but GetLDAPUser returns 404 {"error": "LDAP not configured"}. The inconsistency can confuse callers trying to resolve a known UID when LDAP is disabled.
  • Suggestion: Return 200 with a sentinel (e.g., null) or 503 consistently across all LDAP endpoints when unconfigured.

4. UTF-8 unsafe truncation in sanitizeQuery

  • File: components/backend/ldap/client.go:488
  • Problem: q[:maxQueryLength] slices at a byte offset, which can split a multi-byte UTF-8 rune and produce an invalid string. LDAP queries are typically ASCII, but defensive code should use []rune for safe truncation.
  • Suggested fix:
runes := []rune(q)
if len(runes) > maxQueryLength {
    q = string(runes[:maxQueryLength])
}

5. Identical ConfigMaps duplicated across kind and local-dev overlays

  • Files: components/manifests/overlays/kind/ldap-config.yaml, components/manifests/overlays/local-dev/ldap-config.yaml
  • Problem: The two files are byte-for-byte identical. A change to one must be mirrored manually to the other.
  • Suggestion: Move the shared content to components/manifests/base/ and reference it from both overlays, or note if the duplication is intentional.

Positive Highlights

  • Excellent input sanitization in Go: sanitizeQuery strips newlines/carriage-returns and truncates, and all log calls use %q to prevent log injection — this is done correctly and proactively.
  • LDAP injection prevention: User input is passed through goldap.EscapeFilter before being embedded in LDAP filter strings. This is the correct pattern and is tested.
  • Auth gate on all LDAP endpoints: Every handler calls GetK8sClientsForRequest and returns 401 on failure — consistent with the project's mandatory auth pattern.
  • Graceful degradation: The feature degrades safely when LDAP_URL is not set — no panics, no crashes, just empty results or silent no-ops.
  • Strong test coverage for the LDAP package: client_test.go covers filter construction, sanitization, cache TTL, edge cases (empty UID, short query), and the ParseGitHubUsername parser thoroughly.
  • Clean React Query integration: use-ldap.ts uses query key factories, proper enabled guards, staleTime aligned to the backend cache TTL, and follows the established api/ + queries/ layer separation perfectly.

Recommendations

  1. [Blocker] Remove LDAP_SKIP_TLS_VERIFY: "true" from overlays/production/ldap-config.yaml before merge.
  2. [Major] Handle error state in LDAPAutocomplete to give users feedback when LDAP is unreachable.
  3. [Major] Refactor ldap-autocomplete.tsx below 200 lines by extracting the list item rendering.
  4. [Minor] Remove or defer the unused useLDAPUser / getUser dead code.
  5. [Minor] Fix byte-level truncation in sanitizeQuery to be rune-safe.
  6. [Minor] Harmonize GetLDAPUser's "LDAP not configured" response with the search endpoints.

🤖 Reviewed with Claude Code (claude-sonnet-4-6)


🔍 View AI decision process (logs available for 90 days)

📋 View memory system files loaded (click to expand)

What Amber Loaded for Code Review

Amber automatically loaded these repository standards from the memory system:

  1. CLAUDE.md - Master project instructions, development standards
  2. backend-development.md - Go backend, K8s integration patterns
  3. frontend-development.md - NextJS, Shadcn UI, React Query patterns
  4. security-standards.md - Auth, RBAC, token handling
  5. k8s-client-usage.md - User token vs service account patterns
  6. error-handling.md - Consistent error patterns
  7. react-query-usage.md - Data fetching patterns

Impact: This review used your repository's specific code quality standards, security patterns, and best practices from the memory system (PRs #359, #360) - not just generic code review guidelines.

@mprpic
Copy link
Contributor Author

mprpic commented Mar 5, 2026

Disabling TLS verification is intentional for now until we switch to the authenticated LDAP server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant