From 86c578bdf2775c472222cf51a637466ae98d6247 Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Thu, 3 Nov 2022 06:15:23 +0000 Subject: [PATCH 1/6] set visibility to internal for GHAE Signed-off-by: Florian Wagner --- src/push.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/push.go b/src/push.go index 37b4cdd1..81c46404 100644 --- a/src/push.go +++ b/src/push.go @@ -25,6 +25,7 @@ const xOAuthScopesHeader = "X-OAuth-Scopes" type PushOnlyFlags struct { BaseURL, Token, ActionsAdminUser string DisableGitAuth bool + IsAE bool } type PushFlags struct { @@ -42,6 +43,7 @@ func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'. Note that the site_admin scope in the token is required for the impersonation to work.") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") + f.IsAE = false } func (f *PushFlags) Validate() Validations { @@ -85,9 +87,9 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error return "", errors.New("the current token doesn't have the `site_admin` scope, the impersonation function requires the `site_admin` permission to be able to impersonate") } - isAE := rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue + flags.IsAE = rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue minimumRepositoryScope := "public_repo" - if isAE { + if flags.IsAE { // the default repository scope for non-ae instances is 'public_repo' // while it is `repo` for ae. minimumRepositoryScope = "repo" @@ -166,7 +168,7 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC } fmt.Printf("syncing `%s`\n", nwo) - ghRepo, err := getOrCreateGitHubRepo(ctx, ghClient, bareRepoName, ownerName) + ghRepo, err := getOrCreateGitHubRepo(ctx, flags, ghClient, bareRepoName, ownerName) if err != nil { return errors.Wrapf(err, "error creating github repository `%s`", nwo) } @@ -178,13 +180,18 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC return nil } -func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, ownerName string) (*github.Repository, error) { +func getOrCreateGitHubRepo(ctx context.Context, flags *PushFlags, client *github.Client, repoName, ownerName string) (*github.Repository, error) { + visibility := github.String("public") + if flags.IsAE { + visibility = github.String("internal") + } repo := &github.Repository{ Name: github.String(repoName), HasIssues: github.Bool(false), HasWiki: github.Bool(false), HasPages: github.Bool(false), HasProjects: github.Bool(false), + Visibility: visibility, } currentUser, _, err := client.Users.Get(ctx, "") From 6d732db1cf8e12f2bb06adf27a15bafd368caf89 Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Thu, 3 Nov 2022 06:56:42 +0000 Subject: [PATCH 2/6] add push flag to indicate target it GHAE Signed-off-by: Florian Wagner --- README.md | 2 ++ src/push.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e081cb0a..c7b5916a 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ When no machine has access to both the public internet and the GHES instance: Limit push to specific repositories in the cache directory. - `actions-admin-user` _(optional)_ The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. Note that `site_admin` scope is required in the token for the impersonation to work. +- `destination-is-ghae` _(optional)_ + Indicates if the destination applicance is GHAE. **Example Usage:** diff --git a/src/push.go b/src/push.go index 81c46404..ffe7314d 100644 --- a/src/push.go +++ b/src/push.go @@ -43,7 +43,7 @@ func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'. Note that the site_admin scope in the token is required for the impersonation to work.") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") - f.IsAE = false + cmd.Flags().BoolVar(&f.IsAE, "destination-is-ghae", false, "Destination instance is GHAE") } func (f *PushFlags) Validate() Validations { From ef922b8e3b5069c7b5d52be2e71eda87dd19689e Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Fri, 4 Nov 2022 04:43:03 +0000 Subject: [PATCH 3/6] refactor repository retrieval/creation Signed-off-by: Florian Wagner --- README.md | 2 -- src/push.go | 61 +++++++++++++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c7b5916a..e081cb0a 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,6 @@ When no machine has access to both the public internet and the GHES instance: Limit push to specific repositories in the cache directory. - `actions-admin-user` _(optional)_ The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. Note that `site_admin` scope is required in the token for the impersonation to work. -- `destination-is-ghae` _(optional)_ - Indicates if the destination applicance is GHAE. **Example Usage:** diff --git a/src/push.go b/src/push.go index ffe7314d..de0fce1e 100644 --- a/src/push.go +++ b/src/push.go @@ -25,7 +25,6 @@ const xOAuthScopesHeader = "X-OAuth-Scopes" type PushOnlyFlags struct { BaseURL, Token, ActionsAdminUser string DisableGitAuth bool - IsAE bool } type PushFlags struct { @@ -43,7 +42,6 @@ func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'. Note that the site_admin scope in the token is required for the impersonation to work.") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") - cmd.Flags().BoolVar(&f.IsAE, "destination-is-ghae", false, "Destination instance is GHAE") } func (f *PushFlags) Validate() Validations { @@ -87,9 +85,9 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error return "", errors.New("the current token doesn't have the `site_admin` scope, the impersonation function requires the `site_admin` permission to be able to impersonate") } - flags.IsAE = rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue + isAE := rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue minimumRepositoryScope := "public_repo" - if flags.IsAE { + if isAE { // the default repository scope for non-ae instances is 'public_repo' // while it is `repo` for ae. minimumRepositoryScope = "repo" @@ -168,7 +166,7 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC } fmt.Printf("syncing `%s`\n", nwo) - ghRepo, err := getOrCreateGitHubRepo(ctx, flags, ghClient, bareRepoName, ownerName) + ghRepo, err := getOrCreateGitHubRepo(ctx, ghClient, bareRepoName, ownerName) if err != nil { return errors.Wrapf(err, "error creating github repository `%s`", nwo) } @@ -180,27 +178,17 @@ func PushWithGitImpl(ctx context.Context, flags *PushFlags, repoName string, ghC return nil } -func getOrCreateGitHubRepo(ctx context.Context, flags *PushFlags, client *github.Client, repoName, ownerName string) (*github.Repository, error) { - visibility := github.String("public") - if flags.IsAE { - visibility = github.String("internal") - } - repo := &github.Repository{ - Name: github.String(repoName), - HasIssues: github.Bool(false), - HasWiki: github.Bool(false), - HasPages: github.Bool(false), - HasProjects: github.Bool(false), - Visibility: visibility, - } - - currentUser, _, err := client.Users.Get(ctx, "") +func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, ownerName string) (*github.Repository, error) { + // retrieve user associated to authentication credentials provided + currentUser, userResponse, err := client.Users.Get(ctx, "") if err != nil { return nil, errors.Wrap(err, "error retrieving authenticated user") } if currentUser == nil || currentUser.Login == nil { return nil, errors.New("error retrieving authenticated user's login name") } + // checking if we talk to GHAE + isAE := userResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue // check if the owner refers to the authenticated user or an organization. var createRepoOrgName string @@ -216,15 +204,34 @@ func getOrCreateGitHubRepo(ctx context.Context, flags *PushFlags, client *github } } - ghRepo, resp, err := client.Repositories.Create(ctx, createRepoOrgName, repo) + // check if repository already exists + ghRepo, resp, err := client.Repositories.Get(ctx, ownerName, repoName) + if err == nil { - fmt.Printf("Created repo `%s/%s`\n", ownerName, repoName) - } else if resp != nil && resp.StatusCode == 422 { - ghRepo, _, err = client.Repositories.Get(ctx, ownerName, repoName) - } - if err != nil { - return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName) + fmt.Printf("Existing repo `%s/%s`\n", ownerName, repoName) + } else if resp != nil && resp.StatusCode == 404 { + // repo not existing yet - try to create + visibility := github.String("public") + if isAE { + visibility = github.String("internal") + } + repo := &github.Repository{ + Name: github.String(repoName), + HasIssues: github.Bool(false), + HasWiki: github.Bool(false), + HasPages: github.Bool(false), + HasProjects: github.Bool(false), + Visibility: visibility, + } + + ghRepo, _, err = client.Repositories.Create(ctx, createRepoOrgName, repo) + if err == nil { + fmt.Printf("Created repo `%s/%s`\n", ownerName, repoName) + } else { + return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName) + } } + if ghRepo == nil { return nil, errors.New("error repository is nil") } From 51dff542d6746110bdd9260cbb84431430ad0e0f Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Fri, 4 Nov 2022 04:44:35 +0000 Subject: [PATCH 4/6] add e2e test for impersonations and visibility Signed-off-by: Florian Wagner --- script/test-build | 21 +++++++++++++++++++ test/github.go | 52 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/script/test-build b/script/test-build index 7a94fe75..25177595 100755 --- a/script/test-build +++ b/script/test-build @@ -123,6 +123,16 @@ function test_push() { push "pushing to authenticated user's account" assert_dest_sha "monalisa/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating monalisa/new-repo" + # Push to GHAE with impersonation + setup_cache "org-already-exists/ghae-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/ghae-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + push_impersonation "ghae-admin" "pushing to GHAE repo" + + # Push to GHES with impersonation + setup_cache "org-already-exists/ghes-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/ghes-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + push_impersonation "ghes-admin" "pushing to GHES repo" + echo "all push tests passed successfully" } @@ -313,6 +323,17 @@ function push2args() { fail $3 } +function push_impersonation() { + bin/actions-sync push \ + --cache-dir "test/tmp/cache" \ + --disable-push-git-auth \ + --destination-token "token" \ + --destination-url "http://localhost:$DEST_API_PORT" \ + --actions-admin-user $1 \ + &> $OUTPUT || + fail "$2" +} + function sync() { bin/actions-sync sync \ --cache-dir "test/tmp/cache" \ diff --git a/test/github.go b/test/github.go index f7677981..7cf95b34 100644 --- a/test/github.go +++ b/test/github.go @@ -8,14 +8,18 @@ import ( "io/ioutil" "net/http" "path" + "strings" "github.com/google/go-github/v43/github" "github.com/gorilla/mux" ) var authenticatedLogin string = "monalisa" -var existingOrg string = "org-already-exists" -var existingRepo string = "repo-already-exists" + +const existingOrg string = "org-already-exists" +const existingRepo string = "repo-already-exists" +const ghaeRepo string = "ghae-repo" +const xOAuthScopesHeader = "X-OAuth-Scopes" func main() { var port, gitDaemonURL string @@ -28,9 +32,14 @@ func main() { r.HandleFunc("/api/v3", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("x-github-enterprise-version", "GitHub AE") + w.Header().Set(xOAuthScopesHeader, "site_admin") }) r.HandleFunc("/api/v3/user", func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if strings.Contains(token, "ghaetoken") { + w.Header().Set("x-github-enterprise-version", "GitHub AE") + } currentUser := github.User{Login: &authenticatedLogin} b, _ := json.Marshal(currentUser) _, err := w.Write(b) @@ -39,8 +48,7 @@ func main() { } }) - r.HandleFunc("/api/v3/admin/users/actions-admin/authorizations", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("x-github-enterprise-version", "GitHub AE") + r.HandleFunc("/api/v3/admin/users/ghes-admin/authorizations", func(w http.ResponseWriter, r *http.Request) { token := "token" auth := github.Authorization{Token: &token} b, _ := json.Marshal(auth) @@ -50,6 +58,17 @@ func main() { } }).Methods("POST") + r.HandleFunc("/api/v3/admin/users/ghae-admin/authorizations", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("x-github-enterprise-version", "GitHub AE") + token := "ghaetoken" + auth := github.Authorization{Token: &token} + b, _ := json.Marshal(auth) + _, err := w.Write(b) + if err != nil { + panic(err) + } + }).Methods("POST") + r.HandleFunc("/api/v3/admin/organizations", func(w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { @@ -114,16 +133,35 @@ func main() { panic(err) } var repoReq struct { - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty"` + Visibility string `json:"visibility,omitempty"` } err = json.Unmarshal(b, &repoReq) if err != nil { panic(err) } - if repoReq.Name == "repo-already-exists" { + var errString string = "" + // check visibility requirements + if repoReq.Name == ghaeRepo { + if repoReq.Visibility != "internal" { + errString = fmt.Sprintf("Provided repo visibility %s for GHAE must be internal", repoReq.Visibility) + } + } else { + if repoReq.Visibility != "public" { + errString = fmt.Sprintf("Provided repo visibility %s for GHES must be public", repoReq.Visibility) + } + } + + // check if we are testing existing Repo + if repoReq.Name == existingRepo { + errString = fmt.Sprintf("Repo %s already exists", html.EscapeString(repoReq.Name)) + } + + // if there is an error throw it back + if errString != "" { w.WriteHeader(http.StatusUnprocessableEntity) - _, err := w.Write([]byte(fmt.Sprintf("Repo %s already exists", html.EscapeString(repoReq.Name)))) + _, err := w.Write([]byte(errString)) if err != nil { panic(err) } From d7e1ea845ef8598ecadb063ffc6221d4cfb593ea Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Fri, 4 Nov 2022 04:52:59 +0000 Subject: [PATCH 5/6] exclude gocyclo lint in E2E mock API server Signed-off-by: Florian Wagner --- test/github.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/github.go b/test/github.go index 7cf95b34..73f5ca3d 100644 --- a/test/github.go +++ b/test/github.go @@ -21,6 +21,7 @@ const existingRepo string = "repo-already-exists" const ghaeRepo string = "ghae-repo" const xOAuthScopesHeader = "X-OAuth-Scopes" +//nolint:gocyclo func main() { var port, gitDaemonURL string flag.StringVar(&port, "p", "", "") From e30fad8050cac52910e456c3356eabd24909ba39 Mon Sep 17 00:00:00 2001 From: Florian Wagner Date: Fri, 4 Nov 2022 06:15:14 +0000 Subject: [PATCH 6/6] adding missing error handling for GET repo Signed-off-by: Florian Wagner --- src/push.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/push.go b/src/push.go index de0fce1e..dd3ce192 100644 --- a/src/push.go +++ b/src/push.go @@ -230,6 +230,8 @@ func getOrCreateGitHubRepo(ctx context.Context, client *github.Client, repoName, } else { return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName) } + } else if err != nil { + return nil, errors.Wrapf(err, "error creating repository %s/%s", ownerName, repoName) } if ghRepo == nil {