Skip to content

Commit

Permalink
Merge pull request #106 from uselagoon/better-logs
Browse files Browse the repository at this point in the history
Improve logging in both the portal and the API
  • Loading branch information
smlx authored Sep 8, 2022
2 parents 01f7d5b + 5267e30 commit ceb0b24
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 44 deletions.
2 changes: 2 additions & 0 deletions internal/k8s/client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package k8s wraps a Kubernetes API client with convenience methods for
// Lagoon services.
package k8s

import (
Expand Down
37 changes: 25 additions & 12 deletions internal/k8s/namespacedetails.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
)

const (
projectIDLabel = "lagoon.sh/projectId"
environmentIDLabel = "lagoon.sh/environmentId"
environmentIDLabel = "lagoon.sh/environmentId"
environmentNameLabel = "lagoon.sh/environment"
projectIDLabel = "lagoon.sh/projectId"
projectNameLabel = "lagoon.sh/project"
)

func intFromLabel(labels map[string]string, label string) (int, error) {
Expand All @@ -22,22 +24,33 @@ func intFromLabel(labels map[string]string, label string) (int, error) {
return strconv.Atoi(value)
}

// NamespaceDetails gets the details for a Lagoon namespace.
// It performs some sanity checks to validate that the namespace is actually a
// Lagoon namespace.
func (c *Client) NamespaceDetails(ctx context.Context, name string) (int, int, error) {
var pid, eid int
// NamespaceDetails gets the environment ID, project ID, and project name from
// the labels on a Lagoon environment namespace for a Lagoon namespace. If one
// of the expected labels is missing or cannot be parsed, it will return an
// error.
func (c *Client) NamespaceDetails(ctx context.Context, name string) (
int, int, string, string, error) {
var eid, pid int
var ename, pname string
var ok bool
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ns, err := c.clientset.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return 0, 0, fmt.Errorf("couldn't get namespace: %v", err)
return 0, 0, "", "", fmt.Errorf("couldn't get namespace: %v", err)
}
if eid, err = intFromLabel(ns.Labels, environmentIDLabel); err != nil {
return 0, 0, "", "", fmt.Errorf("couldn't get environment ID from label: %v", err)
}
if pid, err = intFromLabel(ns.Labels, projectIDLabel); err != nil {
return 0, 0, fmt.Errorf("couldn't get project ID from label: %v", err)
return 0, 0, "", "", fmt.Errorf("couldn't get project ID from label: %v", err)
}
if eid, err = intFromLabel(ns.Labels, environmentIDLabel); err != nil {
return 0, 0, fmt.Errorf("couldn't get environment ID from label: %v", err)
if ename, ok = ns.Labels[environmentNameLabel]; !ok {
return 0, 0, "", "", fmt.Errorf("missing environment name label %v",
environmentNameLabel)
}
if pname, ok = ns.Labels[projectNameLabel]; !ok {
return 0, 0, "", "", fmt.Errorf("missing project name label %v", projectNameLabel)
}
return pid, eid, nil
return eid, pid, ename, pname, nil
}
2 changes: 2 additions & 0 deletions internal/sshportalapi/server.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package sshportalapi implements the lagoon-core component of the ssh-portal
// service.
package sshportalapi

import (
Expand Down
34 changes: 23 additions & 11 deletions internal/sshportalapi/sshportal.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn,
if err = c.Publish(replySubject, false); err != nil {
log.Error("couldn't publish reply",
zap.Any("query", query),
zap.Bool("reply value", false),
zap.Bool("reply", false),
zap.Error(err))
}
return
Expand All @@ -77,7 +77,7 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn,
if err = c.Publish(replySubject, false); err != nil {
log.Error("couldn't publish reply",
zap.Any("query", query),
zap.Bool("reply value", false),
zap.Bool("reply", false),
zap.Error(err))
}
return
Expand All @@ -91,8 +91,8 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn,
if err = c.Publish(replySubject, false); err != nil {
log.Error("couldn't publish reply",
zap.Any("query", query),
zap.Bool("reply value", false),
zap.String("user UUID", user.UUID.String()),
zap.Bool("reply", false),
zap.String("userUUID", user.UUID.String()),
zap.Error(err))
}
return
Expand All @@ -107,23 +107,35 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn,
if err != nil {
log.Error("couldn't query user roles and groups",
zap.Any("query", query),
zap.String("user UUID", user.UUID.String()),
zap.String("userUUID", user.UUID.String()),
zap.Error(err))
return
}
log.Debug("keycloak query response",
zap.Strings("realm roles", realmRoles),
zap.Strings("user groups", userGroups),
zap.Any("group project IDs", groupProjectIDs),
zap.String("user UUID", user.UUID.String()))
zap.Strings("realmRoles", realmRoles),
zap.Strings("userGroups", userGroups),
zap.Any("groupProjectIDs", groupProjectIDs),
zap.String("userUUID", user.UUID.String()))
// calculate permission
ok := permission.UserCanSSHToEnvironment(ctx, env, realmRoles, userGroups,
groupProjectIDs)
if ok {
log.Info("validated SSH access",
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()),
)
}
if err = c.Publish(replySubject, ok); err != nil {
log.Error("couldn't publish reply",
zap.Any("query", query),
zap.Bool("reply value", ok),
zap.String("user UUID", user.UUID.String()),
zap.Bool("reply", ok),
zap.String("userUUID", user.UUID.String()),
zap.Error(err))
}
}
Expand Down
25 changes: 20 additions & 5 deletions internal/sshserver/authhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import (
gossh "golang.org/x/crypto/ssh"
)

type ctxKey int

const (
environmentIDKey ctxKey = iota
environmentNameKey
projectIDKey
projectNameKey
sshFingerprint
)

var (
natsTimeout = 8 * time.Second
)
Expand All @@ -38,15 +48,15 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn,
pubKey, err := gossh.ParsePublicKey(key.Marshal())
if err != nil {
log.Warn("couldn't parse SSH public key",
zap.String("session-id", ctx.SessionID()),
zap.String("sessionID", ctx.SessionID()),
zap.Error(err))
return false
}
// get Lagoon labels from namespace if available
pid, eid, err := c.NamespaceDetails(ctx, ctx.User())
eid, pid, ename, pname, err := c.NamespaceDetails(ctx, ctx.User())
if err != nil {
log.Debug("couldn't get namespace details",
zap.String("session-id", ctx.SessionID()),
zap.String("sessionID", ctx.SessionID()),
zap.String("namespace", ctx.User()), zap.Error(err))
return false
}
Expand All @@ -65,15 +75,20 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn,
natsTimeout)
if err != nil {
log.Warn("couldn't make NATS request",
zap.String("session-id", ctx.SessionID()),
zap.String("sessionID", ctx.SessionID()),
zap.Error(err))
return false
}
// handle response
if response {
authSuccessTotal.Inc()
ctx.SetValue(environmentIDKey, eid)
ctx.SetValue(environmentNameKey, ename)
ctx.SetValue(projectIDKey, pid)
ctx.SetValue(projectNameKey, pname)
ctx.SetValue(sshFingerprint, fingerprint)
log.Debug("authentication successful",
zap.String("session-id", ctx.SessionID()),
zap.String("sessionID", ctx.SessionID()),
zap.String("fingerprint", fingerprint),
zap.String("namespace", ctx.User()))
return true
Expand Down
1 change: 1 addition & 0 deletions internal/sshserver/serve.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package sshserver is the SSH server component of the Lagoon ssh-portal.
package sshserver

import (
Expand Down
59 changes: 43 additions & 16 deletions internal/sshserver/sessionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler {
}
// start the command
log.Debug("starting command exec",
zap.String("session-id", sid),
zap.Strings("raw command", s.Command()),
zap.String("sessionID", sid),
zap.Strings("rawCommand", s.Command()),
zap.String("subsystem", s.Subsystem()),
)
// parse the command line arguments to extract any service or container args
Expand All @@ -48,27 +48,27 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler {
if err := k8s.ValidateLabelValue(service); err != nil {
log.Debug("invalid service name",
zap.String("service", service),
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
_, err = fmt.Fprintf(s.Stderr(), "invalid service name %s. SID: %s\r\n",
service, sid)
if err != nil {
log.Debug("couldn't write to session stream",
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
}
return
}
if err := k8s.ValidateLabelValue(container); err != nil {
log.Debug("invalid container name",
zap.String("container", container),
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
_, err = fmt.Fprintf(s.Stderr(), "invalid container name %s. SID: %s\r\n",
container, sid)
if err != nil {
log.Debug("couldn't write to session stream",
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
}
return
Expand All @@ -78,42 +78,69 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler {
if err != nil {
log.Debug("couldn't find deployment for service",
zap.String("service", service),
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
_, err = fmt.Fprintf(s.Stderr(), "unknown service %s. SID: %s\r\n",
service, sid)
if err != nil {
log.Debug("couldn't write to session stream",
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
}
return
}
// check if a pty was requested
_, _, pty := s.Pty()
log.Info("executing command",
zap.String("namespace", s.User()),
zap.String("deployment", deployment),
// extract info passed through the context by the authhandler
ctx := s.Context()
eid, ok := ctx.Value(environmentIDKey).(int)
if !ok {
log.Warn("couldn't extract environment ID from session context")
}
ename, ok := ctx.Value(environmentNameKey).(string)
if !ok {
log.Warn("couldn't extract environment name from session context")
}
pid, ok := ctx.Value(projectIDKey).(int)
if !ok {
log.Warn("couldn't extract project ID from session context")
}
pname, ok := ctx.Value(projectNameKey).(string)
if !ok {
log.Warn("couldn't extract project name from session context")
}
fingerprint, ok := ctx.Value(sshFingerprint).(string)
if !ok {
log.Warn("couldn't extract SSH key fingerprint from session context")
}
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),
zap.Bool("pty", pty),
zap.String("session-id", sid),
)
err = c.Exec(s.Context(), s.User(), deployment, container, cmd, s,
s.Stderr(), pty)
if err != nil {
log.Warn("couldn't execute command",
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n",
sid)
if err != nil {
log.Warn("couldn't send error to client",
zap.String("session-id", sid),
zap.String("sessionID", sid),
zap.Error(err))
}
}
log.Debug("finished command exec",
zap.String("session-id", sid))
zap.String("sessionID", sid))
}
}

0 comments on commit ceb0b24

Please sign in to comment.