🐛 Bug Report: (LDAP) Checking membership for admin group is inconsistent with populating group memberships #624

Open
opened 2026-02-04 20:40:57 +03:00 by OVERLORD · 1 comment
Owner

Originally created by @chipschipschips on GitHub (Jan 24, 2026).

Reproduction steps

This sets up an LDAP account testadmin and LDAP group pocketid.admin. pocketid.admin will be the PocketID admin group.

  • Create testadmin user account in LDAP
    • DO NOT add a memberOf attribute for the pocketid.admin group
  • Create the pocketid.admin group in LDAP
    • DO add a member attribute for the testadmin user
  • Configure PocketID to sync from LDAP
    • Set LDAP_ADMIN_GROUP_NAME to pocketid.admin
  • Sync from LDAP

Full repro is available here.

Expected behavior

  • The testadmin user should have admin privileges in PocketID
    • The UI should show the admin pages
    • The is_admin field in the users table should be set to true

Actual Behavior

  • The testadmin user does not have admin privileges in PocketID
    • The UI does not show any admin pages
    • The is_admin is false in the users table in the database
  • The testadmin user does show as a member of the pocketid.admin group in the membership table in the database
    • (and in the UI if you can log in with an actual admin user; eg: created before enabling LDAP)

Pocket ID Version

v2.2.0

Database

SQLite and Postgres 18.

OS and Environment

Docker compose on Docker Desktop for macOS, served using Traefik.

Log Output

Logs don't show anything interesting.

pocketid-1  | Jan 24 06:05:58 INF Pocket ID is starting app=pocket-id version=2.2.0
pocketid-1  | Jan 24 06:05:58 INF Connected to database app=pocket-id version=2.2.0 provider=sqlite
pocketid-1  | Jan 24 06:05:58 WRN MAXMIND_LICENSE_KEY environment variable is empty: the GeoLite2 City database won't be updated app=pocket-id version=2.2.0
pocketid-1  | Jan 24 06:05:58 INF Acquired application lock app=pocket-id version=2.2.0 process_id=1 host_id=1eea36e92906
pocketid-1  | Jan 24 06:05:58 INF Starting job scheduler app=pocket-id version=2.2.0
pocketid-1  | Jan 24 06:05:58 INF Server listening app=pocket-id version=2.2.0 addr=0.0.0.0:1411
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOidcRefreshTokens id=bb6fe305-24cc-41ac-a72c-345e3ca66bb8
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOidcAuthorizationCodes id=00908340-8ec2-4221-a05e-cac4556786da
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SendHeartbeat id=b56c8a30-4ca8-47ba-96d1-ac4c4a952719
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SyncScim id=bc94e091-f303-453c-b780-58eae3790f6d
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearWebauthnSessions id=93d6206f-495b-4e58-beba-401c86df9af3
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOneTimeAccessTokens id=de39031a-f759-45c0-93e0-2c7cec1b71b3
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearReauthenticationTokens id=c8ce55ea-d977-49a0-873b-a370e79c80c7
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired OIDC refresh tokens app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOidcRefreshTokens id=bb6fe305-24cc-41ac-a72c-345e3ca66bb8
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired WebAuthn sessions app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearWebauthnSessions id=93d6206f-495b-4e58-beba-401c86df9af3
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=SyncScim id=bc94e091-f303-453c-b780-58eae3790f6d
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired reauthentication tokens app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearReauthenticationTokens id=c8ce55ea-d977-49a0-873b-a370e79c80c7
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired OIDC authorization codes app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOidcAuthorizationCodes id=00908340-8ec2-4221-a05e-cac4556786da
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearSignupTokens id=a2548668-68cb-492d-bf48-e3fa279550e6
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearEmailVerificationTokens id=a4679bf4-20c5-4910-b6df-c3a75c37755b
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SyncLdap id=5cba9e4a-42bc-4088-b26f-a1da9c6b1536
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired one-time access tokens app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOneTimeAccessTokens id=de39031a-f759-45c0-93e0-2c7cec1b71b3
pocketid-1  | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearAuditLogs id=4149ba49-84ff-4b9e-825c-02ea7046c620
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired email verification tokens app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearEmailVerificationTokens id=a4679bf4-20c5-4910-b6df-c3a75c37755b
pocketid-1  | Jan 24 06:05:58 INF Deleted old audit logs app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearAuditLogs id=4149ba49-84ff-4b9e-825c-02ea7046c620
pocketid-1  | Jan 24 06:05:58 INF Cleaned expired tokens app=pocket-id version=2.2.0 count=0
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearSignupTokens id=a2548668-68cb-492d-bf48-e3fa279550e6
pocketid-1  | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=SyncLdap id=5cba9e4a-42bc-4088-b26f-a1da9c6b1536
pocketid-1  | Jan 24 06:06:00 ERR Job failed with error app=pocket-id version=2.2.0 name=SendHeartbeat id=b56c8a30-4ca8-47ba-96d1-ac4c4a952719 error="heartbeat request failed: request failed with status code: 429"
Originally created by @chipschipschips on GitHub (Jan 24, 2026). ### Reproduction steps This sets up an LDAP account `testadmin` and LDAP group `pocketid.admin`. `pocketid.admin` will be the PocketID admin group. - Create `testadmin` user account in LDAP - DO NOT add a `memberOf` attribute for the `pocketid.admin` group - Create the `pocketid.admin` group in LDAP - DO add a `member` attribute for the `testadmin` user - Configure PocketID to sync from LDAP - Set `LDAP_ADMIN_GROUP_NAME` to `pocketid.admin` - Sync from LDAP Full repro is available [here](https://github.com/chipschipschips/repro-pocketid-isadmin-ldap-bug). ### Expected behavior - The `testadmin` user should have admin privileges in PocketID - The UI should show the admin pages - The `is_admin` field in the `users` table should be set to `true` ### Actual Behavior - The `testadmin` user does not have admin privileges in PocketID - The UI does not show any admin pages - The `is_admin` is `false` in the `users` table in the database - The `testadmin` user does show as a member of the `pocketid.admin` group in the `membership` table in the database - (and in the UI if you can log in with an actual admin user; eg: created before enabling LDAP) ### Pocket ID Version v2.2.0 ### Database SQLite and Postgres 18. ### OS and Environment Docker compose on Docker Desktop for macOS, served using Traefik. ### Log Output Logs don't show anything interesting. ``` pocketid-1 | Jan 24 06:05:58 INF Pocket ID is starting app=pocket-id version=2.2.0 pocketid-1 | Jan 24 06:05:58 INF Connected to database app=pocket-id version=2.2.0 provider=sqlite pocketid-1 | Jan 24 06:05:58 WRN MAXMIND_LICENSE_KEY environment variable is empty: the GeoLite2 City database won't be updated app=pocket-id version=2.2.0 pocketid-1 | Jan 24 06:05:58 INF Acquired application lock app=pocket-id version=2.2.0 process_id=1 host_id=1eea36e92906 pocketid-1 | Jan 24 06:05:58 INF Starting job scheduler app=pocket-id version=2.2.0 pocketid-1 | Jan 24 06:05:58 INF Server listening app=pocket-id version=2.2.0 addr=0.0.0.0:1411 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOidcRefreshTokens id=bb6fe305-24cc-41ac-a72c-345e3ca66bb8 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOidcAuthorizationCodes id=00908340-8ec2-4221-a05e-cac4556786da pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SendHeartbeat id=b56c8a30-4ca8-47ba-96d1-ac4c4a952719 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SyncScim id=bc94e091-f303-453c-b780-58eae3790f6d pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearWebauthnSessions id=93d6206f-495b-4e58-beba-401c86df9af3 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearOneTimeAccessTokens id=de39031a-f759-45c0-93e0-2c7cec1b71b3 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearReauthenticationTokens id=c8ce55ea-d977-49a0-873b-a370e79c80c7 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired OIDC refresh tokens app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOidcRefreshTokens id=bb6fe305-24cc-41ac-a72c-345e3ca66bb8 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired WebAuthn sessions app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearWebauthnSessions id=93d6206f-495b-4e58-beba-401c86df9af3 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=SyncScim id=bc94e091-f303-453c-b780-58eae3790f6d pocketid-1 | Jan 24 06:05:58 INF Cleaned expired reauthentication tokens app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearReauthenticationTokens id=c8ce55ea-d977-49a0-873b-a370e79c80c7 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired OIDC authorization codes app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOidcAuthorizationCodes id=00908340-8ec2-4221-a05e-cac4556786da pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearSignupTokens id=a2548668-68cb-492d-bf48-e3fa279550e6 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearEmailVerificationTokens id=a4679bf4-20c5-4910-b6df-c3a75c37755b pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=SyncLdap id=5cba9e4a-42bc-4088-b26f-a1da9c6b1536 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired one-time access tokens app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearOneTimeAccessTokens id=de39031a-f759-45c0-93e0-2c7cec1b71b3 pocketid-1 | Jan 24 06:05:58 INF Starting job app=pocket-id version=2.2.0 name=ClearAuditLogs id=4149ba49-84ff-4b9e-825c-02ea7046c620 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired email verification tokens app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearEmailVerificationTokens id=a4679bf4-20c5-4910-b6df-c3a75c37755b pocketid-1 | Jan 24 06:05:58 INF Deleted old audit logs app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearAuditLogs id=4149ba49-84ff-4b9e-825c-02ea7046c620 pocketid-1 | Jan 24 06:05:58 INF Cleaned expired tokens app=pocket-id version=2.2.0 count=0 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=ClearSignupTokens id=a2548668-68cb-492d-bf48-e3fa279550e6 pocketid-1 | Jan 24 06:05:58 INF Job run successfully app=pocket-id version=2.2.0 name=SyncLdap id=5cba9e4a-42bc-4088-b26f-a1da9c6b1536 pocketid-1 | Jan 24 06:06:00 ERR Job failed with error app=pocket-id version=2.2.0 name=SendHeartbeat id=b56c8a30-4ca8-47ba-96d1-ac4c4a952719 error="heartbeat request failed: request failed with status code: 429" ```
Author
Owner

@chipschipschips commented on GitHub (Jan 24, 2026):

I think the problem is here in SyncUsers, where it checks for membership of the admin group by looking only at the
memberOf attributes on the user object.

// Check if user is admin by checking if they are in the admin group
isAdmin := false
for _, group := range value.GetAttributeValues("memberOf") {
  if getDNProperty(dbConfig.LdapAttributeGroupName.Value, group) == dbConfig.LdapAdminGroupName.Value {
    isAdmin = true
    break
  }
}

Contrast this to how group membership is established in SyncGroups, where it looks at the member attributes in the group to populate the membership table.

// Get group members and add to the correct Group
groupMembers := value.GetAttributeValues(dbConfig.LdapAttributeGroupMember.Value)
membersUserId := make([]string, 0, len(groupMembers))
for _, member := range groupMembers {
  username := getDNProperty(dbConfig.LdapAttributeUserUsername.Value, member)

  ...
}

...

  _, err = s.groupService.updateInternal(ctx, databaseGroup.ID, syncGroup, true, tx)
  if err != nil {
    return fmt.Errorf("failed to update group '%s': %w", syncGroup.Name, err)
  }

  _, err = s.groupService.updateUsersInternal(ctx, databaseGroup.ID, membersUserId, tx)
  if err != nil {
    return fmt.Errorf("failed to sync users for group '%s': %w", syncGroup.Name, err)
  }
@chipschipschips commented on GitHub (Jan 24, 2026): I think the problem is [here in `SyncUsers`](https://github.com/pocket-id/pocket-id/blob/317879bb378b2a70e1f81f3a7163d89b7126dbf0/backend/internal/service/ldap_service.go#L371-L378), where it checks for membership of the admin group by looking only at the `memberOf` attributes on the user object. ```go // Check if user is admin by checking if they are in the admin group isAdmin := false for _, group := range value.GetAttributeValues("memberOf") { if getDNProperty(dbConfig.LdapAttributeGroupName.Value, group) == dbConfig.LdapAdminGroupName.Value { isAdmin = true break } } ``` Contrast this to how group membership is established in [`SyncGroups`](https://github.com/pocket-id/pocket-id/blob/317879bb378b2a70e1f81f3a7163d89b7126dbf0/backend/internal/service/ldap_service.go#L181-L264), where it looks at the member attributes in the _group_ to populate the membership table. ```go // Get group members and add to the correct Group groupMembers := value.GetAttributeValues(dbConfig.LdapAttributeGroupMember.Value) membersUserId := make([]string, 0, len(groupMembers)) for _, member := range groupMembers { username := getDNProperty(dbConfig.LdapAttributeUserUsername.Value, member) ... } ... _, err = s.groupService.updateInternal(ctx, databaseGroup.ID, syncGroup, true, tx) if err != nil { return fmt.Errorf("failed to update group '%s': %w", syncGroup.Name, err) } _, err = s.groupService.updateUsersInternal(ctx, databaseGroup.ID, membersUserId, tx) if err != nil { return fmt.Errorf("failed to sync users for group '%s': %w", syncGroup.Name, err) } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/pocket-id#624