From a6edb1171afdc1d19364a17938edb1527a983443 Mon Sep 17 00:00:00 2001 From: Glenn Lewis Date: Sat, 9 Apr 2016 22:20:04 -0700 Subject: [PATCH] add support for git signing API methods Fixes #334. Change-Id: I9b10ae5e7679f5196778d9eb3ded6168d7facfc3 --- github/authorizations.go | 3 + github/git_commits.go | 28 +++++--- github/git_commits_test.go | 1 + github/git_tags.go | 16 +++-- github/git_tags_test.go | 1 + github/github.go | 3 + github/repos_commits.go | 3 + github/repos_commits_test.go | 1 + github/users_gpg_keys.go | 129 ++++++++++++++++++++++++++++++++++ github/users_gpg_keys_test.go | 110 +++++++++++++++++++++++++++++ 10 files changed, 281 insertions(+), 14 deletions(-) create mode 100644 github/users_gpg_keys.go create mode 100644 github/users_gpg_keys_test.go diff --git a/github/authorizations.go b/github/authorizations.go index cfe6349..4fbb347 100644 --- a/github/authorizations.go +++ b/github/authorizations.go @@ -35,6 +35,9 @@ const ( ScopeReadPublicKey Scope = "read:public_key" ScopeWritePublicKey Scope = "write:public_key" ScopeAdminPublicKey Scope = "admin:public_key" + ScopeReadGPGKey Scope = "read:gpg_key" + ScopeWriteGPGKey Scope = "write:gpg_key" + ScopeAdminGPGKey Scope = "admin:gpg_key" ) // AuthorizationsService handles communication with the authorization related diff --git a/github/git_commits.go b/github/git_commits.go index 41e7ff9..0bcad41 100644 --- a/github/git_commits.go +++ b/github/git_commits.go @@ -10,16 +10,25 @@ import ( "time" ) +// SignatureVerification represents GPG signature verification. +type SignatureVerification struct { + Verified *bool `json:"verified,omitempty"` + Reason *string `json:"reason,omitempty"` + Signature *string `json:"signature,omitempty"` + Payload *string `json:"payload,omitempty"` +} + // Commit represents a GitHub commit. type Commit struct { - SHA *string `json:"sha,omitempty"` - Author *CommitAuthor `json:"author,omitempty"` - Committer *CommitAuthor `json:"committer,omitempty"` - Message *string `json:"message,omitempty"` - Tree *Tree `json:"tree,omitempty"` - Parents []Commit `json:"parents,omitempty"` - Stats *CommitStats `json:"stats,omitempty"` - URL *string `json:"url,omitempty"` + SHA *string `json:"sha,omitempty"` + Author *CommitAuthor `json:"author,omitempty"` + Committer *CommitAuthor `json:"committer,omitempty"` + Message *string `json:"message,omitempty"` + Tree *Tree `json:"tree,omitempty"` + Parents []Commit `json:"parents,omitempty"` + Stats *CommitStats `json:"stats,omitempty"` + URL *string `json:"url,omitempty"` + Verification *SignatureVerification `json:"verification,omitempty"` // CommentCount is the number of GitHub comments on the commit. This // is only populated for requests that fetch GitHub data like @@ -56,6 +65,9 @@ func (s *GitService) GetCommit(owner string, repo string, sha string) (*Commit, return nil, nil, err } + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + c := new(Commit) resp, err := s.client.Do(req, c) if err != nil { diff --git a/github/git_commits_test.go b/github/git_commits_test.go index 538f523..566ac4f 100644 --- a/github/git_commits_test.go +++ b/github/git_commits_test.go @@ -19,6 +19,7 @@ func TestGitService_GetCommit(t *testing.T) { mux.HandleFunc("/repos/o/r/git/commits/s", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) fmt.Fprint(w, `{"sha":"s","message":"m","author":{"name":"n"}}`) }) diff --git a/github/git_tags.go b/github/git_tags.go index 7b53f5c..01b9cb2 100644 --- a/github/git_tags.go +++ b/github/git_tags.go @@ -11,12 +11,13 @@ import ( // Tag represents a tag object. type Tag struct { - Tag *string `json:"tag,omitempty"` - SHA *string `json:"sha,omitempty"` - URL *string `json:"url,omitempty"` - Message *string `json:"message,omitempty"` - Tagger *CommitAuthor `json:"tagger,omitempty"` - Object *GitObject `json:"object,omitempty"` + Tag *string `json:"tag,omitempty"` + SHA *string `json:"sha,omitempty"` + URL *string `json:"url,omitempty"` + Message *string `json:"message,omitempty"` + Tagger *CommitAuthor `json:"tagger,omitempty"` + Object *GitObject `json:"object,omitempty"` + Verification *SignatureVerification `json:"verification,omitempty"` } // createTagRequest represents the body of a CreateTag request. This is mostly @@ -40,6 +41,9 @@ func (s *GitService) GetTag(owner string, repo string, sha string) (*Tag, *Respo return nil, nil, err } + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + tag := new(Tag) resp, err := s.client.Do(req, tag) return tag, resp, err diff --git a/github/git_tags_test.go b/github/git_tags_test.go index fb41bf3..5997e8d 100644 --- a/github/git_tags_test.go +++ b/github/git_tags_test.go @@ -19,6 +19,7 @@ func TestGitService_GetTag(t *testing.T) { mux.HandleFunc("/repos/o/r/git/tags/s", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) fmt.Fprint(w, `{"tag": "t"}`) }) diff --git a/github/github.go b/github/github.go index 3b4ac2d..756a484 100644 --- a/github/github.go +++ b/github/github.go @@ -72,6 +72,9 @@ const ( // https://developer.github.com/changes/2016-04-01-squash-api-preview/ mediaTypeSquashPreview = "application/vnd.github.polaris-preview+json" + + // https://developer.github.com/changes/2016-04-04-git-signing-api-preview/ + mediaTypeGitSigningPreview = "application/vnd.github.cryptographer-preview+json" ) // A Client manages communication with the GitHub API. diff --git a/github/repos_commits.go b/github/repos_commits.go index 9cbdbfd..bf49a6f 100644 --- a/github/repos_commits.go +++ b/github/repos_commits.go @@ -138,6 +138,9 @@ func (s *RepositoriesService) GetCommit(owner, repo, sha string) (*RepositoryCom return nil, nil, err } + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + commit := new(RepositoryCommit) resp, err := s.client.Do(req, commit) if err != nil { diff --git a/github/repos_commits_test.go b/github/repos_commits_test.go index 601e748..ac06d13 100644 --- a/github/repos_commits_test.go +++ b/github/repos_commits_test.go @@ -55,6 +55,7 @@ func TestRepositoriesService_GetCommit(t *testing.T) { mux.HandleFunc("/repos/o/r/commits/s", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) fmt.Fprintf(w, `{ "sha": "s", "commit": { "message": "m" }, diff --git a/github/users_gpg_keys.go b/github/users_gpg_keys.go new file mode 100644 index 0000000..8187652 --- /dev/null +++ b/github/users_gpg_keys.go @@ -0,0 +1,129 @@ +// Copyright 2016 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "fmt" + "time" +) + +// GPGKey represents a GitHub user's public GPG key used to verify GPG signed commits and tags. +// +// https://developer.github.com/changes/2016-04-04-git-signing-api-preview/ +type GPGKey struct { + ID *int `json:"id,omitempty"` + PrimaryKeyID *int `json:"primary_key_id,omitempty"` + KeyID *string `json:"key_id,omitempty"` + PublicKey *string `json:"public_key,omitempty"` + Emails []GPGEmail `json:"emails,omitempty"` + Subkeys []GPGKey `json:"subkeys,omitempty"` + CanSign *bool `json:"can_sign,omitempty"` + CanEncryptComms *bool `json:"can_encrypt_comms,omitempty"` + CanEncryptStorage *bool `json:"can_encrypt_storage,omitempty"` + CanCertify *bool `json:"can_certify,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` +} + +// String stringifies a GPGKey. +func (k GPGKey) String() string { + return Stringify(k) +} + +// GPGEmail represents an email address associated to a GPG key. +type GPGEmail struct { + Email *string `json:"email,omitempty"` + Verified *bool `json:"verified,omitempty"` +} + +// ListGPGKeys lists the current user's GPG keys. It requires authentication +// via Basic Auth or via OAuth with at least read:gpg_key scope. +// +// GitHub API docs: https://developer.github.com/v3/users/gpg_keys/#list-your-gpg-keys +func (s *UsersService) ListGPGKeys() ([]GPGKey, *Response, error) { + req, err := s.client.NewRequest("GET", "user/gpg_keys", nil) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + + var keys []GPGKey + resp, err := s.client.Do(req, &keys) + if err != nil { + return nil, resp, err + } + + return keys, resp, err +} + +// GetGPGKey gets extended details for a single GPG key. It requires authentication +// via Basic Auth or via OAuth with at least read:gpg_key scope. +// +// GitHub API docs: https://developer.github.com/v3/users/gpg_keys/#get-a-single-gpg-key +func (s *UsersService) GetGPGKey(id int) (*GPGKey, *Response, error) { + u := fmt.Sprintf("user/gpg_keys/%v", id) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + + key := &GPGKey{} + resp, err := s.client.Do(req, key) + if err != nil { + return nil, resp, err + } + + return key, resp, err +} + +// CreateGPGKey creates a GPG key. It requires authenticatation via Basic Auth +// or OAuth with at least write:gpg_key scope. +// +// GitHub API docs: https://developer.github.com/v3/users/gpg_keys/#create-a-gpg-key +func (s *UsersService) CreateGPGKey(armoredPublicKey string) (*GPGKey, *Response, error) { + gpgKey := &struct { + ArmoredPublicKey *string `json:"armored_public_key,omitempty"` + }{ + ArmoredPublicKey: String(armoredPublicKey), + } + req, err := s.client.NewRequest("POST", "user/gpg_keys", gpgKey) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + + key := &GPGKey{} + resp, err := s.client.Do(req, key) + if err != nil { + return nil, resp, err + } + + return key, resp, err +} + +// DeleteGPGKey deletes a GPG key. It requires authentication via Basic Auth or +// via OAuth with at least admin:gpg_key scope. +// +// GitHub API docs: https://developer.github.com/v3/users/gpg_keys/#delete-a-gpg-key +func (s *UsersService) DeleteGPGKey(id int) (*Response, error) { + u := fmt.Sprintf("user/gpg_keys/%v", id) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeGitSigningPreview) + + return s.client.Do(req, nil) +} diff --git a/github/users_gpg_keys_test.go b/github/users_gpg_keys_test.go new file mode 100644 index 0000000..3cf7e0a --- /dev/null +++ b/github/users_gpg_keys_test.go @@ -0,0 +1,110 @@ +// Copyright 2016 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestUsersService_ListGPGKeys(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/gpg_keys", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) + fmt.Fprint(w, `[{"id":1,"primary_key_id":2}]`) + }) + + keys, _, err := client.Users.ListGPGKeys() + if err != nil { + t.Errorf("Users.ListGPGKeys returned error: %v", err) + } + + want := []GPGKey{{ID: Int(1), PrimaryKeyID: Int(2)}} + if !reflect.DeepEqual(keys, want) { + t.Errorf("Users.ListGPGKeys = %+v, want %+v", keys, want) + } +} + +func TestUsersService_GetGPGKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/gpg_keys/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) + fmt.Fprint(w, `{"id":1}`) + }) + + key, _, err := client.Users.GetGPGKey(1) + if err != nil { + t.Errorf("Users.GetGPGKey returned error: %v", err) + } + + want := &GPGKey{ID: Int(1)} + if !reflect.DeepEqual(key, want) { + t.Errorf("Users.GetGPGKey = %+v, want %+v", key, want) + } +} + +func TestUsersService_CreateGPGKey(t *testing.T) { + setup() + defer teardown() + + input := ` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mQINBFcEd9kBEACo54TDbGhKlXKWMvJgecEUKPPcv7XdnpKdGb3LRw5MvFwT0V0f +... +=tqfb +-----END PGP PUBLIC KEY BLOCK-----` + + mux.HandleFunc("/user/gpg_keys", func(w http.ResponseWriter, r *http.Request) { + var gpgKey struct { + ArmoredPublicKey *string `json:"armored_public_key,omitempty"` + } + json.NewDecoder(r.Body).Decode(&gpgKey) + + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) + if gpgKey.ArmoredPublicKey == nil || *gpgKey.ArmoredPublicKey != input { + t.Errorf("gpgKey = %+v, want %q", gpgKey, input) + } + + fmt.Fprint(w, `{"id":1}`) + }) + + gpgKey, _, err := client.Users.CreateGPGKey(input) + if err != nil { + t.Errorf("Users.GetGPGKey returned error: %v", err) + } + + want := &GPGKey{ID: Int(1)} + if !reflect.DeepEqual(gpgKey, want) { + t.Errorf("Users.GetGPGKey = %+v, want %+v", gpgKey, want) + } +} + +func TestUsersService_DeleteGPGKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/gpg_keys/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testHeader(t, r, "Accept", mediaTypeGitSigningPreview) + }) + + _, err := client.Users.DeleteGPGKey(1) + if err != nil { + t.Errorf("Users.DeleteGPGKey returned error: %v", err) + } +}