{"record":{"id":"ea09d278bcdc3c9a","repo":"juanfont/headscale","slug":"w-token-q","errorCode":null,"errorMessage":"%w: token %q","messagePattern":"%w: token %q","errorType":"validation","errorClass":"ErrUserNotFound","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":409,"sourceCode":"\t// At parsetime, we require all usernames to contain an \"@\" character, if the\n\t// username token does not naturally do so (like email), the user have to\n\t// add it to the end of the username. We strip it here as we do not expect the\n\t// usernames to be stored with the \"@\".\n\tuTrimmed := strings.TrimSuffix(u.String(), \"@\")\n\n\tfor _, user := range users {\n\t\tif user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == uTrimmed {\n\t\t\t// Prioritize ProviderIdentifier match and exit early\n\t\t\treturn user, nil\n\t\t}\n\n\t\tif user.Email == uTrimmed || user.Name == uTrimmed {\n\t\t\tpotentialUsers = append(potentialUsers, user)\n\t\t}\n\t}\n\n\tif len(potentialUsers) == 0 {\n\t\treturn types.User{}, fmt.Errorf(\"%w: token %q\", ErrUserNotFound, u.String())\n\t}\n\n\tif len(potentialUsers) > 1 {\n\t\treturn types.User{}, fmt.Errorf(\"%w: token %q found: %s\", ErrMultipleUsersFound, u.String(), potentialUsers.String())\n\t}\n\n\treturn potentialUsers[0], nil\n}\n\nfunc (u *Username) Resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (ResolvedAddresses, error) {\n\treturn newResolvedAddresses(u.resolve(nil, users, nodes))\n}\n\nfunc (u *Username) resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (*netipx.IPSet, error) {\n\tvar (\n\t\tips  netipx.IPSetBuilder\n\t\terrs []error\n\t)","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L391-L427","documentation":"Username.resolve searched all users by ProviderIdentifier, Email, and Name and found no match for the policy token. The username is well-formed (has '@') but does not correspond to any user in the database the policy was compiled against.","triggerScenarios":"Policy references 'bob@old.example.com' after the user's email changed; a user deleted via 'headscale users destroy'; a fresh database where tests reference users never created; ProviderIdentifier mismatch after IdP migration.","commonSituations":"Renaming users or changing the OIDC provider without updating policy files; policy files shared across environments (prod ACL in a test DB); typos in the domain part.","solutions":["Verify the exact identifier: run 'headscale users list' and compare email/name/ProviderIdentifier.","Update the policy token to the current email or username.","If the user should exist, recreate it or re-run OIDC login so the user row is populated.","Prefer groups for stable policies so individual renames do not break ACLs."],"exampleFix":"// before\n{\"src\": [\"bob@old.example.com\"], \"dst\": [\"web:80\"]}\n\n// after\n{\"src\": [\"bob@new.example.com\"], \"dst\": [\"web:80\"]}","handlingStrategy":"try-catch","validationCode":"// Pre-check the token against the user DB before compiling the policy.\nif _, err := findUserByToken(users, token); err != nil {\n    return fmt.Errorf(\"policy token %q matches no user; run 'headscale users list'\", token)\n}","typeGuard":null,"tryCatchPattern":"user, err := username.Resolve(pol, users, nodes)\nif err != nil {\n    if errors.Is(err, v2.ErrUserNotFound) {\n        // missing user: sync DB or update policy; do not silently continue\n        return fmt.Errorf(\"policy references unknown user %q: %w\", username, err)\n    }\n    return err\n}","preventionTips":["Keep policy files and user directory in the same repo/review stream so renames update both.","Prefer groups over individual usernames in grants.","Add a CI step that compiles the policy against a production DB snapshot."],"tags":["policy","acl","user-lookup","configuration","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}