Skip to content

feat: use logged-in user credentials for all LDAP operations#430

Merged
CybotTM merged 6 commits intomainfrom
feat/user-bound-ldap-credentials
Mar 4, 2026
Merged

feat: use logged-in user credentials for all LDAP operations#430
CybotTM merged 6 commits intomainfrom
feat/user-bound-ldap-credentials

Conversation

@CybotTM
Copy link
Member

@CybotTM CybotTM commented Feb 11, 2026

Summary

  • Fix "Insufficient Access Rights" bug: Write operations (add/remove group members) now use the logged-in user's own LDAP connection instead of the readonly service account
  • Per-user LDAP reads: All interactive reads also use user credentials, respecting per-user AD ACLs
  • Optional service account: LDAP_READONLY_USER / LDAP_READONLY_PASSWORD are no longer required — when omitted, login uses AD UPN bind (user@domain) and background cache is disabled

Changes

File Change
internal/options/app.go Make readonly user/password optional
internal/web/auth.go Store credentials in session; UPN bind support
internal/web/server.go getUserLDAP() helper; conditional cache init
internal/web/groups.go Use user LDAP for reads + writes
internal/web/users.go Use user LDAP for reads + writes
internal/web/computers.go Use user LDAP for reads
internal/web/health.go Handle nil cache/readonly gracefully
internal/ldap_cache/manager.go Standalone population functions

Security

Password is stored in server-side session (memory or bbolt). Only the session ID cookie reaches the browser. This is the standard pattern for LDAP web apps (phpLDAPadmin, FusionDirectory, etc.).

Test plan

  • go build ./... compiles
  • go test ./... passes (all packages)
  • go vet ./... clean
  • Manual test: login without readonly user configured, browse users/groups, add/remove group members
  • Manual test: login with readonly user configured, verify health endpoints return cache metrics

@gemini-code-assist
Copy link

Summary of Changes

Hello @CybotTM, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the application's LDAP interaction model to enhance security and flexibility. By shifting from a primary readonly service account to using the logged-in user's credentials for most LDAP operations, it resolves access permission issues for write actions and enables granular per-user access control for reads. Furthermore, it introduces the option to run the application without a dedicated service account, simplifying deployment for environments that prefer direct user authentication and dynamic LDAP interactions.

Highlights

  • Improved LDAP Operation Permissions: Write operations (e.g., adding/removing group members) now utilize the logged-in user's LDAP connection, resolving 'Insufficient Access Rights' issues previously encountered with a readonly service account.
  • Per-User LDAP Reads: All interactive read operations now also leverage the logged-in user's credentials, ensuring that Active Directory Access Control Lists (ACLs) are respected on a per-user basis.
  • Optional Service Account Configuration: The LDAP_READONLY_USER and LDAP_READONLY_PASSWORD environment variables are no longer mandatory. When omitted, the application performs authentication via AD UPN bind (user@domain) and disables the background LDAP cache.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • internal/ldap_cache/manager.go
    • Added PopulateGroupsForUserFromData to create full user objects with group memberships from provided data.
    • Added PopulateUsersForGroupFromData to create full group objects with members and parent groups from provided data.
    • Added PopulateGroupsForComputerFromData to create full computer objects with group memberships from provided data.
  • internal/options/app.go
    • Made readonly-user and readonly-password optional in application configuration.
  • internal/options/parse_test.go
    • Updated parsing tests to reflect that readonly-user and readonly-password are now optional.
  • internal/web/auth.go
    • Imported strings and ldap packages.
    • Modified loginHandler to use a new authenticateUser helper and store user credentials (DN, password, username) in the session.
    • Added authenticateUser function to verify credentials using either a service account or direct UPN bind.
    • Implemented authenticateViaUPNBind for authenticating users directly via UPN bind when no service account is configured.
    • Added domainFromBaseDN helper to extract the domain from an LDAP BaseDN string.
  • internal/web/computers.go
    • Imported ldap and ldap_cache packages.
    • Refactored computersHandler to use the logged-in user's LDAP connection for finding computers.
    • Refactored computerHandler to use the logged-in user's LDAP connection for finding computer details and groups.
    • Added findComputerByDN helper function to search for a computer by DN in a slice.
  • internal/web/groups.go
    • Refactored groupsHandler to use the logged-in user's LDAP connection for finding groups.
    • Refactored groupHandler to use the logged-in user's LDAP connection for loading group data.
    • Refactored groupModifyHandler to use the logged-in user's LDAP connection for performing group modifications.
    • Replaced findUnassignedUsers with filterUnassignedUsers and loadGroupData with loadGroupDataFromLDAP to operate on explicit LDAP client data.
    • Updated renderGroupWithError and renderGroupWithSuccess to a single renderGroupWithFlash using the user's LDAP connection.
    • Added filterUnassignedUsers helper to find users not in a given group.
    • Added findGroupByDN helper to search for a group by DN in a slice.
    • Modified performGroupModification to conditionally update the LDAP cache only if it is initialized.
  • internal/web/handlers_test.go
    • Added ldapConfig to the test application setup.
    • Refactored TestFindUnassignedGroupsFunction to TestFilterUnassignedGroups and TestFindUnassignedUsersFunction to TestFilterUnassignedUsers to test standalone filter functions.
    • Added TestDomainFromBaseDN to verify the domain extraction helper.
    • Removed descriptive comments from TestDNWithLDAPEscapeSequences and TestURLEncodingPreservesLDAPBackslash.
    • Added TestFindByDN, TestFindGroupByDN, and TestFindComputerByDN helper function tests.
  • internal/web/health.go
    • Modified healthHandler to provide simplified status when no service account is configured.
    • Modified readinessHandler to report ready status immediately when no service account is configured.
    • Modified livenessHandler to conditionally include uptime metrics only when the LDAP cache is available.
  • internal/web/health_test.go
    • Added setupHealthTestAppNoServiceAccount for testing scenarios without a service account.
    • Added TestHealthHandlerNoServiceAccount, TestLivenessHandlerNoServiceAccount, and TestReadinessHandlerNoServiceAccount.
  • internal/web/server.go
    • Updated App struct to include ldapOpts and made ldapReadonly and ldapCache optional (pointers).
    • Refactored NewApp to conditionally initialize ldapReadonly and ldapCache based on the presence of ReadonlyUser and ReadonlyPassword.
    • Modified Listen to conditionally run the ldapCache manager.
    • Modified Shutdown to conditionally stop the ldapCache manager.
    • Added getUserLDAP helper function to create an LDAP client bound with session credentials.
    • Modified poolStatsHandler to provide a message when no service account is configured.
    • Added findByDN helper function to search for a user by DN in a slice.
    • Refactored indexHandler to use getUserLDAP and retrieve user information via the user's LDAP connection.
  • internal/web/templates/logged_in.templ
    • Imported the version package.
    • Added a footer to the loggedIn template displaying project attribution and version information.
  • internal/web/users.go
    • Refactored usersHandler to use the logged-in user's LDAP connection for finding users.
    • Refactored userHandler to use the logged-in user's LDAP connection for loading user data.
    • Refactored userModifyHandler to use the logged-in user's LDAP connection for performing user modifications.
    • Replaced findUnassignedGroups with filterUnassignedGroups and loadUserData with loadUserDataFromLDAP to operate on explicit LDAP client data.
    • Updated renderUserWithError and renderUserWithSuccess to a single renderUserWithFlash using the user's LDAP connection.
    • Added filterUnassignedGroups helper to find groups a user is not a member of.
    • Modified performUserModification to conditionally update the LDAP cache only if it is initialized.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant architectural change to use the logged-in user's credentials for LDAP operations, fixing an "Insufficient Access Rights" bug and correctly enforcing per-user ACLs. However, a critical security risk has been identified: the storage of plaintext user passwords in the server-side session, which can lead to password exposure if the session store is compromised, especially with persistent storage. Additionally, there's a logic bug in the indexHandler's fallback mechanism that could impact performance and mask errors. The overall implementation is well-structured, with extensive changes across authentication and data handling, and the introduction of ...FromData functions is a clean approach.

Copy link

@entelligence-ai-pr-reviews entelligence-ai-pr-reviews bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walkthrough

This PR introduces support for operating without a service account by implementing per-user LDAP authentication via UPN bind. The application now supports two modes: traditional service account with background cache and connection pooling, or per-user credentials where each request uses the authenticated user's LDAP credentials. Configuration validation was relaxed to make readonly credentials optional. All web handlers were refactored to obtain user-specific LDAP connections instead of relying on shared cache. New utility functions enable relationship population from explicit data slices. Health endpoints gracefully handle both operational modes. Test coverage was expanded with unit tests for helper functions and service-account-less scenarios.

Changes

File(s) Summary
internal/options/app.go
internal/options/parse_test.go
Made readonly-user and readonly-password configuration parameters optional instead of mandatory; removed validation requirements and associated test cases; added comments explaining fallback to per-user credentials when service account is not configured.
internal/web/auth.go Implemented UPN (User Principal Name) bind authentication for Active Directory; added authenticateUser() method to route between service account and UPN bind flows; introduced authenticateViaUPNBind() for temporary LDAP connections using user credentials; added domainFromBaseDN() helper to extract DNS domain from LDAP BaseDN; modified login handler to store username and password in session.
internal/web/server.go Refactored to support optional service account configuration; made readonly LDAP client and background cache conditional; introduced getUserLDAP() method for creating per-user LDAP clients from session credentials; updated indexHandler() to use per-user credentials; added nil-checks for ldapCache and ldapReadonly throughout lifecycle methods; added findByDN() helper function.
internal/web/users.go
internal/web/groups.go
internal/web/computers.go
Refactored all handlers to use per-user LDAP connections via getUserLDAP() instead of shared cache; extracted helper functions for loading data, filtering, and finding entities by DN; added proper resource cleanup with deferred Close() calls; implemented nil-checks for optional cache invalidation; removed extensive documentation comments.
internal/ldap_cache/manager.go Added three new public utility functions (PopulateGroupsForUserFromData, PopulateUsersForGroupFromData, PopulateGroupsForComputerFromData) for populating LDAP entity relationships from explicit data slices without cache dependency; includes DN-based map optimization for O(1) lookups and disabled user filtering support.
internal/web/health.go
internal/web/health_test.go
Added graceful handling for scenarios without service account across health, readiness, and liveness endpoints; returns simplified status indicating 'per-user credentials' mode; added comprehensive test coverage with setupHealthTestAppNoServiceAccount() helper and three new test functions validating endpoint behavior without service account.
internal/web/handlers_test.go Replaced integration-style tests with unit tests for standalone filter functions; added comprehensive test coverage for helper functions including TestDomainFromBaseDN (7 test cases), TestFindByDN, TestFindGroupByDN, and TestFindComputerByDN; added ldapConfig field initialization in setupTestApp(); removed verbose comments while preserving test logic.

Sequence Diagram

This diagram shows the interactions between components:

sequenceDiagram
    participant Caller
    participant PopulateGroupsForUserFromData
    participant PopulateUsersForGroupFromData
    participant PopulateGroupsForComputerFromData
    participant FullLDAPUser
    participant FullLDAPGroup
    participant FullLDAPComputer

    Note over Caller: Three new data-driven population functions

    rect rgb(200, 220, 250)
        Note over Caller,FullLDAPUser: Populate User Groups Flow
        Caller->>PopulateGroupsForUserFromData: user, allGroups[]
        activate PopulateGroupsForUserFromData
        PopulateGroupsForUserFromData->>FullLDAPUser: Create empty FullLDAPUser
        PopulateGroupsForUserFromData->>PopulateGroupsForUserFromData: Get user DN
        loop For each group in allGroups
            loop For each memberDN in group.Members
                alt memberDN matches userDN
                    PopulateGroupsForUserFromData->>FullLDAPUser: Append group to Groups[]
                end
            end
        end
        PopulateGroupsForUserFromData-->>Caller: Return FullLDAPUser with groups
        deactivate PopulateGroupsForUserFromData
    end

    rect rgb(220, 250, 220)
        Note over Caller,FullLDAPGroup: Populate Group Members Flow
        Caller->>PopulateUsersForGroupFromData: group, allUsers[], allGroups[], showDisabled
        activate PopulateUsersForGroupFromData
        PopulateUsersForGroupFromData->>FullLDAPGroup: Create empty FullLDAPGroup
        PopulateUsersForGroupFromData->>PopulateUsersForGroupFromData: Build usersByDN map for O(1) lookup
        loop For each memberDN in group.Members
            PopulateUsersForGroupFromData->>PopulateUsersForGroupFromData: Lookup user by DN in map
            alt User found in map
                alt showDisabled OR user.Enabled
                    PopulateUsersForGroupFromData->>FullLDAPGroup: Append user to Members[]
                end
            end
        end
        PopulateUsersForGroupFromData->>PopulateUsersForGroupFromData: Build groupsByDN map for O(1) lookup
        loop For each parentDN in group.MemberOf
            PopulateUsersForGroupFromData->>PopulateUsersForGroupFromData: Lookup parent group by DN in map
            alt Parent group found in map
                PopulateUsersForGroupFromData->>FullLDAPGroup: Append parent to ParentGroups[]
            end
        end
        PopulateUsersForGroupFromData-->>Caller: Return FullLDAPGroup with members and parents
        deactivate PopulateUsersForGroupFromData
    end

    rect rgb(250, 220, 220)
        Note over Caller,FullLDAPComputer: Populate Computer Groups Flow
        Caller->>PopulateGroupsForComputerFromData: computer, allGroups[]
        activate PopulateGroupsForComputerFromData
        PopulateGroupsForComputerFromData->>FullLDAPComputer: Create empty FullLDAPComputer
        PopulateGroupsForComputerFromData->>PopulateGroupsForComputerFromData: Get computer DN
        loop For each group in allGroups
            loop For each memberDN in group.Members
                alt memberDN matches computerDN
                    PopulateGroupsForComputerFromData->>FullLDAPComputer: Append group to Groups[]
                end
            end
        end
        PopulateGroupsForComputerFromData-->>Caller: Return FullLDAPComputer with groups
        deactivate PopulateGroupsForComputerFromData
    end

    Note over Caller: All functions use DN matching<br/>to establish relationships
Loading

🔗 Cross-Repository Impact Analysis

Enable automatic detection of breaking changes across your dependent repositories. → Set up now

Learn more about Cross-Repository Analysis

What It Does

  • Automatically identifies repositories that depend on this code
  • Analyzes potential breaking changes across your entire codebase
  • Provides risk assessment before merging to prevent cross-repo issues

How to Enable

  1. Visit Settings → Code Management
  2. Configure repository dependencies
  3. Future PRs will automatically include cross-repo impact analysis!

Benefits

  • 🛡️ Prevent breaking changes across repositories
  • 🔍 Catch integration issues before they reach production
  • 📊 Better visibility into your multi-repo architecture

▶️AI Code Reviews for VS Code, Cursor, Windsurf
Install the extension

Note for Windsurf Please change the default marketplace provider to the following in the windsurf settings:

Marketplace Extension Gallery Service URL: https://marketplace.visualstudio.com/_apis/public/gallery

Marketplace Gallery Item URL: https://marketplace.visualstudio.com/items

Entelligence.ai can learn from your feedback. Simply add 👍 / 👎 emojis to teach it your preferences. More shortcuts below

Emoji Descriptions:

  • ⚠️ Potential Issue - May require further investigation.
  • 🔒 Security Vulnerability - Fix to ensure system safety.
  • 💻 Code Improvement - Suggestions to enhance code quality.
  • 🔨 Refactor Suggestion - Recommendations for restructuring code.
  • ℹ️ Others - General comments and information.

Interact with the Bot:

  • Send a message or request using the format:
    @entelligenceai + *your message*
Example: @entelligenceai Can you suggest improvements for this code?
  • Help the Bot learn by providing feedback on its responses.
    @entelligenceai + *feedback*
Example: @entelligenceai Do not comment on `save_auth` function !

Also you can trigger various commands with the bot by doing
@entelligenceai command

The current supported commands are

  1. config - shows the current config
  2. retrigger_review - retriggers the review

More commands to be added soon.

@CybotTM CybotTM force-pushed the feat/user-bound-ldap-credentials branch from b622210 to 9d6f380 Compare March 4, 2026 13:21
CybotTM and others added 6 commits March 4, 2026 17:02
The login page had a "Powered by netresearch/ldap-manager" footer but
all authenticated pages were missing it. Add a matching footer with
GitHub link and version info to the logged-in layout.
Fix "LDAP Result Code 50 Insufficient Access Rights" when modifying
groups/users by using the authenticated user's own LDAP connection
instead of the readonly service account.

All interactive LDAP operations (reads and writes) now use per-user
credentials stored in the server-side session. The readonly service
account becomes optional — when not configured, login authenticates
via AD UPN bind (user@domain) and background cache is disabled.
- Fix errcheck: use deferred closure for Close() calls
- Fix lll: break long function signatures
- Fix revive: rename memberDNs to memberDNS per Go conventions
- Fix dupl: extract assertNoServiceAccountStatusEndpoint helper
- Fix unparam: suppress for intentional utility function
- Fix indexHandler fallback: fail fast on real errors, only fall back on
  ErrUserNotFound
- Refactor cache Populate* methods to delegate to FromData functions,
  eliminating code duplication
- Set proper HTTP status codes in handle500 (500) and fourOhFourHandler (404)
- Redirect to login on LDAP bind failure (expired credentials)
- Sanitize error messages in modify handler flash responses
- Fix readinessHandler to report actual pool health status
- Return rate-limit errors as HTML login page instead of plain text
- Return Fiber shutdown error from Shutdown() instead of swallowing
- Remove dead code: Invalidate, InvalidateByPath, CacheMiddleware
- Merge duplicate invalidation functions into single method
- URL-encode DNs in redirect paths to prevent injection
- Move findUserByDN to users.go for better code organization
- Fix TemplateCache.Get() to use RLock for concurrent readers
- Remove unused accessedAt field (eviction uses createdAt)
- Regenerate session ID after login to prevent session fixation
- Validate username chars and domain in UPN bind authentication
- Fix Cache.Get() to return a copy preventing data races
- Use atomic.Bool for warmupComplete in cache manager
- Add graceful shutdown for periodicCacheLogging goroutine
- Document per-user credential operating mode in configuration guide

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
- Fix variable naming: memberDNs→memberDNS, memberGroupDNs→memberGroupDNS
- Extract health status string constants (goconst)
- Fix gofumpt formatting (trailing newlines, struct alignment)

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
@CybotTM CybotTM force-pushed the feat/user-bound-ldap-credentials branch from b484e32 to 8c1e42f Compare March 4, 2026 16:02
@CybotTM CybotTM added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit 8f9523e Mar 4, 2026
22 checks passed
@CybotTM CybotTM deleted the feat/user-bound-ldap-credentials branch March 4, 2026 16:08
github-merge-queue bot pushed a commit that referenced this pull request Mar 5, 2026
## Summary

Addresses critical bugs, security issues, and code quality findings from
10 independent code reviews of the per-user LDAP credentials feature
(#430).

### Bug Fixes
- **Slice mutation during iteration** in `OnRemoveUserFromGroup` — added
missing `break` after removal
- **Cache pointer escape** — `FindByDN`/`FindBySAMAccountName`/`Find`
now return value copies instead of pointers into cache internals
- **CSRF token cached in HTML** — detail pages (user/group/computer)
excluded from template cache, rendered directly
- **`TemplateCache.Stop()` double-close panic** — guarded with
`sync.Once`
- **`RenderWithCache` ignoring request context** — switched from
`context.Background()` to `c.UserContext()`
- **`handle500` recursion risk** — rewritten to handle all Fiber error
codes, prevent template render recursion, hide internal errors from
users
- **Rate limiter status code** — fixed middleware response chain

### Security
- **Username validation** — added LDAP filter metacharacters (`*`, `(`,
`)`, null byte) to rejection list
- **Error message leakage** — `handle500` now shows generic message
instead of internal error details

### Code Quality
- **Dead code removal** — `OptionalAuth`, `GetCachedManifest`,
`manifestCache`, `manifestMutex`, `App.logger`
- **`createCSRFConfig`** — returns `fiber.Handler` directly instead of
`*fiber.Handler`
- **Shutdown ordering** — drain Fiber first, then stop caches, then
close connections
- **`Filter` pre-allocation** — pre-allocates result slice capacity
- **Mock thread safety** — added `sync.Mutex` to test helper call count
tracking

## Test plan
- [x] All existing tests pass
- [x] `go vet` clean
- [x] `gofumpt` formatting applied
- [x] Race detector clean (`-race` on ldap_cache package)
- [x] Prettier check passes
- [ ] CI pipeline passes
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