diff --git a/cmd/ssh-portal-api/main.go b/cmd/ssh-portal-api/main.go index 031c29f5..ffb5e4f2 100644 --- a/cmd/ssh-portal-api/main.go +++ b/cmd/ssh-portal-api/main.go @@ -1,9 +1,11 @@ -// Package main is the executable ssh-portal-api service. +// Package main implements the ssh-portal-api service. package main import ( + "log/slog" + "os" + "github.com/alecthomas/kong" - "go.uber.org/zap" ) // CLI represents the command-line interface. @@ -20,13 +22,13 @@ func main() { kong.UsageOnError(), ) // init logger - var log *zap.Logger + var log *slog.Logger if cli.Debug { - log = zap.Must(zap.NewDevelopment(zap.AddStacktrace(zap.ErrorLevel))) + log = slog.New(slog.NewJSONHandler(os.Stderr, + &slog.HandlerOptions{Level: slog.LevelDebug})) } else { - log = zap.Must(zap.NewProduction()) + log = slog.New(slog.NewJSONHandler(os.Stderr, nil)) } - defer log.Sync() //nolint:errcheck // execute CLI kctx.FatalIfErrorf(kctx.Run(log)) } diff --git a/cmd/ssh-portal-api/serve.go b/cmd/ssh-portal-api/serve.go index c2ad7f6b..0517e9af 100644 --- a/cmd/ssh-portal-api/serve.go +++ b/cmd/ssh-portal-api/serve.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log/slog" "os/signal" "syscall" @@ -12,7 +13,6 @@ import ( "github.com/uselagoon/ssh-portal/internal/metrics" "github.com/uselagoon/ssh-portal/internal/rbac" "github.com/uselagoon/ssh-portal/internal/sshportalapi" - "go.uber.org/zap" ) // ServeCmd represents the serve command. @@ -29,7 +29,7 @@ type ServeCmd struct { } // Run the serve command to ssh-portal API requests. -func (cmd *ServeCmd) Run(log *zap.Logger) error { +func (cmd *ServeCmd) Run(log *slog.Logger) error { // metrics needs a separate context because deferred Shutdown() will exit // immediately the context is done, which is the case for ctx on SIGTERM. m := metrics.NewServer(log, ":9911") diff --git a/cmd/ssh-portal/main.go b/cmd/ssh-portal/main.go index 121df927..1d13a92b 100644 --- a/cmd/ssh-portal/main.go +++ b/cmd/ssh-portal/main.go @@ -1,10 +1,12 @@ -// Package main implements the ssh-portal executable. +// Package main implements the ssh-portal service. package main import ( + "log/slog" + "os" + "github.com/alecthomas/kong" "github.com/moby/spdystream" - "go.uber.org/zap" ) // CLI represents the command-line interface. @@ -23,13 +25,13 @@ func main() { kong.UsageOnError(), ) // init logger - var log *zap.Logger + var log *slog.Logger if cli.Debug { - log = zap.Must(zap.NewDevelopment(zap.AddStacktrace(zap.ErrorLevel))) + log = slog.New(slog.NewJSONHandler(os.Stderr, + &slog.HandlerOptions{Level: slog.LevelDebug})) } else { - log = zap.Must(zap.NewProduction()) + log = slog.New(slog.NewJSONHandler(os.Stderr, nil)) } - defer log.Sync() //nolint:errcheck // execute CLI kctx.FatalIfErrorf(kctx.Run(log)) } diff --git a/cmd/ssh-portal/serve.go b/cmd/ssh-portal/serve.go index 4384a377..1ef4c827 100644 --- a/cmd/ssh-portal/serve.go +++ b/cmd/ssh-portal/serve.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log/slog" "net" "os/signal" "syscall" @@ -11,7 +12,6 @@ import ( "github.com/uselagoon/ssh-portal/internal/k8s" "github.com/uselagoon/ssh-portal/internal/metrics" "github.com/uselagoon/ssh-portal/internal/sshserver" - "go.uber.org/zap" ) // ServeCmd represents the serve command. @@ -25,7 +25,7 @@ type ServeCmd struct { } // Run the serve command to handle SSH connection requests. -func (cmd *ServeCmd) Run(log *zap.Logger) error { +func (cmd *ServeCmd) Run(log *slog.Logger) error { // metrics needs a separate context because deferred Shutdown() will exit // immediately the context is done, which is the case for ctx on SIGTERM. m := metrics.NewServer(log, ":9912") @@ -42,10 +42,10 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error { stop() }), nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { - log.Warn("nats disconnected", zap.Error(err)) + log.Warn("nats disconnected", slog.Any("error", err)) }), nats.ReconnectHandler(func(nc *nats.Conn) { - log.Info("nats reconnected", zap.String("url", nc.ConnectedUrl())) + log.Info("nats reconnected", slog.String("url", nc.ConnectedUrl())) })) if err != nil { return fmt.Errorf("couldn't connect to NATS server: %v", err) diff --git a/cmd/ssh-token/main.go b/cmd/ssh-token/main.go index 5dc58de0..c7c4f087 100644 --- a/cmd/ssh-token/main.go +++ b/cmd/ssh-token/main.go @@ -1,9 +1,11 @@ -// Package main is the executable ssh-token service. +// Package main implements the ssh-token service. package main import ( + "log/slog" + "os" + "github.com/alecthomas/kong" - "go.uber.org/zap" ) // CLI represents the command-line interface. @@ -20,13 +22,13 @@ func main() { kong.UsageOnError(), ) // init logger - var log *zap.Logger + var log *slog.Logger if cli.Debug { - log = zap.Must(zap.NewDevelopment(zap.AddStacktrace(zap.ErrorLevel))) + log = slog.New(slog.NewJSONHandler(os.Stderr, + &slog.HandlerOptions{Level: slog.LevelDebug})) } else { - log = zap.Must(zap.NewProduction()) + log = slog.New(slog.NewJSONHandler(os.Stderr, nil)) } - defer log.Sync() //nolint:errcheck // execute CLI kctx.FatalIfErrorf(kctx.Run(log)) } diff --git a/cmd/ssh-token/serve.go b/cmd/ssh-token/serve.go index e4fa4fd4..59d988ce 100644 --- a/cmd/ssh-token/serve.go +++ b/cmd/ssh-token/serve.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log/slog" "net" "os/signal" "syscall" @@ -13,7 +14,6 @@ import ( "github.com/uselagoon/ssh-portal/internal/metrics" "github.com/uselagoon/ssh-portal/internal/rbac" "github.com/uselagoon/ssh-portal/internal/sshtoken" - "go.uber.org/zap" ) // ServeCmd represents the serve command. @@ -35,7 +35,7 @@ type ServeCmd struct { } // Run the serve command to ssh-portal API requests. -func (cmd *ServeCmd) Run(log *zap.Logger) error { +func (cmd *ServeCmd) Run(log *slog.Logger) error { // metrics needs a separate context because deferred Shutdown() will exit // immediately the context is done, which is the case for ctx on SIGTERM. m := metrics.NewServer(log, ":9948") diff --git a/go.mod b/go.mod index 37401487..516cfcb8 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/prometheus/client_golang v1.18.0 github.com/zitadel/oidc/v3 v3.10.0 go.opentelemetry.io/otel v1.21.0 - go.uber.org/zap v1.26.0 golang.org/x/crypto v0.18.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.16.0 @@ -68,7 +67,6 @@ require ( github.com/zitadel/schema v1.3.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect - go.uber.org/multierr v1.10.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/term v0.16.0 // indirect diff --git a/go.sum b/go.sum index a5a78d94..158b5069 100644 --- a/go.sum +++ b/go.sum @@ -168,12 +168,6 @@ go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ3 go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/internal/keycloak/client.go b/internal/keycloak/client.go index a98a2f71..c5a37a3e 100644 --- a/internal/keycloak/client.go +++ b/internal/keycloak/client.go @@ -5,13 +5,12 @@ package keycloak import ( "context" "fmt" + "log/slog" "net/http" "net/url" "path" "time" - "go.uber.org/zap" - "github.com/MicahParks/keyfunc/v2" oidcClient "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -24,12 +23,12 @@ type Client struct { clientID string clientSecret string jwks *keyfunc.JWKS - log *zap.Logger + log *slog.Logger oidcConfig *oidc.DiscoveryConfiguration } // NewClient creates a new keycloak client for the lagoon realm. -func NewClient(ctx context.Context, log *zap.Logger, keycloakURL, clientID, +func NewClient(ctx context.Context, log *slog.Logger, keycloakURL, clientID, clientSecret string) (*Client, error) { // discover OIDC config issuerURL, err := url.Parse(keycloakURL) diff --git a/internal/keycloak/jwt_test.go b/internal/keycloak/jwt_test.go index a3bf54b7..b57d7c80 100644 --- a/internal/keycloak/jwt_test.go +++ b/internal/keycloak/jwt_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "io" + "log/slog" "net/http" "net/http/httptest" "os" @@ -14,7 +15,6 @@ import ( "github.com/alecthomas/assert/v2" "github.com/golang-jwt/jwt/v5" "github.com/uselagoon/ssh-portal/internal/keycloak" - "go.uber.org/zap" "golang.org/x/oauth2" ) @@ -135,7 +135,7 @@ func TestUnmarshalLagoonClaims(t *testing.T) { func TestValidateTokenClaims(t *testing.T) { // set up logger - log := zap.Must(zap.NewDevelopment()) + log := slog.New(slog.NewJSONHandler(os.Stderr, nil)) // set up test cases validClaims := keycloak.LagoonClaims{ AuthorizedParty: "auth-server", diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 7cb291e4..1bb27b52 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -1,17 +1,18 @@ +// Package metrics implements the prometheus metrics server. package metrics import ( + "log/slog" "net/http" "time" "github.com/prometheus/client_golang/prometheus/promhttp" - "go.uber.org/zap" ) // NewServer returns a *http.Server serving prometheus metrics in a new // goroutine. // Caller should defer Shutdown() for cleanup. -func NewServer(log *zap.Logger, addr string) *http.Server { +func NewServer(log *slog.Logger, addr string) *http.Server { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) s := http.Server{ @@ -22,7 +23,7 @@ func NewServer(log *zap.Logger, addr string) *http.Server { } go func() { if err := s.ListenAndServe(); err != http.ErrServerClosed { - log.Error("metrics server did not shut down cleanly", zap.Error(err)) + log.Error("metrics server did not shut down cleanly", slog.Any("error", err)) } }() return &s diff --git a/internal/sshportalapi/server.go b/internal/sshportalapi/server.go index 444f9201..9e0b339d 100644 --- a/internal/sshportalapi/server.go +++ b/internal/sshportalapi/server.go @@ -5,13 +5,13 @@ package sshportalapi import ( "context" "fmt" + "log/slog" "sync" "github.com/google/uuid" "github.com/nats-io/nats.go" "github.com/uselagoon/ssh-portal/internal/lagoondb" "github.com/uselagoon/ssh-portal/internal/rbac" - "go.uber.org/zap" ) const ( @@ -31,7 +31,7 @@ type KeycloakService interface { } // ServeNATS sshportalapi NATS requests. -func ServeNATS(ctx context.Context, stop context.CancelFunc, log *zap.Logger, +func ServeNATS(ctx context.Context, stop context.CancelFunc, log *slog.Logger, p *rbac.Permission, l LagoonDBService, k KeycloakService, natsURL string) error { // setup synchronisation wg := sync.WaitGroup{} @@ -46,10 +46,10 @@ func ServeNATS(ctx context.Context, stop context.CancelFunc, log *zap.Logger, wg.Done() }), nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { - log.Warn("nats disconnected", zap.Error(err)) + log.Warn("nats disconnected", slog.Any("error", err)) }), nats.ReconnectHandler(func(nc *nats.Conn) { - log.Info("nats reconnected", zap.String("url", nc.ConnectedUrl())) + log.Info("nats reconnected", slog.String("url", nc.ConnectedUrl())) })) if err != nil { return fmt.Errorf("couldn't connect to NATS server: %v", err) @@ -69,7 +69,7 @@ func ServeNATS(ctx context.Context, stop context.CancelFunc, log *zap.Logger, <-ctx.Done() // drain and log errors if err := nc.Drain(); err != nil { - log.Warn("couldn't drain connection", zap.Error(err)) + log.Warn("couldn't drain connection", slog.Any("error", err)) } // wait for connection to close wg.Wait() diff --git a/internal/sshportalapi/sshportal.go b/internal/sshportalapi/sshportal.go index ed353869..3bc9368e 100644 --- a/internal/sshportalapi/sshportal.go +++ b/internal/sshportalapi/sshportal.go @@ -3,6 +3,7 @@ package sshportalapi import ( "context" "errors" + "log/slog" "github.com/nats-io/nats.go" "github.com/prometheus/client_golang/prometheus" @@ -10,7 +11,6 @@ import ( "github.com/uselagoon/ssh-portal/internal/lagoondb" "github.com/uselagoon/ssh-portal/internal/rbac" "go.opentelemetry.io/otel" - "go.uber.org/zap" ) const ( @@ -27,6 +27,17 @@ type SSHAccessQuery struct { SessionID string } +// LogValue implements the slog.LogValuer interface. +func (q SSHAccessQuery) LogValue() slog.Value { + return slog.GroupValue( + slog.String("sshFingerprint", q.SSHFingerprint), + slog.String("namespaceName", q.NamespaceName), + slog.Int("projectID", q.ProjectID), + slog.Int("environmentID", q.EnvironmentID), + slog.String("sessionID", q.SessionID), + ) +} + var ( requestsCounter = promauto.NewCounter(prometheus.CounterOpts{ Name: "sshportalapi_requests_total", @@ -34,7 +45,7 @@ var ( }) ) -func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, +func sshportal(ctx context.Context, log *slog.Logger, c *nats.EncodedConn, p *rbac.Permission, l LagoonDBService, k KeycloakService) nats.Handler { return func(_, replySubject string, query *SSHAccessQuery) { var realmRoles, userGroups []string @@ -43,27 +54,23 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, ctx, span := otel.Tracer(pkgName).Start(ctx, SubjectSSHAccessQuery) defer span.End() requestsCounter.Inc() + log = log.With(slog.Any("query", query)) // sanity check the query if query.SSHFingerprint == "" || query.NamespaceName == "" { - log.Warn("malformed sshportal query", zap.Any("query", query)) + log.Warn("malformed sshportal query") return } // get the environment env, err := l.EnvironmentByNamespaceName(ctx, query.NamespaceName) if err != nil { if errors.Is(err, lagoondb.ErrNoResult) { - log.Warn("unknown namespace name", - zap.Any("query", query), zap.Error(err)) + log.Warn("unknown namespace name", slog.Any("error", err)) if err = c.Publish(replySubject, false); err != nil { - log.Error("couldn't publish reply", - zap.Any("query", query), - zap.Bool("reply", false), - zap.Error(err)) + log.Error("couldn't publish reply", slog.Any("error", err)) } return } - log.Error("couldn't query environment", - zap.Any("query", query), zap.Error(err)) + log.Error("couldn't query environment", slog.Any("error", err)) return } // sanity check the environment we found @@ -73,12 +80,10 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, if (query.ProjectID != 0 && query.ProjectID != env.ProjectID) || (query.EnvironmentID != 0 && query.EnvironmentID != env.ID) { log.Warn("ID mismatch in environment identification", - zap.Any("query", query), zap.Any("env", env), zap.Error(err)) + slog.Any("env", env), + slog.Any("error", err)) if err = c.Publish(replySubject, false); err != nil { - log.Error("couldn't publish reply", - zap.Any("query", query), - zap.Bool("reply", false), - zap.Error(err)) + log.Error("couldn't publish reply", slog.Any("error", err)) } return } @@ -86,19 +91,13 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, user, err := l.UserBySSHFingerprint(ctx, query.SSHFingerprint) if err != nil { if errors.Is(err, lagoondb.ErrNoResult) { - log.Debug("unknown SSH Fingerprint", - zap.Any("query", query), zap.Error(err)) + log.Debug("unknown SSH Fingerprint", slog.Any("error", err)) if err = c.Publish(replySubject, false); err != nil { - log.Error("couldn't publish reply", - zap.Any("query", query), - zap.Bool("reply", false), - zap.String("userUUID", user.UUID.String()), - zap.Error(err)) + log.Error("couldn't publish reply", slog.Any("error", err)) } return } - log.Error("couldn't query user by ssh fingerprint", - zap.Any("query", query), zap.Error(err)) + log.Error("couldn't query user by ssh fingerprint", slog.Any("error", err)) return } // get the user roles and groups @@ -106,17 +105,16 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, k.UserRolesAndGroups(ctx, user.UUID) if err != nil { log.Error("couldn't query user roles and groups", - zap.Any("query", query), - zap.String("userUUID", user.UUID.String()), - zap.Error(err)) + slog.String("userUUID", user.UUID.String()), + slog.Any("error", err)) return } log.Debug("keycloak user attributes", - zap.Strings("realmRoles", realmRoles), - zap.Strings("userGroups", userGroups), - zap.Any("groupProjectIDs", groupProjectIDs), - zap.String("userUUID", user.UUID.String()), - zap.String("sessionID", query.SessionID), + slog.Any("realmRoles", realmRoles), + slog.Any("userGroups", userGroups), + slog.Any("groupProjectIDs", groupProjectIDs), + slog.String("userUUID", user.UUID.String()), + slog.String("sessionID", query.SessionID), ) // check permission ok := p.UserCanSSHToEnvironment(ctx, env, realmRoles, userGroups, @@ -128,21 +126,19 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, logMsg = "SSH access not authorized" } log.Info(logMsg, - zap.Int("environmentID", env.ID), - zap.Int("projectID", env.ProjectID), - zap.String("SSHFingerprint", query.SSHFingerprint), - zap.String("environmentName", env.Name), - zap.String("namespace", query.NamespaceName), - zap.String("projectName", env.ProjectName), - zap.String("sessionID", query.SessionID), - zap.String("userUUID", user.UUID.String()), + slog.Int("environmentID", env.ID), + slog.Int("projectID", env.ProjectID), + slog.String("SSHFingerprint", query.SSHFingerprint), + slog.String("environmentName", env.Name), + slog.String("namespace", query.NamespaceName), + slog.String("projectName", env.ProjectName), + slog.String("sessionID", query.SessionID), + slog.String("userUUID", user.UUID.String()), ) if err = c.Publish(replySubject, ok); err != nil { log.Error("couldn't publish reply", - zap.Any("query", query), - zap.Bool("reply", ok), - zap.String("userUUID", user.UUID.String()), - zap.Error(err)) + slog.String("userUUID", user.UUID.String()), + slog.Any("error", err)) } } } diff --git a/internal/sshserver/authhandler.go b/internal/sshserver/authhandler.go index f3577cf3..49829b0f 100644 --- a/internal/sshserver/authhandler.go +++ b/internal/sshserver/authhandler.go @@ -1,6 +1,7 @@ package sshserver import ( + "log/slog" "time" "github.com/gliderlabs/ssh" @@ -9,7 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "github.com/uselagoon/ssh-portal/internal/k8s" "github.com/uselagoon/ssh-portal/internal/sshportalapi" - "go.uber.org/zap" gossh "golang.org/x/crypto/ssh" ) @@ -40,24 +40,24 @@ var ( // pubKeyAuth returns a ssh.PublicKeyHandler which queries the remote // ssh-portal-api for Lagoon SSH authorization. -func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn, +func pubKeyAuth(log *slog.Logger, nc *nats.EncodedConn, c *k8s.Client) ssh.PublicKeyHandler { return func(ctx ssh.Context, key ssh.PublicKey) bool { authAttemptsTotal.Inc() + log = log.With( + slog.String("sessionID", ctx.SessionID()), + ) // parse SSH public key pubKey, err := gossh.ParsePublicKey(key.Marshal()) if err != nil { - log.Warn("couldn't parse SSH public key", - zap.String("sessionID", ctx.SessionID()), - zap.Error(err)) + log.Warn("couldn't parse SSH public key", slog.Any("error", err)) return false } // get Lagoon labels from namespace if available eid, pid, ename, pname, err := c.NamespaceDetails(ctx, ctx.User()) if err != nil { log.Debug("couldn't get namespace details", - zap.String("sessionID", ctx.SessionID()), - zap.String("namespace", ctx.User()), zap.Error(err)) + slog.String("namespace", ctx.User()), slog.Any("error", err)) return false } // construct ssh access query @@ -73,17 +73,14 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn, var ok bool err = nc.Request(sshportalapi.SubjectSSHAccessQuery, q, &ok, natsTimeout) if err != nil { - log.Warn("couldn't make NATS request", - zap.String("sessionID", ctx.SessionID()), - zap.Error(err)) + log.Warn("couldn't make NATS request", slog.Any("error", err)) return false } // handle response if !ok { log.Debug("SSH access not authorized", - zap.String("sessionID", ctx.SessionID()), - zap.String("fingerprint", fingerprint), - zap.String("namespace", ctx.User())) + slog.String("fingerprint", fingerprint), + slog.String("namespace", ctx.User())) return false } authSuccessTotal.Inc() @@ -93,9 +90,8 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn, ctx.SetValue(projectNameKey, pname) ctx.SetValue(sshFingerprint, fingerprint) log.Debug("SSH access authorized", - zap.String("sessionID", ctx.SessionID()), - zap.String("fingerprint", fingerprint), - zap.String("namespace", ctx.User())) + slog.String("fingerprint", fingerprint), + slog.String("namespace", ctx.User())) return true } } diff --git a/internal/sshserver/serve.go b/internal/sshserver/serve.go index 52422da9..84389251 100644 --- a/internal/sshserver/serve.go +++ b/internal/sshserver/serve.go @@ -5,19 +5,19 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "time" "github.com/gliderlabs/ssh" "github.com/nats-io/nats.go" "github.com/uselagoon/ssh-portal/internal/k8s" - "go.uber.org/zap" gossh "golang.org/x/crypto/ssh" ) // disableSHA1Kex returns a ServerConfig which relies on default for everything // except key exchange algorithms. There it removes the SHA1 based algorithms. -func disableSHA1Kex(ctx ssh.Context) *gossh.ServerConfig { +func disableSHA1Kex(_ ssh.Context) *gossh.ServerConfig { c := gossh.ServerConfig{} c.Config.KeyExchanges = []string{ "curve25519-sha256", @@ -31,7 +31,7 @@ func disableSHA1Kex(ctx ssh.Context) *gossh.ServerConfig { } // Serve contains the main ssh session logic -func Serve(ctx context.Context, log *zap.Logger, nc *nats.EncodedConn, +func Serve(ctx context.Context, log *slog.Logger, nc *nats.EncodedConn, l net.Listener, c *k8s.Client, hostKeys [][]byte, logAccessEnabled bool) error { srv := ssh.Server{ Handler: sessionHandler(log, c, false, logAccessEnabled), @@ -53,7 +53,7 @@ func Serve(ctx context.Context, log *zap.Logger, nc *nats.EncodedConn, shutCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() if err := srv.Shutdown(shutCtx); err != nil { - log.Warn("couldn't shutdown cleanly", zap.Error(err)) + log.Warn("couldn't shutdown cleanly", slog.Any("error", err)) } }() if err := srv.Serve(l); !errors.Is(ssh.ErrServerClosed, err) { diff --git a/internal/sshserver/sessionhandler.go b/internal/sshserver/sessionhandler.go index 7c16857c..bfe2ad64 100644 --- a/internal/sshserver/sessionhandler.go +++ b/internal/sshserver/sessionhandler.go @@ -3,6 +3,7 @@ package sshserver import ( "context" "fmt" + "log/slog" "strings" "time" @@ -10,7 +11,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/uselagoon/ssh-portal/internal/k8s" - "go.uber.org/zap" "k8s.io/utils/exec" ) @@ -46,45 +46,40 @@ func getSSHIntent(sftp bool, cmd []string) []string { // handler is that the command is set to sftp-server. This implies that the // target container must have a sftp-server binary installed for sftp to work. // There is no support for a built-in sftp server. -func sessionHandler(log *zap.Logger, c *k8s.Client, +func sessionHandler(log *slog.Logger, c *k8s.Client, sftp, logAccessEnabled bool) ssh.Handler { return func(s ssh.Session) { sessionTotal.Inc() ctx := s.Context() - sid := ctx.SessionID() + log = log.With( + slog.String("sessionID", ctx.SessionID()), + ) log.Debug("starting session", - zap.String("sessionID", sid), - zap.Strings("rawCommand", s.Command()), - zap.String("subsystem", s.Subsystem()), + slog.Any("rawCommand", s.Command()), + slog.String("subsystem", s.Subsystem()), ) // parse the command line arguments to extract any service or container args service, container, logs, rawCmd := parseConnectionParams(s.Command()) // validate the service and container if err := k8s.ValidateLabelValue(service); err != nil { log.Debug("invalid service name", - zap.String("service", service), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("service", service), + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "invalid service name %s. SID: %s\r\n", - service, sid) + service, ctx.SessionID()) if err != nil { - log.Debug("couldn't write to session stream", - zap.String("sessionID", sid), - zap.Error(err)) + log.Debug("couldn't write to session stream", slog.Any("error", err)) } return } if err := k8s.ValidateLabelValue(container); err != nil { log.Debug("invalid container name", - zap.String("container", container), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("container", container), + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "invalid container name %s. SID: %s\r\n", - container, sid) + container, ctx.SessionID()) if err != nil { - log.Debug("couldn't write to session stream", - zap.String("sessionID", sid), - zap.Error(err)) + log.Debug("couldn't write to session stream", slog.Any("error", err)) } return } @@ -92,15 +87,12 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, deployment, err := c.FindDeployment(ctx, s.User(), service) if err != nil { log.Debug("couldn't find deployment for service", - zap.String("service", service), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("service", service), + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "unknown service %s. SID: %s\r\n", - service, sid) + service, ctx.SessionID()) if err != nil { - log.Debug("couldn't write to session stream", - zap.String("sessionID", sid), - zap.Error(err)) + log.Debug("couldn't write to session stream", slog.Any("error", err)) } return } @@ -128,62 +120,51 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, if len(logs) != 0 { if !logAccessEnabled { log.Debug("logs access is not enabled", - zap.String("logsArgument", logs), - zap.String("sessionID", sid)) + slog.String("logsArgument", logs)) _, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { - log.Warn("couldn't send error to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send error to client", slog.Any("error", err)) } // Send a non-zero exit code to the client on internal logs error. // OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to // differentiate this error. if err = s.Exit(253); err != nil { - log.Warn("couldn't send exit code to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send exit code to client", slog.Any("error", err)) } return } follow, tailLines, err := parseLogsArg(service, logs, rawCmd) if err != nil { log.Debug("couldn't parse logs argument", - zap.String("logsArgument", logs), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("logsArgument", logs), + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { - log.Warn("couldn't send error to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send error to client", slog.Any("error", err)) } // Send a non-zero exit code to the client on internal logs error. // OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to // differentiate this error. if err = s.Exit(253); err != nil { - log.Warn("couldn't send exit code to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send exit code to client", slog.Any("error", err)) } return } log.Info("sending logs to SSH client", - zap.Int("environmentID", eid), - zap.Int("projectID", pid), - zap.String("SSHFingerprint", fingerprint), - zap.String("container", container), - zap.String("deployment", deployment), - zap.String("environmentName", ename), - zap.String("namespace", s.User()), - zap.String("projectName", pname), - zap.String("sessionID", sid), - zap.Bool("follow", follow), - zap.Int64("tailLines", tailLines), + slog.Int("environmentID", eid), + slog.Int("projectID", pid), + slog.String("SSHFingerprint", fingerprint), + slog.String("container", container), + slog.String("deployment", deployment), + slog.String("environmentName", ename), + slog.String("namespace", s.User()), + slog.String("projectName", pname), + slog.Bool("follow", follow), + slog.Int64("tailLines", tailLines), ) - doLogs(ctx, log, s, deployment, container, follow, tailLines, c, sid) + doLogs(ctx, log, s, deployment, container, follow, tailLines, c) return } // handle sftp and sh fallback @@ -191,19 +172,18 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, // check if a pty was requested, and get the window size channel _, winch, pty := s.Pty() log.Info("executing SSH command", - zap.Bool("pty", pty), - zap.Int("environmentID", eid), - zap.Int("projectID", pid), - zap.String("SSHFingerprint", fingerprint), - zap.String("container", container), - zap.String("deployment", deployment), - zap.String("environmentName", ename), - zap.String("namespace", s.User()), - zap.String("projectName", pname), - zap.String("sessionID", sid), - zap.Strings("command", cmd), + slog.Bool("pty", pty), + slog.Int("environmentID", eid), + slog.Int("projectID", pid), + slog.String("SSHFingerprint", fingerprint), + slog.String("container", container), + slog.String("deployment", deployment), + slog.String("environmentName", ename), + slog.String("namespace", s.User()), + slog.String("projectName", pname), + slog.Any("command", cmd), ) - doExec(ctx, log, s, deployment, container, cmd, c, pty, winch, sid) + doExec(ctx, log, s, deployment, container, cmd, c, pty, winch) } } @@ -211,7 +191,7 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, // embedded in ssh.Session at a regular interval. If the client fails to // respond, the channel is closed, and cancel is called. func startClientKeepalive(ctx context.Context, cancel context.CancelFunc, - log *zap.Logger, s ssh.Session) { + log *slog.Logger, s ssh.Session) { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for { @@ -221,7 +201,7 @@ func startClientKeepalive(ctx context.Context, cancel context.CancelFunc, // edc2ef4e418e514c99701451fae4428ec04ce538/serverloop.c#L127-L158 _, err := s.SendRequest("keepalive@openssh.com", true, nil) if err != nil { - log.Debug("client closed connection", zap.Error(err)) + log.Debug("client closed connection", slog.Any("error", err)) _ = s.Close() cancel() return @@ -232,8 +212,8 @@ func startClientKeepalive(ctx context.Context, cancel context.CancelFunc, } } -func doLogs(ctx ssh.Context, log *zap.Logger, s ssh.Session, deployment, - container string, follow bool, tailLines int64, c *k8s.Client, sid string) { +func doLogs(ctx ssh.Context, log *slog.Logger, s ssh.Session, deployment, + container string, follow bool, tailLines int64, c *k8s.Client) { // Wrap the ssh.Context so we can cancel goroutines started from this // function without affecting the SSH session. childCtx, cancel := context.WithCancel(ctx) @@ -249,62 +229,46 @@ func doLogs(ctx ssh.Context, log *zap.Logger, s ssh.Session, deployment, go startClientKeepalive(childCtx, cancel, log, s) err := c.Logs(childCtx, s.User(), deployment, container, follow, tailLines, s) if err != nil { - log.Warn("couldn't send logs", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send logs", slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { - log.Warn("couldn't send error to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send error to client", slog.Any("error", err)) } // Send a non-zero exit code to the client on internal logs error. // OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to // differentiate this error. if err = s.Exit(253); err != nil { - log.Warn("couldn't send exit code to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send exit code to client", slog.Any("error", err)) } } - log.Debug("finished command logs", zap.String("sessionID", sid)) + log.Debug("finished command logs") } -func doExec(ctx ssh.Context, log *zap.Logger, s ssh.Session, deployment, +func doExec(ctx ssh.Context, log *slog.Logger, s ssh.Session, deployment, container string, cmd []string, c *k8s.Client, pty bool, - winch <-chan ssh.Window, sid string) { + winch <-chan ssh.Window) { err := c.Exec(ctx, s.User(), deployment, container, cmd, s, s.Stderr(), pty, winch) if err != nil { if exitErr, ok := err.(exec.ExitError); ok { - log.Debug("couldn't execute command", - zap.String("sessionID", sid), - zap.Error(err)) + log.Debug("couldn't execute command", slog.Any("error", err)) if err = s.Exit(exitErr.ExitStatus()); err != nil { - log.Warn("couldn't send exit code to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send exit code to client", slog.Any("error", err)) } } else { - log.Warn("couldn't execute command", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't execute command", slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { - log.Warn("couldn't send error to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send error to client", slog.Any("error", err)) } // Send a non-zero exit code to the client on internal exec error. // OpenSSH uses 255 for this, so use 254 to differentiate the error. if err = s.Exit(254); err != nil { - log.Warn("couldn't send exit code to client", - zap.String("sessionID", sid), - zap.Error(err)) + log.Warn("couldn't send exit code to client", slog.Any("error", err)) } } } - log.Debug("finished command exec", zap.String("sessionID", sid)) + log.Debug("finished command exec") } diff --git a/internal/sshtoken/authhandler.go b/internal/sshtoken/authhandler.go index 35143c3a..4613ed89 100644 --- a/internal/sshtoken/authhandler.go +++ b/internal/sshtoken/authhandler.go @@ -2,12 +2,12 @@ package sshtoken import ( "errors" + "log/slog" "github.com/gliderlabs/ssh" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/uselagoon/ssh-portal/internal/lagoondb" - "go.uber.org/zap" gossh "golang.org/x/crypto/ssh" ) @@ -30,29 +30,26 @@ var ( // pubKeyAuth returns a ssh.PublicKeyHandler which accepts any key which // matches a user, and the associated user UUID to the ssh context. -func pubKeyAuth(log *zap.Logger, ldb LagoonDBService) ssh.PublicKeyHandler { +func pubKeyAuth(log *slog.Logger, ldb LagoonDBService) ssh.PublicKeyHandler { return func(ctx ssh.Context, key ssh.PublicKey) bool { authnAttemptsTotal.Inc() + log = log.With(slog.String("sessionID", ctx.SessionID())) // parse SSH public key pubKey, err := gossh.ParsePublicKey(key.Marshal()) if err != nil { - log.Warn("couldn't parse SSH public key", - zap.String("sessionID", ctx.SessionID()), - zap.Error(err)) + log.Warn("couldn't parse SSH public key", slog.Any("error", err)) return false } // identify Lagoon user by ssh key fingerprint fingerprint := gossh.FingerprintSHA256(pubKey) + log = log.With(slog.String("fingerprint", fingerprint)) user, err := ldb.UserBySSHFingerprint(ctx, fingerprint) if err != nil { if errors.Is(err, lagoondb.ErrNoResult) { - log.Debug("unknown SSH Fingerprint", - zap.String("sessionID", ctx.SessionID())) + log.Debug("unknown SSH Fingerprint") } else { log.Warn("couldn't query for user by SSH key fingerprint", - zap.String("sessionID", ctx.SessionID()), - zap.String("fingerprint", fingerprint), - zap.Error(err)) + slog.Any("error", err)) } return false } @@ -62,9 +59,8 @@ func pubKeyAuth(log *zap.Logger, ldb LagoonDBService) ssh.PublicKeyHandler { authnSuccessTotal.Inc() ctx.SetValue(userUUID, user.UUID) log.Info("authentication successful", - zap.String("sessionID", ctx.SessionID()), - zap.String("fingerprint", fingerprint), - zap.String("userID", user.UUID.String())) + slog.String("fingerprint", fingerprint), + slog.String("userID", user.UUID.String())) return true } } diff --git a/internal/sshtoken/serve.go b/internal/sshtoken/serve.go index 4923dc02..34c435b9 100644 --- a/internal/sshtoken/serve.go +++ b/internal/sshtoken/serve.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "time" @@ -12,7 +13,6 @@ import ( "github.com/uselagoon/ssh-portal/internal/keycloak" "github.com/uselagoon/ssh-portal/internal/lagoondb" "github.com/uselagoon/ssh-portal/internal/rbac" - "go.uber.org/zap" ) // give an 8 second deadline to shut down cleanly. @@ -26,7 +26,7 @@ type LagoonDBService interface { } // Serve contains the main ssh session logic -func Serve(ctx context.Context, log *zap.Logger, l net.Listener, +func Serve(ctx context.Context, log *slog.Logger, l net.Listener, p *rbac.Permission, ldb *lagoondb.Client, keycloakToken, keycloakPermission *keycloak.Client, hostKeys [][]byte) error { @@ -45,7 +45,7 @@ func Serve(ctx context.Context, log *zap.Logger, l net.Listener, shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() if err := srv.Shutdown(shutCtx); err != nil { - log.Warn("couldn't shutdown cleanly", zap.Error(err)) + log.Warn("couldn't shutdown cleanly", slog.Any("error", err)) } }() if err := srv.Serve(l); !errors.Is(ssh.ErrServerClosed, err) { diff --git a/internal/sshtoken/sessionhandler.go b/internal/sshtoken/sessionhandler.go index 033d5185..3143903e 100644 --- a/internal/sshtoken/sessionhandler.go +++ b/internal/sshtoken/sessionhandler.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "github.com/gliderlabs/ssh" "github.com/google/uuid" @@ -11,7 +12,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "github.com/uselagoon/ssh-portal/internal/lagoondb" "github.com/uselagoon/ssh-portal/internal/rbac" - "go.uber.org/zap" ) // KeycloakTokenService provides methods for querying the Keycloak API for user @@ -45,28 +45,24 @@ var ( // tokenSession returns a bare access token or full access token response based // on the user ID -func tokenSession(s ssh.Session, log *zap.Logger, +func tokenSession(s ssh.Session, log *slog.Logger, keycloakToken KeycloakTokenService, uid *uuid.UUID) { // valid commands: // - grant: returns a full access token response as per // https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4 // - token: returns a bare access token (the contents of the access_token // field inside a full token access token response) - sid := s.Context().SessionID() + ctx := s.Context() cmd := s.Command() if len(cmd) != 1 { log.Debug("too many arguments", - zap.Strings("command", cmd), - zap.String("sessionID", sid), - zap.String("userUUID", uid.String())) + slog.Any("command", cmd)) _, err := fmt.Fprintf(s.Stderr(), "invalid command: only \"grant\" and \"token\" are supported. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } @@ -75,52 +71,41 @@ func tokenSession(s ssh.Session, log *zap.Logger, var err error switch cmd[0] { case "grant": - response, err = keycloakToken.UserAccessTokenResponse(s.Context(), uid) + response, err = keycloakToken.UserAccessTokenResponse(ctx, uid) if err != nil { log.Warn("couldn't get user access token response", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), - "internal error. SID: %s\r\n", sid) + "internal error. SID: %s\r\n", ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } case "token": - response, err = keycloakToken.UserAccessToken(s.Context(), uid) + response, err = keycloakToken.UserAccessToken(ctx, uid) if err != nil { log.Warn("couldn't get user access token", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), - "internal error. SID: %s\r\n", sid) + "internal error. SID: %s\r\n", + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } default: log.Debug("invalid command", - zap.Strings("command", cmd), - zap.String("sessionID", sid), - zap.String("userUUID", uid.String())) + slog.Any("command", cmd)) _, err := fmt.Fprintf(s.Stderr(), "invalid command: only \"grant\" and \"token\" are supported. SID: %s\r\n", - sid) + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } @@ -128,40 +113,33 @@ func tokenSession(s ssh.Session, log *zap.Logger, _, err = fmt.Fprintf(s, "%s\r\n", response) if err != nil { log.Debug("couldn't write response to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) return } tokensGeneratedTotal.Inc() - log.Info("generated token for user", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String())) + log.Info("generated token for user") } // redirectSession inspects the user string, and if it matches a namespace that // the user has access to, returns an error message to the user with the SSH // endpoint to use for ssh shell access. If the user doesn't have access to the // environment a generic error message is returned. -func redirectSession(s ssh.Session, log *zap.Logger, +func redirectSession(s ssh.Session, log *slog.Logger, p *rbac.Permission, keycloakUserInfo KeycloakUserInfoService, ldb LagoonDBService, uid *uuid.UUID) { - sid := s.Context().SessionID() + ctx := s.Context() // get the user roles and groups realmRoles, userGroups, groupProjectIDs, err := keycloakUserInfo.UserRolesAndGroups(s.Context(), uid) if err != nil { log.Error("couldn't query user roles and groups", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) _, err = fmt.Fprintf(s.Stderr(), - "This SSH server does not provide shell access. SID: %s\r\n", sid) + "This SSH server does not provide shell access. SID: %s\r\n", + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } @@ -169,91 +147,67 @@ func redirectSession(s ssh.Session, log *zap.Logger, if err != nil { if errors.Is(err, lagoondb.ErrNoResult) { log.Info("unknown namespace name", - zap.String("namespaceName", s.User()), - zap.String("userUUID", uid.String()), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("namespaceName", s.User()), + slog.Any("error", err)) } else { log.Error("couldn't get environment by namespace name", - zap.String("namespaceName", s.User()), - zap.String("userUUID", uid.String()), - zap.String("sessionID", sid), - zap.Error(err)) + slog.String("namespaceName", s.User()), + slog.Any("error", err)) } _, err = fmt.Fprintf(s.Stderr(), - "This SSH server does not provide shell access. SID: %s\r\n", sid) + "This SSH server does not provide shell access. SID: %s\r\n", + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } + log = log.With( + slog.Int("environmentID", env.ID), + slog.Int("projectID", env.ProjectID), + slog.String("environmentName", env.Name), + slog.String("namespaceName", s.User()), + slog.String("projectName", env.ProjectName), + ) // check permission ok := p.UserCanSSHToEnvironment(s.Context(), env, realmRoles, userGroups, groupProjectIDs) if !ok { - log.Info("user cannot SSH to environment", - zap.Int("environmentID", env.ID), - zap.Int("projectID", env.ProjectID), - zap.String("environmentName", env.Name), - zap.String("namespace", s.User()), - zap.String("projectName", env.ProjectName), - zap.String("sessionID", sid), - zap.String("userUUID", uid.String())) + log.Info("user cannot SSH to environment") log.Debug("user permissions", - zap.String("userUUID", uid.String()), - zap.Strings("realmRoles", realmRoles), - zap.Strings("userGroups", userGroups), - zap.Any("groupProjectIDs", groupProjectIDs)) + slog.Any("realmRoles", realmRoles), + slog.Any("userGroups", userGroups), + slog.Any("groupProjectIDs", groupProjectIDs)) _, err = fmt.Fprintf(s.Stderr(), - "This SSH server does not provide shell access. SID: %s\r\n", sid) + "This SSH server does not provide shell access. SID: %s\r\n", + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } - log.Info("user can SSH to environment", - zap.Int("environmentID", env.ID), - zap.Int("projectID", env.ProjectID), - zap.String("environmentName", env.Name), - zap.String("namespace", s.User()), - zap.String("projectName", env.ProjectName), - zap.String("sessionID", sid), - zap.String("userUUID", uid.String())) + log.Info("user can SSH to environment") log.Debug("user permissions", - zap.String("userUUID", uid.String()), - zap.Strings("realmRoles", realmRoles), - zap.Strings("userGroups", userGroups), - zap.Any("groupProjectIDs", groupProjectIDs)) + slog.Any("realmRoles", realmRoles), + slog.Any("userGroups", userGroups), + slog.Any("groupProjectIDs", groupProjectIDs)) sshHost, sshPort, err := ldb.SSHEndpointByEnvironmentID(s.Context(), env.ID) if err != nil { if errors.Is(err, lagoondb.ErrNoResult) { log.Warn("no results for ssh endpoint by environment ID", - zap.String("namespaceName", s.User()), - zap.String("userUUID", uid.String()), - zap.String("sessionID", sid), - zap.Int("environmentID", env.ID), - zap.Error(err)) + slog.Any("error", err)) } else { log.Error("couldn't get ssh endpoint by environment ID", - zap.String("namespaceName", s.User()), - zap.String("userUUID", uid.String()), - zap.String("sessionID", sid), - zap.Int("environmentID", env.ID), - zap.Error(err)) + slog.Any("error", err)) } _, err = fmt.Fprintf(s.Stderr(), - "This SSH server does not provide shell access. SID: %s\r\n", sid) + "This SSH server does not provide shell access. SID: %s\r\n", + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) } return } @@ -264,50 +218,46 @@ func redirectSession(s ssh.Session, log *zap.Logger, if sshPort == "22" { _, err = fmt.Fprintf(s.Stderr(), preamble+"\tssh %s@%s\r\n\nSID: %s\r\n", - s.User(), sshHost, sid) + s.User(), sshHost, ctx.SessionID()) } else { _, err = fmt.Fprintf(s.Stderr(), preamble+"\tssh -p %s %s@%s\r\n\nSID: %s\r\n", - sshPort, s.User(), sshHost, sid) + sshPort, s.User(), sshHost, ctx.SessionID()) } if err != nil { log.Debug("couldn't write response to session stream", - zap.String("sessionID", sid), - zap.String("userUUID", uid.String()), - zap.Error(err)) + slog.Any("error", err)) return } redirectsTotal.Inc() log.Info("redirected user to SSH portal endpoint", - zap.String("sessionID", sid), - zap.String("namespaceName", s.User()), - zap.String("userUUID", uid.String()), - zap.String("sshHost", sshHost), - zap.String("sshPort", sshPort)) + slog.String("sshHost", sshHost), + slog.String("sshPort", sshPort)) } // sessionHandler returns a ssh.Handler which writes a Lagoon access token to // the session stream and then closes the connection. -func sessionHandler(log *zap.Logger, p *rbac.Permission, +func sessionHandler(log *slog.Logger, p *rbac.Permission, keycloakToken KeycloakTokenService, keycloakPermission KeycloakUserInfoService, ldb LagoonDBService) ssh.Handler { return func(s ssh.Session) { sessionTotal.Inc() // extract required info from the session context - uid, ok := s.Context().Value(userUUID).(*uuid.UUID) + ctx := s.Context() + log = log.With(slog.String("sessionID", ctx.SessionID())) + uid, ok := ctx.Value(userUUID).(*uuid.UUID) if !ok { - log.Warn("couldn't get user UUID from context", - zap.String("sessionID", s.Context().SessionID())) + log.Warn("couldn't get user UUID from context") _, err := fmt.Fprintf(s.Stderr(), "internal error. SID: %s\r\n", - s.Context().SessionID()) + ctx.SessionID()) if err != nil { log.Debug("couldn't write error message to session stream", - zap.String("sessionID", s.Context().SessionID()), - zap.Error(err)) + slog.Any("error", err)) } return } + log = log.With(slog.String("userUUID", uid.String())) if s.User() == "lagoon" { tokenSession(s, log, keycloakToken, uid) } else {