Skip to content

Allow creating and editing system webhooks. #23142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
34 changes: 16 additions & 18 deletions models/webhook/webhook_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,11 @@ import (
"code.gitea.io/gitea/modules/util"
)

// GetDefaultWebhooks returns all admin-default webhooks.
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
webhooks := make([]*Webhook, 0, 5)
return webhooks, db.GetEngine(ctx).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false).
Find(&webhooks)
}

// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) {
// GetSystemWebhook returns admin default webhook by given ID.
func GetAdminWebhook(ctx context.Context, id int64, isSystemWebhook bool) (*Webhook, error) {
webhook := &Webhook{ID: id}
has, err := db.GetEngine(ctx).
Where("repo_id=? AND owner_id=?", 0, 0).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook).
Get(webhook)
if err != nil {
return nil, err
Expand All @@ -33,21 +25,27 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
return webhook, nil
}

// GetSystemWebhooks returns all admin system webhooks.
func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) {
// returns all admin system or default webhooks.
// isSystemWebhook == true gives system webhooks, otherwise gives default webhooks.
// isActive filters system webhooks to those currently enabled or disabled; pass util.OptionalBoolNone to get both.
// isActive is ignored when requesting default webhooks.
func GetAdminWebhooks(ctx context.Context, isSystemWebhook bool, isActive util.OptionalBool) ([]*Webhook, error) {
if !isSystemWebhook {
isActive = util.OptionalBoolNone
}
webhooks := make([]*Webhook, 0, 5)
if isActive.IsNone() {
return webhooks, db.GetEngine(ctx).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook).
Find(&webhooks)
}
return webhooks, db.GetEngine(ctx).
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()).
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, isSystemWebhook, isActive.IsTrue()).
Find(&webhooks)
}

// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {
// DeleteWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
func DeleteAdminWebhook(ctx context.Context, id int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
count, err := db.GetEngine(ctx).
Where("repo_id=? AND owner_id=?", 0, 0).
Expand All @@ -65,7 +63,7 @@ func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {

// CopyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error {
ws, err := GetDefaultWebhooks(ctx)
ws, err := GetAdminWebhooks(ctx, false, util.OptionalBoolNone)
if err != nil {
return fmt.Errorf("GetDefaultWebhooks: %v", err)
}
Expand Down
90 changes: 62 additions & 28 deletions routers/api/v1/admin/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,36 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)

// ListHooks list system's webhooks
// swaggeroperation GET /admin/{hookType:default-hooks|system-hooks} admin adminListHooks

// list system or default webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks admin adminListHooks
// swagger:operation GET /admin/{hookType} admin adminListHooks
// ---
// summary: List system's webhooks
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: hookType
// in: path
// description: whether the hook is system-wide or copied-to-each-new-repo
// type: string
// enum: [system-hooks, default-hooks]
// required: true
// responses:
// "200":
// "$ref": "#/responses/HookList"

sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
isSystemWebhook := ctx.Params(":hookType") == "system-hooks"

adminHooks, err := webhook.GetAdminWebhooks(ctx, isSystemWebhook, util.OptionalBoolNone)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
ctx.Error(http.StatusInternalServerError, "GetAdminWebhooks", err)
return
}
hooks := make([]*api.Hook, len(sysHooks))
for i, hook := range sysHooks {

hooks := make([]*api.Hook, len(adminHooks))
for i, hook := range adminHooks {
h, err := webhook_service.ToHook(setting.AppURL+"/admin", hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
Expand All @@ -54,14 +57,20 @@ func ListHooks(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, hooks)
}

// GetHook get an organization's hook by id
// get a system/default hook by id
func GetHook(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks/{id} admin adminGetHook
// swagger:operation GET /admin/{hookType}/{id} admin adminGetHook
// ---
// summary: Get a hook
// produces:
// - application/json
// parameters:
// - name: hookType
// in: path
// description: whether the hook is system-wide or copied-to-each-new-repo
// type: string
// enum: [system-hooks, default-hooks]
// required: true
// - name: id
// in: path
// description: id of the hook to get
Expand All @@ -72,16 +81,19 @@ func GetHook(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/Hook"

isSystemWebhook := ctx.Params(":hookType") == "system-hooks"

hookID := ctx.ParamsInt64(":id")
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
hook, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
}
return
}

h, err := webhook_service.ToHook("/admin/", hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
Expand All @@ -90,16 +102,22 @@ func GetHook(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, h)
}

// CreateHook create a hook for an organization
// create a system or default hook
func CreateHook(ctx *context.APIContext) {
// swagger:operation POST /admin/hooks admin adminCreateHook
// swagger:operation POST /admin/{hookType} admin adminCreateHook
// ---
// summary: Create a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: hookType
// in: path
// description: whether the hook is system-wide or copied-to-each-new-repo
// type: string
// enum: [system-hooks, default-hooks]
// required: true
// - name: body
// in: body
// required: true
Expand All @@ -109,21 +127,29 @@ func CreateHook(ctx *context.APIContext) {
// "201":
// "$ref": "#/responses/Hook"

isSystemWebhook := ctx.Params(":hookType") == "system-hooks"

form := web.GetForm(ctx).(*api.CreateHookOption)

utils.AddSystemHook(ctx, form)
utils.AddAdminHook(ctx, form, isSystemWebhook)
}

// EditHook modify a hook of a repository
// modify a system or default hook
func EditHook(ctx *context.APIContext) {
// swagger:operation PATCH /admin/hooks/{id} admin adminEditHook
// swagger:operation PATCH /admin/{hookType}/{id} admin adminEditHook
// ---
// summary: Update a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: hookType
// in: path
// description: whether the hook is system-wide or copied-to-each-new-repo
// type: string
// enum: [system-hooks, default-hooks]
// required: true
// - name: id
// in: path
// description: id of the hook to update
Expand All @@ -138,21 +164,29 @@ func EditHook(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/Hook"

isSystemWebhook := ctx.Params(":hookType") == "system-hooks"

form := web.GetForm(ctx).(*api.EditHookOption)

// TODO in body params
hookID := ctx.ParamsInt64(":id")
utils.EditSystemHook(ctx, form, hookID)
utils.EditHook(ctx, form, hookID, isSystemWebhook)
}

// DeleteHook delete a system hook
// delete a system or default hook
func DeleteHook(ctx *context.APIContext) {
// swagger:operation DELETE /admin/hooks/{id} admin adminDeleteHook
// swagger:operation DELETE /admin/{hookType}/{id} admin adminDeleteHook
// ---
// summary: Delete a hook
// produces:
// - application/json
// parameters:
// - name: hookType
// in: path
// description: whether the hook is system-wide or copied-to-each-new-repo
// type: string
// enum: [system-hooks, default-hooks]
// required: true
// - name: id
// in: path
// description: id of the hook to delete
Expand All @@ -164,11 +198,11 @@ func DeleteHook(ctx *context.APIContext) {
// "$ref": "#/responses/empty"

hookID := ctx.ParamsInt64(":id")
if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
if err := webhook.DeleteAdminWebhook(ctx, hookID); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err)
ctx.Error(http.StatusInternalServerError, "DeleteAdminWebhook", err)
}
return
}
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1376,12 +1376,12 @@ func Routes() *web.Route {
m.Get("", admin.GetAllEmails)
m.Get("/search", admin.SearchEmail)
})
m.Group("/unadopted", func() {
m.Group("/uinadopted", func() {
m.Get("", admin.ListUnadoptedRepositories)
m.Post("/{username}/{reponame}", admin.AdoptRepository)
m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
})
m.Group("/hooks", func() {
m.Group("/{hookType:system-hooks|default-hooks}", func() {
m.Combo("").Get(admin.ListHooks).
Post(bind(api.CreateHookOption{}), admin.CreateHook)
m.Combo("/{id}").Get(admin.GetHook).
Expand Down
58 changes: 39 additions & 19 deletions routers/api/v1/utils/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
return true
}

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

// AddOwnerHook adds a hook to an user or organization
func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) {
hook, ok := addHook(ctx, form, owner.ID, 0)
hook, ok := addHook(ctx, form, owner.ID, 0, false)
if !ok {
return
}
Expand All @@ -129,7 +129,7 @@ func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.Cre
// AddRepoHook add a hook to a repo. Writes to `ctx` accordingly
func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) {
repo := ctx.Repo
hook, ok := addHook(ctx, form, 0, repo.Repository.ID)
hook, ok := addHook(ctx, form, 0, repo.Repository.ID, false)
if !ok {
return
}
Expand Down Expand Up @@ -159,9 +159,18 @@ func pullHook(events []string, event string) bool {
return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
}

// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
// an error, write to `ctx` accordingly. Return (webhook, ok)
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
// addHook add the hook specified by `form`, `ownerID`, `repoID`, and `isSystemWebhook`.
// `isSystemWebhook` == true means it's a hook attached automatically to all repos
// `isSystemWebhook` == false && ownerID == 0 && repoID == 0 means it's a default hook, automatically copied to all new repos
// `ownerID` != 0 means it runs on all their repos
// `repoID` != 0 means it is an active hook, attached to that repo
// If there is an error, write to `ctx` accordingly. Return (webhook, ok)
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64, isSystemWebhook bool) (*webhook.Webhook, bool) {
if isSystemWebhook && (ownerID != 0 || repoID != 0) {
ctx.Error(http.StatusInternalServerError, "addHook", fmt.Errorf("cannot create a hook with an owner or repo that is also a system hook"))
return nil, false
}

if !checkCreateHookOption(ctx, form) {
return nil, false
}
Expand All @@ -170,12 +179,13 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
form.Events = []string{"push"}
}
w := &webhook.Webhook{
OwnerID: ownerID,
RepoID: repoID,
URL: form.Config["url"],
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
Secret: form.Config["secret"],
HTTPMethod: "POST",
OwnerID: ownerID,
RepoID: repoID,
URL: form.Config["url"],
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
Secret: form.Config["secret"],
IsSystemWebhook: isSystemWebhook,
HTTPMethod: "POST",
HookEvent: &webhook_module.HookEvent{
ChooseEvents: true,
HookEvents: webhook_module.HookEvents{
Expand Down Expand Up @@ -247,21 +257,28 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
}

// EditSystemHook edit system webhook `w` according to `form`. Writes to `ctx` accordingly
func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) {
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
func EditHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64, isSystemWebhook bool) {
hook, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
return
}

if err != nil {
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
ctx.Error(http.StatusInternalServerError, "Edithook", err)
return
}
if !editHook(ctx, form, hook) {
ctx.Error(http.StatusInternalServerError, "editHook", err)
return
}
updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)

updated, err := webhook.GetAdminWebhook(ctx, hookID, isSystemWebhook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
ctx.Error(http.StatusInternalServerError, "GetAdminWebhook", err)
return
}

h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
Expand Down Expand Up @@ -325,6 +342,9 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
}
w.ContentType = webhook.ToHookContentType(ct)
}
if secret, ok := form.Config["secret"]; ok {
w.Secret = secret
}

if w.Type == webhook_module.SLACK {
if channel, ok := form.Config["channel"]; ok {
Expand Down
Loading