From 0a78102fee09aa0ef49ef890f696165a9ab4e2f1 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 5 Feb 2023 17:35:24 +0100 Subject: [PATCH 1/6] Chore: upgrade go to 1.19 --- .github/workflows/build.yml | 2 +- go.mod | 9 ++++++++- go.sum | 11 ++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b35171f..79f78cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: - push env: - GO_VERSION: 1.18 + GO_VERSION: 1.19 jobs: build: diff --git a/go.mod b/go.mod index 1fc9f41..b1b2e5c 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,16 @@ module github.com/sosedoff/gitkit -go 1.16 +go 1.19 require ( github.com/gofrs/uuid v4.0.0+incompatible github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 1607b45..61f751a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -9,13 +10,9 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= From 7ca9dd905215df790715a903255e6ceffebdf203 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 5 Feb 2023 18:11:32 +0100 Subject: [PATCH 2/6] Chore: update actions --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79f78cb..205e757 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: ${{ env.GO_VERSION }} From 1d2f53d33e5708b8221a46064b8ba05b5affbe06 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 5 Feb 2023 18:04:09 +0100 Subject: [PATCH 3/6] Update: allow push to certain branches This extends MasterOnly and makes it configurable. The idea is to set arbitrary branch names which are allowed to be pushed to and otherwise return an error. Resolves: sosedoff/gitkit#33 --- go.mod | 1 + go.sum | 2 ++ receiver.go | 30 ++++++++++++---- receiver_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 receiver_test.go diff --git a/go.mod b/go.mod index b1b2e5c..1cea0d1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gofrs/uuid v4.0.0+incompatible github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a + golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 ) require ( diff --git a/go.sum b/go.sum index 61f751a..e0b183a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 h1:frX3nT9RkKybPnjyI+yvZh6ZucTZatCCEm9D47sZ2zo= +golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= diff --git a/receiver.go b/receiver.go index f4a02fb..592668c 100644 --- a/receiver.go +++ b/receiver.go @@ -9,15 +9,17 @@ import ( "strings" "github.com/gofrs/uuid" + "golang.org/x/exp/slices" ) const ZeroSHA = "0000000000000000000000000000000000000000" type Receiver struct { - Debug bool - MasterOnly bool - TmpDir string - HandlerFunc func(*HookInfo, string) error + Debug bool + MasterOnly bool + AllowedBranches []string + TmpDir string + HandlerFunc func(*HookInfo, string) error } func ReadCommitMessage(sha string) (string, error) { @@ -45,14 +47,30 @@ func IsForcePush(hook *HookInfo) (bool, error) { return base != hook.OldRev, nil } +func (r *Receiver) CheckAllowedBranch(hook *HookInfo) error { + if r.MasterOnly { // for BC + r.AllowedBranches = append(r.AllowedBranches, "refs/heads/master") + } + + if len(r.AllowedBranches) == 0 { + return nil + } + + if !slices.Contains(r.AllowedBranches, hook.Ref) { + return fmt.Errorf("cannot push branch, allowed branches: %s", strings.Join(r.AllowedBranches, ", ")) + } + + return nil +} + func (r *Receiver) Handle(reader io.Reader) error { hook, err := ReadHookInput(reader) if err != nil { return err } - if r.MasterOnly && hook.Ref != "refs/heads/master" { - return fmt.Errorf("cant push to non-master branch") + if err = r.CheckAllowedBranch(hook); err != nil { + return err } id, err := uuid.NewV4() diff --git a/receiver_test.go b/receiver_test.go new file mode 100644 index 0000000..261049c --- /dev/null +++ b/receiver_test.go @@ -0,0 +1,94 @@ +package gitkit_test + +import ( + "testing" + + "github.com/sosedoff/gitkit" + "github.com/stretchr/testify/assert" +) + +type gitReceiveMock struct { + name string + masterOnly bool + allowedBranches []string + ref string + isErr bool +} + +func TestMasterOnly(t *testing.T) { + testCases := []gitReceiveMock{ + { + name: "push to master, no error", + masterOnly: true, + ref: "refs/heads/master", + isErr: false, + }, + { + name: "push to a branch, should trigger error", + masterOnly: true, + ref: "refs/heads/branch", + isErr: true, + }, + } + + for _, tc := range testCases { + r := &gitkit.Receiver{ + MasterOnly: tc.masterOnly, + } + + err := r.CheckAllowedBranch(&gitkit.HookInfo{ + Ref: tc.ref, + }) + + if !tc.isErr { + assert.NoError(t, err, "expected no error: %s", tc.name) + } else { + assert.Error(t, err, "expected an error: %s", tc.name) + } + } +} + +func TestAllowedBranches(t *testing.T) { + testCases := []gitReceiveMock{ + { + name: "push to master, no error", + allowedBranches: []string{"refs/heads/master"}, + ref: "refs/heads/master", + isErr: false, + }, + { + name: "push to a branch, should trigger error", + allowedBranches: []string{"refs/heads/master"}, + ref: "refs/heads/some-branch", + isErr: true, + }, + { + name: "push to another-branch", + allowedBranches: []string{"refs/heads/another-branch"}, + ref: "refs/heads/another-branch", + isErr: false, + }, + { + name: "push to main and only allow main", + allowedBranches: []string{"refs/heads/main"}, + ref: "refs/heads/main", + isErr: false, + }, + } + + for _, tc := range testCases { + r := &gitkit.Receiver{ + AllowedBranches: tc.allowedBranches, + } + + err := r.CheckAllowedBranch(&gitkit.HookInfo{ + Ref: tc.ref, + }) + + if !tc.isErr { + assert.NoError(t, err, "expected no error: %s", tc.name) + } else { + assert.Error(t, err, "expected an error: %s", tc.name) + } + } +} From 17559cfee87d265796247a15a8baac7f1eabbf96 Mon Sep 17 00:00:00 2001 From: till Date: Fri, 16 Jun 2023 17:35:00 +0200 Subject: [PATCH 4/6] Chore: address feedback from CR - renamed AllowedBranches to AllowedRefs - use t.Run() in tests --- receiver.go | 18 ++++++++-------- receiver_test.go | 55 +++++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/receiver.go b/receiver.go index 592668c..29ade01 100644 --- a/receiver.go +++ b/receiver.go @@ -15,11 +15,11 @@ import ( const ZeroSHA = "0000000000000000000000000000000000000000" type Receiver struct { - Debug bool - MasterOnly bool - AllowedBranches []string - TmpDir string - HandlerFunc func(*HookInfo, string) error + Debug bool + MasterOnly bool + AllowedRefs []string + TmpDir string + HandlerFunc func(*HookInfo, string) error } func ReadCommitMessage(sha string) (string, error) { @@ -49,15 +49,15 @@ func IsForcePush(hook *HookInfo) (bool, error) { func (r *Receiver) CheckAllowedBranch(hook *HookInfo) error { if r.MasterOnly { // for BC - r.AllowedBranches = append(r.AllowedBranches, "refs/heads/master") + r.AllowedRefs = append(r.AllowedRefs, "refs/heads/master") } - if len(r.AllowedBranches) == 0 { + if len(r.AllowedRefs) == 0 { return nil } - if !slices.Contains(r.AllowedBranches, hook.Ref) { - return fmt.Errorf("cannot push branch, allowed branches: %s", strings.Join(r.AllowedBranches, ", ")) + if !slices.Contains(r.AllowedRefs, hook.Ref) { + return fmt.Errorf("cannot push branch, allowed branches: %s", strings.Join(r.AllowedRefs, ", ")) } return nil diff --git a/receiver_test.go b/receiver_test.go index 261049c..2c6309f 100644 --- a/receiver_test.go +++ b/receiver_test.go @@ -1,6 +1,7 @@ package gitkit_test import ( + "fmt" "testing" "github.com/sosedoff/gitkit" @@ -12,7 +13,7 @@ type gitReceiveMock struct { masterOnly bool allowedBranches []string ref string - isErr bool + err error } func TestMasterOnly(t *testing.T) { @@ -21,30 +22,28 @@ func TestMasterOnly(t *testing.T) { name: "push to master, no error", masterOnly: true, ref: "refs/heads/master", - isErr: false, + err: nil, }, { name: "push to a branch, should trigger error", masterOnly: true, ref: "refs/heads/branch", - isErr: true, + err: fmt.Errorf("cannot push branch, allowed branches: refs/heads/master"), }, } for _, tc := range testCases { - r := &gitkit.Receiver{ - MasterOnly: tc.masterOnly, - } + t.Run(tc.name, func(t *testing.T) { + r := &gitkit.Receiver{ + MasterOnly: tc.masterOnly, + } - err := r.CheckAllowedBranch(&gitkit.HookInfo{ - Ref: tc.ref, - }) + err := r.CheckAllowedBranch(&gitkit.HookInfo{ + Ref: tc.ref, + }) - if !tc.isErr { - assert.NoError(t, err, "expected no error: %s", tc.name) - } else { - assert.Error(t, err, "expected an error: %s", tc.name) - } + assert.Equal(t, tc.err, err) + }) } } @@ -54,41 +53,39 @@ func TestAllowedBranches(t *testing.T) { name: "push to master, no error", allowedBranches: []string{"refs/heads/master"}, ref: "refs/heads/master", - isErr: false, + err: nil, }, { name: "push to a branch, should trigger error", allowedBranches: []string{"refs/heads/master"}, ref: "refs/heads/some-branch", - isErr: true, + err: fmt.Errorf("cannot push branch, allowed branches: refs/heads/master"), }, { name: "push to another-branch", allowedBranches: []string{"refs/heads/another-branch"}, ref: "refs/heads/another-branch", - isErr: false, + err: nil, }, { name: "push to main and only allow main", allowedBranches: []string{"refs/heads/main"}, ref: "refs/heads/main", - isErr: false, + err: nil, }, } for _, tc := range testCases { - r := &gitkit.Receiver{ - AllowedBranches: tc.allowedBranches, - } + t.Run(tc.name, func(t *testing.T) { + r := &gitkit.Receiver{ + AllowedRefs: tc.allowedBranches, + } - err := r.CheckAllowedBranch(&gitkit.HookInfo{ - Ref: tc.ref, - }) + err := r.CheckAllowedBranch(&gitkit.HookInfo{ + Ref: tc.ref, + }) - if !tc.isErr { - assert.NoError(t, err, "expected no error: %s", tc.name) - } else { - assert.Error(t, err, "expected an error: %s", tc.name) - } + assert.Equal(t, tc.err, err) + }) } } From f89a7087d231c171cd89662d14c51714c62b3ea7 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 18 Jun 2023 18:01:39 +0200 Subject: [PATCH 5/6] Refactor: move key to its own struct This is to be able to re-use the code when you e.g. supply your own listener for the sshd server and the code skips all the internal setup. --- key.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ssh.go | 55 +++++--------------------------------------- 2 files changed, 78 insertions(+), 49 deletions(-) create mode 100644 key.go diff --git a/key.go b/key.go new file mode 100644 index 0000000..c649a08 --- /dev/null +++ b/key.go @@ -0,0 +1,72 @@ +package gitkit + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + + "golang.org/x/crypto/ssh" +) + +type Key struct { + keyDir string + keyName string +} + +func NewKey(keyDir string) *Key { + return &Key{ + keyDir: keyDir, + keyName: "gitkit.rsa", + } +} + +func (k *Key) CreateRSA() error { + if err := os.MkdirAll(k.keyDir, os.ModePerm); err != nil { + return err + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + keyPath := filepath.Join(k.keyDir, k.keyName) + + privateKeyFile, err := os.Create(keyPath) + if err != nil { + return err + } + + if err := os.Chmod(keyPath, 0600); err != nil { + return err + } + defer privateKeyFile.Close() + if err != nil { + return err + } + privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil { + return err + } + + pubKeyPath := keyPath + ".pub" + pub, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return err + } + + return os.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0644) +} + +func (k *Key) GetRSA() (ssh.Signer, error) { + keyPath := filepath.Join(k.keyDir, k.keyName) + privateBytes, err := os.ReadFile(keyPath) + if err != nil { + return nil, err + } + + return ssh.ParsePrivateKey(privateBytes) +} diff --git a/ssh.go b/ssh.go index 5712a7e..fbaccf1 100644 --- a/ssh.go +++ b/ssh.go @@ -2,14 +2,9 @@ package gitkit import ( "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "errors" "fmt" "io" - "io/ioutil" "log" "net" "os" @@ -195,41 +190,6 @@ func (s *SSH) handleConnection(keyID string, chans <-chan ssh.NewChannel) { } } -func (s *SSH) createServerKey() error { - if err := os.MkdirAll(s.config.KeyDir, os.ModePerm); err != nil { - return err - } - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return err - } - - privateKeyFile, err := os.Create(s.config.KeyPath()) - if err != nil { - return err - } - - if err := os.Chmod(s.config.KeyPath(), 0600); err != nil { - return err - } - defer privateKeyFile.Close() - if err != nil { - return err - } - privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} - if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil { - return err - } - - pubKeyPath := s.config.KeyPath() + ".pub" - pub, err := ssh.NewPublicKey(&privateKey.PublicKey) - if err != nil { - return err - } - return ioutil.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0644) -} - func (s *SSH) setup() error { if s.sshconfig != nil { return nil @@ -263,19 +223,16 @@ func (s *SSH) setup() error { } } - keypath := s.config.KeyPath() - if !fileExists(keypath) { - if err := s.createServerKey(); err != nil { + keyPath := s.config.KeyPath() + + k := NewKey(s.config.KeyDir) + if !fileExists(keyPath) { + if err := k.CreateRSA(); err != nil { return err } } - privateBytes, err := ioutil.ReadFile(keypath) - if err != nil { - return err - } - - private, err := ssh.ParsePrivateKey(privateBytes) + private, err := k.GetRSA() if err != nil { return err } From 0720f2dc59d33d4a1d42e76df4f0521165665537 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 18 Jun 2023 18:51:52 +0200 Subject: [PATCH 6/6] Update: callback for repositories - adds a callback for ACL (to return the list of repositories for the current user) - adds all public key data to extensions - exposes the extensions via environment variables (GITKIT_) to the hook --- README.md | 1 + go.mod | 8 +++-- go.sum | 23 ++++++++++----- ssh.go | 47 +++++++++++++++++++++++------ ssh_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 ssh_test.go diff --git a/README.md b/README.md index 5c68227..ba182d6 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ Paste the following: ``` Host localhost Port 2222 + ForwardAgent no ``` Now that the server is configured, we can fire it up: diff --git a/go.mod b/go.mod index 1cea0d1..6e5eb54 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,15 @@ go 1.19 require ( github.com/gofrs/uuid v4.0.0+incompatible github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a + golang.org/x/crypto v0.9.0 golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.1.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + golang.org/x/sys v0.8.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e0b183a..d05d469 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,30 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 h1:frX3nT9RkKybPnjyI+yvZh6ZucTZatCCEm9D47sZ2zo= golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ssh.go b/ssh.go index fbaccf1..3654d9a 100644 --- a/ssh.go +++ b/ssh.go @@ -30,9 +30,10 @@ type PublicKey struct { type SSH struct { listener net.Listener - sshconfig *ssh.ServerConfig - config *Config - PublicKeyLookupFunc func(string) (*PublicKey, error) + sshconfig *ssh.ServerConfig + config *Config + PublicKeyLookupFunc func(string) (*PublicKey, error) + ReposForKeyLookupFunc func(*PublicKey) ([]string, error) } func NewSSH(config Config) *SSH { @@ -75,7 +76,7 @@ func execCommand(cmdname string, args ...string) (string, string, error) { return string(bufOut), string(bufErr), err } -func (s *SSH) handleConnection(keyID string, chans <-chan ssh.NewChannel) { +func (s *SSH) handleConnection(exts map[string]string, chans <-chan ssh.NewChannel) { for newChan := range chans { if newChan.ChannelType() != "session" { newChan.Reject(ssh.UnknownChannelType, "unknown channel type") @@ -142,7 +143,15 @@ func (s *SSH) handleConnection(keyID string, chans <-chan ssh.NewChannel) { cmd := exec.Command(gitcmd.Command, gitcmd.Repo) cmd.Dir = s.config.Dir - cmd.Env = append(os.Environ(), "GITKIT_KEY="+keyID) + + envVariables := os.Environ() + // append data via ssh.Permissions.Extensions + for k, v := range exts { + log.Println("k=" + k + ", v=" + v) + envVariables = append(envVariables, "GITKIT_"+strings.ToUpper(k)+"="+v) + } + cmd.Env = envVariables + // cmd.Env = append(os.Environ(), "SSH_ORIGINAL_COMMAND="+cmdName) stdout, err := cmd.StdoutPipe() @@ -209,6 +218,10 @@ func (s *SSH) setup() error { return fmt.Errorf("public key lookup func is not provided") } + if s.ReposForKeyLookupFunc == nil { + log.Println("no repository callback, an authorized user may access any repositories") + } + config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { pkey, err := s.PublicKeyLookupFunc(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))) if err != nil { @@ -219,7 +232,23 @@ func (s *SSH) setup() error { return nil, fmt.Errorf("auth handler did not return a key") } - return &ssh.Permissions{Extensions: map[string]string{"key-id": pkey.Id}}, nil + var repos []string + + if s.ReposForKeyLookupFunc != nil { + repos, err = s.ReposForKeyLookupFunc(pkey) + if err != nil { + return nil, err + } + } + + return &ssh.Permissions{ + Extensions: map[string]string{ + "key": pkey.Id, + "fingerprint": pkey.Fingerprint, + "name": pkey.Name, + "repositories": strings.Join(repos, ","), + }, + }, nil } } @@ -296,13 +325,13 @@ func (s *SSH) Serve() error { return } - keyId := "" + var exts map[string]string if sConn.Permissions != nil { - keyId = sConn.Permissions.Extensions["key-id"] + exts = sConn.Permissions.Extensions } go ssh.DiscardRequests(reqs) - go s.handleConnection(keyId, chans) + go s.handleConnection(exts, chans) }() } } diff --git a/ssh_test.go b/ssh_test.go new file mode 100644 index 0000000..ca944fa --- /dev/null +++ b/ssh_test.go @@ -0,0 +1,85 @@ +package gitkit_test + +import ( + "errors" + "fmt" + "net" + "path/filepath" + "testing" + + "github.com/sosedoff/gitkit" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/ssh" +) + +func TestKeyLookupFunctionIsNeeded(t *testing.T) { + s := newSSH(t, t.TempDir()) + + err := s.Listen(":0") // random port + assert.Equal(t, errors.New("public key lookup func is not provided"), err) +} + +func TestListener(t *testing.T) { + testDir := t.TempDir() + + s := newSSH(t, testDir) + s.PublicKeyLookupFunc = func(content string) (*gitkit.PublicKey, error) { + return &gitkit.PublicKey{Id: "1234"}, nil + } + + sshdConfig, err := setup(t, testDir) + assert.NoError(t, err) + + s.SetSSHConfig(sshdConfig) + + listener, err := net.Listen("tcp4", ":0") + assert.NoError(t, err) + + s.SetListener(listener) + t.Logf("address: %s", listener.Addr()) + + go func() { + defer s.Stop() + err := s.Serve() + assert.NoError(t, err) + }() + + // assert the keys are created + assert.FileExists(t, filepath.Join(testDir, "keys/gitkit.rsa")) + assert.FileExists(t, filepath.Join(testDir, "keys/gitkit.rsa.pub")) +} + +func newSSH(t *testing.T, baseDir string) *gitkit.SSH { + t.Helper() + + return gitkit.NewSSH(gitkit.Config{ + Auth: true, + AutoCreate: true, + KeyDir: filepath.Join(baseDir, "keys"), + Dir: filepath.Join(baseDir, "repos"), + }) +} + +// custom setup function to replicate what gitkit does to setup the ssh server, +// but doesn't do when you supply a custom listener +func setup(t *testing.T, dir string) (*ssh.ServerConfig, error) { + t.Helper() + + config := &ssh.ServerConfig{ + ServerVersion: fmt.Sprintf("SSH-2.0-gitkit %s", "testing"), + } + + config.NoClientAuth = true + + // create server key + k := gitkit.NewKey(filepath.Join(dir, "keys")) + + err := k.CreateRSA() + assert.NoError(t, err) + + private, err := k.GetRSA() + assert.NoError(t, err) + + config.AddHostKey(private) + return config, nil +}