mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-31 11:32:17 +00:00
enhance(admin): allow regenerating access tokens for bot accounts (#40109)
Bot accounts cannot sign in, so admins manage their access tokens, but the regenerate button was hidden for them. This adds an admin route to regenerate a bot's token. Token regeneration is now also recorded in the audit log. Fixes: https://github.com/go-gitea/gitea/issues/40074 Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
1 parent
08c5fcafd3
commit
365cb2bd5e
6 files changed
+41
-26
No files matched your search
@@ -91,6 +91,7 @@ var (
|
||||
UserOpenIDAdd = define("user:openid:add", "Associated OpenID {openid} to user {scope}.")
|
||||
UserOpenIDRemove = define("user:openid:remove", "Removed OpenID {openid} from user {scope}.")
|
||||
UserAccessTokenAdd = define("user:accesstoken:add", "Added access token {token} for user {scope} with scope {token_scope}.")
|
||||
UserAccessTokenRegenerate = define("user:accesstoken:regenerate", "Regenerated access token {token} of user {scope}.")
|
||||
UserAccessTokenRemove = define("user:accesstoken:remove", "Removed access token {token} from user {scope}.")
|
||||
UserOAuth2ApplicationAdd = define("user:oauth2application:add", "Added OAuth2 application {oauth2_application} for user {scope}.")
|
||||
UserOAuth2ApplicationUpdate = define("user:oauth2application:update", "Updated OAuth2 application {oauth2_application} of user {scope}.")
|
||||
|
||||
@@ -388,6 +388,12 @@ func DeleteBotToken(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func RegenerateBotToken(ctx *context.Context) {
|
||||
if u := getTargetBot(ctx); u != nil {
|
||||
user_setting.RegenerateAccessToken(ctx, u)
|
||||
}
|
||||
}
|
||||
|
||||
func editUserCommon(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
|
||||
@@ -167,16 +167,23 @@ func DeleteAccessToken(ctx *context.Context, owner *user_model.User) {
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
// RegenerateAccessToken response for regenerating a user's access token
|
||||
func RegenerateAccessToken(ctx *context.Context) {
|
||||
t, err := auth_model.RegenerateAccessToken(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
|
||||
// ApplicationsRegenerateToken response for regenerating the current user's access token
|
||||
func ApplicationsRegenerateToken(ctx *context.Context) {
|
||||
RegenerateAccessToken(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// RegenerateAccessToken regenerates an access token belonging to owner
|
||||
func RegenerateAccessToken(ctx *context.Context, owner *user_model.User) {
|
||||
t, err := auth_model.RegenerateAccessToken(ctx, ctx.FormInt64("id"), owner.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("RegenerateAccessToken", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.UserAccessTokenRegenerate, owner, "token", t.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
|
||||
ctx.Flash.Info(t.Token)
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
func loadApplicationsData(ctx *context.Context) {
|
||||
|
||||
+2
-1
@@ -695,7 +695,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Combo("").Get(user_setting.Applications).
|
||||
Post(user_setting.ApplicationsPost)
|
||||
m.Post("/delete", user_setting.DeleteApplication)
|
||||
m.Post("/regenerate", user_setting.RegenerateAccessToken)
|
||||
m.Post("/regenerate", user_setting.ApplicationsRegenerateToken)
|
||||
})
|
||||
|
||||
m.Combo("/keys").Get(user_setting.Keys).
|
||||
@@ -824,6 +824,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
||||
m.Post("/{userid}/access_tokens", admin.NewBotTokenPost)
|
||||
m.Post("/{userid}/access_tokens/delete", admin.DeleteBotToken)
|
||||
m.Post("/{userid}/access_tokens/regenerate", admin.RegenerateBotToken)
|
||||
m.Post("/{userid}/orgs/{org_id}/remove", admin.RemoveUserFromOrg)
|
||||
m.Post("/{userid}/orgs/remove-all", admin.RemoveUserFromAllOrgs)
|
||||
})
|
||||
|
||||
@@ -48,12 +48,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-trailing">
|
||||
{{if not $.IsBot}}
|
||||
<button type="button" class="ui tiny button link-action" data-modal-confirm="#regenerate-token" data-url="{{$.Link}}/regenerate?id={{$token.ID}}">
|
||||
{{svg "octicon-sync"}}
|
||||
{{ctx.Locale.Tr "settings.regenerate_token"}}
|
||||
</button>
|
||||
{{end}}
|
||||
<button type="button" class="ui tiny button link-action" data-modal-confirm="#regenerate-token" data-url="{{$.Link}}/regenerate?id={{$token.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={{$token.ID}}">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "settings.delete_token"}}
|
||||
@@ -112,14 +110,12 @@
|
||||
{{template "base/modal_actions_confirm"}}
|
||||
</dialog>
|
||||
|
||||
{{if not .IsBot}}
|
||||
<dialog class="modal-dialog" 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"}}
|
||||
</dialog>
|
||||
{{end}}
|
||||
<dialog class="modal-dialog" 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"}}
|
||||
</dialog>
|
||||
@@ -220,6 +220,7 @@ func TestAdminBotUser(t *testing.T) {
|
||||
assert.Empty(t, doc.Find("#password").Nodes)
|
||||
doc = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("/-/admin/users/%d", bot.ID)), http.StatusOK).Body)
|
||||
assert.NotEmpty(t, doc.Find(`form[action$="/access_tokens"]`).Nodes)
|
||||
assert.NotEmpty(t, doc.Find("dialog#regenerate-token").Nodes)
|
||||
})
|
||||
|
||||
t.Run("EditWithoutAuthSource", func(t *testing.T) {
|
||||
@@ -264,12 +265,15 @@ func TestAdminBotUser(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AccessToken{UID: 2, Name: "not-a-bot"})
|
||||
|
||||
token := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{UID: bot.ID, Name: "ci"})
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", tokenURL+"/delete", map[string]string{
|
||||
"id": strconv.FormatInt(token.ID, 10),
|
||||
}), http.StatusOK)
|
||||
tokenID := map[string]string{"id": strconv.FormatInt(token.ID, 10)}
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", tokenURL+"/regenerate", tokenID), http.StatusOK)
|
||||
assert.NotEqual(t, token.TokenHash, unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: token.ID}).TokenHash)
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/-/admin/users/2/access_tokens/regenerate", map[string]string{"id": "3"}), http.StatusBadRequest)
|
||||
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", tokenURL+"/delete", tokenID), http.StatusOK)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{UID: bot.ID}))
|
||||
|
||||
for _, action := range []audit_model.Action{audit_model.UserAccessTokenAdd, audit_model.UserAccessTokenRemove} {
|
||||
for _, action := range []audit_model.Action{audit_model.UserAccessTokenAdd, audit_model.UserAccessTokenRegenerate, audit_model.UserAccessTokenRemove} {
|
||||
events, _, err := audit_model.FindEvents(t.Context(), &audit_model.EventSearchOptions{Action: action, ScopeType: audit_model.ScopeUser, ScopeID: bot.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, events, 1, "audit events for %s", action)
|
||||
|
||||
Reference in new issue
Block a user