diff --git a/github/apps.go b/github/apps.go index 1ad9a9818fb..bc42e144572 100644 --- a/github/apps.go +++ b/github/apps.go @@ -32,8 +32,17 @@ type App struct { // InstallationToken represents an installation token. type InstallationToken struct { - Token *string `json:"token,omitempty"` - ExpiresAt *time.Time `json:"expires_at,omitempty"` + Token *string `json:"token,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + Permissions *InstallationPermissions `json:"permissions,omitempty"` + Repositories []*Repository `json:"repositories,omitempty"` +} + +// ScopedInstallationTokenRequest represents a request to create an +// installation token with limited repository and permission access. +type ScopedInstallationTokenRequest struct { + RepositoryIds *[]int64 `json:"repository_ids,omitempty"` + Permissions *InstallationPermissions `json:"permissions,omitempty"` } // InstallationPermissions lists the permissions for metadata, contents, issues and single file for an installation. @@ -190,6 +199,30 @@ func (s *AppsService) CreateInstallationToken(ctx context.Context, id int64) (*I return t, resp, nil } +// CreateScopedInstallationToken creates a new installation token +// with limited repository and permission access. +// +// GitHub API docs: https://developer.github.com/v3/apps/#create-a-new-installation-token +func (s *AppsService) CreateScopedInstallationToken(ctx context.Context, id int64, request *ScopedInstallationTokenRequest) (*InstallationToken, *Response, error) { + u := fmt.Sprintf("app/installations/%v/access_tokens", id) + + req, err := s.client.NewRequest("POST", u, request) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeIntegrationPreview) + + t := new(InstallationToken) + resp, err := s.client.Do(ctx, req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, nil +} + // Create a new attachment on user comment containing a url. // // GitHub API docs: https://developer.github.com/v3/apps/#create-a-content-attachment diff --git a/github/apps_test.go b/github/apps_test.go index 8456662b7d1..c34df32a41d 100644 --- a/github/apps_test.go +++ b/github/apps_test.go @@ -7,6 +7,7 @@ package github import ( "context" + "encoding/json" "fmt" "net/http" "reflect" @@ -185,6 +186,88 @@ func TestAppsService_CreateInstallationToken(t *testing.T) { } } +func TestAppsService_CreateRepositoryScopedInstallationToken(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + input := &ScopedInstallationTokenRequest{RepositoryIds: &[]int64{1}} + + mux.HandleFunc("/app/installations/1/access_tokens", func(w http.ResponseWriter, r *http.Request) { + v := new(ScopedInstallationTokenRequest) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeIntegrationPreview) + if !reflect.DeepEqual(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + fmt.Fprint(w, `{"token":"t", "repositories": [{"id": 1}]}`) + }) + + token, _, err := client.Apps.CreateScopedInstallationToken(context.Background(), 1, input) + if err != nil { + t.Errorf("Apps.CreateScopedInstallationToken returned error: %v", err) + } + + want := &InstallationToken{ + Token: String("t"), + Repositories: []*Repository{ + {ID: Int64(1)}, + }, + } + + if !reflect.DeepEqual(token, want) { + t.Errorf("Apps.CreateScopedInstallationToken returned %+v, want %+v", token, want) + } +} + +func TestAppsService_CreateRepositoryAndPermissionScopedInstallationToken(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + input := &ScopedInstallationTokenRequest{ + RepositoryIds: &[]int64{1}, + Permissions: &InstallationPermissions{ + Metadata: String("read"), + Contents: String("read"), + }, + } + + mux.HandleFunc("/app/installations/1/access_tokens", func(w http.ResponseWriter, r *http.Request) { + v := new(ScopedInstallationTokenRequest) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeIntegrationPreview) + if !reflect.DeepEqual(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + fmt.Fprint(w, `{"token":"t", "repositories": [{"id": 1}], "permissions": { "metadata": "read", "contents": "read" }}`) + }) + + token, _, err := client.Apps.CreateScopedInstallationToken(context.Background(), 1, input) + if err != nil { + t.Errorf("Apps.CreateScopedInstallationToken returned error: %v", err) + } + + want := &InstallationToken{ + Token: String("t"), + Repositories: []*Repository{ + {ID: Int64(1)}, + }, + Permissions: &InstallationPermissions{ + Metadata: String("read"), + Contents: String("read"), + }, + } + + if !reflect.DeepEqual(token, want) { + t.Errorf("Apps.CreateScopedInstallationToken returned %+v, want %+v", token, want) + } +} + func TestAppsService_CreateAttachement(t *testing.T) { client, mux, _, teardown := setup() defer teardown() diff --git a/github/github-accessors.go b/github/github-accessors.go index 9129909aee0..c0db090cd86 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -3676,6 +3676,14 @@ func (i *InstallationToken) GetExpiresAt() time.Time { return *i.ExpiresAt } +// GetPermissions returns the Permissions field. +func (i *InstallationToken) GetPermissions() *InstallationPermissions { + if i == nil { + return nil + } + return i.Permissions +} + // GetToken returns the Token field if it's non-nil, zero value otherwise. func (i *InstallationToken) GetToken() string { if i == nil || i.Token == nil { @@ -10548,6 +10556,22 @@ func (r *ReviewersRequest) GetNodeID() string { return *r.NodeID } +// GetPermissions returns the Permissions field. +func (s *ScopedInstallationTokenRequest) GetPermissions() *InstallationPermissions { + if s == nil { + return nil + } + return s.Permissions +} + +// GetRepositoryIds returns the RepositoryIds field if it's non-nil, zero value otherwise. +func (s *ScopedInstallationTokenRequest) GetRepositoryIds() []int64 { + if s == nil || s.RepositoryIds == nil { + return nil + } + return *s.RepositoryIds +} + // GetName returns the Name field if it's non-nil, zero value otherwise. func (s *ServiceHook) GetName() string { if s == nil || s.Name == nil {