Skip to content

Commit

Permalink
Update: callback for repositories
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
till committed Jun 18, 2023
1 parent f89a708 commit 1f05ff5
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 19 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Paste the following:
```
Host localhost
Port 2222
ForwardAgent no
```

Now that the server is configured, we can fire it up:
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
23 changes: 16 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
49 changes: 40 additions & 9 deletions ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -142,7 +143,17 @@ 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()
envVariables = append(envVariables, "GITKIT_CURRENT_REPOSITORY="+gitcmd.Repo)

// 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()
Expand Down Expand Up @@ -209,6 +220,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 {
Expand All @@ -219,7 +234,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
}
}

Expand Down Expand Up @@ -296,13 +327,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)
}()
}
}
Expand Down
85 changes: 85 additions & 0 deletions ssh_test.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 1f05ff5

Please sign in to comment.