Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update last used timestamp on ssh keys when used #457

Merged
merged 2 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading