Skip to content

Commit 2173f14

Browse files
authored
Add user webhooks (#21563)
Currently we can add webhooks for organizations but not for users. This PR adds the latter. You can access it from the current users settings. ![grafik](https://user-images.githubusercontent.com/1666336/197391408-15dfdc23-b476-4d0c-82f7-9bc9b065988f.png)
1 parent dad057b commit 2173f14

File tree

28 files changed

+737
-234
lines changed

28 files changed

+737
-234
lines changed

docs/content/doc/developers/oauth2-provider.en-us.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Gitea supports the following scopes for tokens:
6060
|     **write:public_key** | Grant read/write access to public keys |
6161
|     **read:public_key** | Grant read-only access to public keys |
6262
| **admin:org_hook** | Grants full access to organizational-level hooks |
63+
| **admin:user_hook** | Grants full access to user-level hooks |
6364
| **notification** | Grants full access to notifications |
6465
| **user** | Grants full access to user profile info |
6566
|     **read:user** | Grants read access to user's profile |

models/auth/token_scope.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const (
3232

3333
AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook"
3434

35+
AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook"
36+
3537
AccessTokenScopeNotification AccessTokenScope = "notification"
3638

3739
AccessTokenScopeUser AccessTokenScope = "user"
@@ -64,7 +66,7 @@ type AccessTokenScopeBitmap uint64
6466
const (
6567
// AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`.
6668
AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits |
67-
AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits |
69+
AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits |
6870
AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits |
6971
AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits
7072

@@ -86,6 +88,8 @@ const (
8688

8789
AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota
8890

91+
AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota
92+
8993
AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota
9094

9195
AccessTokenScopeUserBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits
@@ -123,6 +127,7 @@ var allAccessTokenScopes = []AccessTokenScope{
123127
AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey,
124128
AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook,
125129
AccessTokenScopeAdminOrgHook,
130+
AccessTokenScopeAdminUserHook,
126131
AccessTokenScopeNotification,
127132
AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow,
128133
AccessTokenScopeDeleteRepo,
@@ -147,6 +152,7 @@ var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{
147152
AccessTokenScopeWriteRepoHook: AccessTokenScopeWriteRepoHookBits,
148153
AccessTokenScopeReadRepoHook: AccessTokenScopeReadRepoHookBits,
149154
AccessTokenScopeAdminOrgHook: AccessTokenScopeAdminOrgHookBits,
155+
AccessTokenScopeAdminUserHook: AccessTokenScopeAdminUserHookBits,
150156
AccessTokenScopeNotification: AccessTokenScopeNotificationBits,
151157
AccessTokenScopeUser: AccessTokenScopeUserBits,
152158
AccessTokenScopeReadUser: AccessTokenScopeReadUserBits,
@@ -263,7 +269,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
263269
scope := AccessTokenScope(strings.Join(scopes, ","))
264270
scope = AccessTokenScope(strings.ReplaceAll(
265271
string(scope),
266-
"repo,admin:org,admin:public_key,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application",
272+
"repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application",
267273
"all",
268274
))
269275
return scope

models/auth/token_scope_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
4040
{"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil},
4141
{"admin:application,write:application,user", "user,admin:application", nil},
4242
{"all", "all", nil},
43-
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil},
44-
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil},
43+
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil},
44+
{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil},
4545
}
4646

4747
for _, test := range tests {

models/fixtures/webhook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
-
1818
id: 3
19-
org_id: 3
19+
owner_id: 3
2020
repo_id: 3
2121
url: www.example.com/url3
2222
content_type: 1 # json

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ var migrations = []Migration{
467467

468468
// v244 -> v245
469469
NewMigration("Add NeedApproval to actions tables", v1_20.AddNeedApprovalToActionRun),
470+
// v245 -> v246
471+
NewMigration("Rename Webhook org_id to owner_id", v1_20.RenameWebhookOrgToOwner),
470472
}
471473

472474
// GetCurrentDBVersion returns the current db version

models/migrations/v1_20/v245.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_20 //nolint
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"code.gitea.io/gitea/models/migrations/base"
11+
"code.gitea.io/gitea/modules/setting"
12+
13+
"xorm.io/xorm"
14+
)
15+
16+
func RenameWebhookOrgToOwner(x *xorm.Engine) error {
17+
type Webhook struct {
18+
OrgID int64 `xorm:"INDEX"`
19+
}
20+
21+
// This migration maybe rerun so that we should check if it has been run
22+
ownerExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "owner_id")
23+
if err != nil {
24+
return err
25+
}
26+
27+
if ownerExist {
28+
orgExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "org_id")
29+
if err != nil {
30+
return err
31+
}
32+
if !orgExist {
33+
return nil
34+
}
35+
}
36+
37+
sess := x.NewSession()
38+
defer sess.Close()
39+
if err := sess.Begin(); err != nil {
40+
return err
41+
}
42+
43+
if err := sess.Sync2(new(Webhook)); err != nil {
44+
return err
45+
}
46+
47+
if ownerExist {
48+
if err := base.DropTableColumns(sess, "webhook", "owner_id"); err != nil {
49+
return err
50+
}
51+
}
52+
53+
switch {
54+
case setting.Database.Type.IsMySQL():
55+
inferredTable, err := x.TableInfo(new(Webhook))
56+
if err != nil {
57+
return err
58+
}
59+
sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id"))
60+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil {
61+
return err
62+
}
63+
case setting.Database.Type.IsMSSQL():
64+
if _, err := sess.Exec("sp_rename 'webhook.org_id', 'owner_id', 'COLUMN'"); err != nil {
65+
return err
66+
}
67+
default:
68+
if _, err := sess.Exec("ALTER TABLE `webhook` RENAME COLUMN org_id TO owner_id"); err != nil {
69+
return err
70+
}
71+
}
72+
73+
return sess.Commit()
74+
}

models/webhook/webhook.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func IsValidHookContentType(name string) bool {
122122
type Webhook struct {
123123
ID int64 `xorm:"pk autoincr"`
124124
RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook
125-
OrgID int64 `xorm:"INDEX"`
125+
OwnerID int64 `xorm:"INDEX"`
126126
IsSystemWebhook bool
127127
URL string `xorm:"url TEXT"`
128128
HTTPMethod string `xorm:"http_method"`
@@ -412,19 +412,19 @@ func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
412412
})
413413
}
414414

415-
// GetWebhookByOrgID returns webhook of organization by given ID.
416-
func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
415+
// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
416+
func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) {
417417
return getWebhook(&Webhook{
418-
ID: id,
419-
OrgID: orgID,
418+
ID: id,
419+
OwnerID: ownerID,
420420
})
421421
}
422422

423423
// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
424424
type ListWebhookOptions struct {
425425
db.ListOptions
426426
RepoID int64
427-
OrgID int64
427+
OwnerID int64
428428
IsActive util.OptionalBool
429429
}
430430

@@ -433,8 +433,8 @@ func (opts *ListWebhookOptions) toCond() builder.Cond {
433433
if opts.RepoID != 0 {
434434
cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID})
435435
}
436-
if opts.OrgID != 0 {
437-
cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID})
436+
if opts.OwnerID != 0 {
437+
cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
438438
}
439439
if !opts.IsActive.IsNone() {
440440
cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()})
@@ -503,10 +503,10 @@ func DeleteWebhookByRepoID(repoID, id int64) error {
503503
})
504504
}
505505

506-
// DeleteWebhookByOrgID deletes webhook of organization by given ID.
507-
func DeleteWebhookByOrgID(orgID, id int64) error {
506+
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
507+
func DeleteWebhookByOwnerID(ownerID, id int64) error {
508508
return deleteWebhook(&Webhook{
509-
ID: id,
510-
OrgID: orgID,
509+
ID: id,
510+
OwnerID: ownerID,
511511
})
512512
}

models/webhook/webhook_system.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ import (
1515
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
1616
webhooks := make([]*Webhook, 0, 5)
1717
return webhooks, db.GetEngine(ctx).
18-
Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false).
18+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false).
1919
Find(&webhooks)
2020
}
2121

2222
// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
2323
func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) {
2424
webhook := &Webhook{ID: id}
2525
has, err := db.GetEngine(ctx).
26-
Where("repo_id=? AND org_id=?", 0, 0).
26+
Where("repo_id=? AND owner_id=?", 0, 0).
2727
Get(webhook)
2828
if err != nil {
2929
return nil, err
@@ -38,19 +38,19 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh
3838
webhooks := make([]*Webhook, 0, 5)
3939
if isActive.IsNone() {
4040
return webhooks, db.GetEngine(ctx).
41-
Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true).
41+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
4242
Find(&webhooks)
4343
}
4444
return webhooks, db.GetEngine(ctx).
45-
Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
45+
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
4646
Find(&webhooks)
4747
}
4848

4949
// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
5050
func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {
5151
return db.WithTx(ctx, func(ctx context.Context) error {
5252
count, err := db.GetEngine(ctx).
53-
Where("repo_id=? AND org_id=?", 0, 0).
53+
Where("repo_id=? AND owner_id=?", 0, 0).
5454
Delete(&Webhook{ID: id})
5555
if err != nil {
5656
return err

models/webhook/webhook_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ func TestGetWebhookByRepoID(t *testing.T) {
109109
assert.True(t, IsErrWebhookNotExist(err))
110110
}
111111

112-
func TestGetWebhookByOrgID(t *testing.T) {
112+
func TestGetWebhookByOwnerID(t *testing.T) {
113113
assert.NoError(t, unittest.PrepareTestDatabase())
114-
hook, err := GetWebhookByOrgID(3, 3)
114+
hook, err := GetWebhookByOwnerID(3, 3)
115115
assert.NoError(t, err)
116116
assert.Equal(t, int64(3), hook.ID)
117117

118-
_, err = GetWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID)
118+
_, err = GetWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID)
119119
assert.Error(t, err)
120120
assert.True(t, IsErrWebhookNotExist(err))
121121
}
@@ -140,19 +140,19 @@ func TestGetWebhooksByRepoID(t *testing.T) {
140140
}
141141
}
142142

143-
func TestGetActiveWebhooksByOrgID(t *testing.T) {
143+
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
144144
assert.NoError(t, unittest.PrepareTestDatabase())
145-
hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue})
145+
hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue})
146146
assert.NoError(t, err)
147147
if assert.Len(t, hooks, 1) {
148148
assert.Equal(t, int64(3), hooks[0].ID)
149149
assert.True(t, hooks[0].IsActive)
150150
}
151151
}
152152

153-
func TestGetWebhooksByOrgID(t *testing.T) {
153+
func TestGetWebhooksByOwnerID(t *testing.T) {
154154
assert.NoError(t, unittest.PrepareTestDatabase())
155-
hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3})
155+
hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3})
156156
assert.NoError(t, err)
157157
if assert.Len(t, hooks, 1) {
158158
assert.Equal(t, int64(3), hooks[0].ID)
@@ -181,13 +181,13 @@ func TestDeleteWebhookByRepoID(t *testing.T) {
181181
assert.True(t, IsErrWebhookNotExist(err))
182182
}
183183

184-
func TestDeleteWebhookByOrgID(t *testing.T) {
184+
func TestDeleteWebhookByOwnerID(t *testing.T) {
185185
assert.NoError(t, unittest.PrepareTestDatabase())
186-
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OrgID: 3})
187-
assert.NoError(t, DeleteWebhookByOrgID(3, 3))
188-
unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OrgID: 3})
186+
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3})
187+
assert.NoError(t, DeleteWebhookByOwnerID(3, 3))
188+
unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3})
189189

190-
err := DeleteWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID)
190+
err := DeleteWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID)
191191
assert.Error(t, err)
192192
assert.True(t, IsErrWebhookNotExist(err))
193193
}

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,8 @@ remove_account_link = Remove Linked Account
821821
remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue?
822822
remove_account_link_success = The linked account has been removed.
823823

824+
hooks.desc = Add webhooks which will be triggered for <strong>all repositories</strong> owned by this user.
825+
824826
orgs_none = You are not a member of any organizations.
825827
repos_none = You do not own any repositories
826828

routers/api/v1/admin/hooks.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ func CreateHook(ctx *context.APIContext) {
105105
// "$ref": "#/responses/Hook"
106106

107107
form := web.GetForm(ctx).(*api.CreateHookOption)
108-
// TODO in body params
109-
if !utils.CheckCreateHookOption(ctx, form) {
110-
return
111-
}
108+
112109
utils.AddSystemHook(ctx, form)
113110
}
114111

routers/api/v1/api.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,13 @@ func Routes(ctx gocontext.Context) *web.Route {
835835
m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches)
836836
m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos)
837837
m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams)
838+
m.Group("/hooks", func() {
839+
m.Combo("").Get(user.ListHooks).
840+
Post(bind(api.CreateHookOption{}), user.CreateHook)
841+
m.Combo("/{id}").Get(user.GetHook).
842+
Patch(bind(api.EditHookOption{}), user.EditHook).
843+
Delete(user.DeleteHook)
844+
}, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled())
838845
}, reqToken(""))
839846

840847
// Repositories

0 commit comments

Comments
 (0)