feat: manage bot accounts from the admin UI, API and CLI (#38966)

Adds first-class bot accounts (`UserTypeBot`): local, password-less
users for automation that authenticate only with access tokens.

1. Admin UI: create bots, filter users by type, manage a bot's access
tokens, convert between user and bot
2. API: `POST /admin/users/{username}/convert-type`, and user objects
gain a GitHub-compatible `type` (`User`, `Organization`, `Bot`)
3. CLI: `gitea admin user change-type`, `--user-type` accepts `User` or
`Bot` case-insensitively
4. Converting keeps the password, 2FA, OAuth2 grants and access tokens,
and since sign-in rejects bots, converting back restores the account.
Only local, non-admin accounts can be converted, and conversions are
audited
5. Session, reverse proxy, SSPI, external source and password reset
sign-in reject non-individual users, so a bot never gets an interactive
session
6. Bots receive no notifications or emails

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: joestump <joe@joestump.net>
Co-authored-by: Joe Stump <joe@stu.mp>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
Joe (Agent) Stump
2026-09-18 12:43:36 +00:00
committed by GitHub
co-authored by Nicolas joestump Joe Stump silverwind Lunny Xiao
parent db7dbd5a6b
commit 3bec08f998
71 changed files with 1246 additions and 330 deletions
+8 -12
View File
@@ -44,8 +44,8 @@ func microcmdUserCreate() *cli.Command {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user-type",
Usage: "Set user's type: individual or bot",
Value: "individual",
Usage: "Set user's type: User or Bot",
Value: "User",
},
&cli.StringFlag{
Name: "password",
@@ -105,20 +105,16 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
}
userType, ok := userTypes[c.String("user-type")]
if !ok {
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
userType, err := parseUserTypeFlag(c.String("user-type"))
if err != nil {
return err
}
if userType != user_model.UserTypeIndividual {
// Some other commands like "change-password" also only support individual users.
// Some other commands like "change-password" also only support regular user accounts.
// It needs to clarify the "password" behavior for bot users in the future.
// At the moment, we do not allow setting password for bot users.
if c.IsSet("password") || c.IsSet("random-password") {
return errors.New("password can only be set for individual users")
return errors.New("password can only be set for user accounts")
}
}
@@ -162,7 +158,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
mustChangePassword := userType == user_model.UserTypeIndividual
if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for individual users")
return errors.New("must-change-password flag can only be set for user accounts")
}
// if the flag is set, use the value provided by the user
mustChangePassword = c.Bool("must-change-password")