summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTaylan Develioglu <taylan.develioglu@booking.com>2020-07-06 14:09:55 +0200
committerTaylan Develioglu <taylan.develioglu@booking.com>2020-08-17 17:16:06 +0200
commitb8d66d7923150402f54f13d793d3051efab3a832 (patch)
treedd67dbef7c4c06e3a1ac5cf981be9ee37d355a03
parent4b1ee791a1bdc927becee37ae84f7ba226d17791 (diff)
downloadgitlab-shell-b8d66d7923150402f54f13d793d3051efab3a832.tar.gz
Add support obtaining personal access tokens via SSH
Implements the feature requested in gitlab-org/gitlab#19672 This requires the internal api counterpart in gitlab-org/gitlab!36302 to be merged first. It can be used as follows: ``` censored@censored-VirtualBox:~/git/gitlab$ ssh git@gitlab-2004 personal_access_token remote: remote: ======================================================================== remote: remote: Usage: personal_access_token <name> <scope1[,scope2,...]> [ttl_days] remote: remote: ======================================================================== remote: censored@censored-VirtualBox:~/git/gitlab$ ssh git@gitlab-2004 personal_access_token newtoken read_api,read_repository 30 Token: aAY1G3YPeemECgUvxuXY Scopes: read_api,read_repository Expires: 2020-08-07 ```
-rw-r--r--.gitignore1
-rw-r--r--internal/command/command.go3
-rw-r--r--internal/command/command_test.go7
-rw-r--r--internal/command/commandargs/shell.go13
-rw-r--r--internal/command/personalaccesstoken/personalaccesstoken.go86
-rw-r--r--internal/command/personalaccesstoken/personalaccesstoken_test.go186
-rw-r--r--internal/gitlabnet/personalaccesstoken/client.go93
-rw-r--r--internal/gitlabnet/personalaccesstoken/client_test.go177
-rw-r--r--spec/gitlab_shell_personal_access_token_spec.rb119
9 files changed, 679 insertions, 6 deletions
diff --git a/.gitignore b/.gitignore
index c454751..08977b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ cover.out
tmp/*
.idea
*.log
+*.swp
/*.log*
authorized_keys.lock
.gitlab_shell_secret
diff --git a/internal/command/command.go b/internal/command/command.go
index af63862..283b4a1 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -7,6 +7,7 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/command/discover"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/healthcheck"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/lfsauthenticate"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/personalaccesstoken"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/receivepack"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/disallowedcommand"
@@ -63,6 +64,8 @@ func buildShellCommand(args *commandargs.Shell, config *config.Config, readWrite
return &uploadpack.Command{Config: config, Args: args, ReadWriter: readWriter}
case commandargs.UploadArchive:
return &uploadarchive.Command{Config: config, Args: args, ReadWriter: readWriter}
+ case commandargs.PersonalAccessToken:
+ return &personalaccesstoken.Command{Config: config, Args: args, ReadWriter: readWriter}
}
return nil
diff --git a/internal/command/command_test.go b/internal/command/command_test.go
index 2ca319e..db55e7d 100644
--- a/internal/command/command_test.go
+++ b/internal/command/command_test.go
@@ -11,6 +11,7 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/command/discover"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/healthcheck"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/lfsauthenticate"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/personalaccesstoken"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/receivepack"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/disallowedcommand"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/twofactorrecover"
@@ -98,6 +99,12 @@ func TestNew(t *testing.T) {
arguments: []string{"key", "principal"},
expectedType: &authorizedprincipals.Command{},
},
+ {
+ desc: "it returns a PersonalAccessToken command",
+ executable: gitlabShellExec,
+ environment: buildEnv("personal_access_token"),
+ expectedType: &personalaccesstoken.Command{},
+ },
}
for _, tc := range testCases {
diff --git a/internal/command/commandargs/shell.go b/internal/command/commandargs/shell.go
index 1fe59fb..4632cff 100644
--- a/internal/command/commandargs/shell.go
+++ b/internal/command/commandargs/shell.go
@@ -9,12 +9,13 @@ import (
)
const (
- Discover CommandType = "discover"
- TwoFactorRecover CommandType = "2fa_recovery_codes"
- LfsAuthenticate CommandType = "git-lfs-authenticate"
- ReceivePack CommandType = "git-receive-pack"
- UploadPack CommandType = "git-upload-pack"
- UploadArchive CommandType = "git-upload-archive"
+ Discover CommandType = "discover"
+ TwoFactorRecover CommandType = "2fa_recovery_codes"
+ LfsAuthenticate CommandType = "git-lfs-authenticate"
+ ReceivePack CommandType = "git-receive-pack"
+ UploadPack CommandType = "git-upload-pack"
+ UploadArchive CommandType = "git-upload-archive"
+ PersonalAccessToken CommandType = "personal_access_token"
GitProtocolEnv = "GIT_PROTOCOL"
)
diff --git a/internal/command/personalaccesstoken/personalaccesstoken.go b/internal/command/personalaccesstoken/personalaccesstoken.go
new file mode 100644
index 0000000..b283890
--- /dev/null
+++ b/internal/command/personalaccesstoken/personalaccesstoken.go
@@ -0,0 +1,86 @@
+package personalaccesstoken
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/personalaccesstoken"
+)
+
+const (
+ usageText = "Usage: personal_access_token <name> <scope1[,scope2,...]> [ttl_days]"
+ expiresDateFormat = "2006-01-02"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+ TokenArgs *tokenArgs
+}
+
+type tokenArgs struct {
+ Name string
+ Scopes []string
+ ExpiresDate string // Calculated, a TTL is passed from command-line.
+}
+
+func (c *Command) Execute() error {
+ err := c.parseTokenArgs()
+ if err != nil {
+ return err
+ }
+
+ response, err := c.getPersonalAccessToken()
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprint(c.ReadWriter.Out, "Token: "+response.Token+"\n")
+ fmt.Fprint(c.ReadWriter.Out, "Scopes: "+strings.Join(response.Scopes, ",")+"\n")
+ if response.ExpiresAt == "" {
+ fmt.Fprint(c.ReadWriter.Out, "Expires: never\n")
+ } else {
+ fmt.Fprint(c.ReadWriter.Out, "Expires: "+response.ExpiresAt+"\n")
+ }
+ return nil
+}
+
+func (c *Command) parseTokenArgs() error {
+ if len(c.Args.SshArgs) < 3 || len(c.Args.SshArgs) > 4 {
+ return errors.New(usageText)
+ }
+ c.TokenArgs = &tokenArgs{
+ Name: c.Args.SshArgs[1],
+ Scopes: strings.Split(c.Args.SshArgs[2], ","),
+ }
+
+ if len(c.Args.SshArgs) < 4 {
+ return nil
+ }
+ rawTTL := c.Args.SshArgs[3]
+
+ TTL, err := strconv.Atoi(rawTTL)
+ if err != nil || TTL < 0 {
+ return fmt.Errorf("Invalid value for days_ttl: '%s'", rawTTL)
+ }
+
+ c.TokenArgs.ExpiresDate = time.Now().AddDate(0, 0, TTL+1).Format(expiresDateFormat)
+
+ return nil
+}
+
+func (c *Command) getPersonalAccessToken() (*personalaccesstoken.Response, error) {
+ client, err := personalaccesstoken.NewClient(c.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ return client.GetPersonalAccessToken(c.Args, c.TokenArgs.Name, &c.TokenArgs.Scopes, c.TokenArgs.ExpiresDate)
+}
diff --git a/internal/command/personalaccesstoken/personalaccesstoken_test.go b/internal/command/personalaccesstoken/personalaccesstoken_test.go
new file mode 100644
index 0000000..bc748ab
--- /dev/null
+++ b/internal/command/personalaccesstoken/personalaccesstoken_test.go
@@ -0,0 +1,186 @@
+package personalaccesstoken
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-shell/client/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/personalaccesstoken"
+)
+
+var (
+ requests []testserver.TestRequestHandler
+)
+
+func setup(t *testing.T) {
+ requests = []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/personal_access_token",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ b, err := ioutil.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *personalaccesstoken.RequestBody
+ json.Unmarshal(b, &requestBody)
+
+ switch requestBody.KeyId {
+ case "forbidden":
+ body := map[string]interface{}{
+ "success": false,
+ "message": "Forbidden!",
+ }
+ json.NewEncoder(w).Encode(body)
+ case "broken":
+ w.WriteHeader(http.StatusInternalServerError)
+ case "badresponse":
+ default:
+ var expiresAt interface{}
+ if requestBody.ExpiresAt == "" {
+ expiresAt = nil
+ } else {
+ expiresAt = "9001-11-17"
+ }
+ body := map[string]interface{}{
+ "success": true,
+ "token": "YXuxvUgCEmeePY3G1YAa",
+ "scopes": requestBody.Scopes,
+ "expires_at": expiresAt,
+ }
+ json.NewEncoder(w).Encode(body)
+ }
+ },
+ },
+ }
+}
+
+const (
+ cmdname = "personal_access_token"
+)
+
+func TestExecute(t *testing.T) {
+ setup(t)
+
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.Shell
+ expectedOutput string
+ expectedError string
+ }{
+ {
+ desc: "Without any arguments",
+ arguments: &commandargs.Shell{},
+ expectedError: usageText,
+ },
+ {
+ desc: "With too few arguments",
+ arguments: &commandargs.Shell{
+ SshArgs: []string{cmdname, "newtoken"},
+ },
+ expectedError: usageText,
+ },
+ {
+ desc: "With too many arguments",
+ arguments: &commandargs.Shell{
+ SshArgs: []string{cmdname, "newtoken", "api", "bad_ttl", "toomany"},
+ },
+ expectedError: usageText,
+ },
+ {
+ desc: "With a bad ttl_days argument",
+ arguments: &commandargs.Shell{
+ SshArgs: []string{cmdname, "newtoken", "api", "bad_ttl"},
+ },
+ expectedError: "Invalid value for days_ttl: 'bad_ttl'",
+ },
+ {
+ desc: "Without a ttl argument",
+ arguments: &commandargs.Shell{
+ GitlabKeyId: "default",
+ SshArgs: []string{cmdname, "newtoken", "read_api,read_repository"},
+ },
+ expectedOutput: "Token: YXuxvUgCEmeePY3G1YAa\n" +
+ "Scopes: read_api,read_repository\n" +
+ "Expires: never\n",
+ },
+ {
+ desc: "With a ttl argument",
+ arguments: &commandargs.Shell{
+ GitlabKeyId: "default",
+ SshArgs: []string{cmdname, "newtoken", "api", "30"},
+ },
+ expectedOutput: "Token: YXuxvUgCEmeePY3G1YAa\n" +
+ "Scopes: api\n" +
+ "Expires: 9001-11-17\n",
+ },
+ {
+ desc: "With bad response",
+ arguments: &commandargs.Shell{
+ GitlabKeyId: "badresponse",
+ SshArgs: []string{cmdname, "newtoken", "read_api,read_repository"},
+ },
+ expectedError: "Parsing failed",
+ },
+ {
+ desc: "when API returns an error",
+ arguments: &commandargs.Shell{
+ GitlabKeyId: "forbidden",
+ SshArgs: []string{cmdname, "newtoken", "read_api,read_repository"},
+ },
+ expectedError: "Forbidden!",
+ },
+ {
+ desc: "When API fails",
+ arguments: &commandargs.Shell{
+ GitlabKeyId: "broken",
+ SshArgs: []string{cmdname, "newtoken", "read_api,read_repository"},
+ },
+ expectedError: "Internal API error (500)",
+ },
+ {
+ desc: "Without KeyID or User",
+ arguments: &commandargs.Shell{
+ SshArgs: []string{cmdname, "newtoken", "read_api,read_repository"},
+ },
+ expectedError: "who='' is invalid",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ output := &bytes.Buffer{}
+ input := bytes.NewBufferString("")
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+
+ if tc.expectedError == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.expectedError)
+ }
+
+ if tc.expectedOutput != "" {
+ assert.Equal(t, tc.expectedOutput, output.String())
+ }
+ })
+ }
+}
diff --git a/internal/gitlabnet/personalaccesstoken/client.go b/internal/gitlabnet/personalaccesstoken/client.go
new file mode 100644
index 0000000..588bead
--- /dev/null
+++ b/internal/gitlabnet/personalaccesstoken/client.go
@@ -0,0 +1,93 @@
+package personalaccesstoken
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "gitlab.com/gitlab-org/gitlab-shell/client"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/discover"
+)
+
+type Client struct {
+ config *config.Config
+ client *client.GitlabNetClient
+}
+
+type Response struct {
+ Success bool `json:"success"`
+ Token string `json:"token"`
+ Scopes []string `json:"scopes"`
+ ExpiresAt string `json:"expires_at"`
+ Message string `json:"message"`
+}
+
+type RequestBody struct {
+ KeyId string `json:"key_id,omitempty"`
+ UserId int64 `json:"user_id,omitempty"`
+ Name string `json:"name"`
+ Scopes []string `json:"scopes"`
+ ExpiresAt string `json:"expires_at,omitempty"`
+}
+
+func NewClient(config *config.Config) (*Client, error) {
+ client, err := gitlabnet.GetClient(config)
+ if err != nil {
+ return nil, fmt.Errorf("Error creating http client: %v", err)
+ }
+
+ return &Client{config: config, client: client}, nil
+}
+
+func (c *Client) GetPersonalAccessToken(args *commandargs.Shell, name string, scopes *[]string, expiresAt string) (*Response, error) {
+ requestBody, err := c.getRequestBody(args, name, scopes, expiresAt)
+ if err != nil {
+ return nil, err
+ }
+
+ response, err := c.client.Post("/personal_access_token", requestBody)
+ if err != nil {
+ return nil, err
+ }
+ defer response.Body.Close()
+
+ return parse(response)
+}
+
+func parse(hr *http.Response) (*Response, error) {
+ response := &Response{}
+ if err := gitlabnet.ParseJSON(hr, response); err != nil {
+ return nil, err
+ }
+
+ if !response.Success {
+ return nil, errors.New(response.Message)
+ }
+
+ return response, nil
+}
+
+func (c *Client) getRequestBody(args *commandargs.Shell, name string, scopes *[]string, expiresAt string) (*RequestBody, error) {
+ client, err := discover.NewClient(c.config)
+ if err != nil {
+ return nil, err
+ }
+
+ requestBody := &RequestBody{Name: name, Scopes: *scopes, ExpiresAt: expiresAt}
+ if args.GitlabKeyId != "" {
+ requestBody.KeyId = args.GitlabKeyId
+
+ return requestBody, nil
+ }
+
+ userInfo, err := client.GetByCommandArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ requestBody.UserId = userInfo.UserId
+
+ return requestBody, nil
+}
diff --git a/internal/gitlabnet/personalaccesstoken/client_test.go b/internal/gitlabnet/personalaccesstoken/client_test.go
new file mode 100644
index 0000000..de45975
--- /dev/null
+++ b/internal/gitlabnet/personalaccesstoken/client_test.go
@@ -0,0 +1,177 @@
+package personalaccesstoken
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitlab-shell/client"
+ "gitlab.com/gitlab-org/gitlab-shell/client/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/discover"
+)
+
+var (
+ requests []testserver.TestRequestHandler
+)
+
+func initialize(t *testing.T) {
+ requests = []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/personal_access_token",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ b, err := ioutil.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *RequestBody
+ json.Unmarshal(b, &requestBody)
+
+ switch requestBody.KeyId {
+ case "0":
+ body := map[string]interface{}{
+ "success": true,
+ "token": "aAY1G3YPeemECgUvxuXY",
+ "scopes": [2]string{"read_api", "read_repository"},
+ "expires_at": "9001-11-17",
+ }
+ json.NewEncoder(w).Encode(body)
+ case "1":
+ body := map[string]interface{}{
+ "success": false,
+ "message": "missing user",
+ }
+ json.NewEncoder(w).Encode(body)
+ case "2":
+ w.WriteHeader(http.StatusForbidden)
+ body := &client.ErrorResponse{
+ Message: "Not allowed!",
+ }
+ json.NewEncoder(w).Encode(body)
+ case "3":
+ w.Write([]byte("{ \"message\": \"broken json!\""))
+ case "4":
+ w.WriteHeader(http.StatusForbidden)
+ }
+
+ if requestBody.UserId == 1 {
+ body := map[string]interface{}{
+ "success": true,
+ "token": "YXuxvUgCEmeePY3G1YAa",
+ "scopes": [1]string{"api"},
+ "expires_at": nil,
+ }
+ json.NewEncoder(w).Encode(body)
+ }
+ },
+ },
+ {
+ Path: "/api/v4/internal/discover",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ body := &discover.Response{
+ UserId: 1,
+ Username: "jane-doe",
+ Name: "Jane Doe",
+ }
+ json.NewEncoder(w).Encode(body)
+ },
+ },
+ }
+}
+
+func TestGetPersonalAccessTokenByKeyId(t *testing.T) {
+ client, cleanup := setup(t)
+ defer cleanup()
+
+ args := &commandargs.Shell{GitlabKeyId: "0"}
+ result, err := client.GetPersonalAccessToken(
+ args, "newtoken", &[]string{"read_api", "read_repository"}, "",
+ )
+ assert.NoError(t, err)
+ response := &Response{
+ true,
+ "aAY1G3YPeemECgUvxuXY",
+ []string{"read_api", "read_repository"},
+ "9001-11-17",
+ "",
+ }
+ assert.Equal(t, response, result)
+}
+
+func TestGetRecoveryCodesByUsername(t *testing.T) {
+ client, cleanup := setup(t)
+ defer cleanup()
+
+ args := &commandargs.Shell{GitlabUsername: "jane-doe"}
+ result, err := client.GetPersonalAccessToken(
+ args, "newtoken", &[]string{"api"}, "",
+ )
+ assert.NoError(t, err)
+ response := &Response{true, "YXuxvUgCEmeePY3G1YAa", []string{"api"}, "", ""}
+ assert.Equal(t, response, result)
+}
+
+func TestMissingUser(t *testing.T) {
+ client, cleanup := setup(t)
+ defer cleanup()
+
+ args := &commandargs.Shell{GitlabKeyId: "1"}
+ _, err := client.GetPersonalAccessToken(
+ args, "newtoken", &[]string{"api"}, "",
+ )
+ assert.Equal(t, "missing user", err.Error())
+}
+
+func TestErrorResponses(t *testing.T) {
+ client, cleanup := setup(t)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ fakeId string
+ expectedError string
+ }{
+ {
+ desc: "A response with an error message",
+ fakeId: "2",
+ expectedError: "Not allowed!",
+ },
+ {
+ desc: "A response with bad JSON",
+ fakeId: "3",
+ expectedError: "Parsing failed",
+ },
+ {
+ desc: "An error response without message",
+ fakeId: "4",
+ expectedError: "Internal API error (403)",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
+ resp, err := client.GetPersonalAccessToken(
+ args, "newtoken", &[]string{"api"}, "",
+ )
+
+ assert.EqualError(t, err, tc.expectedError)
+ assert.Nil(t, resp)
+ })
+ }
+}
+
+func setup(t *testing.T) (*Client, func()) {
+ initialize(t)
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+
+ client, err := NewClient(&config.Config{GitlabUrl: url})
+ require.NoError(t, err)
+
+ return client, cleanup
+}
diff --git a/spec/gitlab_shell_personal_access_token_spec.rb b/spec/gitlab_shell_personal_access_token_spec.rb
new file mode 100644
index 0000000..64bc34b
--- /dev/null
+++ b/spec/gitlab_shell_personal_access_token_spec.rb
@@ -0,0 +1,119 @@
+require_relative 'spec_helper'
+
+require 'json'
+require 'open3'
+
+describe 'bin/gitlab-shell personal_access_token' do
+ include_context 'gitlab shell'
+
+ before(:context) do
+ write_config("gitlab_url" => "http+unix://#{CGI.escape(tmp_socket_path)}")
+ end
+
+ def mock_server(server)
+ server.mount_proc('/api/v4/internal/personal_access_token') do |req, res|
+ params = JSON.parse(req.body)
+
+ res.content_type = 'application/json'
+ res.status = 200
+
+ if params['key_id'] == '000'
+ res.body = { success: false, message: "Something wrong!"}.to_json
+ else
+ res.body = {
+ success: true,
+ token: 'aAY1G3YPeemECgUvxuXY',
+ scopes: params['scopes'],
+ expires_at: (params['expires_at'] && '9001-12-01')
+ }.to_json
+ end
+ end
+
+ server.mount_proc('/api/v4/internal/discover') do |req, res|
+ res.status = 200
+ res.content_type = 'application/json'
+ res.body = '{"id":100, "name": "Some User", "username": "someuser"}'
+ end
+ end
+
+ describe 'command' do
+ let(:key_id) { 'key-100' }
+
+ let(:output) do
+ env = {
+ 'SSH_CONNECTION' => 'fake',
+ 'SSH_ORIGINAL_COMMAND' => "personal_access_token #{args}"
+ }
+ Open3.popen2e(env, "#{gitlab_shell_path} #{key_id}")[1].read()
+ end
+
+ let(:help_message) do
+ <<~OUTPUT
+ remote:
+ remote: ========================================================================
+ remote:
+ remote: Usage: personal_access_token <name> <scope1[,scope2,...]> [ttl_days]
+ remote:
+ remote: ========================================================================
+ remote:
+ OUTPUT
+ end
+
+ context 'without any arguments' do
+ let(:args) { '' }
+
+ it 'prints the help message' do
+ expect(output).to eq(help_message)
+ end
+ end
+
+ context 'with only the name argument' do
+ let(:args) { 'newtoken' }
+
+ it 'prints the help message' do
+ expect(output).to eq(help_message)
+ end
+ end
+
+ context 'without a ttl argument' do
+ let(:args) { 'newtoken api' }
+
+ it 'prints a token without an expiration date' do
+ expect(output).to eq(<<~OUTPUT)
+ Token: aAY1G3YPeemECgUvxuXY
+ Scopes: api
+ Expires: never
+ OUTPUT
+ end
+ end
+
+ context 'with a ttl argument' do
+ let(:args) { 'newtoken read_api,read_user 30' }
+
+ it 'prints a token with an expiration date' do
+ expect(output).to eq(<<~OUTPUT)
+ Token: aAY1G3YPeemECgUvxuXY
+ Scopes: read_api,read_user
+ Expires: 9001-12-01
+ OUTPUT
+ end
+ end
+
+ context 'with an API error response' do
+ let(:args) { 'newtoken api' }
+ let(:key_id) { 'key-000' }
+
+ it 'prints the error response' do
+ expect(output).to eq(<<~OUTPUT)
+ remote:
+ remote: ========================================================================
+ remote:
+ remote: Something wrong!
+ remote:
+ remote: ========================================================================
+ remote:
+ OUTPUT
+ end
+ end
+ end
+end