{"record":{"id":"a67bf3fbe519815e","repo":"juanfont/headscale","slug":"w-got-q","errorCode":null,"errorMessage":"%w, got: %q","messagePattern":"%w, got: %q","errorType":"validation","errorClass":"ErrInvalidUsername","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":346,"sourceCode":"\t\t\tif !seen.Contains(s) {\n\t\t\t\tseen.Add(s)\n\t\t\t\troutes = append(routes, s)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn routes\n}\n\n// Username is a string that represents a username, it must contain an @.\ntype Username string\n\nfunc (u *Username) Validate() error {\n\tif isUser(string(*u)) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"%w, got: %q\", ErrInvalidUsername, *u)\n}\n\nfunc (u *Username) String() string {\n\treturn string(*u)\n}\n\n// MarshalJSON marshals the Username to JSON.\nfunc (u *Username) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(string(*u))\n}\n\n// MarshalJSON marshals the Prefix to JSON.\nfunc (p *Prefix) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(p.String())\n}\n\nfunc (u *Username) UnmarshalJSON(b []byte) error {\n\t*u = Username(strings.Trim(string(b), `\"`))","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L328-L364","documentation":"Username.Validate rejected a policy username because it does not contain '@' (the isUser check failed). In headscale's v2 policy, a username alias is always an email-style token; this error fires during policy unmarshalling/validation, before any user lookup happens.","triggerScenarios":"A grants/tests entry uses \"alice\", \"alice@\", or a bare LDAP uid without a domain as src/dst/user. Username.UnmarshalJSON -> Validate fails via isUser(string) == false.","commonSituations":"Migrating from ACL systems that allow bare usernames; using local-part-only names from an OIDC provider that does not supply emails; typos deleting the domain.","solutions":["Use the full email, e.g. 'alice@example.com'.","If you intended a group, use 'group:engineering'; for a device label use 'tag:name'.","If your IdP users genuinely lack emails, map them to groups or tags instead of username aliases."],"exampleFix":"// before\n{\"action\": \"accept\", \"src\": [\"alice\"], \"dst\": [\"web:80\"]}\n\n// after\n{\"action\": \"accept\", \"src\": [\"alice@example.com\"], \"dst\": [\"web:80\"]}","handlingStrategy":"validation","validationCode":"// Cheap pre-flight: a username alias must contain '@'.\nfunc isValidUsernameAlias(s string) bool { return strings.Contains(s, \"@\") }","typeGuard":null,"tryCatchPattern":"if err := policy.Unmarshal(data); err != nil {\n    if errors.Is(err, v2.ErrInvalidUsername) {\n        // point the user at the offending token in the message ('got: ...')\n    }\n    return err\n}","preventionTips":["Always use full emails as user aliases.","If names lack a domain, wrap them in groups or tags instead.","Run 'headscale policy check' (or equivalent loader) before applying ACL changes."],"tags":["policy","acl","validation","username","configuration","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}