diff --git a/api/webhook/post.go b/api/webhook/post.go index c4670d8ea..7eab225f5 100644 --- a/api/webhook/post.go +++ b/api/webhook/post.go @@ -701,7 +701,7 @@ func handleRepositoryEvent(c *gin.Context, m *types.Metadata, h *library.Hook, r switch h.GetEventAction() { // if action is rename, go through rename routine - case constants.ActionRenamed: + case constants.ActionRenamed, constants.ActionTransferred: r, err := renameRepository(h, r, c, m) if err != nil { h.SetStatus(constants.StatusFailure) @@ -779,34 +779,16 @@ func handleRepositoryEvent(c *gin.Context, m *types.Metadata, h *library.Hook, r // renameRepository is a helper function that takes the old name of the repo, // queries the database for the repo that matches that name and org, and updates // that repo to its new name in order to preserve it. It also updates the secrets -// associated with that repo. +// associated with that repo as well as build links for the UI. func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types.Metadata) (*library.Repo, error) { - logrus.Debugf("renaming repository from %s to %s", r.GetPreviousName(), r.GetName()) + logrus.Infof("renaming repository from %s to %s", r.GetPreviousName(), r.GetName()) // get the old name of the repo - previousName := r.GetPreviousName() - // get the repo from the database that matches the old name - dbR, err := database.FromContext(c).GetRepoForOrg(r.GetOrg(), previousName) - if err != nil { - retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, r.GetOrg(), previousName) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return nil, retErr - } + prevOrg, prevRepo := util.SplitFullName(r.GetPreviousName()) - // update the repo name information - dbR.SetName(r.GetName()) - dbR.SetFullName(r.GetFullName()) - dbR.SetClone(r.GetClone()) - dbR.SetLink(r.GetLink()) - dbR.SetPreviousName(previousName) - - // update the repo in the database - err = database.FromContext(c).UpdateRepo(dbR) + // get the repo from the database that matches the old name + dbR, err := database.FromContext(c).GetRepoForOrg(prevOrg, prevRepo) if err != nil { - retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, r.GetOrg(), previousName) + retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, prevOrg, prevRepo) util.HandleError(c, http.StatusBadRequest, retErr) h.SetStatus(constants.StatusFailure) @@ -840,7 +822,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types // get total number of secrets associated with repository t, err := database.FromContext(c).CountSecretsForRepo(dbR, map[string]interface{}{}) if err != nil { - return nil, fmt.Errorf("unable to get secret count for repo %s/%s: %w", r.GetOrg(), previousName, err) + return nil, fmt.Errorf("unable to get secret count for repo %s/%s: %w", prevOrg, prevRepo, err) } secrets := []*library.Secret{} @@ -849,7 +831,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types for repoSecrets := int64(0); repoSecrets < t; repoSecrets += 100 { s, _, err := database.FromContext(c).ListSecretsForRepo(dbR, map[string]interface{}{}, page, 100) if err != nil { - return nil, fmt.Errorf("unable to get secret list for repo %s/%s: %w", r.GetOrg(), previousName, err) + return nil, fmt.Errorf("unable to get secret list for repo %s/%s: %w", prevOrg, prevRepo, err) } secrets = append(secrets, s...) @@ -859,11 +841,12 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types // update secrets to point to the new repository name for _, secret := range secrets { + secret.SetOrg(r.GetOrg()) secret.SetRepo(r.GetName()) err = database.FromContext(c).UpdateSecret(secret) if err != nil { - return nil, fmt.Errorf("unable to update secret for repo %s/%s: %w", r.GetOrg(), previousName, err) + return nil, fmt.Errorf("unable to update secret for repo %s/%s: %w", prevOrg, prevRepo, err) } } @@ -890,7 +873,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types // update build link to route to proper repo name for _, build := range builds { build.SetLink( - fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, dbR.GetFullName(), build.GetNumber()), + fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), build.GetNumber()), ) _, err = database.FromContext(c).UpdateBuild(build) @@ -899,5 +882,25 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types } } + // update the repo name information + dbR.SetName(r.GetName()) + dbR.SetOrg(r.GetOrg()) + dbR.SetFullName(r.GetFullName()) + dbR.SetClone(r.GetClone()) + dbR.SetLink(r.GetLink()) + dbR.SetPreviousName(r.GetPreviousName()) + + // update the repo in the database + err = database.FromContext(c).UpdateRepo(dbR) + if err != nil { + retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, prevOrg, prevRepo) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + return dbR, nil } diff --git a/router/middleware/perm/perm.go b/router/middleware/perm/perm.go index da4c18013..8028275ae 100644 --- a/router/middleware/perm/perm.go +++ b/router/middleware/perm/perm.go @@ -221,14 +221,7 @@ func MustSecretAdmin() gin.HandlerFunc { // if caller is worker with build token, verify it has access to requested secret if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) { - // split repo full name into org and repo - repoSlice := strings.Split(cl.Repo, "/") - if len(repoSlice) != 2 { - logger.Errorf("unable to parse repo claim in build token") - } - - org := repoSlice[0] - repo := repoSlice[1] + org, repo := util.SplitFullName(cl.Repo) switch t { case constants.SecretShared: diff --git a/scm/github/testdata/hooks/repository_transferred.json b/scm/github/testdata/hooks/repository_transferred.json new file mode 100644 index 000000000..2fdea23f6 --- /dev/null +++ b/scm/github/testdata/hooks/repository_transferred.json @@ -0,0 +1,159 @@ +{ + "action": "transferred", + "changes": { + "owner": { + "from": { + "user": { + "login": "Old-Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + } + } + } + }, + "repository": { + "id": 118, + "node_id": "MDEwOlJlcG9zaXRvcnkxMTg=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://octocoders.github.io/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World", + "forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks", + "keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events", + "assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges", + "archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T19:37:07Z", + "updated_at": "2019-05-15T19:38:25Z", + "pushed_at": "2019-05-15T19:38:23Z", + "git_url": "git://octocoders.github.io/Codertocat/Hello-World.git", + "ssh_url": "git@octocoders.github.io:Codertocat/Hello-World.git", + "clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git", + "svn_url": "https://octocoders.github.io/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "enterprise": { + "id": 1, + "slug": "github", + "name": "GitHub", + "node_id": "MDg6QnVzaW5lc3Mx", + "avatar_url": "https://octocoders.github.io/avatars/b/1?", + "description": null, + "website_url": null, + "html_url": "https://octocoders.github.io/businesses/github", + "created_at": "2019-05-14T19:31:12Z", + "updated_at": "2019-05-14T19:31:12Z" + }, + "sender": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 5, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ==" + } + } \ No newline at end of file diff --git a/scm/github/webhook.go b/scm/github/webhook.go index ad4eda5e6..ceaa5b35d 100644 --- a/scm/github/webhook.go +++ b/scm/github/webhook.go @@ -465,8 +465,19 @@ func (c *client) processRepositoryEvent(h *library.Hook, payload *github.Reposit r.SetTopics(repo.Topics) // if action is renamed, then get the previous name from payload - if payload.GetAction() == "renamed" { - r.SetPreviousName(payload.GetChanges().GetRepo().GetName().GetFrom()) + if payload.GetAction() == constants.ActionRenamed { + r.SetPreviousName(repo.GetOwner().GetLogin() + "/" + payload.GetChanges().GetRepo().GetName().GetFrom()) + } + + // if action is transferred, then get the previous owner from payload + // could be a user or an org, but both are User structs + if payload.GetAction() == constants.ActionTransferred { + org := payload.GetChanges().GetOwner().GetOwnerInfo().GetOrg() + if org == nil { + org = payload.GetChanges().GetOwner().GetOwnerInfo().GetUser() + } + + r.SetPreviousName(org.GetLogin() + "/" + repo.GetName()) } h.SetEvent(constants.EventRepository) diff --git a/scm/github/webhook_test.go b/scm/github/webhook_test.go index b1f3a1f0e..1bef4c0ca 100644 --- a/scm/github/webhook_test.go +++ b/scm/github/webhook_test.go @@ -982,7 +982,72 @@ func TestGitHub_ProcessWebhook_RepositoryRename(t *testing.T) { wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) - wantRepo.SetPreviousName("Hello-Old-World") + wantRepo.SetPreviousName("Codertocat/Hello-Old-World") + wantRepo.SetTopics(nil) + + want := &types.Webhook{ + Comment: "", + Hook: wantHook, + Repo: wantRepo, + } + + got, err := client.ProcessWebhook(request) + + if err != nil { + t.Errorf("ProcessWebhook returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ProcessWebhook is %v, want %v", got, want) + } +} + +func TestGitHub_ProcessWebhook_RepositoryTransfer(t *testing.T) { + // setup router + s := httptest.NewServer(http.NotFoundHandler()) + defer s.Close() + + // setup request + body, err := os.Open("testdata/hooks/repository_transferred.json") + if err != nil { + t.Errorf("unable to open file: %v", err) + } + + defer body.Close() + + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") + request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") + request.Header.Set("X-GitHub-Event", "repository") + + // setup client + client, _ := NewTest(s.URL) + + // run test + wantHook := new(library.Hook) + wantHook.SetNumber(1) + wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) + wantHook.SetCreated(time.Now().UTC().Unix()) + wantHook.SetHost("github.com") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction(constants.ActionTransferred) + wantHook.SetBranch("master") + wantHook.SetStatus(constants.StatusSuccess) + wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") + + wantRepo := new(library.Repo) + wantRepo.SetActive(true) + wantRepo.SetOrg("Codertocat") + wantRepo.SetName("Hello-World") + wantRepo.SetFullName("Codertocat/Hello-World") + wantRepo.SetLink("https://octocoders.github.io/Codertocat/Hello-World") + wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") + wantRepo.SetBranch("master") + wantRepo.SetPrivate(false) + wantRepo.SetPreviousName("Old-Codertocat/Hello-World") wantRepo.SetTopics(nil) want := &types.Webhook{ diff --git a/util/util.go b/util/util.go index 667653fbe..31cdd1104 100644 --- a/util/util.go +++ b/util/util.go @@ -64,6 +64,20 @@ func PathParameter(c *gin.Context, parameter string) string { return EscapeValue(c.Param(parameter)) } +// SplitFullName safely splits the repo.FullName field into an org and name. +func SplitFullName(value string) (string, string) { + // split repo full name into org and repo + repoSlice := strings.Split(value, "/") + if len(repoSlice) != 2 { + return "", "" + } + + org := repoSlice[0] + repo := repoSlice[1] + + return org, repo +} + // EscapeValue safely escapes any string by removing any new lines and HTML escaping it. func EscapeValue(value string) string { // replace all new lines in the value