Skip to content

Commit

Permalink
Merge pull request #457 from uselagoon/last-used
Browse files Browse the repository at this point in the history
Update last used timestamp on ssh keys when used
  • Loading branch information
smlx authored Jul 2, 2024
2 parents 218d428 + 7a89996 commit a79c370
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 26 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
79 changes: 53 additions & 26 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 @@ -61,7 +62,7 @@ func NewClient(ctx context.Context, dsn string) (*Client, error) {
}

// EnvironmentByNamespaceName returns the Environment associated with the given
// Namespace name (on Openshift this is the project name).
// Namespace name.
func (c *Client) EnvironmentByNamespaceName(
ctx context.Context,
name string,
Expand All @@ -71,18 +72,17 @@ func (c *Client) EnvironmentByNamespaceName(
defer span.End()
// run query
env := Environment{}
err := c.db.GetContext(ctx, &env, `
SELECT
environment.environment_type AS type,
environment.id AS id,
environment.name AS name,
environment.openshift_project_name AS namespace_name,
project.id AS project_id,
project.name AS project_name
FROM environment JOIN project ON environment.project = project.id
WHERE environment.openshift_project_name = ?
AND environment.deleted = '0000-00-00 00:00:00'
LIMIT 1`, name)
err := c.db.GetContext(ctx, &env,
`SELECT environment.environment_type AS type, `+
`environment.id AS id, `+
`environment.name AS name, `+
`environment.openshift_project_name AS namespace_name, `+
`project.id AS project_id, `+
`project.name AS project_name `+
`FROM environment JOIN project ON environment.project = project.id `+
`WHERE environment.openshift_project_name = ? `+
`AND environment.deleted = '0000-00-00 00:00:00' `+
`LIMIT 1`, name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoResult
Expand All @@ -103,10 +103,11 @@ func (c *Client) UserBySSHFingerprint(
defer span.End()
// run query
user := User{}
err := c.db.GetContext(ctx, &user, `
SELECT user_ssh_key.usid AS uuid
FROM user_ssh_key JOIN ssh_key ON user_ssh_key.skid = ssh_key.id
WHERE ssh_key.key_fingerprint = ?`, fingerprint)
err := c.db.GetContext(ctx, &user,
`SELECT user_ssh_key.usid AS uuid `+
`FROM user_ssh_key JOIN ssh_key ON user_ssh_key.skid = ssh_key.id `+
`WHERE ssh_key.key_fingerprint = ?`,
fingerprint)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoResult
Expand All @@ -128,12 +129,12 @@ func (c *Client) SSHEndpointByEnvironmentID(ctx context.Context,
Host string `db:"ssh_host"`
Port string `db:"ssh_port"`
}{}
err := c.db.GetContext(ctx, &ssh, `
SELECT
openshift.ssh_host AS ssh_host,
openshift.ssh_port AS ssh_port
FROM environment JOIN openshift ON environment.openshift = openshift.id
WHERE environment.id = ?`, envID)
err := c.db.GetContext(ctx, &ssh,
`SELECT openshift.ssh_host AS ssh_host, `+
`openshift.ssh_port AS ssh_port `+
`FROM environment JOIN openshift ON environment.openshift = openshift.id `+
`WHERE environment.id = ?`,
envID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", "", ErrNoResult
Expand All @@ -149,9 +150,9 @@ func (c *Client) GroupIDProjectIDsMap(
ctx context.Context,
) (map[string][]int, error) {
var gpms []groupProjectMapping
err := c.db.SelectContext(ctx, &gpms, `
SELECT group_id, project_id
FROM kc_group_projects`)
err := c.db.SelectContext(ctx, &gpms,
`SELECT group_id, project_id `+
`FROM kc_group_projects`)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoResult
Expand All @@ -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
7 changes: 7 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,12 @@ 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.Error("couldn't update ssh key last used: %v",
slog.Any("error", err))
return
}
// get the user roles and groups
realmRoles, userGroups, err = k.UserRolesAndGroups(ctx, user.UUID)
if err != nil {
Expand Down
7 changes: 7 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,12 @@ 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.Error("couldn't update ssh key last used: %v",
slog.Any("error", err))
return false
}
// 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 a79c370

Please sign in to comment.