Skip to content

Commit

Permalink
feat: update last_used timestamp for SSH keys
Browse files Browse the repository at this point in the history
Update the last_used timestamp to the current time whenever an SSH key
is used to either generate a token or query exec permissions on a Lagoon
environment.

The timestamp is updated every time the key is used, regardless of
whether or not the permission check or token generation succeeds.

Timestamps are converted and stored in UTC.
  • Loading branch information
smlx committed Jul 1, 2024
1 parent 801981b commit b83eeae
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/uselagoon/ssh-portal
go 1.22.2

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/alecthomas/assert/v2 v2.10.0
github.com/alecthomas/kong v0.9.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
Expand Down Expand Up @@ -79,6 +81,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down
27 changes: 27 additions & 0 deletions internal/lagoondb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -167,3 +168,29 @@ func (c *Client) GroupIDProjectIDsMap(
}
return groupIDProjectIDsMap, nil
}

// SSHKeyUsed sets the last_used attribute of the ssh key identified by the
// given fingerprint to used.
//
// The value of used is converted to UTC before being stored in a DATETIME
// column in the MySQL database.
func (c *Client) SSHKeyUsed(
ctx context.Context,
fingerprint string,
used time.Time,
) error {
// set up tracing
ctx, span := otel.Tracer(pkgName).Start(ctx, "SSHKeyUsed")
defer span.End()
_, err := c.db.ExecContext(ctx,
`UPDATE ssh_key `+
`SET last_used = ? `+
`WHERE key_fingerprint = ?`,
used.UTC().Format(time.DateTime),
fingerprint)
if err != nil {
return fmt.Errorf("couldn't update last_used for key_fingerprint=%s: %v",
fingerprint, err)
}
return nil
}
61 changes: 61 additions & 0 deletions internal/lagoondb/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package lagoondb_test

import (
"context"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/alecthomas/assert/v2"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
)

func TestLastUsed(t *testing.T) {
var testCases = map[string]struct {
fingerprint string
used time.Time
usedString string
expectError bool
}{
"right time": {
fingerprint: "SHA256:yARVMVDnP2B2QzTvE8eSs5ZZlkZEoMFEIKjtYv1adfU",
used: time.Unix(1719825567, 0),
usedString: "2024-07-01 09:19:27",
expectError: false,
},
"wrong time": {
fingerprint: "SHA256:yARVMVDnP2B2QzTvE8eSs5ZZlkZEoMFEIKjtYv1adfU",
used: time.Unix(1719825567, 0),
usedString: "2024-07-01 17:19:27",
expectError: true,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
// set up mocks
mockDB, mock, err := sqlmock.New()
assert.NoError(tt, err, name)
mock.ExpectExec(
`UPDATE ssh_key `+
`SET last_used = (.+) `+
`WHERE key_fingerprint = (.+)`).
WithArgs(tc.usedString, tc.fingerprint).
WillReturnResult(sqlmock.NewErrorResult(nil))
// execute expected database operations
db := lagoondb.NewClientFromDB(mockDB)
err = db.SSHKeyUsed(context.Background(), tc.fingerprint, tc.used)
if tc.expectError {
assert.Error(tt, err, name)
} else {
assert.NoError(tt, err, name)
}
// check expectations
err = mock.ExpectationsWereMet()
if tc.expectError {
assert.Error(tt, err, name)
} else {
assert.NoError(tt, err, name)
}
})
}
}
11 changes: 11 additions & 0 deletions internal/lagoondb/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package lagoondb

import (
"database/sql"

"github.com/jmoiron/sqlx"
)

func NewClientFromDB(db *sql.DB) *Client {
return &Client{db: sqlx.NewDb(db, "mysql")}
}
2 changes: 2 additions & 0 deletions internal/sshportalapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"sync"
"time"

"github.com/google/uuid"
"github.com/nats-io/nats.go"
Expand All @@ -25,6 +26,7 @@ type LagoonDBService interface {
lagoon.DBService
EnvironmentByNamespaceName(context.Context, string) (*lagoondb.Environment, error)
UserBySSHFingerprint(context.Context, string) (*lagoondb.User, error)
SSHKeyUsed(context.Context, string, time.Time) error
}

// KeycloakService provides methods for querying the Keycloak API.
Expand Down
6 changes: 6 additions & 0 deletions internal/sshportalapi/sshportal.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"log/slog"
"time"

"github.com/nats-io/nats.go"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -106,6 +107,11 @@ func sshportal(
log.Error("couldn't query user by ssh fingerprint", slog.Any("error", err))
return
}
// update last_used
if err := l.SSHKeyUsed(ctx, query.SSHFingerprint, time.Now()); err != nil {
log.Warn("couldn't update ssh key last used: %v",
slog.Any("error", err))
}
// get the user roles and groups
realmRoles, userGroups, err = k.UserRolesAndGroups(ctx, user.UUID)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions internal/sshtoken/authhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sshtoken
import (
"errors"
"log/slog"
"time"

"github.com/gliderlabs/ssh"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -53,6 +54,11 @@ func pubKeyAuth(log *slog.Logger, ldb LagoonDBService) ssh.PublicKeyHandler {
}
return false
}
// update last_used
if err := ldb.SSHKeyUsed(ctx, fingerprint, time.Now()); err != nil {
log.Warn("couldn't update ssh key last used: %v",
slog.Any("error", err))
}
// The SSH key fingerprint was in the database so "authentication" was
// successful. Inject the user UUID into the context so it can be used in
// the session handler.
Expand Down
1 change: 1 addition & 0 deletions internal/sshtoken/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type LagoonDBService interface {
EnvironmentByNamespaceName(context.Context, string) (*lagoondb.Environment, error)
UserBySSHFingerprint(context.Context, string) (*lagoondb.User, error)
SSHEndpointByEnvironmentID(context.Context, int) (string, string, error)
SSHKeyUsed(context.Context, string, time.Time) error
}

// Serve contains the main ssh session logic
Expand Down

0 comments on commit b83eeae

Please sign in to comment.