{"record":{"id":"680c503356e41c32","repo":"hashicorp/nomad","slug":"cannot-toggle-global-mode","errorCode":null,"errorMessage":"cannot toggle global mode","messagePattern":"cannot toggle global mode","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/structs/acl.go","lineNumber":812,"sourceCode":"\t\t\t}\n\n\t\t\t// Create a time duration which details the time-til-expiry, so we can\n\t\t\t// check this against the regions max and min values.\n\t\t\texpiresIn := a.ExpirationTime.Sub(a.CreateTime)\n\t\t\tif expiresIn > maxTTL {\n\t\t\t\tmErr.Errors = append(mErr.Errors,\n\t\t\t\t\tfmt.Errorf(\"expiration time cannot be more than %s in the future (was %s)\",\n\t\t\t\t\t\tmaxTTL, expiresIn))\n\n\t\t\t} else if expiresIn < minTTL {\n\t\t\t\tmErr.Errors = append(mErr.Errors,\n\t\t\t\t\tfmt.Errorf(\"expiration time cannot be less than %s in the future (was %s)\",\n\t\t\t\t\t\tminTTL, expiresIn))\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tif existing.Global != a.Global {\n\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot toggle global mode\"))\n\t\t}\n\t\tif existing.ExpirationTTL != a.ExpirationTTL {\n\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot update expiration TTL\"))\n\t\t}\n\t\tif a.ExpirationTime != nil {\n\t\t\tif !existing.ExpirationTime.Equal(*a.ExpirationTime) {\n\t\t\t\tmErr.Errors = append(mErr.Errors, errors.New(\"cannot update expiration time\"))\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn mErr.ErrorOrNil()\n}\n\n// HasExpirationTime checks whether the ACL token has an expiration time value\n// set.\nfunc (a *ACLToken) HasExpirationTime() bool {","sourceCodeStart":794,"sourceCodeEnd":830,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/structs/acl.go#L794-L830","documentation":"A validation error from ACLToken.Validate on update: when updating an existing token, the Global flag is immutable. Nomad rejects an update whose Global value differs from the existing token's Global value.","triggerScenarios":"Calling ACL Upsert (update) on an existing token where the new token flips Global from true to false or vice versa.","commonSituations":"Scripts copying a global token into a local-region edit (or the reverse); UIs exposing the global checkbox on token edit forms; automation that normalizes tokens and rewrites the global field.","solutions":["Keep the existing token's Global value unchanged on update","If the region scope must change, delete and recreate the token with the desired Global setting","Set Global to the same value read from the existing token before submitting the update"],"exampleFix":"// before\nupdate := existing\nupdate.Global = !existing.Global // toggling scope\n// after\nupdate := existing\n// keep update.Global as-is; recreate token if scope must change\napiClient.ACL().Delete(existing.AccessorID, nil)\nnewToken.Global = desiredScope\napiClient.ACL().Create(newToken, nil)","handlingStrategy":"validation","validationCode":"if existing != nil && existing.Global != update.Global {\n    return errors.New(\"Global is immutable; recreate the token to change region scope\")\n}","typeGuard":"func globalFlagStable(existing, update *structs.ACLToken) bool {\n    return existing == nil || existing.Global == update.Global\n}","tryCatchPattern":"if err := update.Validate(minTTL, maxTTL, existing); err != nil {\n    if strings.Contains(err.Error(), \"cannot toggle global mode\") {\n        update.Global = existing.Global\n        err = update.Validate(minTTL, maxTTL, existing)\n    }\n}","preventionTips":["Copy the Global flag verbatim from the existing token on update","Hide/disable the global toggle in edit UIs; only show it at creation","To change scope, delete and recreate the token"],"tags":["nomad","acl","token","immutability","validation"],"backgroundTag":"immutable-field-modified","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}