Skip to content

broker: deduplicate extra/owner extra groups to prevent UNIQUE-constraint failure on offline retry#1496

Open
yetanotheralex wants to merge 1 commit intocanonical:mainfrom
yetanotheralex:dedupe-extra-groups-on-cached-userinfo
Open

broker: deduplicate extra/owner extra groups to prevent UNIQUE-constraint failure on offline retry#1496
yetanotheralex wants to merge 1 commit intocanonical:mainfrom
yetanotheralex:dedupe-extra-groups-on-cached-userinfo

Conversation

@yetanotheralex
Copy link
Copy Markdown

Fixes #1495.

What

Broker.finishAuth now deduplicates b.cfg.extraGroups and b.cfg.ownerExtraGroups against authInfo.UserInfo.Groups before appending them, so configured local groups can no longer be added more than once when the cached authInfo already contains them.

Why

On the online auth path the calling code resets authInfo.UserInfo.Groups to a fresh provider response before finishAuth runs, so the unconditional append never produces duplicates. On the offline path that reset is skipped (no provider call possible), so authInfo is loaded from cache with the previous extras already present and the append silently doubles them.

The daemon (authd) then does a transactional DELETE; INSERT VALUES (?, ?), (?, ?), … against users_to_local_groups and the duplicate rows collide intra-transaction on the (uid, group_name) UNIQUE constraint. The user is rejected at GDM and (worse) ends up with no local groups recorded at all once the transaction rolls back.

See #1495 for the full root-cause writeup.

Test

Manually verified on a single-user laptop with owner_extra_groups = sudo, docker, dialout:

  1. Online sign-in: cached Groups slice contains sudo, docker, dialout exactly once. id <user> matches.
  2. nmcli networking off, sign in again: succeeds. Cached slice unchanged. Daemon's users_to_local_groups rows unchanged.
  3. Repeat offline sign-in 5×: still works, no duplicates ever introduced.

Without this patch, step 2 produces the UNIQUE constraint failed: users_to_local_groups.uid, users_to_local_groups.group_name error and step 3 never gets started. Patch has been running on my daily driver for ~24h with no regressions.

Out of scope (suggested follow-up)

The daemon-side handleUsersToLocalGroupsUpdate would benefit from INSERT … ON CONFLICT DO NOTHING (or its own dedupe pass) so that a misbehaving broker can't take user provisioning down. Happy to file a separate issue for that if useful.

…ed UserInfo

Build a set of group names already in `authInfo.UserInfo.Groups` before appending
configured `extraGroups` and `ownerExtraGroups`, so we never add the same group
twice when the cached UserInfo already contains it.

On the online auth path the calling code resets `authInfo.UserInfo.Groups` to a
fresh provider response before `finishAuth` runs, so the unconditional append
never produces duplicates. On the offline path that reset is skipped (no
provider call possible), so `authInfo` is loaded from cache *with the previous
extras already present* and the append silently doubles them. The daemon then
does a transactional `DELETE; INSERT VALUES (?, ?), (?, ?), ...` against
`users_to_local_groups` and the duplicates collide intra-transaction on the
`(uid, group_name)` UNIQUE constraint, rejecting the user at GDM and rolling
back the local groups for them entirely.

Reproducible with `owner_extra_groups = sudo, docker, dialout` in broker.conf:
sign in online (works, groups recorded once), `nmcli networking off`, sign in
again -> "Authentication failure" + "UNIQUE constraint failed:
users_to_local_groups.uid, users_to_local_groups.group_name" in
`journalctl -u authd`.
Copy link
Copy Markdown
Member

@nooreldeenmansour nooreldeenmansour left a comment

Choose a reason for hiding this comment

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

Thanks a lot @yetanotheralex, nice work!

I've left a few small comments. Please add a test in broker_test.go that covers this fix :)

A few other things:

// every offline retry would append another copy of the configured local groups, which then
// explodes the daemon's UNIQUE constraint on users_to_local_groups (uid, group_name) because
// handleUsersToLocalGroupsUpdate does DELETE-then-INSERT and the duplicates collide intra-tx.
existing := make(map[string]struct{}, len(authInfo.UserInfo.Groups))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
existing := make(map[string]struct{}, len(authInfo.UserInfo.Groups))
existingGroups := make(map[string]struct{}, len(authInfo.UserInfo.Groups))

for _, g := range authInfo.UserInfo.Groups {
existing[g.Name] = struct{}{}
}
addExtra := func(name string) {
Copy link
Copy Markdown
Member

@nooreldeenmansour nooreldeenmansour Apr 29, 2026

Choose a reason for hiding this comment

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

The old paths used to log Adding extra group %q for the normal path, and Adding owner extra group %q for the owner path. To preserve those, I suggest passing a groupLabel string and building the log line inside the function:

Suggested change
addExtra := func(name string) {
addConfiguredGroup := func(name, groupLabel string) {

addConfiguredGroup is also more descriptive than addExtra.

Update L950 to log.Debugf(context.Background(), "Adding %s %q", groupLabel, name) and call it as addConfiguredGroup(g, "extra group") / addConfiguredGroup(g, "owner extra group").

Comment on lines +937 to +941
// Build a set of group names already in the user info (e.g. from cached/loaded auth state)
// so we don't append a configured extra/owner group that's already in the slice. Without this,
// every offline retry would append another copy of the configured local groups, which then
// explodes the daemon's UNIQUE constraint on users_to_local_groups (uid, group_name) because
// handleUsersToLocalGroupsUpdate does DELETE-then-INSERT and the duplicates collide intra-tx.
Copy link
Copy Markdown
Member

@nooreldeenmansour nooreldeenmansour Apr 29, 2026

Choose a reason for hiding this comment

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

The comment is overly descriptive, I suggest keeping it short, perhaps something like

// Deduplicate to avoid UNIQUE constraint violations on offline logins

your commit message is already descriptive enough, so that's sufficient imo

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.

authd-oidc: offline login fails with UNIQUE constraint on users_to_local_groups after first online sign-in when owner_extra_groups is set

2 participants