diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index f553dfbcda955..741f4626c957c 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -82,6 +82,7 @@ import ( apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/types/installers" "github.com/gravitational/teleport/api/types/wrappers" + apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/auth/accessmonitoringrules/accessmonitoringrulesv1" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/autoupdate/autoupdatev1" @@ -1977,11 +1978,58 @@ func (g *GRPCServer) DeleteAllKubernetesServers(ctx context.Context, req *authpb // version for some features of the role returns a shallow copy of the given // role downgraded for compatibility with the older version. func maybeDowngradeRole(ctx context.Context, role *types.RoleV6) (*types.RoleV6, error) { - // Teleport 16 supports all role features that Teleport 15 does, - // so no downgrade is necessary. + clientVersionString, ok := metadata.ClientVersionFromContext(ctx) + if !ok { + // This client is not reporting its version via gRPC metadata. Teleport + // clients have been reporting their version for long enough that older + // clients won't even support v6 roles at all, so this is likely a + // third-party client, and we shouldn't assume that downgrading the role + // will do more good than harm. + return role, nil + } + + clientVersion, err := semver.NewVersion(clientVersionString) + if err != nil { + return nil, trace.BadParameter("unrecognized client version: %s is not a valid semver", clientVersionString) + } + + role = maybeDowngradeRoleSSHPortForwarding(role, clientVersion) return role, nil } +var minSupportedSSHPortForwardingVersions = map[int64]semver.Version{ + 17: {Major: 17, Minor: 1, Patch: 0}, +} + +func maybeDowngradeRoleSSHPortForwarding(role *types.RoleV6, clientVersion *semver.Version) *types.RoleV6 { + sshPortForwarding := role.GetOptions().SSHPortForwarding + if sshPortForwarding == nil || (sshPortForwarding.Remote == nil && sshPortForwarding.Local == nil) { + return role + } + + minSupportedVersion, ok := minSupportedSSHPortForwardingVersions[clientVersion.Major] + if ok { + if supported, err := utils.MinVerWithoutPreRelease(clientVersion.String(), minSupportedVersion.String()); supported || err != nil { + return role + } + } + + role = apiutils.CloneProtoMsg(role) + options := role.GetOptions() + + //nolint:staticcheck // this field is preserved for backwards compatibility + options.PortForwarding = types.NewBoolOption(services.RoleSet{role}.CanPortForward()) + role.SetOptions(options) + reason := fmt.Sprintf(`Client version %q does not support granular SSH port forwarding. Role %q will be downgraded `+ + `to simple port forwarding rules instead. In order to support granular SSH port forwarding, all clients must be `+ + `updated to version %q or higher.`, clientVersion, role.GetName(), minSupportedVersion) + if role.Metadata.Labels == nil { + role.Metadata.Labels = make(map[string]string, 1) + } + role.Metadata.Labels[types.TeleportDowngradedLabel] = reason + return role +} + // GetRole retrieves a role by name. func (g *GRPCServer) GetRole(ctx context.Context, req *authpb.GetRoleRequest) (*types.RoleV6, error) { auth, err := g.authenticate(ctx) diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go index ed8e61bd00a85..8a91f952e001e 100644 --- a/lib/auth/grpcserver_test.go +++ b/lib/auth/grpcserver_test.go @@ -57,6 +57,7 @@ import ( clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1" mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" "github.com/gravitational/teleport/api/internalutils/stream" + "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/observability/tracing" "github.com/gravitational/teleport/api/types" @@ -4604,6 +4605,255 @@ func TestGRPCServer_GetInstallers(t *testing.T) { } } +func TestRoleVersions(t *testing.T) { + t.Parallel() + srv := newTestTLSServer(t) + + newRole := func(name string, version string, spec types.RoleSpecV6) types.Role { + role, err := types.NewRoleWithVersion(name, version, spec) + meta := role.GetMetadata() + role.SetMetadata(meta) + require.NoError(t, err) + return role + } + + enabledRole := newRole("test_role_enabled", types.V7, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindRole, services.RW()), + }, + }, + Options: types.RoleOptions{ + SSHPortForwarding: &types.SSHPortForwarding{ + Remote: &types.SSHRemotePortForwarding{ + Enabled: types.NewBoolOption(true), + }, + Local: &types.SSHLocalPortForwarding{ + Enabled: types.NewBoolOption(true), + }, + }, + }, + }) + + disabledRole := newRole("test_role_disabled", types.V7, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindRole, services.RW()), + }, + }, + Options: types.RoleOptions{ + SSHPortForwarding: &types.SSHPortForwarding{ + Remote: &types.SSHRemotePortForwarding{ + Enabled: types.NewBoolOption(false), + }, + Local: &types.SSHLocalPortForwarding{ + Enabled: types.NewBoolOption(false), + }, + }, + }, + }) + + undefinedRole := newRole("test_role_implicit", types.V7, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindRole, services.RW()), + }, + }, + }) + + user, err := CreateUser(context.Background(), srv.Auth(), "user", enabledRole, disabledRole, undefinedRole) + require.NoError(t, err) + + client, err := srv.NewClient(TestUser(user.GetName())) + require.NoError(t, err) + + for _, tc := range []struct { + desc string + clientVersions []string + expectError bool + inputRole types.Role + expectedRole types.Role + expectDowngraded bool + }{ + { + desc: "up to date - enabled", + clientVersions: []string{ + "17.1.0", "17.1.0-dev", "", + }, + inputRole: enabledRole, + expectedRole: enabledRole, + }, + { + desc: "up to date - disabled", + clientVersions: []string{ + "17.1.0", "17.1.0-dev", "", + }, + inputRole: disabledRole, + expectedRole: disabledRole, + }, + { + desc: "up to date - undefined", + clientVersions: []string{ + "17.1.0", "17.1.0-dev", "", + }, + inputRole: undefinedRole, + expectedRole: undefinedRole, + }, + { + desc: "downgrade SSH access control granularity - enabled", + clientVersions: []string{ + "17.0.0", + }, + inputRole: enabledRole, + expectedRole: newRole(enabledRole.GetName(), types.V7, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindRole, services.RW()), + }, + }, + Options: types.RoleOptions{ + PortForwarding: types.NewBoolOption(true), + SSHPortForwarding: enabledRole.GetOptions().SSHPortForwarding, + }, + }), + expectDowngraded: true, + }, + { + desc: "downgrade SSH access control granularity - disabled", + clientVersions: []string{ + "17.0.0", + }, + inputRole: disabledRole, + expectedRole: newRole(disabledRole.GetName(), types.V7, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindRole, services.RW()), + }, + }, + Options: types.RoleOptions{ + PortForwarding: types.NewBoolOption(false), + SSHPortForwarding: disabledRole.GetOptions().SSHPortForwarding, + }, + }), + expectDowngraded: true, + }, + { + desc: "downgrade SSH access control granularity - undefined", + clientVersions: []string{ + "17.0.0", + }, + inputRole: undefinedRole, + expectedRole: undefinedRole, + expectDowngraded: false, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + for _, clientVersion := range tc.clientVersions { + t.Run(clientVersion, func(t *testing.T) { + // setup client metadata + ctx := context.Background() + if clientVersion == "" { + ctx = context.WithValue(ctx, metadata.DisableInterceptors{}, struct{}{}) + } else { + ctx = metadata.AddMetadataToContext(ctx, map[string]string{ + metadata.VersionKey: clientVersion, + }) + } + + checkRole := func(gotRole types.Role) { + t.Helper() + if tc.expectError { + return + } + require.Empty(t, cmp.Diff(tc.expectedRole, gotRole, + cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Labels"))) + // The downgraded label value won't match exactly because it + // includes the client version, so just check it's not empty + // and ignore it in the role diff. + if tc.expectDowngraded { + require.NotEmpty(t, gotRole.GetMetadata().Labels[types.TeleportDowngradedLabel]) + } + } + checkErr := func(err error) { + t.Helper() + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + + // Test GetRole + gotRole, err := client.GetRole(ctx, tc.inputRole.GetName()) + checkErr(err) + checkRole(gotRole) + + // Test GetRoles + gotRoles, err := client.GetRoles(ctx) + checkErr(err) + if !tc.expectError { + foundTestRole := false + for _, gotRole := range gotRoles { + if gotRole.GetName() != tc.inputRole.GetName() { + continue + } + checkRole(gotRole) + foundTestRole = true + break + } + require.True(t, foundTestRole, "GetRoles result does not include expected role") + } + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // Test WatchEvents + watcher, err := client.NewWatcher(ctx, types.Watch{Name: "roles", Kinds: []types.WatchKind{{Kind: types.KindRole}}}) + require.NoError(t, err) + defer watcher.Close() + + // Swallow the init event + e := <-watcher.Events() + require.Equal(t, types.OpInit, e.Type) + + // Re-upsert the role so that the watcher sees it, do this + // on the auth server directly to avoid the + // TeleportDowngradedLabel check in ServerWithRoles + tc.inputRole, err = srv.Auth().UpsertRole(ctx, tc.inputRole) + require.NoError(t, err) + + gotRole, err = func() (types.Role, error) { + for { + select { + case <-watcher.Done(): + return nil, watcher.Error() + case e := <-watcher.Events(): + if gotRole, ok := e.Resource.(types.Role); ok && gotRole.GetName() == tc.inputRole.GetName() { + return gotRole, nil + } + } + } + }() + checkErr(err) + checkRole(gotRole) + + if !tc.expectError { + // Try to re-upsert the role we got. If it was + // downgraded, it should be rejected due to the + // TeleportDowngradedLabel + _, err = client.UpsertRole(ctx, gotRole) + if tc.expectDowngraded { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + }) + } + }) + } +} + func TestUpsertApplicationServerOrigin(t *testing.T) { t.Parallel() diff --git a/lib/autoupdate/rollout/strategy_timebased.go b/lib/autoupdate/rollout/strategy_timebased.go new file mode 100644 index 0000000000000..c5abc34be5588 --- /dev/null +++ b/lib/autoupdate/rollout/strategy_timebased.go @@ -0,0 +1,110 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package rollout + +import ( + "context" + "log/slog" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + + "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1" + update "github.com/gravitational/teleport/api/types/autoupdate" +) + +const ( + updateReasonInWindow = "in_window" + updateReasonOutsideWindow = "outside_window" +) + +type timeBasedStrategy struct { + log *slog.Logger + clock clockwork.Clock +} + +func (h *timeBasedStrategy) name() string { + return update.AgentsStrategyTimeBased +} + +func newTimeBasedStrategy(log *slog.Logger, clock clockwork.Clock) (rolloutStrategy, error) { + if log == nil { + return nil, trace.BadParameter("missing log") + } + if clock == nil { + return nil, trace.BadParameter("missing clock") + } + return &timeBasedStrategy{ + log: log.With("strategy", update.AgentsStrategyTimeBased), + clock: clock, + }, nil +} + +func (h *timeBasedStrategy) progressRollout(ctx context.Context, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error { + now := h.clock.Now() + // We always process every group regardless of the order. + var errs []error + for _, group := range groups { + switch group.State { + case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE: + // We start any group unstarted group in window. + // Done groups can transition back to active if they enter their maintenance window again. + // Some agents might have missed the previous windows and might expected to try again. + shouldBeActive, err := inWindow(group, now) + if err != nil { + // In time-based rollouts, groups are not dependent. + // Failing to transition a group should affect other groups. + // We reflect that something went wrong in the status and go to the next group. + setGroupState(group, group.State, updateReasonReconcilerError, now) + errs = append(errs, err) + continue + } + if shouldBeActive { + setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now) + } else { + setGroupState(group, group.State, updateReasonOutsideWindow, now) + } + case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK: + // We don't touch any group that was manually rolled back. + // Something happened and we should not try to update again. + case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE: + // The group is currently being updated. We check if the maintenance + // is over and if we should transition it to the done state + shouldBeActive, err := inWindow(group, now) + if err != nil { + // In time-based rollouts, groups are not dependent. + // Failing to transition a group should affect other groups. + // We reflect that something went wrong in the status and go to the next group. + setGroupState(group, group.State, updateReasonReconcilerError, now) + errs = append(errs, err) + continue + } + + if shouldBeActive { + setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now) + } else { + setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, updateReasonOutsideWindow, now) + } + default: + return trace.BadParameter("unknown autoupdate group state: %v", group.State) + } + } + return trace.NewAggregate(errs...) +} diff --git a/lib/autoupdate/rollout/strategy_timebased_test.go b/lib/autoupdate/rollout/strategy_timebased_test.go new file mode 100644 index 0000000000000..91db29d42e469 --- /dev/null +++ b/lib/autoupdate/rollout/strategy_timebased_test.go @@ -0,0 +1,314 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package rollout + +import ( + "context" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1" + "github.com/gravitational/teleport/lib/utils" +) + +func Test_progressGroupsTimeBased(t *testing.T) { + clock := clockwork.NewFakeClockAt(testSunday) + log := utils.NewSlogLoggerForTests() + strategy, err := newTimeBasedStrategy(log, clock) + require.NoError(t, err) + + groupName := "test-group" + canStartToday := everyWeekday + cannotStartToday := everyWeekdayButSunday + lastUpdate := timestamppb.New(clock.Now().Add(-5 * time.Minute)) + ctx := context.Background() + + tests := []struct { + name string + initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup + }{ + { + name: "unstarted -> unstarted", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: lastUpdate, + LastUpdateReason: updateReasonCreated, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "unstarted -> active", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: lastUpdate, + LastUpdateReason: updateReasonCreated, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: timestamppb.New(clock.Now()), + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "done -> done", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + LastUpdateTime: lastUpdate, + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + LastUpdateTime: lastUpdate, + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "done -> active", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + LastUpdateTime: lastUpdate, + StartTime: lastUpdate, + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: timestamppb.New(clock.Now()), + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "active -> active", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: lastUpdate, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: lastUpdate, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "active -> done", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: lastUpdate, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName, + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + StartTime: lastUpdate, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "rolledback is a dead end", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName + "-in-maintenance", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: groupName + "-out-of-maintenance", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: groupName + "-in-maintenance", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: groupName + "-out-of-maintenance", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + { + name: "mix of everything", + initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: "new group should start", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED, + LastUpdateTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "done group should start", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + LastUpdateTime: lastUpdate, + StartTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "rolledback group should do nothing", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "old group should stop", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + LastUpdateTime: lastUpdate, + StartTime: lastUpdate, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{ + { + Name: "new group should start", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: timestamppb.New(clock.Now()), + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "done group should start", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, + StartTime: timestamppb.New(clock.Now()), + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonInWindow, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "rolledback group should do nothing", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK, + LastUpdateTime: lastUpdate, + ConfigDays: canStartToday, + ConfigStartHour: matchingStartHour, + }, + { + Name: "old group should stop", + State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, + StartTime: lastUpdate, + LastUpdateTime: timestamppb.New(clock.Now()), + LastUpdateReason: updateReasonOutsideWindow, + ConfigDays: cannotStartToday, + ConfigStartHour: matchingStartHour, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := strategy.progressRollout(ctx, tt.initialState) + require.NoError(t, err) + // We use require.Equal instead of Elements match because group order matters. + // It's not super important for time-based, but is crucial for halt-on-error. + // So it's better to be more conservative and validate order never changes for + // both strategies. + require.Equal(t, tt.expectedState, tt.initialState) + }) + } +} diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index ca8ad121e81f9..c7be6dd702016 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -291,6 +291,12 @@ func (c *Client) startRustRDP(ctx context.Context) error { return trace.Wrap(err) } + // [username] need only be valid for the duration of + // C.client_run. It is copied on the Rust side and + // thus can be freed here. + username := C.CString(c.username) + defer C.free(unsafe.Pointer(username)) + // [addr] need only be valid for the duration of // C.client_run. It is copied on the Rust side and // thus can be freed here. @@ -328,6 +334,7 @@ func (c *Client) startRustRDP(ctx context.Context) error { C.CGOConnectParams{ ad: C.bool(c.cfg.AD), nla: C.bool(c.cfg.NLA), + go_username: username, go_addr: addr, go_computer_name: computerName, go_kdc_addr: kdcAddr, diff --git a/lib/srv/desktop/rdp/rdpclient/src/client.rs b/lib/srv/desktop/rdp/rdpclient/src/client.rs index d4e010c8e1fa9..3dae0fc453b59 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/client.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/client.rs @@ -44,6 +44,7 @@ use ironrdp_pdu::input::fast_path::{ }; use ironrdp_pdu::input::mouse::PointerFlags; use ironrdp_pdu::input::{InputEventError, MousePdu}; +use ironrdp_pdu::nego::NegoRequestData; use ironrdp_pdu::rdp::capability_sets::MajorPlatformType; use ironrdp_pdu::rdp::client_info::PerformanceFlags; use ironrdp_pdu::rdp::RdpError; @@ -1442,8 +1443,11 @@ fn create_config(params: &ConnectParams, pin: String) -> Config { platform: MajorPlatformType::UNSPECIFIED, no_server_pointer: false, autologon: true, - request_data: None, pointer_software_rendering: false, + // Send the username in the request cookie, which is sent in the initial connection request. + // The RDP server ignores this value, but load balancers sitting in front of the server + // can use it to implement persistence. + request_data: Some(NegoRequestData::cookie(params.username.clone())), performance_flags: PerformanceFlags::default() | PerformanceFlags::DISABLE_CURSOR_SHADOW // this is required for pointer to work correctly in Windows 2019 | if !params.show_desktop_wallpaper { @@ -1457,6 +1461,7 @@ fn create_config(params: &ConnectParams, pin: String) -> Config { #[derive(Debug)] pub struct ConnectParams { + pub username: String, pub addr: String, pub kdc_addr: Option, pub computer_name: Option, diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 2c79722e73af0..200f1b6a6186a 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -92,6 +92,7 @@ pub unsafe extern "C" fn free_string(ptr: *mut c_char) { pub unsafe extern "C" fn client_run(cgo_handle: CgoHandle, params: CGOConnectParams) -> CGOResult { trace!("client_run"); // Convert from C to Rust types. + let username = from_c_string(params.go_username); let addr = from_c_string(params.go_addr); let cert_der = from_go_array(params.cert_der, params.cert_der_len); let key_der = from_go_array(params.key_der, params.key_der_len); @@ -111,6 +112,7 @@ pub unsafe extern "C" fn client_run(cgo_handle: CgoHandle, params: CGOConnectPar ConnectParams { ad: params.ad, nla: params.nla, + username, addr, computer_name, cert_der, @@ -480,6 +482,7 @@ pub unsafe extern "C" fn client_write_screen_resize( pub struct CGOConnectParams { ad: bool, nla: bool, + go_username: *const c_char, go_addr: *const c_char, go_domain: *const c_char, go_kdc_addr: *const c_char, diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx index 35cfac569c462..a751566765c27 100644 --- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx +++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex, LabelInput, Text } from 'design'; import { IconTooltip } from 'design/Tooltip'; diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx index 426cb1ca81729..39c2c77c0a954 100644 --- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx +++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex, Text } from 'design'; import { IconTooltip } from 'design/Tooltip'; diff --git a/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx b/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx index f33c43f4892b8..d20ea9527bc00 100644 --- a/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx +++ b/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Box, Text } from 'design'; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx b/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx index 510e889ba6c61..46b29b8333b26 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex, Text } from 'design'; import { components, OptionProps } from 'react-select'; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx index f27c721e77914..2d6f89e019c02 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design'; import * as Icon from 'design/Icon'; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx index 88d76f0183fab..55ef62d05b61a 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { MemoryRouter, Link } from 'react-router-dom'; import { Box, ButtonPrimary, ButtonText } from 'design'; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx index aedade1435fe7..b1ea398954d48 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState, useRef } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { components } from 'react-select'; import ReactSelectCreatable from 'react-select/creatable'; import styled from 'styled-components'; @@ -40,7 +40,7 @@ export function SelectReviewers({ () => reviewers.map(r => ({ value: r, label: r, isDisabled: true })) ); - React.useEffect(() => { + useEffect(() => { // When editing reviewers, auto focus on input box. if (editReviewers) { reactSelectRef.current.focus(); diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx index 52482cc5f1274..2e947ab2b8dfd 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { makeEmptyAttempt, makeProcessingAttempt, diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx index 447da3448da94..8870401c429c6 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { ButtonWarning, ButtonSecondary, Flex, Alert } from 'design'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx index 7348178391c47..f755aa1106e31 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { makeSuccessAttempt, makeEmptyAttempt, diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx index 5880a1f5c94ee..3feab1d9a952c 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { makeSuccessAttempt, makeEmptyAttempt, diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx index 9a3e424787378..e6655f64668ac 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React from 'react'; +import { Fragment } from 'react'; import styled from 'styled-components'; import { Alert, @@ -617,7 +617,7 @@ function Reviews({ reviews }: { reviews: AccessRequestReview[] }) { review; return ( - + )} - + ); }); diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx index 40716141f6339..ccdeb3bcd6c1b 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Box, Label } from 'design'; export default function RolesRequested({ roles }: { roles: string[] }) { diff --git a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx index 2159f6309c95e..eb8b9ef0bf86a 100644 --- a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx +++ b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { ButtonPrimary, Text, Box, ButtonIcon, Menu } from 'design'; import { Info } from 'design/Icon'; import { displayDateWithPrefixedTime } from 'design/datetime'; diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx index b2efff7f98c3c..0ce3d0dd52b98 100644 --- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx +++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { AdvancedSearchToggle } from './AdvancedSearchToggle'; diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx index ea37fe2de003d..36a607dbbc5ea 100644 --- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx +++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { Text, Toggle, Link, Flex, H2 } from 'design'; import { P } from 'design/Text/Text'; diff --git a/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx b/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx index d2923a8ae5029..3a320d93dc248 100644 --- a/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx +++ b/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { KeywordHighlight, diff --git a/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx b/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx index b025bb8e0e80e..5e231094578ea 100644 --- a/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx +++ b/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import { Fragment, useEffect, useLayoutEffect, useRef } from 'react'; import styled from 'styled-components'; import { BufferEntry } from 'shared/components/AnimatedTerminal/content'; @@ -125,14 +125,14 @@ function renderLines(lines: BufferEntry[], highlights?: KeywordHighlight[]) { } const result = lines.map(line => ( - + {line.isCommand ? ( ${line.text.length > 0 ? ' ' : ''} ) : null} {formatText(line.text, line.isCommand, highlights)} {line.isCurrent && line.isCommand ? : null}
-
+ )); return result; @@ -193,7 +193,7 @@ function formatText( outer: for (const [index, word] of words.entries()) { if (!isCommand && /(https?:\/\/\S+)/g.test(word)) { result.push( - + {word} {' '} - + ); continue; diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx index 7d87e3963bb0c..52a6de6d1c41d 100644 --- a/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx +++ b/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import ButtonSso from './ButtonSso'; export default { diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx index fb337001d13b8..aefdc8d5360ba 100644 --- a/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx +++ b/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { render } from 'design/utils/testing'; diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.tsx index 9cc4d8c208660..349128a9a1c95 100644 --- a/web/packages/shared/components/ButtonSso/ButtonSso.tsx +++ b/web/packages/shared/components/ButtonSso/ButtonSso.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { forwardRef } from 'react'; +import { forwardRef } from 'react'; import * as Icons from 'design/Icon'; import { ButtonProps, ButtonSecondary } from 'design/Button'; diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx index 23ffd4ff43a3b..6036e570303b3 100644 --- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx +++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import Flex from 'design/Flex'; import { ButtonTextWithAddIcon } from './ButtonTextWithAddIcon'; diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx index 6d88f3c1d26b5..06d24ef84b182 100644 --- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx +++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { render, fireEvent, screen } from 'design/utils/testing'; import { ButtonTextWithAddIcon } from './ButtonTextWithAddIcon'; diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx index d025dbbceacbe..3d65b59944f06 100644 --- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx +++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { ButtonText } from 'design'; import { Add as AddIcon } from 'design/Icon'; diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx index 1a26d46287c49..b982926874c1d 100644 --- a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx +++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { MemoryRouter } from 'react-router'; import { Cluster } from 'teleport/services/clusters'; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx index 62e5f94b36a3a..2cacc5cdf1d90 100644 --- a/web/packages/shared/components/Controls/ViewModeSwitch.tsx +++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { Rows, SquaresFour } from 'design/Icon'; diff --git a/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx b/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx index 9758176f55156..fa206cb57b914 100644 --- a/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx +++ b/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Box, Text } from 'design'; import { Platform } from 'design/platform'; diff --git a/web/packages/shared/components/Editor/Tabs.tsx b/web/packages/shared/components/Editor/Tabs.tsx index 80c43c890e7a5..582e973f78c69 100644 --- a/web/packages/shared/components/Editor/Tabs.tsx +++ b/web/packages/shared/components/Editor/Tabs.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import * as Icons from 'design/Icon'; diff --git a/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx b/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx index fd646a956a7e2..3c940aeb61a0c 100644 --- a/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx +++ b/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import Box from 'design/Box'; import { FieldCheckbox } from '.'; diff --git a/web/packages/shared/components/FieldInput/FieldInput.story.tsx b/web/packages/shared/components/FieldInput/FieldInput.story.tsx index 1cbcef38cc66e..8de1116cf6f31 100644 --- a/web/packages/shared/components/FieldInput/FieldInput.story.tsx +++ b/web/packages/shared/components/FieldInput/FieldInput.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { ButtonPrimary, Text } from 'design'; import { EmailSolid } from 'design/Icon'; diff --git a/web/packages/shared/components/FieldInput/FieldInput.test.tsx b/web/packages/shared/components/FieldInput/FieldInput.test.tsx index 064ca9f612bf9..2440f5587659a 100644 --- a/web/packages/shared/components/FieldInput/FieldInput.test.tsx +++ b/web/packages/shared/components/FieldInput/FieldInput.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { render, fireEvent } from 'design/utils/testing'; diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx index 2f798d4d923d1..565f1769ace84 100644 --- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import Box from 'design/Box'; diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx index 89b191e1e5b2d..4d10c34449f26 100644 --- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx @@ -17,7 +17,7 @@ */ import userEvent from '@testing-library/user-event'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { act, render, screen } from 'design/utils/testing'; diff --git a/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx b/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx index 08d9a384b05d3..2c3f1cd829fbd 100644 --- a/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx +++ b/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { render, fireEvent } from 'design/utils/testing'; diff --git a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx index 13a5ae39089ea..a2f013ad13254 100644 --- a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx +++ b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { render } from 'design/utils/testing'; diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx index bdfc4534ba585..ac63d6eee4373 100644 --- a/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx +++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { ButtonPrimary, Text } from 'design'; import Validation from '../../components/Validation'; diff --git a/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx b/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx index 11de431965aad..fbc7caaed47a4 100644 --- a/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { act, fireEvent, diff --git a/web/packages/shared/components/FileTransfer/FileTransfer.tsx b/web/packages/shared/components/FileTransfer/FileTransfer.tsx index a192461b61447..72d88a8dcc0fc 100644 --- a/web/packages/shared/components/FileTransfer/FileTransfer.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransfer.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { useFileTransferContext } from './FileTransferContextProvider'; import { FileTransferDialogDirection, diff --git a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx index 74c0616f8add0..ccb61201fca83 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex, ButtonIcon, Text } from 'design'; import * as Icons from 'design/Icon'; import { HoverTooltip } from 'design/Tooltip'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx b/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx index 031d4bbb0608c..bf804472cc17d 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { +import { useContext, useState, FC, diff --git a/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx b/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx index 72c33603237b7..d36e79d7ea9f0 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import ConsoleContextProvider from 'teleport/Console/consoleContextProvider'; import ConsoleContext from 'teleport/Console/consoleContext'; import { FileTransferRequest } from 'teleport/Console/DocumentSsh/useFileTransfer'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx b/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx index 16e864ef7a16b..e526e7c945c44 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { ButtonBorder, Box, Flex, Text, Button } from 'design'; import * as Icons from 'design/Icon'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx index be91459bbd712..be88e37eb1c57 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { fireEvent, render, screen } from 'design/utils/testing'; import { DownloadForm } from './DownloadForm'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx index f0b457dbf4447..d428d76f4d09d 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useId, useState } from 'react'; +import { useId, useState } from 'react'; import { Flex, LabelInput } from 'design'; import { ButtonPrimary } from 'design/Button'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx index 9e1c64d6e8b73..ebc4d8fc620cc 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { fireEvent, render, screen } from 'design/utils/testing'; import { TransferredFile } from '../types'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx index 052e9e100a86e..305be469d9bc4 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { TransferredFile } from '../types'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx index f55a27612a0bd..9c048535de51a 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { FC, PropsWithChildren, useEffect } from 'react'; +import { FC, PropsWithChildren, useEffect } from 'react'; import styled from 'styled-components'; import { ButtonIcon, Flex, Text } from 'design'; import { CircleCheck, Cross, Warning } from 'design/Icon'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx index c2a212ea68b4d..b7ae036ad3070 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { FileTransferContainer } from '../FileTransferContainer'; import { diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx index a67f6ac03de16..1d9eea8d995f7 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { ButtonIcon, Flex, Text } from 'design'; import { Cross as CloseIcon } from 'design/Icon'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx index 465387081d940..4493b575717fe 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { fireEvent, render, screen } from 'design/utils/testing'; import { UploadForm } from './UploadForm'; diff --git a/web/packages/shared/components/Highlight/Highlight.story.tsx b/web/packages/shared/components/Highlight/Highlight.story.tsx index 946dc1f512ddf..46a1817087d88 100644 --- a/web/packages/shared/components/Highlight/Highlight.story.tsx +++ b/web/packages/shared/components/Highlight/Highlight.story.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { Flex, Text } from 'design'; diff --git a/web/packages/shared/components/Highlight/Highlight.tsx b/web/packages/shared/components/Highlight/Highlight.tsx index 017c1903729c4..c1a3c73767471 100644 --- a/web/packages/shared/components/Highlight/Highlight.tsx +++ b/web/packages/shared/components/Highlight/Highlight.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { findAll } from 'highlight-words-core'; /** diff --git a/web/packages/shared/components/MenuAction/MenuAction.story.tsx b/web/packages/shared/components/MenuAction/MenuAction.story.tsx index 156193ea6aeac..4b5bbc4a5ed1e 100644 --- a/web/packages/shared/components/MenuAction/MenuAction.story.tsx +++ b/web/packages/shared/components/MenuAction/MenuAction.story.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex } from 'design'; import { Cog } from 'design/Icon'; diff --git a/web/packages/shared/components/MenuAction/MenuAction.test.tsx b/web/packages/shared/components/MenuAction/MenuAction.test.tsx index 184bb434e495a..e376c2ecf98dc 100644 --- a/web/packages/shared/components/MenuAction/MenuAction.test.tsx +++ b/web/packages/shared/components/MenuAction/MenuAction.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { screen } from '@testing-library/react'; import { render, fireEvent } from 'design/utils/testing'; diff --git a/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx b/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx index 585fc04b8b54e..fec647363bb23 100644 --- a/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx +++ b/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { render, fireEvent, screen, waitFor } from 'design/utils/testing'; import { MenuLogin } from './MenuLogin'; diff --git a/web/packages/shared/components/Notification/Notification.story.tsx b/web/packages/shared/components/Notification/Notification.story.tsx index 4df7b2e7ee299..b3b2b1b9fe5ad 100644 --- a/web/packages/shared/components/Notification/Notification.story.tsx +++ b/web/packages/shared/components/Notification/Notification.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Bots } from 'design/Icon'; import Flex from 'design/Flex'; diff --git a/web/packages/shared/components/Search/SearchPagination.tsx b/web/packages/shared/components/Search/SearchPagination.tsx index d4d31851abf7d..f2cdbe89ec7e6 100644 --- a/web/packages/shared/components/Search/SearchPagination.tsx +++ b/web/packages/shared/components/Search/SearchPagination.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import { Flex } from 'design'; import { StyledPanel } from 'design/DataTable/StyledTable'; import { StyledArrowBtn } from 'design/DataTable/Pager/StyledPager'; diff --git a/web/packages/shared/components/Search/SearchPanel.tsx b/web/packages/shared/components/Search/SearchPanel.tsx index e4dec55a30776..4c470b6b790b9 100644 --- a/web/packages/shared/components/Search/SearchPanel.tsx +++ b/web/packages/shared/components/Search/SearchPanel.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import styled from 'styled-components'; import { Flex } from 'design'; import InputSearch from 'design/DataTable/InputSearch'; diff --git a/web/packages/shared/components/Select/Select.tsx b/web/packages/shared/components/Select/Select.tsx index 2f86a5d87a0cf..1beaab055b905 100644 --- a/web/packages/shared/components/Select/Select.tsx +++ b/web/packages/shared/components/Select/Select.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import ReactSelect, { ClearIndicatorProps, DropdownIndicatorProps, diff --git a/web/packages/shared/components/Select/SelectCreatable.story.tsx b/web/packages/shared/components/Select/SelectCreatable.story.tsx index 02bfcba2cc647..e813e6f4496cd 100644 --- a/web/packages/shared/components/Select/SelectCreatable.story.tsx +++ b/web/packages/shared/components/Select/SelectCreatable.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React from 'react'; +import { useState } from 'react'; import { Flex, Box } from 'design'; import { SelectCreatable, Option } from '../Select'; @@ -26,10 +26,10 @@ export default { }; export const Selects = () => { - const [input, setInput] = React.useState(''); - const [inputMulti, setInputMulti] = React.useState(''); - const [selected, setSelected] = React.useState