From fe564bb98ddffb8e8e4e0fd1eb7ffabe91792aca Mon Sep 17 00:00:00 2001 From: vntw Date: Mon, 15 Apr 2024 23:13:06 +0200 Subject: [PATCH 1/2] Add new project and group access token events to webhook event types --- event_parsing.go | 51 ++++++---- event_parsing_webhook_test.go | 38 ++++++- event_webhook_types.go | 55 +++++++++++ event_webhook_types_test.go | 98 +++++++++++++++++++ .../webhooks/resource_access_token_group.json | 18 ++++ .../resource_access_token_project.json | 30 ++++++ 6 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 testdata/webhooks/resource_access_token_group.json create mode 100644 testdata/webhooks/resource_access_token_project.json diff --git a/event_parsing.go b/event_parsing.go index 3943abadb..cf4cfca5a 100644 --- a/event_parsing.go +++ b/event_parsing.go @@ -27,24 +27,25 @@ type EventType string // List of available event types. const ( - EventConfidentialIssue EventType = "Confidential Issue Hook" - EventConfidentialNote EventType = "Confidential Note Hook" - EventTypeBuild EventType = "Build Hook" - EventTypeDeployment EventType = "Deployment Hook" - EventTypeFeatureFlag EventType = "Feature Flag Hook" - EventTypeIssue EventType = "Issue Hook" - EventTypeJob EventType = "Job Hook" - EventTypeMember EventType = "Member Hook" - EventTypeMergeRequest EventType = "Merge Request Hook" - EventTypeNote EventType = "Note Hook" - EventTypePipeline EventType = "Pipeline Hook" - EventTypePush EventType = "Push Hook" - EventTypeRelease EventType = "Release Hook" - EventTypeServiceHook EventType = "Service Hook" - EventTypeSubGroup EventType = "Subgroup Hook" - EventTypeSystemHook EventType = "System Hook" - EventTypeTagPush EventType = "Tag Push Hook" - EventTypeWikiPage EventType = "Wiki Page Hook" + EventConfidentialIssue EventType = "Confidential Issue Hook" + EventConfidentialNote EventType = "Confidential Note Hook" + EventTypeBuild EventType = "Build Hook" + EventTypeDeployment EventType = "Deployment Hook" + EventTypeFeatureFlag EventType = "Feature Flag Hook" + EventTypeIssue EventType = "Issue Hook" + EventTypeJob EventType = "Job Hook" + EventTypeMember EventType = "Member Hook" + EventTypeMergeRequest EventType = "Merge Request Hook" + EventTypeNote EventType = "Note Hook" + EventTypePipeline EventType = "Pipeline Hook" + EventTypePush EventType = "Push Hook" + EventTypeRelease EventType = "Release Hook" + EventTypeResourceAccessToken EventType = "Resource Access Token Hook" + EventTypeServiceHook EventType = "Service Hook" + EventTypeSubGroup EventType = "Subgroup Hook" + EventTypeSystemHook EventType = "System Hook" + EventTypeTagPush EventType = "Tag Push Hook" + EventTypeWikiPage EventType = "Wiki Page Hook" ) const ( @@ -252,6 +253,20 @@ func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err e event = &PushEvent{} case EventTypeRelease: event = &ReleaseEvent{} + case EventTypeResourceAccessToken: + data := map[string]interface{}{} + err := json.Unmarshal(payload, &data) + if err != nil { + return nil, err + } + + if _, ok := data["project"]; ok { + event = &ProjectResourceAccessTokenEvent{} + } else if _, ok := data["group"]; ok { + event = &GroupResourceAccessTokenEvent{} + } else { + return nil, fmt.Errorf("unexpected resource access token payload") + } case EventTypeServiceHook: service := &serviceEvent{} err := json.Unmarshal(payload, service) diff --git a/event_parsing_webhook_test.go b/event_parsing_webhook_test.go index c9d6b9d8b..163eeff1d 100644 --- a/event_parsing_webhook_test.go +++ b/event_parsing_webhook_test.go @@ -101,7 +101,7 @@ func TestParseCommitCommentHook(t *testing.T) { } } -func TestParseFeatureFLagHook(t *testing.T) { +func TestParseFeatureFlagHook(t *testing.T) { raw := loadFixture("testdata/webhooks/feature_flag.json") parsedEvent, err := ParseWebhook("Feature Flag Hook", raw) @@ -147,6 +147,24 @@ func TestParseFeatureFLagHook(t *testing.T) { } } +func TestParseGroupResourceAccessTokenHook(t *testing.T) { + raw := loadFixture("testdata/webhooks/resource_access_token_group.json") + + parsedEvent, err := ParseWebhook("Resource Access Token Hook", raw) + if err != nil { + t.Errorf("Error parsing group resource access token hook: %s", err) + } + + event, ok := parsedEvent.(*GroupResourceAccessTokenEvent) + if !ok { + t.Errorf("Expected GroupResourceAccessTokenEvent, but parsing produced %T", parsedEvent) + } + + if event.GroupID != 35 { + t.Errorf("GroupID is %v, want %v", event.GroupID, 35) + } +} + func TestParseHookWebHook(t *testing.T) { parsedEvent1, err := ParseHook("Merge Request Hook", loadFixture("testdata/webhooks/merge_request.json")) if err != nil { @@ -351,6 +369,24 @@ func TestParsePipelineHook(t *testing.T) { } } +func TestParseProjectResourceAccessTokenHook(t *testing.T) { + raw := loadFixture("testdata/webhooks/resource_access_token_project.json") + + parsedEvent, err := ParseWebhook("Resource Access Token Hook", raw) + if err != nil { + t.Errorf("Error parsing project resource access token hook: %s", err) + } + + event, ok := parsedEvent.(*ProjectResourceAccessTokenEvent) + if !ok { + t.Errorf("Expected ProjectResourceAccessTokenEvent, but parsing produced %T", parsedEvent) + } + + if event.ProjectID != 7 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 7) + } +} + func TestParsePushHook(t *testing.T) { raw := loadFixture("testdata/webhooks/push.json") diff --git a/event_webhook_types.go b/event_webhook_types.go index 01b098308..2817c74bf 100644 --- a/event_webhook_types.go +++ b/event_webhook_types.go @@ -205,6 +205,32 @@ type FeatureFlagEvent struct { } `json:"object_attributes"` } +// GroupResourceAccessTokenEvent represents a resource access token event for a group +// +// GitLab API docs: +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events +type GroupResourceAccessTokenEvent struct { + EventName string `json:"event_name"` + ObjectKind string `json:"object_kind"` + GroupID int `json:"group_id"` + Group struct { + GroupID int `json:"group_id"` + GroupName string `json:"group_name"` + GroupPath string `json:"group_path"` + FullPath string `json:"full_path"` + } `json:"group"` + ObjectAttributes *ResourceAccessToken `json:"object_attributes"` +} + +// ResourceAccessToken represents an access token record. +type ResourceAccessToken struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Name string `json:"name"` + CreatedAt *time.Time `json:"created_at"` + ExpiresAt *ISOTime `json:"expires_at"` +} + // IssueCommentEvent represents a comment on an issue event. // // GitLab API docs: @@ -899,6 +925,35 @@ type PipelineEvent struct { } `json:"builds"` } +// ProjectResourceAccessTokenEvent represents a resource access token event for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events +type ProjectResourceAccessTokenEvent struct { + EventName string `json:"event_name"` + ObjectKind string `json:"object_kind"` + ProjectID int `json:"project_id"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CIConfigPath string `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + ObjectAttributes ResourceAccessToken `json:"object_attributes"` +} + // PushEvent represents a push event. // // GitLab API docs: diff --git a/event_webhook_types_test.go b/event_webhook_types_test.go index 29a19698f..ed96520ac 100644 --- a/event_webhook_types_test.go +++ b/event_webhook_types_test.go @@ -286,6 +286,49 @@ func TestFeatureFlagEventUnmarshal(t *testing.T) { } } +func TestGroupResourceAccessTokenEventUnmarshal(t *testing.T) { + jsonObject := loadFixture("testdata/webhooks/resource_access_token_group.json") + var event *GroupResourceAccessTokenEvent + err := json.Unmarshal(jsonObject, &event) + if err != nil { + t.Errorf("could not unmarshal event: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("event is null") + } + + createdAt, err := time.Parse(time.RFC3339, "2024-02-05T03:13:44.855Z") + if err != nil { + t.Fatalf("could not parse time: %v", err) + } + + expiresAt, err := ParseISOTime("2024-01-26") + if err != nil { + t.Fatalf("could not parse ISO time: %v", err) + } + + expected := &GroupResourceAccessTokenEvent{ + GroupID: 35, + ObjectKind: "access_token", + EventName: "expiring_access_token", + ObjectAttributes: &ResourceAccessToken{ + ID: 25, + UserID: 90, + Name: "acd", + CreatedAt: &createdAt, + ExpiresAt: &expiresAt, + }, + } + + expected.Group.GroupID = 35 + expected.Group.GroupName = "Twitter" + expected.Group.GroupPath = "twitter" + expected.Group.FullPath = "twitter" + + assert.Equal(t, expected, event) +} + func TestIssueCommentEventUnmarshal(t *testing.T) { jsonObject := loadFixture("testdata/webhooks/note_issue.json") @@ -1014,6 +1057,61 @@ func TestPipelineEventUnmarshal(t *testing.T) { } } +func TestProjectResourceAccessTokenEventUnmarshal(t *testing.T) { + jsonObject := loadFixture("testdata/webhooks/resource_access_token_project.json") + var event *ProjectResourceAccessTokenEvent + err := json.Unmarshal(jsonObject, &event) + if err != nil { + t.Errorf("could not unmarshal event: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("event is null") + } + + createdAt, err := time.Parse(time.RFC3339, "2024-02-05T03:13:44.855Z") + if err != nil { + t.Fatalf("could not parse time: %v", err) + } + + expiresAt, err := ParseISOTime("2024-01-26") + if err != nil { + t.Fatalf("could not parse ISO time: %v", err) + } + + expected := &ProjectResourceAccessTokenEvent{ + ProjectID: 7, + ObjectKind: "access_token", + EventName: "expiring_access_token", + ObjectAttributes: ResourceAccessToken{ + ID: 25, + UserID: 90, + Name: "acd", + CreatedAt: &createdAt, + ExpiresAt: &expiresAt, + }, + } + + expected.Project.ID = 7 + expected.Project.Name = "Flight" + expected.Project.Description = "Eum dolore maxime atque reprehenderit voluptatem." + expected.Project.WebURL = "https://example.com/flightjs/Flight" + expected.Project.AvatarURL = "" + expected.Project.GitSSHURL = "ssh://git@example.com/flightjs/Flight.git" + expected.Project.GitHTTPURL = "https://example.com/flightjs/Flight.git" + expected.Project.Namespace = "Flightjs" + expected.Project.VisibilityLevel = 0 + expected.Project.PathWithNamespace = "flightjs/Flight" + expected.Project.DefaultBranch = "master" + expected.Project.CIConfigPath = "" + expected.Project.Homepage = "https://example.com/flightjs/Flight" + expected.Project.URL = "ssh://git@example.com/flightjs/Flight.git" + expected.Project.SSHURL = "ssh://git@example.com/flightjs/Flight.git" + expected.Project.HTTPURL = "https://example.com/flightjs/Flight.git" + + assert.Equal(t, expected, event) +} + func TestPushEventUnmarshal(t *testing.T) { jsonObject := loadFixture("testdata/webhooks/push.json") var event *PushEvent diff --git a/testdata/webhooks/resource_access_token_group.json b/testdata/webhooks/resource_access_token_group.json new file mode 100644 index 000000000..613c79506 --- /dev/null +++ b/testdata/webhooks/resource_access_token_group.json @@ -0,0 +1,18 @@ +{ + "object_kind": "access_token", + "group_id": 35, + "group": { + "group_name": "Twitter", + "group_path": "twitter", + "full_path": "twitter", + "group_id": 35 + }, + "object_attributes": { + "user_id": 90, + "created_at": "2024-02-05T03:13:44.855Z", + "id": 25, + "name": "acd", + "expires_at": "2024-01-26" + }, + "event_name": "expiring_access_token" +} diff --git a/testdata/webhooks/resource_access_token_project.json b/testdata/webhooks/resource_access_token_project.json new file mode 100644 index 000000000..efc4dd9fd --- /dev/null +++ b/testdata/webhooks/resource_access_token_project.json @@ -0,0 +1,30 @@ +{ + "object_kind": "access_token", + "project_id": 7, + "project": { + "id": 7, + "name": "Flight", + "description": "Eum dolore maxime atque reprehenderit voluptatem.", + "web_url": "https://example.com/flightjs/Flight", + "avatar_url": null, + "git_ssh_url": "ssh://git@example.com/flightjs/Flight.git", + "git_http_url": "https://example.com/flightjs/Flight.git", + "namespace": "Flightjs", + "visibility_level": 0, + "path_with_namespace": "flightjs/Flight", + "default_branch": "master", + "ci_config_path": null, + "homepage": "https://example.com/flightjs/Flight", + "url": "ssh://git@example.com/flightjs/Flight.git", + "ssh_url": "ssh://git@example.com/flightjs/Flight.git", + "http_url": "https://example.com/flightjs/Flight.git" + }, + "object_attributes": { + "user_id": 90, + "created_at": "2024-02-05T03:13:44.855Z", + "id": 25, + "name": "acd", + "expires_at": "2024-01-26" + }, + "event_name": "expiring_access_token" +} From c187a2d84b1df8d5d2ee8b7afe2dd5937e53b941 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Thu, 18 Apr 2024 09:41:16 +0200 Subject: [PATCH 2/2] Minor tweaks for consistency --- event_parsing.go | 12 +++++++---- event_webhook_types.go | 41 ++++++++++++++++++++++--------------- event_webhook_types_test.go | 26 +++++++++++------------ 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/event_parsing.go b/event_parsing.go index cf4cfca5a..0c3f2ffed 100644 --- a/event_parsing.go +++ b/event_parsing.go @@ -260,11 +260,15 @@ func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err e return nil, err } - if _, ok := data["project"]; ok { - event = &ProjectResourceAccessTokenEvent{} - } else if _, ok := data["group"]; ok { + _, groupEvent := data["group"] + _, projectEvent := data["project"] + + switch { + case groupEvent: event = &GroupResourceAccessTokenEvent{} - } else { + case projectEvent: + event = &ProjectResourceAccessTokenEvent{} + default: return nil, fmt.Errorf("unexpected resource access token payload") } case EventTypeServiceHook: diff --git a/event_webhook_types.go b/event_webhook_types.go index 2817c74bf..b75a8eb31 100644 --- a/event_webhook_types.go +++ b/event_webhook_types.go @@ -131,7 +131,7 @@ type CommitCommentEvent struct { } `json:"commit"` } -// DeploymentEvent represents a deployment event +// DeploymentEvent represents a deployment event. // // GitLab API docs: // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#deployment-events @@ -171,7 +171,7 @@ type DeploymentEvent struct { CommitTitle string `json:"commit_title"` } -// FeatureFlagEvent represents a feature flag event +// FeatureFlagEvent represents a feature flag event. // // GitLab API docs: // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#feature-flag-events @@ -205,7 +205,8 @@ type FeatureFlagEvent struct { } `json:"object_attributes"` } -// GroupResourceAccessTokenEvent represents a resource access token event for a group +// GroupResourceAccessTokenEvent represents a resource access token event for a +// group. // // GitLab API docs: // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events @@ -219,16 +220,13 @@ type GroupResourceAccessTokenEvent struct { GroupPath string `json:"group_path"` FullPath string `json:"full_path"` } `json:"group"` - ObjectAttributes *ResourceAccessToken `json:"object_attributes"` -} - -// ResourceAccessToken represents an access token record. -type ResourceAccessToken struct { - ID int `json:"id"` - UserID int `json:"user_id"` - Name string `json:"name"` - CreatedAt *time.Time `json:"created_at"` - ExpiresAt *ISOTime `json:"expires_at"` + ObjectAttributes struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Name string `json:"name"` + CreatedAt *time.Time `json:"created_at"` + ExpiresAt *ISOTime `json:"expires_at"` + } `json:"object_attributes"` } // IssueCommentEvent represents a comment on an issue event. @@ -763,7 +761,8 @@ type MergeEvent struct { Reviewers []*EventUser `json:"reviewers"` } -// EventUser represents a user record in an event and is used as an even initiator or a merge assignee. +// EventUser represents a user record in an event and is used as an even +// initiator or a merge assignee. type EventUser struct { ID int `json:"id"` Name string `json:"name"` @@ -779,7 +778,8 @@ type MergeParams struct { // UnmarshalJSON decodes the merge parameters // -// This allows support of ForceRemoveSourceBranch for both type bool (>11.9) and string (<11.9) +// This allows support of ForceRemoveSourceBranch for both type +// bool (>11.9) and string (<11.9) func (p *MergeParams) UnmarshalJSON(b []byte) error { type Alias MergeParams raw := struct { @@ -925,7 +925,8 @@ type PipelineEvent struct { } `json:"builds"` } -// ProjectResourceAccessTokenEvent represents a resource access token event for a project +// ProjectResourceAccessTokenEvent represents a resource access token event for +// a project. // // GitLab API docs: // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events @@ -951,7 +952,13 @@ type ProjectResourceAccessTokenEvent struct { SSHURL string `json:"ssh_url"` HTTPURL string `json:"http_url"` } `json:"project"` - ObjectAttributes ResourceAccessToken `json:"object_attributes"` + ObjectAttributes struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Name string `json:"name"` + CreatedAt *time.Time `json:"created_at"` + ExpiresAt *ISOTime `json:"expires_at"` + } `json:"object_attributes"` } // PushEvent represents a push event. diff --git a/event_webhook_types_test.go b/event_webhook_types_test.go index ed96520ac..f31b2fe0b 100644 --- a/event_webhook_types_test.go +++ b/event_webhook_types_test.go @@ -312,13 +312,6 @@ func TestGroupResourceAccessTokenEventUnmarshal(t *testing.T) { GroupID: 35, ObjectKind: "access_token", EventName: "expiring_access_token", - ObjectAttributes: &ResourceAccessToken{ - ID: 25, - UserID: 90, - Name: "acd", - CreatedAt: &createdAt, - ExpiresAt: &expiresAt, - }, } expected.Group.GroupID = 35 @@ -326,6 +319,12 @@ func TestGroupResourceAccessTokenEventUnmarshal(t *testing.T) { expected.Group.GroupPath = "twitter" expected.Group.FullPath = "twitter" + expected.ObjectAttributes.ID = 25 + expected.ObjectAttributes.UserID = 90 + expected.ObjectAttributes.Name = "acd" + expected.ObjectAttributes.CreatedAt = &createdAt + expected.ObjectAttributes.ExpiresAt = &expiresAt + assert.Equal(t, expected, event) } @@ -1083,15 +1082,14 @@ func TestProjectResourceAccessTokenEventUnmarshal(t *testing.T) { ProjectID: 7, ObjectKind: "access_token", EventName: "expiring_access_token", - ObjectAttributes: ResourceAccessToken{ - ID: 25, - UserID: 90, - Name: "acd", - CreatedAt: &createdAt, - ExpiresAt: &expiresAt, - }, } + expected.ObjectAttributes.ID = 25 + expected.ObjectAttributes.UserID = 90 + expected.ObjectAttributes.Name = "acd" + expected.ObjectAttributes.CreatedAt = &createdAt + expected.ObjectAttributes.ExpiresAt = &expiresAt + expected.Project.ID = 7 expected.Project.Name = "Flight" expected.Project.Description = "Eum dolore maxime atque reprehenderit voluptatem."