Skip to content

Commit 8d6a9a0

Browse files
committed
Let API create and edit system webhooks
"System" webhooks -- webhooks run on all repos on a Gitea instance -- were added in #14537 (I believe?) but managing them by the API is buggy. - In routers/api/v1/utils/hook.go, correctly handle the distinction between system and default webhooks. This enables actually creating, editing and deleting both kinds. - In routers/api/, move `/api/v1/admin/hooks` to `/api/v1/admin/hooks/{system,default}`. This allows users to access the code in the previous point. - In routers/web/, move `/admin/{system,default}-hooks` and most of `/admin/hooks/` into `/admin/hooks/{system,default}` to match API. - In model/, normalize vocabulary. Since the new sub-type, the terminology has been a confusing mix of "SystemWebhook", "DefaultSystemWebhook", "SystemOrDefaultWebhook" and "DefaultWebhook". Standardize on "AdminWebhook" everywhere with `isSystemWebhook bool` to separate the two sub-types. - Using a bool made it easier to handle both cases without duplicating the router endpoints - Make PATCH /admin/hooks/{system,default}/:id appropriately return 404. Fixes #23139. Supersedes #23142.
1 parent 7e382a5 commit 8d6a9a0

File tree

9 files changed

+212
-109
lines changed

9 files changed

+212
-109
lines changed

models/webhook/webhook_system.go

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,11 @@ import (
1111
"code.gitea.io/gitea/modules/util"
1212
)
1313

14-
// GetDefaultWebhooks returns all admin-default webhooks.
15-
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
16-
webhooks := make([]*Webhook, 0, 5)
17-
return webhooks, db.GetEngine(ctx).
18-
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false).
19-
Find(&webhooks)
20-
}
21-
22-
// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
23-
func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) {
14+
// GetSystemWebhook returns admin default webhook by given ID.
15+
func GetAdminWebhook(ctx context.Context, id int64, isSystemWebhook bool) (*Webhook, error) {
2416
webhook := &Webhook{ID: id}
2517
has, err := db.GetEngine(ctx).
26-
Where("repo_id=? AND owner_id=?", 0, 0).
18+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook).
2719
Get(webhook)
2820
if err != nil {
2921
return nil, err
@@ -35,22 +27,37 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
3527

3628
// GetSystemWebhooks returns all admin system webhooks.
3729
func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
30+
return GetAdminWebhooks(ctx, true, isActive)
31+
}
32+
33+
// GetDefaultWebhooks returns all webhooks that are copied to new repos.
34+
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
35+
return GetAdminWebhooks(ctx, false, util.OptionalBoolNone)
36+
}
37+
38+
// returns all admin system or default webhooks.
39+
// isSystemWebhook == true gives system webhooks, otherwise gives default webhooks.
40+
// isActive filters system webhooks to those currently enabled or disabled; pass util.OptionalBoolNone to get both.
41+
func GetAdminWebhooks(ctx context.Context, isSystemWebhook bool, isActive util.OptionalBool) ([]*Webhook, error) {
42+
if !isSystemWebhook && isActive.IsTrue() {
43+
return nil, fmt.Errorf("GetAdminWebhooks: active (isActive) default (!isSystemWebhook) hooks are impossible")
44+
}
3845
webhooks := make([]*Webhook, 0, 5)
3946
if isActive.IsNone() {
4047
return webhooks, db.GetEngine(ctx).
41-
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
48+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook).
4249
Find(&webhooks)
4350
}
4451
return webhooks, db.GetEngine(ctx).
45-
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
52+
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, isSystemWebhook, isActive.IsTrue()).
4653
Find(&webhooks)
4754
}
4855

49-
// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
50-
func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {
56+
// DeleteWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
57+
func DeleteAdminWebhook(ctx context.Context, id int64, isSystemWebhook bool) error {
5158
return db.WithTx(ctx, func(ctx context.Context) error {
5259
count, err := db.GetEngine(ctx).
53-
Where("repo_id=? AND owner_id=?", 0, 0).
60+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook).
5461
Delete(&Webhook{ID: id})
5562
if err != nil {
5663
return err

routers/api/v1/admin/hooks.go

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,34 @@ import (
1717
webhook_service "code.gitea.io/gitea/services/webhook"
1818
)
1919

20-
// ListHooks list system's webhooks
20+
// list system or default webhooks
2121
func ListHooks(ctx *context.APIContext) {
22-
// swagger:operation GET /admin/hooks admin adminListHooks
22+
// swagger:operation GET /admin/hooks/{configType} admin adminListHooks
2323
// ---
2424
// summary: List system's webhooks
2525
// produces:
2626
// - application/json
2727
// parameters:
28-
// - name: page
29-
// in: query
30-
// description: page number of results to return (1-based)
31-
// type: integer
32-
// - name: limit
33-
// in: query
34-
// description: page size of results
35-
// type: integer
28+
// - name: configType
29+
// in: path
30+
// description: whether the hook is system-wide or copied-to-each-new-repo
31+
// type: string
32+
// enum: [system, default]
33+
// required: true
3634
// responses:
3735
// "200":
3836
// "$ref": "#/responses/HookList"
3937

40-
sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
38+
isSystemWebhook := ctx.Params(":configType") == "system"
39+
40+
adminHooks, err := webhook.GetAdminWebhooks(ctx, isSystemWebhook, util.OptionalBoolNone)
4141
if err != nil {
42-
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
42+
ctx.Error(http.StatusInternalServerError, "GetAdminWebhooks", err)
4343
return
4444
}
45-
hooks := make([]*api.Hook, len(sysHooks))
46-
for i, hook := range sysHooks {
45+
46+
hooks := make([]*api.Hook, len(adminHooks))
47+
for i, hook := range adminHooks {
4748
h, err := webhook_service.ToHook(setting.AppURL+"/admin", hook)
4849
if err != nil {
4950
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
@@ -54,14 +55,20 @@ func ListHooks(ctx *context.APIContext) {
5455
ctx.JSON(http.StatusOK, hooks)
5556
}
5657

57-
// GetHook get an organization's hook by id
58+
// get a system/default hook by id
5859
func GetHook(ctx *context.APIContext) {
59-
// swagger:operation GET /admin/hooks/{id} admin adminGetHook
60+
// swagger:operation GET /admin/hooks/{configType}/{id} admin adminGetHook
6061
// ---
6162
// summary: Get a hook
6263
// produces:
6364
// - application/json
6465
// parameters:
66+
// - name: configType
67+
// in: path
68+
// description: whether the hook is system-wide or copied-to-each-new-repo
69+
// type: string
70+
// enum: [system, default]
71+
// required: true
6572
// - name: id
6673
// in: path
6774
// description: id of the hook to get
@@ -72,16 +79,19 @@ func GetHook(ctx *context.APIContext) {
7279
// "200":
7380
// "$ref": "#/responses/Hook"
7481

82+
isSystemWebhook := ctx.Params(":configType") == "system"
83+
7584
hookID := ctx.ParamsInt64(":id")
76-
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
85+
hook, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
7786
if err != nil {
7887
if errors.Is(err, util.ErrNotExist) {
7988
ctx.NotFound()
8089
} else {
81-
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
90+
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
8291
}
8392
return
8493
}
94+
8595
h, err := webhook_service.ToHook("/admin/", hook)
8696
if err != nil {
8797
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
@@ -90,16 +100,22 @@ func GetHook(ctx *context.APIContext) {
90100
ctx.JSON(http.StatusOK, h)
91101
}
92102

93-
// CreateHook create a hook for an organization
103+
// create a system or default hook
94104
func CreateHook(ctx *context.APIContext) {
95-
// swagger:operation POST /admin/hooks admin adminCreateHook
105+
// swagger:operation POST /admin/hooks/{configType} admin adminCreateHook
96106
// ---
97107
// summary: Create a hook
98108
// consumes:
99109
// - application/json
100110
// produces:
101111
// - application/json
102112
// parameters:
113+
// - name: configType
114+
// in: path
115+
// description: whether the hook is system-wide or copied-to-each-new-repo
116+
// type: string
117+
// enum: [system, default]
118+
// required: true
103119
// - name: body
104120
// in: body
105121
// required: true
@@ -109,21 +125,29 @@ func CreateHook(ctx *context.APIContext) {
109125
// "201":
110126
// "$ref": "#/responses/Hook"
111127

128+
isSystemWebhook := ctx.Params(":configType") == "system"
129+
112130
form := web.GetForm(ctx).(*api.CreateHookOption)
113131

114-
utils.AddSystemHook(ctx, form)
132+
utils.AddAdminHook(ctx, form, isSystemWebhook)
115133
}
116134

117-
// EditHook modify a hook of a repository
135+
// modify a system or default hook
118136
func EditHook(ctx *context.APIContext) {
119-
// swagger:operation PATCH /admin/hooks/{id} admin adminEditHook
137+
// swagger:operation PATCH /admin/hooks/{configType}/{id} admin adminEditHook
120138
// ---
121139
// summary: Update a hook
122140
// consumes:
123141
// - application/json
124142
// produces:
125143
// - application/json
126144
// parameters:
145+
// - name: configType
146+
// in: path
147+
// description: whether the hook is system-wide or copied-to-each-new-repo
148+
// type: string
149+
// enum: [system, default]
150+
// required: true
127151
// - name: id
128152
// in: path
129153
// description: id of the hook to update
@@ -138,21 +162,29 @@ func EditHook(ctx *context.APIContext) {
138162
// "200":
139163
// "$ref": "#/responses/Hook"
140164

165+
isSystemWebhook := ctx.Params(":configType") == "system"
166+
141167
form := web.GetForm(ctx).(*api.EditHookOption)
142168

143169
// TODO in body params
144170
hookID := ctx.ParamsInt64(":id")
145-
utils.EditSystemHook(ctx, form, hookID)
171+
utils.EditAdminHook(ctx, form, hookID, isSystemWebhook)
146172
}
147173

148-
// DeleteHook delete a system hook
174+
// delete a system or default hook
149175
func DeleteHook(ctx *context.APIContext) {
150-
// swagger:operation DELETE /admin/hooks/{id} admin adminDeleteHook
176+
// swagger:operation DELETE /admin/hooks/{configType}/{id} admin adminDeleteHook
151177
// ---
152178
// summary: Delete a hook
153179
// produces:
154180
// - application/json
155181
// parameters:
182+
// - name: configType
183+
// in: path
184+
// description: whether the hook is system-wide or copied-to-each-new-repo
185+
// type: string
186+
// enum: [system, default]
187+
// required: true
156188
// - name: id
157189
// in: path
158190
// description: id of the hook to delete
@@ -163,12 +195,14 @@ func DeleteHook(ctx *context.APIContext) {
163195
// "204":
164196
// "$ref": "#/responses/empty"
165197

198+
isSystemWebhook := ctx.Params(":configType") == "system"
199+
166200
hookID := ctx.ParamsInt64(":id")
167-
if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
201+
if err := webhook.DeleteAdminWebhook(ctx, hookID, isSystemWebhook); err != nil {
168202
if errors.Is(err, util.ErrNotExist) {
169203
ctx.NotFound()
170204
} else {
171-
ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err)
205+
ctx.Error(http.StatusInternalServerError, "DeleteAdminWebhook", err)
172206
}
173207
return
174208
}

routers/api/v1/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1381,7 +1381,7 @@ func Routes() *web.Route {
13811381
m.Post("/{username}/{reponame}", admin.AdoptRepository)
13821382
m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
13831383
})
1384-
m.Group("/hooks", func() {
1384+
m.Group("/hooks/{configType:system|default}", func() {
13851385
m.Combo("").Get(admin.ListHooks).
13861386
Post(bind(api.CreateHookOption{}), admin.CreateHook)
13871387
m.Combo("/{id}").Get(admin.GetHook).

routers/api/v1/utils/hook.go

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package utils
55

66
import (
7+
"errors"
78
"fmt"
89
"net/http"
910
"strings"
@@ -100,9 +101,9 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
100101
return true
101102
}
102103

103-
// AddSystemHook add a system hook
104-
func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) {
105-
hook, ok := addHook(ctx, form, 0, 0)
104+
// add a system or default hook
105+
func AddAdminHook(ctx *context.APIContext, form *api.CreateHookOption, isSystemWebhook bool) {
106+
hook, ok := addHook(ctx, form, 0, 0, isSystemWebhook)
106107
if ok {
107108
h, err := webhook_service.ToHook(setting.AppSubURL+"/admin", hook)
108109
if err != nil {
@@ -115,7 +116,7 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) {
115116

116117
// AddOwnerHook adds a hook to an user or organization
117118
func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) {
118-
hook, ok := addHook(ctx, form, owner.ID, 0)
119+
hook, ok := addHook(ctx, form, owner.ID, 0, false)
119120
if !ok {
120121
return
121122
}
@@ -129,7 +130,7 @@ func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.Cre
129130
// AddRepoHook add a hook to a repo. Writes to `ctx` accordingly
130131
func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) {
131132
repo := ctx.Repo
132-
hook, ok := addHook(ctx, form, 0, repo.Repository.ID)
133+
hook, ok := addHook(ctx, form, 0, repo.Repository.ID, false)
133134
if !ok {
134135
return
135136
}
@@ -159,9 +160,18 @@ func pullHook(events []string, event string) bool {
159160
return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
160161
}
161162

162-
// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
163-
// an error, write to `ctx` accordingly. Return (webhook, ok)
164-
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
163+
// addHook add the hook specified by `form`, `ownerID`, `repoID`, and `isSystemWebhook`.
164+
// `isSystemWebhook` == true means it's a hook attached automatically to all repos
165+
// `isSystemWebhook` == false && ownerID == 0 && repoID == 0 means it's a default hook, automatically copied to all new repos
166+
// `ownerID` != 0 means it runs on all their repos
167+
// `repoID` != 0 means it is an active hook, attached to that repo
168+
// If there is an error, write to `ctx` accordingly. Return (webhook, ok)
169+
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64, isSystemWebhook bool) (*webhook.Webhook, bool) {
170+
if isSystemWebhook && (ownerID != 0 || repoID != 0) {
171+
ctx.Error(http.StatusInternalServerError, "addHook", fmt.Errorf("cannot create a hook with an owner or repo that is also a system hook"))
172+
return nil, false
173+
}
174+
165175
if !checkCreateHookOption(ctx, form) {
166176
return nil, false
167177
}
@@ -170,12 +180,13 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
170180
form.Events = []string{"push"}
171181
}
172182
w := &webhook.Webhook{
173-
OwnerID: ownerID,
174-
RepoID: repoID,
175-
URL: form.Config["url"],
176-
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
177-
Secret: form.Config["secret"],
178-
HTTPMethod: "POST",
183+
OwnerID: ownerID,
184+
RepoID: repoID,
185+
URL: form.Config["url"],
186+
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
187+
Secret: form.Config["secret"],
188+
IsSystemWebhook: isSystemWebhook,
189+
HTTPMethod: "POST",
179190
HookEvent: &webhook_module.HookEvent{
180191
ChooseEvents: true,
181192
HookEvents: webhook_module.HookEvents{
@@ -246,22 +257,28 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
246257
return w, true
247258
}
248259

249-
// EditSystemHook edit system webhook `w` according to `form`. Writes to `ctx` accordingly
250-
func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
251-
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
260+
// EditAdminHook edits system/default webhook `w` according to `form`. Writes to `ctx` accordingly.
261+
func EditAdminHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64, isSystemWebhook bool) {
262+
hook, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
252263
if err != nil {
253-
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
264+
if errors.Is(err, util.ErrNotExist) {
265+
ctx.NotFound()
266+
} else {
267+
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
268+
}
254269
return
255270
}
271+
256272
if !editHook(ctx, form, hook) {
257-
ctx.Error(http.StatusInternalServerError, "editHook", err)
258273
return
259274
}
260-
updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
275+
276+
updated, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
261277
if err != nil {
262-
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
278+
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
263279
return
264280
}
281+
265282
h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated)
266283
if err != nil {
267284
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)

0 commit comments

Comments
 (0)