Skip to content

Commit

Permalink
sync: add option to prune non-managed keys (#47)
Browse files Browse the repository at this point in the history
* sync: add --prune flag for removing all non-github managed keys

This PR adds a `--prune` flag for force removing all non-github
managed keys. Will refuse to write an empty file - if `--prune`
is set but github returns no keys, we exit with an error code
without writing anything, to prevent mishaps.

Closes #46

* readme: update with prune

* sync: fix verbose and prune args
  • Loading branch information
shoenig authored Feb 12, 2023
1 parent adb2027 commit 42146c1
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Usage of ./ssh-key-sync:
specify the github user
-system-user string
specify the unix system user (default "$USER")
-prune
delete all keys not found in github
-verbose
print verbose logging
```
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/shoenig/ssh-key-sync
go 1.18

require (
github.com/hashicorp/go-set v0.1.8
github.com/hashicorp/go-set v0.1.9
github.com/shoenig/go-landlock v0.1.5
github.com/shoenig/ignore v0.4.0
github.com/shoenig/test v0.6.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-set v0.1.8 h1:q2r58lFkJrikmC4I+vS3A+bn6QgR7EYeFD8kRiAIAnk=
github.com/hashicorp/go-set v0.1.8/go.mod h1:wedp+UE6HoxBywExd7mrdGdcXOo3awtiELmnRnpzsKI=
github.com/hashicorp/go-set v0.1.9 h1:XuQSsDfOAvgRjoKWG2qg8NxVEQJMXGdrZh8BgX6O8n4=
github.com/hashicorp/go-set v0.1.9/go.mod h1:/IR7VHUqnKI+QfKkaMjZ575bf65Y8DzHRKnOobRpNcQ=
github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
Expand Down
7 changes: 7 additions & 0 deletions hack/tests/generatedOutput3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Autogenerated by ssh-key-sync on Sun, 02 Oct 2022 08:53:00 UTC

# managed by ssh-key-sync
def345 bob@a2

# managed by ssh-key-sync
ghi456 carla@c3
2 changes: 1 addition & 1 deletion internal/command/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ func Start(args []string) error {

reader := ssh.NewKeysReader()
githubClient := netapi.NewGithubClient(arguments)
exe := NewExec(arguments.Verbose, reader, githubClient)
exe := NewExec(arguments.Prune, arguments.Verbose, reader, githubClient)
return exe.Execute(arguments)
}
22 changes: 17 additions & 5 deletions internal/command/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ type Exec interface {
}

func NewExec(
prune bool,
verbose bool,
reader ssh.KeysReader,
githubClient netapi.Client,
) Exec {
return &exec{
prune: prune,
logger: logs.New(verbose),
reader: reader,
githubClient: githubClient,
Expand All @@ -34,6 +36,7 @@ func NewExec(
}

type exec struct {
prune bool
logger *log.Logger
reader ssh.KeysReader
githubClient netapi.Client
Expand Down Expand Up @@ -79,21 +82,30 @@ func (e *exec) processUser(systemUser, githubUser, keyFile string) error {
e.logger.Printf("retrieved %d keys for github user: %s", githubKeys.Size(), githubUser)

// 3) combine the keys, purging old managed keys with the new set
newKeys := combine(localKeys, githubKeys)
content := generateFileContent(newKeys, e.clock.Now())
newKeys := e.combine(localKeys, githubKeys)
if len(newKeys) == 0 {
return fmt.Errorf("no keys! refusing to write empty set of keys")
}

// 4) write the new file content to the authorized keys file
content := generateFileContent(newKeys, e.clock.Now())
return e.writeKeyFile(keyFile, content)
}

func combine(local, gh *set.Set[ssh.Key]) []ssh.Key {
func (e *exec) combine(local, gh *set.Set[ssh.Key]) []ssh.Key {
if e.prune {
e.logger.Printf("pruning %d non-github managed keys", local.Size())
result := gh.Slice()
sort.Sort(ssh.KeySorter(result))
return result
}
union := local.Union(gh)
result := union.List()
result := union.Slice()
sort.Sort(ssh.KeySorter(result))
return result
}

func lockdown(keyfile string) error {
ll := landlock.New(paths(keyfile)...)
return ll.Lock(landlock.OnlySupported)
}
}
113 changes: 111 additions & 2 deletions internal/command/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import (
)

type mockKeysReader struct {
readKeysResult *set.Set[ssh.Key]
}

func (r *mockKeysReader) ReadKeys(filename string) (*set.Set[ssh.Key], error) {
func (r *mockKeysReader) ReadKeys(_ string) (*set.Set[ssh.Key], error) {
if r.readKeysResult != nil {
return r.readKeysResult, nil
}

return set.From[ssh.Key]([]ssh.Key{{
Managed: false,
Value: "abc123",
Expand All @@ -31,9 +36,13 @@ func (r *mockKeysReader) ReadKeys(filename string) (*set.Set[ssh.Key], error) {
}

type mockClient struct {
getKeysResult *set.Set[ssh.Key]
}

func (c *mockClient) GetKeys(username string) (*set.Set[ssh.Key], error) {
func (c *mockClient) GetKeys(_ string) (*set.Set[ssh.Key], error) {
if c.getKeysResult != nil {
return c.getKeysResult, nil
}
return set.From[ssh.Key]([]ssh.Key{{
Managed: true,
Value: "def345",
Expand Down Expand Up @@ -61,6 +70,7 @@ func TestExec_Execute(t *testing.T) {
clock.NowMock.Return(time.Date(2022, 10, 2, 8, 53, 0, 0, time.UTC))

e := &exec{
prune: false,
logger: logs.New(true),
reader: new(mockKeysReader),
githubClient: new(mockClient),
Expand All @@ -71,3 +81,102 @@ func TestExec_Execute(t *testing.T) {
err = e.processUser("bob", "bob-gh", "/nothing/keys")
must.NoError(t, err)
}

func TestExec_Execute_prune(t *testing.T) {
exp, err := os.ReadFile("../../hack/tests/generatedOutput3.txt")
must.NoError(t, err)

writer := func(filename, content string) error {
must.Eq(t, "/nothing/keys", filename)
must.Eq(t, strings.TrimSpace(string(exp)), strings.TrimSpace(content))
return nil
}

clock := libtimetest.NewClockMock(t)
clock.NowMock.Return(time.Date(2022, 10, 2, 8, 53, 0, 0, time.UTC))

e := &exec{
prune: true,
logger: logs.New(true),
reader: new(mockKeysReader),
githubClient: new(mockClient),
clock: clock,
writeKeyFile: writer,
}

err = e.processUser("bob", "bob-gh", "/nothing/keys")
must.NoError(t, err)
}

func TestExec_Execute_prune_empty(t *testing.T) {
writer := func(filename, _ string) error {
must.Eq(t, "/nothing/keys", filename)
return nil
}

clock := libtimetest.NewClockMock(t)
clock.NowMock.Return(time.Date(2022, 10, 2, 8, 53, 0, 0, time.UTC))

mkr := &mockKeysReader{
readKeysResult: set.From([]ssh.Key{{
Managed: false,
Value: "abc123",
User: "alice",
Host: "a1",
}}),
}

mc := &mockClient{
getKeysResult: set.New[ssh.Key](0),
}

e := &exec{
prune: true,
logger: logs.New(true),
reader: mkr,
githubClient: mc,
clock: clock,
writeKeyFile: writer,
}

err := e.processUser("bob", "bob-gh", "/nothing/keys")
must.ErrorContains(t, err, "no keys!")
}

func TestExec_combine(t *testing.T) {
locals := set.From[ssh.Key]([]ssh.Key{{
Managed: false,
Value: "abc123",
User: "alice",
Host: "a1",
}, {
Managed: false,
Value: "def345",
User: "bob",
Host: "a2",
}})

github := set.From[ssh.Key]([]ssh.Key{{
Managed: true,
Value: "yyy111",
User: "alice",
Host: "a1",
}, {
Managed: true,
Value: "zzz222",
User: "bob",
Host: "a2",
}})

t.Run("normal", func(t *testing.T) {
e := &exec{prune: false}
result := e.combine(locals, github)
must.Len(t, 4, result)
})

t.Run("prune", func(t *testing.T) {
e := &exec{prune: true, logger: logs.New(true)}
result := e.combine(locals, github)
must.Len(t, 2, result)
})
}
6 changes: 6 additions & 0 deletions internal/config/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func init() {

type Arguments struct {
Verbose bool
Prune bool

SystemUser string
SystemHome string
Expand Down Expand Up @@ -51,6 +52,11 @@ func ParseArguments(program string, args []string) Arguments {
"verbose", false, "print verbose logging",
)

flags.BoolVar(
&arguments.Prune,
"prune", false, "delete all keys not found in github",
)

flags.StringVar(
&arguments.SystemUser,
"system-user", defaultUser(), "specify the unix system user",
Expand Down
12 changes: 12 additions & 0 deletions internal/config/arguments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ func TestSystemUser(t *testing.T) {
must.Eq(t, u.Username, args.SystemUser)
})
}

func TestPrune(t *testing.T) {
t.Run("no", func(t *testing.T) {
args := ParseArguments(program, []string{})
must.False(t, args.Prune)
})

t.Run("yes", func(t *testing.T) {
args := ParseArguments(program, []string{"--prune"})
must.True(t, args.Prune)
})
}

0 comments on commit 42146c1

Please sign in to comment.