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
+1 -110
View File
@@ -1,94 +1,6 @@
{{template "user/settings/layout_head" (dict "pageClass" "user settings applications")}}
<div class="user-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.manage_access_token"}}
</h4>
<div class="ui attached segment">
<div class="flex-divided-list items-with-main">
<div class="item">
{{ctx.Locale.Tr "settings.tokens_desc"}}
</div>
{{range .Tokens}}
<div class="item">
<div class="item-leading">
<span class="{{if .HasRecentActivity}}tw-text-green{{end}}" {{if .HasRecentActivity}}data-tooltip-content="{{ctx.Locale.Tr "settings.token_state_desc"}}"{{end}}>
{{svg "fontawesome-send" 32}}
</span>
</div>
<div class="item-main">
<details>
<summary><span class="item-title">{{.Name}}</span></summary>
<p class="tw-my-1">
{{ctx.Locale.Tr "settings.repo_and_org_access"}}:
{{if .DisplayPublicOnly}}
{{ctx.Locale.Tr "settings.permissions_public_only"}}
{{else}}
{{ctx.Locale.Tr "settings.permissions_access_all"}}
{{end}}
</p>
<p class="tw-my-1">{{ctx.Locale.Tr "settings.permissions_list"}}</p>
<ul class="tw-my-1">
{{range .Scope.StringSlice}}
{{if (ne . $.AccessTokenScopePublicOnly)}}
<li>{{.}}</li>
{{end}}
{{end}}
</ul>
</details>
<div class="item-body">
<i>{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}{{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="tw-text-green"{{end}}>{{DateUtils.AbsoluteShort .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i>
</div>
</div>
<div class="item-trailing">
<button type="button" class="ui tiny button link-action" data-modal-confirm="#regenerate-token" data-url="{{$.Link}}/regenerate?id={{.ID}}">
{{svg "octicon-sync"}}
{{ctx.Locale.Tr "settings.regenerate_token"}}
</button>
<button type="button" class="ui red tiny button link-action" data-modal-confirm="#delete-token" data-url="{{$.Link}}/delete?id={{.ID}}">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_token"}}
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="ui bottom attached segment">
<details {{if or .name (not .Tokens)}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.generate_new_token"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
<div class="field {{if .Err_Name}}error{{end}}">
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
<input id="name" name="name" value="{{.name}}" required maxlength="255">
</div>
<div class="field">
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
<label class="gt-checkbox">
<input type="radio" name="scope-public-only" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
</label>
<label class="gt-checkbox">
<input type="radio" name="scope-public-only" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
</label>
</div>
<div>
<div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (HTMLFormat `href="%s" target="_blank"` "https://docs.gitea.com/development/oauth2-provider#scopes")}}</div>
<table class="ui table unstackable tw-my-2">
{{range $category := .TokenCategories}}
<tr>
<td>{{$category}}</td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
</tr>
{{end}}
</table>
</div>
<button type="submit" class="ui primary button">
{{ctx.Locale.Tr "settings.generate_token"}}
</button>
</form>
</details>
</div>
{{template "shared/user/access_tokens" .AccessTokens}}
{{if .EnableOAuth2}}
{{template "user/settings/grants_oauth2" .}}
@@ -96,25 +8,4 @@
{{end}}
</div>
<div class="ui small modal" id="delete-token">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.access_token_deletion"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.access_token_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm"}}
</div>
<div class="ui small modal" id="regenerate-token">
<div class="header">
{{ctx.Locale.Tr "settings.access_token_regeneration"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.access_token_regeneration_desc"}}</p>
</div>
{{template "base/modal_actions_confirm"}}
</div>
{{template "user/settings/layout_footer" .}}
+1 -1
View File
@@ -17,7 +17,7 @@
{{ctx.AvatarUtils.Avatar . 28 "mini"}}
</div>
<div class="item-main">
<div class="item-title">{{template "shared/user/name" .}}</div>
<div class="item-title">{{template "shared/user/name" .AsUser}}</div>
<div class="flex-text-body">
{{.Description}}
</div>