Skip to content
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
100 changes: 89 additions & 11 deletions cmd/auth_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type AuthConnectionCreateInput struct {
SaveCredentials bool
NoSaveCredentials bool
HealthCheckInterval int
NoHealthChecks bool
NoAutoReauth bool
RecordSession BoolFlag
Telemetry string
Output string
}
Expand Down Expand Up @@ -80,6 +83,9 @@ type AuthConnectionUpdateInput struct {
SaveCredentials BoolFlag
HealthCheckInterval int
HealthCheckIntervalSet bool
HealthChecks BoolFlag
AutoReauth BoolFlag
RecordSession BoolFlag
Telemetry string
Output string
}
Expand All @@ -98,11 +104,12 @@ type AuthConnectionDeleteInput struct {
}

type AuthConnectionLoginInput struct {
ID string
ProxyID string
ProxyName string
Telemetry string
Output string
ID string
ProxyID string
ProxyName string
RecordSession BoolFlag
Telemetry string
Output string
}

type AuthConnectionSubmitInput struct {
Expand Down Expand Up @@ -201,6 +208,18 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn
params.ManagedAuthCreateRequest.SaveCredentials = kernel.Opt(false)
}

if in.NoHealthChecks {
params.ManagedAuthCreateRequest.HealthChecks = kernel.Opt(false)
}

if in.NoAutoReauth {
params.ManagedAuthCreateRequest.AutoReauth = kernel.Opt(false)
}

if in.RecordSession.Set {
params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(in.RecordSession.Value)
}

if in.Telemetry != "" {
t, err := buildAuthConnectionCreateTelemetryParam(in.Telemetry)
if err != nil {
Expand Down Expand Up @@ -276,6 +295,18 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn
params.ManagedAuthUpdateRequest.SaveCredentials = kernel.Opt(in.SaveCredentials.Value)
hasChanges = true
}
if in.HealthChecks.Set {
params.ManagedAuthUpdateRequest.HealthChecks = kernel.Opt(in.HealthChecks.Value)
hasChanges = true
}
if in.AutoReauth.Set {
params.ManagedAuthUpdateRequest.AutoReauth = kernel.Opt(in.AutoReauth.Value)
hasChanges = true
}
if in.RecordSession.Set {
params.ManagedAuthUpdateRequest.RecordSession = kernel.Opt(in.RecordSession.Value)
hasChanges = true
}
if in.AllowedDomainsSet {
params.ManagedAuthUpdateRequest.AllowedDomains = in.AllowedDomains
hasChanges = true
Expand Down Expand Up @@ -620,6 +651,10 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu
}
}

if in.RecordSession.Set {
params.RecordSession = kernel.Opt(in.RecordSession.Value)
}

if in.Telemetry != "" {
t, err := buildAuthConnectionLoginTelemetryParam(in.Telemetry)
if err != nil {
Expand Down Expand Up @@ -839,7 +874,7 @@ func (c AuthConnectionCmd) Timeline(ctx context.Context, in AuthConnectionTimeli
return nil
}

tableData := pterm.TableData{{"Timestamp", "Type", "Status", "Step", "Browser Session", "Details"}}
tableData := pterm.TableData{{"Timestamp", "Type", "Status", "Step", "Browser Session", "Telemetry", "Details"}}
for _, e := range events {
details := e.ErrorMessage
if details == "" {
Expand All @@ -848,12 +883,20 @@ func (c AuthConnectionCmd) Timeline(ctx context.Context, in AuthConnectionTimeli
if details == "" && e.PreviousStatus != "" {
details = fmt.Sprintf("%s -> %s", e.PreviousStatus, e.Status)
}
// Telemetry only means something when the event has a browser session
// whose events `kernel browsers telemetry` could fetch, and when the API
// actually reported the field.
telemetry := "-"
if e.BrowserSessionID != "" && e.JSON.TelemetryCaptured.Valid() {
telemetry = lo.Ternary(e.TelemetryCaptured, "yes", "no")
}
tableData = append(tableData, []string{
util.FormatLocal(e.Timestamp),
string(e.Type),
string(e.Status),
string(e.Step),
util.OrDash(e.BrowserSessionID),
telemetry,
util.OrDash(details),
})
}
Expand Down Expand Up @@ -1050,6 +1093,9 @@ func init() {
authConnectionsCreateCmd.Flags().String("proxy-name", "", "Proxy name to use")
authConnectionsCreateCmd.Flags().Bool("no-save-credentials", false, "Disable saving credentials after successful login")
authConnectionsCreateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks (300-86400)")
authConnectionsCreateCmd.Flags().Bool("no-health-checks", false, "Disable periodic health checks (enabled by default)")
authConnectionsCreateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication (auto re-auth is enabled by default)")
authConnectionsCreateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default (useful for debugging)")
authConnectionsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for this connection's browser sessions (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
_ = authConnectionsCreateCmd.MarkFlagRequired("domain")
_ = authConnectionsCreateCmd.MarkFlagRequired("profile-name")
Expand All @@ -1071,9 +1117,16 @@ func init() {
authConnectionsUpdateCmd.Flags().Bool("save-credentials", false, "Enable saving credentials after successful login")
authConnectionsUpdateCmd.Flags().Bool("no-save-credentials", false, "Disable saving credentials after successful login")
authConnectionsUpdateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks")
authConnectionsUpdateCmd.Flags().Bool("health-checks", false, "Enable periodic health checks")
authConnectionsUpdateCmd.Flags().Bool("no-health-checks", false, "Disable periodic health checks")
authConnectionsUpdateCmd.Flags().Bool("auto-reauth", false, "Permit automatic re-authentication when a health check detects an expired session")
authConnectionsUpdateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication")
authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Set whether browser sessions are recorded by default; use --record-session=false to disable")
authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)")
authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider")
authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials")
authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks")
authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("auto-reauth", "no-auto-reauth")

// List flags
addJSONOutputFlag(authConnectionsListCmd)
Expand All @@ -1089,6 +1142,7 @@ func init() {
addJSONOutputFlag(authConnectionsLoginCmd)
authConnectionsLoginCmd.Flags().String("proxy-id", "", "Proxy ID to use for this login")
authConnectionsLoginCmd.Flags().String("proxy-name", "", "Proxy name to use for this login")
authConnectionsLoginCmd.Flags().Bool("record-session", false, "Override whether this login's browser session is recorded; use --record-session=false to disable")
authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network")

// Submit flags
Expand Down Expand Up @@ -1139,6 +1193,8 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error {
proxyName, _ := cmd.Flags().GetString("proxy-name")
noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials")
healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval")
noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks")
noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth")
telemetry, _ := cmd.Flags().GetString("telemetry")

svc := client.Auth.Connections
Expand All @@ -1156,6 +1212,9 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error {
ProxyName: proxyName,
NoSaveCredentials: noSaveCredentials,
HealthCheckInterval: healthCheckInterval,
NoHealthChecks: noHealthChecks,
NoAutoReauth: noAutoReauth,
RecordSession: readBoolFlag(cmd.Flags(), "record-session"),
Telemetry: telemetry,
Output: output,
})
Expand Down Expand Up @@ -1190,13 +1249,28 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error {
telemetry, _ := cmd.Flags().GetString("telemetry")

saveCredentialsFlag := BoolFlag{}

if cmd.Flags().Changed("save-credentials") {
saveCredentialsFlag = BoolFlag{Set: true, Value: saveCredentials}
}
if cmd.Flags().Changed("no-save-credentials") {
saveCredentialsFlag = BoolFlag{Set: true, Value: !noSaveCredentials}
}

// Each of these is a tri-state override: --x sets true, --no-x sets false, and
// omitting both leaves the connection's current value untouched.
togglePair := func(onFlag, offFlag string) BoolFlag {
if cmd.Flags().Changed(onFlag) {
v, _ := cmd.Flags().GetBool(onFlag)
return BoolFlag{Set: true, Value: v}
}
if cmd.Flags().Changed(offFlag) {
v, _ := cmd.Flags().GetBool(offFlag)
return BoolFlag{Set: true, Value: !v}
}
return BoolFlag{}
}

svc := client.Auth.Connections
c := AuthConnectionCmd{svc: &svc}
return c.Update(cmd.Context(), AuthConnectionUpdateInput{
Expand All @@ -1219,6 +1293,9 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error {
SaveCredentials: saveCredentialsFlag,
HealthCheckInterval: healthCheckInterval,
HealthCheckIntervalSet: cmd.Flags().Changed("health-check-interval"),
HealthChecks: togglePair("health-checks", "no-health-checks"),
AutoReauth: togglePair("auto-reauth", "no-auto-reauth"),
RecordSession: readBoolFlag(cmd.Flags(), "record-session"),
Telemetry: telemetry,
Output: output,
})
Expand Down Expand Up @@ -1265,11 +1342,12 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error {
svc := client.Auth.Connections
c := AuthConnectionCmd{svc: &svc}
return c.Login(cmd.Context(), AuthConnectionLoginInput{
ID: args[0],
ProxyID: proxyID,
ProxyName: proxyName,
Telemetry: telemetry,
Output: output,
ID: args[0],
ProxyID: proxyID,
ProxyName: proxyName,
RecordSession: readBoolFlag(cmd.Flags(), "record-session"),
Telemetry: telemetry,
Output: output,
})
}

Expand Down
89 changes: 88 additions & 1 deletion cmd/auth_connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,42 @@ func TestAuthConnectionsCreate_CredentialName_UnaffectedByAutoDefault(t *testing
assert.False(t, cred.Path.Valid())
}

func TestAuthConnectionsCreate_RecordSession(t *testing.T) {
tests := []struct {
name string
flag BoolFlag
}{
{name: "omitted"},
{name: "enabled", flag: BoolFlag{Set: true, Value: true}},
{name: "disabled", flag: BoolFlag{Set: true, Value: false}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured kernel.AuthConnectionNewParams
fake := &FakeAuthConnectionService{
NewFunc: func(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
captured = body
return &kernel.ManagedAuth{ID: "conn-new"}, nil
},
}

c := AuthConnectionCmd{svc: fake}
require.NoError(t, c.Create(context.Background(), AuthConnectionCreateInput{
Domain: "example.com",
ProfileName: "profile-1",
RecordSession: tt.flag,
Output: "json",
}))

assert.Equal(t, tt.flag.Set, captured.ManagedAuthCreateRequest.RecordSession.Valid())
if tt.flag.Set {
assert.Equal(t, tt.flag.Value, captured.ManagedAuthCreateRequest.RecordSession.Value)
}
})
}
}

func TestAuthConnectionsUpdate_MapsParams(t *testing.T) {
var captured kernel.AuthConnectionUpdateParams
fake := &FakeAuthConnectionService{
Expand Down Expand Up @@ -358,6 +394,7 @@ func TestAuthConnectionsUpdate_MapsParams(t *testing.T) {
ProxyID: "proxy-123",
ProxyIDSet: true,
SaveCredentials: BoolFlag{Set: true, Value: false},
RecordSession: BoolFlag{Set: true, Value: false},
HealthCheckInterval: 900,
HealthCheckIntervalSet: true,
})
Expand All @@ -375,10 +412,47 @@ func TestAuthConnectionsUpdate_MapsParams(t *testing.T) {
assert.Equal(t, "proxy-123", captured.ManagedAuthUpdateRequest.Proxy.ID.Value)
require.True(t, captured.ManagedAuthUpdateRequest.SaveCredentials.Valid())
assert.False(t, captured.ManagedAuthUpdateRequest.SaveCredentials.Value)
require.True(t, captured.ManagedAuthUpdateRequest.RecordSession.Valid())
assert.False(t, captured.ManagedAuthUpdateRequest.RecordSession.Value)
require.True(t, captured.ManagedAuthUpdateRequest.HealthCheckInterval.Valid())
assert.Equal(t, int64(900), captured.ManagedAuthUpdateRequest.HealthCheckInterval.Value)
}

func TestAuthConnectionsLogin_RecordSession(t *testing.T) {
tests := []struct {
name string
flag BoolFlag
}{
{name: "inherits connection default"},
{name: "enabled", flag: BoolFlag{Set: true, Value: true}},
{name: "disabled", flag: BoolFlag{Set: true, Value: false}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured kernel.AuthConnectionLoginParams
fake := &FakeAuthConnectionService{
LoginFunc: func(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error) {
captured = body
return &kernel.LoginResponse{}, nil
},
}

c := AuthConnectionCmd{svc: fake}
require.NoError(t, c.Login(context.Background(), AuthConnectionLoginInput{
ID: "conn-1",
RecordSession: tt.flag,
Output: "json",
}))

assert.Equal(t, tt.flag.Set, captured.RecordSession.Valid())
if tt.flag.Set {
assert.Equal(t, tt.flag.Value, captured.RecordSession.Value)
}
})
}
}

func newFakeWithMfaOptions(options []kernel.ManagedAuthMfaOption) *FakeAuthConnectionService {
return &FakeAuthConnectionService{
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
Expand Down Expand Up @@ -690,13 +764,23 @@ func TestSubmit_CanonicalAndLegacyAreMutuallyExclusive(t *testing.T) {
func TestTimeline_RendersEventsAndPagination(t *testing.T) {
buf := capturePtermOutput(t)
var captured kernel.AuthConnectionTimelineParams
// Decoded from JSON so the telemetry_captured field registers as present;
// the CLI distinguishes "reported false" from "not reported at all".
var loginEvent kernel.ManagedAuthTimelineEvent
require.NoError(t, json.Unmarshal([]byte(`{
"id": "e1",
"type": "login",
"status": "SUCCESS",
"browser_session_id": "browser_1",
"telemetry_captured": true
}`), &loginEvent))
fake := &FakeAuthConnectionService{
TimelineFunc: func(ctx context.Context, id string, query kernel.AuthConnectionTimelineParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuthTimelineEvent], error) {
captured = query
// Return perPage+1 events so the CLI reports another page exists.
return &pagination.OffsetPagination[kernel.ManagedAuthTimelineEvent]{
Items: []kernel.ManagedAuthTimelineEvent{
{ID: "e1", Type: "login", Status: "SUCCESS", BrowserSessionID: "browser_1"},
loginEvent,
{ID: "e2", Type: "reauth", Status: "FAILED", ErrorMessage: "boom"},
{ID: "e3", Type: "health_check", Status: "SUCCESS"},
},
Expand All @@ -714,6 +798,9 @@ func TestTimeline_RendersEventsAndPagination(t *testing.T) {
out := buf.String()
assert.Contains(t, out, "browser_1")
assert.Contains(t, out, "boom")
// Telemetry capture is reported for events that have a browser session.
assert.Contains(t, out, "Telemetry")
assert.Regexp(t, `browser_1.*yes`, out)
// The third event is truncated off the page.
assert.NotContains(t, out, "health_check")
assert.Contains(t, out, "Has more: yes")
Expand Down
8 changes: 7 additions & 1 deletion cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,7 @@ type BrowsersReplaysStartInput struct {
Identifier string
Framerate int
MaxDurationSeconds int
RecordAudio bool
Output string
}

Expand Down Expand Up @@ -1395,6 +1396,9 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn
if in.MaxDurationSeconds > 0 {
body.MaxDurationInSeconds = kernel.Opt(int64(in.MaxDurationSeconds))
}
if in.RecordAudio {
body.RecordAudio = kernel.Opt(true)
}
res, err := b.replays.Start(ctx, br.SessionID, body)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand Down Expand Up @@ -2536,6 +2540,7 @@ func init() {
replaysStart := &cobra.Command{Use: "start <id>", Short: "Start a replay recording", Args: cobra.ExactArgs(1), RunE: runBrowsersReplaysStart}
replaysStart.Flags().Int("framerate", 0, "Recording framerate (fps)")
replaysStart.Flags().Int("max-duration", 0, "Maximum duration in seconds")
replaysStart.Flags().Bool("record-audio", false, "Record audio in addition to video (default is video-only)")
addJSONOutputFlag(replaysStart)
replaysStop := &cobra.Command{Use: "stop <id> <replay-id>", Short: "Stop a replay recording", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysStop}
replaysDownload := &cobra.Command{Use: "download <id> <replay-id>", Short: "Download a replay video", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysDownload}
Expand Down Expand Up @@ -3141,9 +3146,10 @@ func runBrowsersReplaysStart(cmd *cobra.Command, args []string) error {
svc := client.Browsers
fr, _ := cmd.Flags().GetInt("framerate")
md, _ := cmd.Flags().GetInt("max-duration")
recordAudio, _ := cmd.Flags().GetBool("record-audio")
output, _ := cmd.Flags().GetString("output")
b := BrowsersCmd{browsers: &svc, replays: &svc.Replays}
return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md, Output: output})
return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md, RecordAudio: recordAudio, Output: output})
}

func runBrowsersReplaysStop(cmd *cobra.Command, args []string) error {
Expand Down
Loading
Loading