Skip to content

Commit 91fa231

Browse files
ashmckenzieIgor Drozdov
and
Igor Drozdov
committed
Merge branch 'id-ssh-certificates' into 'main'
Support authentication using SSH Certificates See merge request https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/812 Merged-by: Ash McKenzie <[email protected]> Approved-by: Alejandro Rodríguez <[email protected]> Approved-by: Jessie Young <[email protected]> Approved-by: Ash McKenzie <[email protected]> Reviewed-by: Igor Drozdov <[email protected]> Reviewed-by: Ash McKenzie <[email protected]> Reviewed-by: Jessie Young <[email protected]> Co-authored-by: Igor Drozdov <[email protected]>
2 parents b9daf6b + f3c8016 commit 91fa231

File tree

12 files changed

+477
-47
lines changed

12 files changed

+477
-47
lines changed

cmd/gitlab-shell/command/command.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ func NewWithKrb5Principal(gitlabKrb5Principal string, env sshenv.Env, config *co
5858
return nil, disallowedcommand.Error
5959
}
6060

61+
func NewWithUsername(gitlabUsername string, env sshenv.Env, config *config.Config, readWriter *readwriter.ReadWriter) (command.Command, error) {
62+
args, err := Parse(nil, env)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
args.GitlabUsername = gitlabUsername
68+
if cmd := Build(args, config, readWriter); cmd != nil {
69+
return cmd, nil
70+
}
71+
72+
return nil, disallowedcommand.Error
73+
}
74+
6175
func Parse(arguments []string, env sshenv.Env) (*commandargs.Shell, error) {
6276
args := &commandargs.Shell{Arguments: arguments, Env: env}
6377

cmd/gitlab-shell/command/command_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,16 @@ func TestParseFailure(t *testing.T) {
300300
})
301301
}
302302
}
303+
304+
func TestNewWithUsername(t *testing.T) {
305+
env := sshenv.Env{IsSSHConnection: true, OriginalCommand: "git-receive-pack 'group/repo'"}
306+
c, err := cmd.NewWithUsername("username", env, nil, nil)
307+
require.NoError(t, err)
308+
require.IsType(t, &receivepack.Command{}, c)
309+
require.Equal(t, c.(*receivepack.Command).Args.GitlabUsername, "username")
310+
311+
env = sshenv.Env{IsSSHConnection: true, OriginalCommand: "invalid"}
312+
c, err = cmd.NewWithUsername("username", env, nil, nil)
313+
require.Error(t, err)
314+
require.Nil(t, c)
315+
}

internal/gitlabnet/accessverifier/client.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type Request struct {
3030
Username string `json:"username,omitempty"`
3131
Krb5Principal string `json:"krb5principal,omitempty"`
3232
CheckIp string `json:"check_ip,omitempty"`
33+
// NamespacePath is the full path of the namespace in which the authenticated
34+
// user is allowed to perform operation.
35+
NamespacePath string `json:"namespace_path,omitempty"`
3336
}
3437

3538
type Gitaly struct {
@@ -81,7 +84,13 @@ func NewClient(config *config.Config) (*Client, error) {
8184
}
8285

8386
func (c *Client) Verify(ctx context.Context, args *commandargs.Shell, action commandargs.CommandType, repo string) (*Response, error) {
84-
request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges}
87+
request := &Request{
88+
Action: action,
89+
Repo: repo,
90+
Protocol: protocol,
91+
Changes: anyChanges,
92+
NamespacePath: args.Env.NamespacePath,
93+
}
8594

8695
if args.GitlabUsername != "" {
8796
request.Username = args.GitlabUsername

internal/gitlabnet/accessverifier/client_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import (
1919
)
2020

2121
var (
22+
namespace = "group"
2223
repo = "group/private"
2324
receivePackAction = commandargs.ReceivePack
2425
uploadPackAction = commandargs.UploadPack
26+
defaultEnv = sshenv.Env{NamespacePath: namespace}
2527
)
2628

2729
func buildExpectedResponse(who string) *Response {
@@ -71,7 +73,7 @@ func TestSuccessfulResponses(t *testing.T) {
7173
who: "key-1",
7274
}, {
7375
desc: "Provide username within the request",
74-
args: &commandargs.Shell{GitlabUsername: "first"},
76+
args: &commandargs.Shell{GitlabUsername: "first", Env: defaultEnv},
7577
who: "user-1",
7678
}, {
7779
desc: "Provide krb5principal within the request",
@@ -99,7 +101,7 @@ func TestGeoPushGetCustomAction(t *testing.T) {
99101
},
100102
}, nil)
101103

102-
args := &commandargs.Shell{GitlabUsername: "custom"}
104+
args := &commandargs.Shell{GitlabUsername: "custom", Env: defaultEnv}
103105
result, err := client.Verify(context.Background(), args, receivePackAction, repo)
104106
require.NoError(t, err)
105107

@@ -128,7 +130,7 @@ func TestGeoPullGetCustomAction(t *testing.T) {
128130
},
129131
}, nil)
130132

131-
args := &commandargs.Shell{GitlabUsername: "custom"}
133+
args := &commandargs.Shell{GitlabUsername: "custom", Env: defaultEnv}
132134
result, err := client.Verify(context.Background(), args, uploadPackAction, repo)
133135
require.NoError(t, err)
134136

@@ -263,6 +265,7 @@ func setup(t *testing.T, userResponses, keyResponses map[string]testResponse) *C
263265
w.WriteHeader(tr.status)
264266
_, err := w.Write(tr.body)
265267
require.NoError(t, err)
268+
require.Equal(t, namespace, requestBody.NamespacePath)
266269
} else if tr, ok := userResponses[requestBody.Krb5Principal]; ok {
267270
w.WriteHeader(tr.status)
268271
_, err := w.Write(tr.body)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package authorizedcerts
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
8+
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
9+
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
10+
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet"
11+
)
12+
13+
const (
14+
AuthorizedCertsPath = "/authorized_certs"
15+
)
16+
17+
type Client struct {
18+
config *config.Config
19+
client *client.GitlabNetClient
20+
}
21+
22+
type Response struct {
23+
Username string `json:"username"`
24+
Namespace string `json:"namespace"`
25+
}
26+
27+
func NewClient(config *config.Config) (*Client, error) {
28+
client, err := gitlabnet.GetClient(config)
29+
if err != nil {
30+
return nil, fmt.Errorf("Error creating http client: %v", err)
31+
}
32+
33+
return &Client{config: config, client: client}, nil
34+
}
35+
36+
func (c *Client) GetByKey(ctx context.Context, userId, fingerprint string) (*Response, error) {
37+
path, err := pathWithKey(userId, fingerprint)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
response, err := c.client.Get(ctx, path)
43+
if err != nil {
44+
return nil, err
45+
}
46+
defer response.Body.Close()
47+
48+
parsedResponse := &Response{}
49+
if err := gitlabnet.ParseJSON(response, parsedResponse); err != nil {
50+
return nil, err
51+
}
52+
53+
return parsedResponse, nil
54+
}
55+
56+
func pathWithKey(userId, fingerprint string) (string, error) {
57+
u, err := url.Parse(AuthorizedCertsPath)
58+
if err != nil {
59+
return "", err
60+
}
61+
62+
params := u.Query()
63+
params.Set("key", fingerprint)
64+
params.Set("user_identifier", userId)
65+
u.RawQuery = params.Encode()
66+
67+
return u.String(), nil
68+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package authorizedcerts
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
11+
"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
12+
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
13+
)
14+
15+
var (
16+
requests []testserver.TestRequestHandler
17+
)
18+
19+
func init() {
20+
requests = []testserver.TestRequestHandler{
21+
{
22+
Path: "/api/v4/internal/authorized_certs",
23+
Handler: func(w http.ResponseWriter, r *http.Request) {
24+
if r.URL.Query().Get("key") == "key" {
25+
body := &Response{
26+
Namespace: "group",
27+
Username: r.URL.Query().Get("user_identifier"),
28+
}
29+
json.NewEncoder(w).Encode(body)
30+
} else if r.URL.Query().Get("key") == "broken-message" {
31+
w.WriteHeader(http.StatusForbidden)
32+
body := &client.ErrorResponse{
33+
Message: "Not allowed!",
34+
}
35+
json.NewEncoder(w).Encode(body)
36+
} else if r.URL.Query().Get("key") == "broken-json" {
37+
w.Write([]byte("{ \"message\": \"broken json!\""))
38+
} else if r.URL.Query().Get("key") == "broken-empty" {
39+
w.WriteHeader(http.StatusForbidden)
40+
} else {
41+
w.WriteHeader(http.StatusNotFound)
42+
}
43+
},
44+
},
45+
}
46+
}
47+
48+
func TestGetByKey(t *testing.T) {
49+
client := setup(t)
50+
51+
result, err := client.GetByKey(context.Background(), "user-id", "key")
52+
require.NoError(t, err)
53+
require.Equal(t, &Response{Namespace: "group", Username: "user-id"}, result)
54+
}
55+
56+
func TestGetByKeyErrorResponses(t *testing.T) {
57+
client := setup(t)
58+
59+
testCases := []struct {
60+
desc string
61+
key string
62+
expectedError string
63+
}{
64+
{
65+
desc: "A response with an error message",
66+
key: "broken-message",
67+
expectedError: "Not allowed!",
68+
},
69+
{
70+
desc: "A response with bad JSON",
71+
key: "broken-json",
72+
expectedError: "Parsing failed",
73+
},
74+
{
75+
desc: "A forbidden (403) response without message",
76+
key: "broken-empty",
77+
expectedError: "Internal API error (403)",
78+
},
79+
{
80+
desc: "A not found (404) response without message",
81+
key: "not-found",
82+
expectedError: "Internal API error (404)",
83+
},
84+
}
85+
86+
for _, tc := range testCases {
87+
t.Run(tc.desc, func(t *testing.T) {
88+
resp, err := client.GetByKey(context.Background(), "user-id", tc.key)
89+
90+
require.EqualError(t, err, tc.expectedError)
91+
require.Nil(t, resp)
92+
})
93+
}
94+
}
95+
96+
func setup(t *testing.T) *Client {
97+
url := testserver.StartSocketHttpServer(t, requests)
98+
99+
client, err := NewClient(&config.Config{GitlabUrl: url})
100+
require.NoError(t, err)
101+
102+
return client
103+
}

0 commit comments

Comments
 (0)