ssh: improve 'whoami' format (#5714)

Old:
```
User ID:    xxx
Session ID: xxx
Expires at: 2025-07-10 08:39:40.64992461 +0000 UTC
Claims:
  aud: [xxx]
  email: [foo@bar.com]
  email_verified: [true]
  exp: [1.75212238e+09]
  family_name: [bar]
  given_name: [foo]
  iat: [1.75208638e+09]
  iss: [https://example.com]
  name: [Foo Bar]
  nickname: [foobar]
  picture: [https://example.com]
  sub: [xxx]
  updated_at: [2025-07-09T18:12:15.226Z]
```

New:
```
User ID:    xxx
Session ID: xxx
Expires at: 2025-07-10 11:23:27.641004885 +0000 UTC (in 13h59m57s)
Claims:
  aud: "xxx"
  email: "foo@bar.com"
  email_verified: true
  exp: 2025-07-10 07:23:27 +0000 UTC (in 9h59m56s)
  family_name: "bar"
  given_name: "foo"
  iat: 2025-07-09 21:23:27 +0000 UTC (4s ago)
  iss: "https://example.com"
  name: "Foo Bar"
  nickname: "foobar"
  picture: "https://example.com"
  sub: "xxx"
  updated_at: "2025-07-09T18:12:15.226Z"

```
This commit is contained in:
Joe Kralicky 2025-07-10 15:57:07 -04:00 committed by GitHub
parent 88c7a6537a
commit 33abea3ea6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 64 additions and 32 deletions

View file

@ -337,7 +337,7 @@ func (state state) GetUserInfo(users map[string]*User) *userInfo {
for _, u := range users {
if u.Email == state.Email {
userInfo.Subject = u.ID
userInfo.Name = u.FirstName + " " + u.LastName
userInfo.Name = strings.TrimSpace(u.FirstName + " " + u.LastName)
userInfo.FamilyName = u.LastName
userInfo.GivenName = u.FirstName
}

View file

@ -6,7 +6,8 @@ import (
"crypto/sha256"
"encoding/base64"
"errors"
"text/template"
"fmt"
"slices"
"time"
oteltrace "go.opentelemetry.io/otel/trace"
@ -258,9 +259,48 @@ func (a *Auth) FormatSession(ctx context.Context, info StreamAuthInfo) ([]byte,
return nil, err
}
var b bytes.Buffer
err = sessionInfoTmpl.Execute(&b, session)
if err != nil {
return nil, err
fmt.Fprintf(&b, "User ID: %s\n", session.UserId)
fmt.Fprintf(&b, "Session ID: %s\n", sessionID)
fmt.Fprintf(&b, "Expires at: %s (in %s)\n",
session.ExpiresAt.AsTime().String(),
time.Until(session.ExpiresAt.AsTime()).Round(time.Second))
fmt.Fprintf(&b, "Claims:\n")
keys := make([]string, 0, len(session.Claims))
for key := range session.Claims {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
fmt.Fprintf(&b, " %s: ", key)
vs := session.Claims[key].AsSlice()
if len(vs) != 1 {
b.WriteRune('[')
}
if len(vs) == 1 {
switch key {
case "iat":
d, _ := vs[0].(float64)
t := time.Unix(int64(d), 0)
fmt.Fprintf(&b, "%s (%s ago)", t, time.Since(t).Round(time.Second))
case "exp":
d, _ := vs[0].(float64)
t := time.Unix(int64(d), 0)
fmt.Fprintf(&b, "%s (in %s)", t, time.Until(t).Round(time.Second))
default:
fmt.Fprintf(&b, "%#v", vs[0])
}
} else if len(vs) > 1 {
for i, v := range vs {
fmt.Fprintf(&b, "%#v", v)
if i < len(vs)-1 {
b.WriteString(", ")
}
}
}
if len(vs) != 1 {
b.WriteRune(']')
}
b.WriteRune('\n')
}
return b.Bytes(), nil
}
@ -375,13 +415,3 @@ func sshRequestFromStreamAuthInfo(info StreamAuthInfo) (*Request, error) {
LogOnlyIfDenied: info.InitialAuthComplete,
}, nil
}
var sessionInfoTmpl = template.Must(template.New("session-info").Parse(`
User ID: {{.UserId}}
Session ID: {{.Id}}
Expires at: {{.ExpiresAt.AsTime}}
Claims:
{{- range $k, $v := .Claims }}
{{ $k }}: {{ $v.AsSlice }}
{{- end }}
`))

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -361,6 +362,7 @@ func TestFormatSession(t *testing.T) {
assert.ErrorContains(t, err, "invalid public key fingerprint")
})
t.Run("ok", func(t *testing.T) {
exp := time.Now().Add(1 * time.Minute)
client := fakeDataBrokerServiceClient{
get: func(
_ context.Context, in *databroker.GetRequest, _ ...grpc.CallOption,
@ -379,7 +381,7 @@ func TestFormatSession(t *testing.T) {
Data: protoutil.NewAny(&session.Session{
Id: expectedID,
UserId: "USER-ID",
ExpiresAt: &timestamppb.Timestamp{Seconds: 1750965358},
ExpiresAt: timestamppb.New(exp),
Claims: claims.ToPB(),
}),
},
@ -392,14 +394,14 @@ func TestFormatSession(t *testing.T) {
}
b, err := a.FormatSession(t.Context(), info)
assert.NoError(t, err)
assert.Equal(t, string(b), `
assert.Regexp(t, `
User ID: USER-ID
Session ID: sshkey-SHA256:QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoxMjM0NTY
Expires at: 2025-06-26 19:15:58 +0000 UTC
Expires at: .* \(in 1m0s\)
Claims:
foo: [bar baz]
quux: [42]
`)
foo: \["bar", "baz"\]
quux: 42
`[1:], string(b))
})
}

View file

@ -331,18 +331,18 @@ func (s *SSHTestSuite) TestWhoami() {
s.Regexp(s.executeTemplate(`
User ID: .*
Session ID: sshkey-{{.PublicKeyFingerprint | quoteMeta}}
Expires at: .*
Expires at: .* \(in \d+h\d+m\d+s\)
Claims:
aud: \[CLIENT_ID\]
email: \[{{.Email | quoteMeta}}\]
exp: \[.*\]
family_name: \[\]
given_name: \[\]
iat: \[.*\]
iss: \[https://mock-idp\..*\]
name: \[ \]
sub: \[.*\]
`), string(output))
aud: "CLIENT_ID"
email: "{{.Email | quoteMeta}}"
exp: .* \(in \d+h\d+m\d+s\)
family_name: ""
given_name: ""
iat: .* \(\d+s ago\)
iss: "https://mock-idp\..*"
name: ""
sub: ".*"
`[1:]), string(output))
}
func TestSSH(t *testing.T) {