Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
enhance(secrets): verify casing and existence of orgs and repos in SC…
…M before adding secret
  • Loading branch information
ecrupper committed Sep 22, 2022
commit 848510660bc1ba139550d9c43ca55a123339aebc
42 changes: 42 additions & 0 deletions api/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,48 @@ func CreateSecret(c *gin.Context) {
}
}

if strings.EqualFold(t, constants.SecretOrg) {
// retrieve org name from SCM
org, err := scm.FromContext(c).GetOrgName(u, o)
if err != nil {
retErr := fmt.Errorf("unable to retrieve organization %s", o)

util.HandleError(c, http.StatusNotFound, retErr)

return
}

// check if casing is accurate
if strings.EqualFold(org, o) && org != o {
retErr := fmt.Errorf("unable to retrieve organization %s. Did you mean %s?", o, org)

util.HandleError(c, http.StatusNotFound, retErr)

return
}
}

if strings.EqualFold(t, constants.SecretRepo) {
// retrieve repo name from SCM
scmRepo, err := scm.FromContext(c).GetRepoName(u, o, n)
if err != nil {
retErr := fmt.Errorf("unable to retrieve repository %s/%s", o, n)

util.HandleError(c, http.StatusNotFound, retErr)

return
}

// check if casing is accurate
if strings.EqualFold(scmRepo, n) && scmRepo != n {
retErr := fmt.Errorf("unable to retrieve repository %s. Did you mean %s?", n, scmRepo)

util.HandleError(c, http.StatusNotFound, retErr)

return
}
}

// update engine logger with API metadata
//
// https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields
Expand Down
43 changes: 43 additions & 0 deletions scm/github/org.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2022 Target Brands, Inc. All rights reserved.
//
// Use of this source code is governed by the LICENSE file in this repository.

package github

import (
"net/http"

"github.com/sirupsen/logrus"

"github.com/go-vela/types/library"
)

// GetOrg gets repo information from Github.
func (c *client) GetOrgName(u *library.User, o string) (string, error) {
c.Logger.WithFields(logrus.Fields{
"org": o,
"user": u.GetName(),
}).Tracef("retrieving org information for %s", o)

// create GitHub OAuth client with user's token
client := c.newClientToken(u.GetToken())

// send an API call to get the org info
orgInfo, resp, err := client.Organizations.Get(ctx, o)

orgName := orgInfo.GetLogin()

// if org is not found, return the personal org
if resp.StatusCode == http.StatusNotFound {
user, _, err := client.Users.Get(ctx, "")
if err != nil {
return "", err
}

orgName = user.GetLogin()
} else if err != nil {
return "", err
}

return orgName, nil
}
131 changes: 131 additions & 0 deletions scm/github/org_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2022 Target Brands, Inc. All rights reserved.
//
// Use of this source code is governed by the LICENSE file in this repository.

package github

import (
"net/http"
"net/http/httptest"
"reflect"
"testing"

"github.com/gin-gonic/gin"

"github.com/go-vela/types/library"
)

func TestGithub_GetOrgName(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/orgs/:org", func(c *gin.Context) {
c.Header("Content-Type", "application/json")
c.Status(http.StatusOK)
c.File("testdata/get_org.json")
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

want := "github"

client, _ := NewTest(s.URL)

// run test
got, err := client.GetOrgName(u, "github")

if resp.Code != http.StatusOK {
t.Errorf("GetOrgName returned %v, want %v", resp.Code, http.StatusOK)
}

if err != nil {
t.Errorf("GetOrgName returned err: %v", err)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("GetOrgName is %v, want %v", got, want)
}
}

func TestGithub_GetOrgName_Personal(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/user", func(c *gin.Context) {
c.Header("Content-Type", "application/json")
c.Status(http.StatusOK)
c.File("testdata/user.json")
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

want := "octocat"

client, _ := NewTest(s.URL)

// run test
got, err := client.GetOrgName(u, "octocat")

if resp.Code != http.StatusOK {
t.Errorf("GetOrgName returned %v, want %v", resp.Code, http.StatusOK)
}

if err != nil {
t.Errorf("GetOrgName returned err: %v", err)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("GetOrgName is %v, want %v", got, want)
}
}

func TestGithub_GetOrgName_Fail(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/orgs/:org", func(c *gin.Context) {
c.Header("Content-Type", "application/json")
c.Status(http.StatusNotFound)
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

client, _ := NewTest(s.URL)

// run test
_, err := client.GetOrgName(u, "octocat")

if err == nil {
t.Error("GetOrgName should return error")
}
}
20 changes: 20 additions & 0 deletions scm/github/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,26 @@ func (c *client) GetRepo(u *library.User, r *library.Repo) (*library.Repo, error
return toLibraryRepo(*repo), nil
}

// IsRepo checks to see if repo exists in GitHub.
func (c *client) GetRepoName(u *library.User, o string, r string) (string, error) {
c.Logger.WithFields(logrus.Fields{
"org": o,
"repo": r,
"user": u.GetName(),
}).Tracef("retrieving repository information for %s/%s", o, r)

// create GitHub OAuth client with user's token
client := c.newClientToken(u.GetToken())

// send an API call to get the repo info
repo, _, err := client.Repositories.Get(ctx, o, r)
if err != nil {
return "", err
}

return repo.GetName(), nil
}

// ListUserRepos returns a list of all repos the user has access to.
func (c *client) ListUserRepos(u *library.User) ([]*library.Repo, error) {
c.Logger.WithFields(logrus.Fields{
Expand Down
73 changes: 73 additions & 0 deletions scm/github/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,79 @@ func TestGithub_GetRepo_Fail(t *testing.T) {
}
}

func TestGithub_GetRepoName(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/repos/:owner/:repo", func(c *gin.Context) {
c.Header("Content-Type", "application/json")
c.Status(http.StatusOK)
c.File("testdata/get_repo.json")
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

want := "Hello-World"

client, _ := NewTest(s.URL)

// run test
got, err := client.GetRepoName(u, "octocat", "Hello-World")

if resp.Code != http.StatusOK {
t.Errorf("GetRepoName returned %v, want %v", resp.Code, http.StatusOK)
}

if err != nil {
t.Errorf("GetRepoName returned err: %v", err)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("GetRepoName is %v, want %v", got, want)
}
}

func TestGithub_GetRepoName_Fail(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/repos/:owner/:repo", func(c *gin.Context) {
c.Header("Content-Type", "application/json")
c.Status(http.StatusNotFound)
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

client, _ := NewTest(s.URL)

// run test
_, err := client.GetRepoName(u, "octocat", "Hello-World")

if err == nil {
t.Error("GetRepoName should return error")
}
}

func TestGithub_ListUserRepos(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)
Expand Down
53 changes: 53 additions & 0 deletions scm/github/testdata/get_org.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"login": "github",
"id": 1,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE=",
"url": "https://api.github.com/orgs/github",
"repos_url": "https://api.github.com/orgs/github/repos",
"events_url": "https://api.github.com/orgs/github/events",
"hooks_url": "https://api.github.com/orgs/github/hooks",
"issues_url": "https://api.github.com/orgs/github/issues",
"members_url": "https://api.github.com/orgs/github/members{/member}",
"public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"description": "A great organization",
"name": "github",
"company": "GitHub",
"blog": "https://github.com/blog",
"location": "San Francisco",
"email": "[email protected]",
"twitter_username": "github",
"is_verified": true,
"has_organization_projects": true,
"has_repository_projects": true,
"public_repos": 2,
"public_gists": 1,
"followers": 20,
"following": 0,
"html_url": "https://github.com/octocat",
"created_at": "2008-01-14T04:33:35Z",
"updated_at": "2014-03-03T18:58:10Z",
"type": "Organization",
"total_private_repos": 100,
"owned_private_repos": 100,
"private_gists": 81,
"disk_usage": 10000,
"collaborators": 8,
"billing_email": "[email protected]",
"plan": {
"name": "Medium",
"space": 400,
"private_repos": 20,
"filled_seats": 4,
"seats": 5
},
"default_repository_permission": "read",
"members_can_create_repositories": true,
"two_factor_requirement_enabled": true,
"members_allowed_repository_creation_type": "all",
"members_can_create_public_repositories": false,
"members_can_create_private_repositories": false,
"members_can_create_internal_repositories": false,
"members_can_create_pages": true,
"members_can_fork_private_repositories": false
}
6 changes: 6 additions & 0 deletions scm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ type Service interface {
// GetRepo defines a function that retrieves
// details for a repo.
GetRepo(*library.User, *library.Repo) (*library.Repo, error)
// GetRepoName defines a function that retrieves
// the name of the repo in the SCM.
GetRepoName(*library.User, string, string) (string, error)
// GetOrg defines a function that retrieves
// the name for an org in the SCM.
GetOrgName(*library.User, string) (string, error)
// GetHTMLURL defines a function that retrieves
// a repository file's html_url.
GetHTMLURL(*library.User, string, string, string, string) (string, error)
Expand Down