diff --git a/api/client/events.go b/api/client/events.go index 70b4eb77be0eb..d4910a00073d6 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -25,7 +25,6 @@ import ( kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" accesslistv1conv "github.com/gravitational/teleport/api/types/accesslist/convert/v1" @@ -35,6 +34,8 @@ import ( secreprotsv1conv "github.com/gravitational/teleport/api/types/secreports/convert/v1" "github.com/gravitational/teleport/api/types/userloginstate" userloginstatev1conv "github.com/gravitational/teleport/api/types/userloginstate/convert/v1" + "github.com/gravitational/teleport/api/types/userprovisioning" + userprovisioningv1conv "github.com/gravitational/teleport/api/types/userprovisioning/convert/v1" ) // EventToGRPC converts types.Event to proto.Event. @@ -95,9 +96,9 @@ func EventToGRPC(in types.Event) (*proto.Event, error) { out.Resource = &proto.Event_SPIFFEFederation{ SPIFFEFederation: r, } - case *userprovisioningpb.StaticHostUser: + case *userprovisioning.StaticHostUser: out.Resource = &proto.Event_StaticHostUser{ - StaticHostUser: r, + StaticHostUser: userprovisioningv1conv.ToProto(r), } default: return nil, trace.BadParameter("resource type %T is not supported", r) @@ -540,7 +541,11 @@ func EventFromGRPC(in *proto.Event) (*types.Event, error) { out.Resource = types.Resource153ToLegacy(r) return &out, nil } else if r := in.GetStaticHostUser(); r != nil { - out.Resource = types.Resource153ToLegacy(r) + hostUser, err := userprovisioningv1conv.FromProto(r) + if err != nil { + return nil, trace.Wrap(err) + } + out.Resource = types.Resource153ToLegacy(hostUser) return &out, nil } else { return nil, trace.BadParameter("received unsupported resource %T", in.Resource) diff --git a/api/client/statichostuser/statichostuser.go b/api/client/statichostuser/statichostuser.go index f8100ff9998fc..c3d9b3e2f4b7c 100644 --- a/api/client/statichostuser/statichostuser.go +++ b/api/client/statichostuser/statichostuser.go @@ -20,6 +20,8 @@ import ( "github.com/gravitational/trace" userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" + "github.com/gravitational/teleport/api/types/userprovisioning" + convertv1 "github.com/gravitational/teleport/api/types/userprovisioning/convert/v1" ) // Client is a StaticHostUser client. @@ -35,7 +37,7 @@ func NewClient(grpcClient userprovisioningpb.StaticHostUsersServiceClient) *Clie } // ListStaticHostUsers lists static host users. -func (c *Client) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) { +func (c *Client) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) { resp, err := c.grpcClient.ListStaticHostUsers(ctx, &userprovisioningpb.ListStaticHostUsersRequest{ PageSize: int32(pageSize), PageToken: pageToken, @@ -43,42 +45,66 @@ func (c *Client) ListStaticHostUsers(ctx context.Context, pageSize int, pageToke if err != nil { return nil, "", trace.Wrap(err) } - return resp.Users, resp.NextPageToken, nil + hostUsers := make([]*userprovisioning.StaticHostUser, 0, len(resp.Users)) + for _, hostUserProto := range resp.Users { + hostUser, err := convertv1.FromProto(hostUserProto) + if err != nil { + return nil, "", trace.Wrap(err) + } + hostUsers = append(hostUsers, hostUser) + } + return hostUsers, resp.NextPageToken, nil } // GetStaticHostUser returns a static host user by name. -func (c *Client) GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) { +func (c *Client) GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) { if name == "" { return nil, trace.BadParameter("missing name") } out, err := c.grpcClient.GetStaticHostUser(ctx, &userprovisioningpb.GetStaticHostUserRequest{ Name: name, }) - return out, trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + hostUser, err := convertv1.FromProto(out) + return hostUser, trace.Wrap(err) } // CreateStaticHostUser creates a static host user. -func (c *Client) CreateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (c *Client) CreateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { out, err := c.grpcClient.CreateStaticHostUser(ctx, &userprovisioningpb.CreateStaticHostUserRequest{ - User: in, + User: convertv1.ToProto(in), }) - return out, trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + hostUser, err := convertv1.FromProto(out) + return hostUser, trace.Wrap(err) } // UpdateStaticHostUser updates a static host user. -func (c *Client) UpdateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (c *Client) UpdateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { out, err := c.grpcClient.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{ - User: in, + User: convertv1.ToProto(in), }) - return out, trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + hostUser, err := convertv1.FromProto(out) + return hostUser, trace.Wrap(err) } // UpsertStaticHostUser upserts a static host user. -func (c *Client) UpsertStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (c *Client) UpsertStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { out, err := c.grpcClient.UpsertStaticHostUser(ctx, &userprovisioningpb.UpsertStaticHostUserRequest{ - User: in, + User: convertv1.ToProto(in), }) - return out, trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + hostUser, err := convertv1.FromProto(out) + return hostUser, trace.Wrap(err) } // DeleteStaticHostUser deletes a static host user. Note that this does not diff --git a/api/types/userprovisioning/convert/v1/statichostuser.go b/api/types/userprovisioning/convert/v1/statichostuser.go new file mode 100644 index 0000000000000..8c262dc144335 --- /dev/null +++ b/api/types/userprovisioning/convert/v1/statichostuser.go @@ -0,0 +1,74 @@ +// Copyright 2024 Gravitational, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/gravitational/trace" + + userprovisioningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" +) + +// FromProto converts a v1 static host user into an internal static host user. +func FromProto(msg *userprovisioningv1.StaticHostUser) (*userprovisioning.StaticHostUser, error) { + if msg == nil { + return nil, trace.BadParameter("static host user message is missing") + } + if msg.Spec == nil { + return nil, trace.BadParameter("spec is missing") + } + if msg.Spec.Login == "" { + return nil, trace.BadParameter("login is missing") + } + + labels := make(types.Labels) + if msgLabels := msg.Spec.NodeLabels; msgLabels != nil { + for k, v := range msgLabels.Values { + labels[k] = v.Values + } + } + + u := userprovisioning.NewStaticHostUser(msg.GetMetadata(), userprovisioning.Spec{ + Login: msg.Spec.Login, + Groups: msg.Spec.Groups, + Sudoers: msg.Spec.Sudoers, + Uid: msg.Spec.Uid, + Gid: msg.Spec.Gid, + NodeLabels: labels, + NodeLabelsExpression: msg.Spec.NodeLabelsExpression, + }) + return u, nil +} + +// ToProto converts an internal static host user into a v1 static host user. +func ToProto(hostUser *userprovisioning.StaticHostUser) *userprovisioningv1.StaticHostUser { + u := &userprovisioningv1.StaticHostUser{ + Kind: hostUser.GetKind(), + SubKind: hostUser.GetSubKind(), + Version: hostUser.GetVersion(), + Metadata: hostUser.GetMetadata(), + Spec: &userprovisioningv1.StaticHostUserSpec{ + Login: hostUser.Spec.Login, + Groups: hostUser.Spec.Groups, + Sudoers: hostUser.Spec.Sudoers, + Uid: hostUser.Spec.Uid, + Gid: hostUser.Spec.Gid, + NodeLabels: hostUser.Spec.NodeLabels.ToProto(), + NodeLabelsExpression: hostUser.Spec.NodeLabelsExpression, + }, + } + return u +} diff --git a/api/types/userprovisioning/convert/v1/statichostuser_test.go b/api/types/userprovisioning/convert/v1/statichostuser_test.go new file mode 100644 index 0000000000000..79a2cd50c02c2 --- /dev/null +++ b/api/types/userprovisioning/convert/v1/statichostuser_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 Gravitational, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" +) + +func TestRoundtrip(t *testing.T) { + t.Parallel() + hostUser := newStaticHostUser() + converted, err := FromProto(ToProto(hostUser)) + require.NoError(t, err) + require.Empty(t, cmp.Diff(hostUser, converted, + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}))) +} + +func TestNoPanicOnNilSpec(t *testing.T) { + hostUser := ToProto(newStaticHostUser()) + hostUser.Spec = nil + _, err := FromProto(hostUser) + require.Error(t, err) +} + +func newStaticHostUser() *userprovisioning.StaticHostUser { + return userprovisioning.NewStaticHostUser(&headerv1.Metadata{ + Name: "test-user", + }, userprovisioning.Spec{ + Login: "alice", + Groups: []string{"foo", "bar"}, + Sudoers: []string{"abcd1234"}, + Uid: "1234", + Gid: "5678", + NodeLabels: types.Labels{ + "foo": {"bar"}, + }, + NodeLabelsExpression: "labels['foo'] == labels['bar']", + }) +} diff --git a/api/types/userprovisioning/statichostuser.go b/api/types/userprovisioning/statichostuser.go index e9bab8cbf5d55..dfee91de49551 100644 --- a/api/types/userprovisioning/statichostuser.go +++ b/api/types/userprovisioning/statichostuser.go @@ -20,17 +20,44 @@ package userprovisioning import ( headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" ) +// StaticHostUser is a resource that represents host users that should be +// created on matching nodes. +type StaticHostUser struct { + headerv1.ResourceHeader + // Spec is the static host user spec. + Spec Spec +} + +// Spec is the static host user spec. +type Spec struct { + // Login is the login to create on the node. + Login string `json:"login"` + // Groups is a list of additional groups to add the user to. + Groups []string `json:"groups"` + // Sudoers is a list of sudoer entries to add. + Sudoers []string `json:"sudoers"` + // Uid is the new user's uid. + Uid string `json:"uid"` + // Gid is the new user's gid. + Gid string `json:"gid"` + // NodeLabels is a map of node labels that will create a user from this + // resource. + NodeLabels types.Labels `json:"node_labels"` + // NodeLabelsExpression is a predicate expression to create a user from + // this resource. + NodeLabelsExpression string `json:"node_labels_expression"` +} + // NewStaticHostUser creates a new host user to be applied to matching SSH nodes. -func NewStaticHostUser(name string, spec *userprovisioningpb.StaticHostUserSpec) *userprovisioningpb.StaticHostUser { - return &userprovisioningpb.StaticHostUser{ - Kind: types.KindStaticHostUser, - Version: types.V1, - Metadata: &headerv1.Metadata{ - Name: name, +func NewStaticHostUser(metadata *headerv1.Metadata, spec Spec) *StaticHostUser { + return &StaticHostUser{ + ResourceHeader: headerv1.ResourceHeader{ + Kind: types.KindStaticHostUser, + Version: types.V1, + Metadata: metadata, }, Spec: spec, } diff --git a/lib/auth/authclient/api.go b/lib/auth/authclient/api.go index b4a3fd3479e65..d3486c63eab49 100644 --- a/lib/auth/authclient/api.go +++ b/lib/auth/authclient/api.go @@ -33,11 +33,11 @@ import ( integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/types/discoveryconfig" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/services" ) @@ -1186,9 +1186,9 @@ type Cache interface { ListSPIFFEFederations(ctx context.Context, pageSize int, lastToken string) ([]*machineidv1.SPIFFEFederation, string, error) // ListStaticHostUsers lists static host users. - ListStaticHostUsers(ctx context.Context, pageSize int, startKey string) ([]*userprovisioningpb.StaticHostUser, string, error) + ListStaticHostUsers(ctx context.Context, pageSize int, startKey string) ([]*userprovisioning.StaticHostUser, string, error) // GetStaticHostUser returns a static host user by name. - GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) + GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) } type NodeWrapper struct { diff --git a/lib/auth/statichostuser/service.go b/lib/auth/statichostuser/service.go index 2e157bbe9dec1..49dd8147a1051 100644 --- a/lib/auth/statichostuser/service.go +++ b/lib/auth/statichostuser/service.go @@ -24,6 +24,8 @@ import ( userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" + convertv1 "github.com/gravitational/teleport/api/types/userprovisioning/convert/v1" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/services" ) @@ -41,9 +43,9 @@ type ServiceConfig struct { // Cache is a subset of the service interface for reading items from the cache. type Cache interface { // ListStaticHostUsers lists static host users. - ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) + ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) // GetStaticHostUser returns a static host user by name. - GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) + GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) } // Service implements the static host user RPC service. @@ -90,8 +92,12 @@ func (s *Service) ListStaticHostUsers(ctx context.Context, req *userprovisioning if err != nil { return nil, trace.Wrap(err) } + userProtos := make([]*userprovisioningpb.StaticHostUser, 0, len(users)) + for _, u := range users { + userProtos = append(userProtos, convertv1.ToProto(u)) + } return &userprovisioningpb.ListStaticHostUsersResponse{ - Users: users, + Users: userProtos, NextPageToken: nextToken, }, nil } @@ -113,7 +119,10 @@ func (s *Service) GetStaticHostUser(ctx context.Context, req *userprovisioningpb } out, err := s.cache.GetStaticHostUser(ctx, req.Name) - return out, trace.Wrap(err) + if err != nil { + return nil, trace.Wrap(err) + } + return convertv1.ToProto(out), nil } // CreateStaticHostUser creates a static host user. @@ -129,8 +138,15 @@ func (s *Service) CreateStaticHostUser(ctx context.Context, req *userprovisionin return nil, trace.Wrap(err) } - out, err := s.backend.CreateStaticHostUser(ctx, req.User) - return out, trace.Wrap(err) + hostUser, err := convertv1.FromProto(req.User) + if err != nil { + return nil, trace.Wrap(err) + } + out, err := s.backend.CreateStaticHostUser(ctx, hostUser) + if err != nil { + return nil, trace.Wrap(err) + } + return convertv1.ToProto(out), trace.Wrap(err) } // UpdateStaticHostUser updates a static host user. @@ -146,8 +162,15 @@ func (s *Service) UpdateStaticHostUser(ctx context.Context, req *userprovisionin return nil, trace.Wrap(err) } - out, err := s.backend.UpdateStaticHostUser(ctx, req.User) - return out, trace.Wrap(err) + hostUser, err := convertv1.FromProto(req.User) + if err != nil { + return nil, trace.Wrap(err) + } + out, err := s.backend.UpdateStaticHostUser(ctx, hostUser) + if err != nil { + return nil, trace.Wrap(err) + } + return convertv1.ToProto(out), trace.Wrap(err) } // UpsertStaticHostUser upserts a static host user. @@ -163,8 +186,15 @@ func (s *Service) UpsertStaticHostUser(ctx context.Context, req *userprovisionin return nil, trace.Wrap(err) } - out, err := s.backend.UpsertStaticHostUser(ctx, req.User) - return out, trace.Wrap(err) + hostUser, err := convertv1.FromProto(req.User) + if err != nil { + return nil, trace.Wrap(err) + } + out, err := s.backend.UpsertStaticHostUser(ctx, hostUser) + if err != nil { + return nil, trace.Wrap(err) + } + return convertv1.ToProto(out), trace.Wrap(err) } // DeleteStaticHostUser deletes a static host user. Note that this does not diff --git a/lib/auth/statichostuser/service_test.go b/lib/auth/statichostuser/service_test.go index 8bb954be0a672..28929c57d6d11 100644 --- a/lib/auth/statichostuser/service_test.go +++ b/lib/auth/statichostuser/service_test.go @@ -25,10 +25,11 @@ import ( "github.com/gravitational/trace" "github.com/stretchr/testify/require" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/userprovisioning" - "github.com/gravitational/teleport/api/types/wrappers" + convertv1 "github.com/gravitational/teleport/api/types/userprovisioning/convert/v1" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/backend/memory" "github.com/gravitational/teleport/lib/services/local" @@ -43,17 +44,15 @@ func staticHostUserName(i int) string { func makeStaticHostUser(i int) *userprovisioningpb.StaticHostUser { name := staticHostUserName(i) - return userprovisioning.NewStaticHostUser(name, &userprovisioningpb.StaticHostUserSpec{ + return convertv1.ToProto(userprovisioning.NewStaticHostUser(&headerv1.Metadata{ + Name: name, + }, userprovisioning.Spec{ Login: name, Groups: []string{"foo", "bar"}, - NodeLabels: &wrappers.LabelValues{ - Values: map[string]wrappers.StringValues{ - "foo": { - Values: []string{"bar"}, - }, - }, + NodeLabels: types.Labels{ + "foo": {"bar"}, }, - }) + })) } func authorizeWithVerbs(verbs []string, mfaVerified bool) authorizerFactory { @@ -123,7 +122,7 @@ func TestStaticHostUserCRUD(t *testing.T) { } hostUser.Spec.Login = "bob" _, err = svc.UpdateStaticHostUser(ctx, &userprovisioningpb.UpdateStaticHostUserRequest{ - User: hostUser, + User: convertv1.ToProto(hostUser), }) return err }, @@ -360,7 +359,10 @@ func initSvc(t *testing.T, authorizerFn func(t *testing.T, client localClient) a localResourceService, err := local.NewStaticHostUserService(backend) require.NoError(t, err) for i := 0; i < 10; i++ { - _, err := localResourceService.CreateStaticHostUser(ctx, makeStaticHostUser(i)) + hostUser := makeStaticHostUser(i) + hostUserProto, err := convertv1.FromProto(hostUser) + require.NoError(t, err) + _, err = localResourceService.CreateStaticHostUser(ctx, hostUserProto) require.NoError(t, err) } diff --git a/lib/cache/cache.go b/lib/cache/cache.go index 9229996865a2e..bc485772ade1c 100644 --- a/lib/cache/cache.go +++ b/lib/cache/cache.go @@ -43,7 +43,6 @@ import ( dbobjectv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1" kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1" "github.com/gravitational/teleport/api/internalutils/stream" apitracing "github.com/gravitational/teleport/api/observability/tracing" @@ -52,6 +51,7 @@ import ( "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/types/secreports" "github.com/gravitational/teleport/api/types/userloginstate" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/defaults" @@ -2310,7 +2310,7 @@ func (c *Cache) GetKubernetesWaitingContainer(ctx context.Context, req *kubewait } // ListStaticHostUsers lists static host users. -func (c *Cache) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) { +func (c *Cache) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) { ctx, span := c.Tracer.Start(ctx, "cache/ListStaticHostUsers") defer span.End() @@ -2323,7 +2323,7 @@ func (c *Cache) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken } // GetStaticHostUser returns a static host user by name. -func (c *Cache) GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) { +func (c *Cache) GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) { ctx, span := c.Tracer.Start(ctx, "cache/GetStaticHostUser") defer span.End() diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go index e3f8e43719635..aa9f4af9efc6c 100644 --- a/lib/cache/cache_test.go +++ b/lib/cache/cache_test.go @@ -49,7 +49,6 @@ import ( headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/types/clusterconfig" @@ -2695,19 +2694,19 @@ func TestStaticHostUsers(t *testing.T) { p := newTestPack(t, ForAuth) t.Cleanup(p.Close) - testResources153(t, p, testFuncs153[*userprovisioningpb.StaticHostUser]{ - newResource: func(name string) (*userprovisioningpb.StaticHostUser, error) { + testResources153(t, p, testFuncs153[*userprovisioning.StaticHostUser]{ + newResource: func(name string) (*userprovisioning.StaticHostUser, error) { return newStaticHostUser(t, name), nil }, - create: func(ctx context.Context, item *userprovisioningpb.StaticHostUser) error { + create: func(ctx context.Context, item *userprovisioning.StaticHostUser) error { _, err := p.staticHostUsers.CreateStaticHostUser(ctx, item) return trace.Wrap(err) }, - list: func(ctx context.Context) ([]*userprovisioningpb.StaticHostUser, error) { + list: func(ctx context.Context) ([]*userprovisioning.StaticHostUser, error) { items, _, err := p.staticHostUsers.ListStaticHostUsers(ctx, 0, "") return items, trace.Wrap(err) }, - cacheList: func(ctx context.Context) ([]*userprovisioningpb.StaticHostUser, error) { + cacheList: func(ctx context.Context) ([]*userprovisioning.StaticHostUser, error) { items, _, err := p.cache.ListStaticHostUsers(ctx, 0, "") return items, trace.Wrap(err) }, @@ -3820,9 +3819,11 @@ func newAccessMonitoringRule(t *testing.T) *accessmonitoringrulesv1.AccessMonito return notification } -func newStaticHostUser(t *testing.T, name string) *userprovisioningpb.StaticHostUser { +func newStaticHostUser(t *testing.T, name string) *userprovisioning.StaticHostUser { t.Helper() - return userprovisioning.NewStaticHostUser(name, &userprovisioningpb.StaticHostUserSpec{ + return userprovisioning.NewStaticHostUser(&headerv1.Metadata{ + Name: name, + }, userprovisioning.Spec{ Login: "foo", Groups: []string{"bar", "baz"}, }) diff --git a/lib/cache/collections.go b/lib/cache/collections.go index 747ede4464b61..d469ba00eaa97 100644 --- a/lib/cache/collections.go +++ b/lib/cache/collections.go @@ -35,13 +35,13 @@ import ( kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/types/secreports" "github.com/gravitational/teleport/api/types/userloginstate" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/lib/services" ) @@ -717,7 +717,7 @@ func setupCollections(c *Cache, watches []types.WatchKind) (*cacheCollections, e if c.StaticHostUsers == nil { return nil, trace.BadParameter("missing parameter StaticHostUsers") } - collections.staticHostUsers = &genericCollection[*userprovisioningpb.StaticHostUser, staticHostUserGetter, staticHostUserExecutor]{ + collections.staticHostUsers = &genericCollection[*userprovisioning.StaticHostUser, staticHostUserGetter, staticHostUserExecutor]{ cache: c, watch: watch, } @@ -2338,10 +2338,10 @@ var _ executor[*kubewaitingcontainerpb.KubernetesWaitingContainer, kubernetesWai type staticHostUserExecutor struct{} -func (staticHostUserExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]*userprovisioningpb.StaticHostUser, error) { +func (staticHostUserExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets bool) ([]*userprovisioning.StaticHostUser, error) { var ( startKey string - allUsers []*userprovisioningpb.StaticHostUser + allUsers []*userprovisioning.StaticHostUser ) for { users, nextKey, err := cache.StaticHostUsers.ListStaticHostUsers(ctx, 0, startKey) @@ -2359,7 +2359,7 @@ func (staticHostUserExecutor) getAll(ctx context.Context, cache *Cache, loadSecr return allUsers, nil } -func (staticHostUserExecutor) upsert(ctx context.Context, cache *Cache, resource *userprovisioningpb.StaticHostUser) error { +func (staticHostUserExecutor) upsert(ctx context.Context, cache *Cache, resource *userprovisioning.StaticHostUser) error { _, err := cache.staticHostUsersCache.UpsertStaticHostUser(ctx, resource) return trace.Wrap(err) } @@ -2369,10 +2369,10 @@ func (staticHostUserExecutor) deleteAll(ctx context.Context, cache *Cache) error } func (staticHostUserExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error { - var hostUser *userprovisioningpb.StaticHostUser + var hostUser *userprovisioning.StaticHostUser r, ok := resource.(types.Resource153Unwrapper) if ok { - hostUser, ok = r.Unwrap().(*userprovisioningpb.StaticHostUser) + hostUser, ok = r.Unwrap().(*userprovisioning.StaticHostUser) if ok { err := cache.staticHostUsersCache.DeleteStaticHostUser(ctx, hostUser.Metadata.Name) return trace.Wrap(err) @@ -2391,8 +2391,8 @@ func (staticHostUserExecutor) getReader(cache *Cache, cacheOK bool) staticHostUs } type staticHostUserGetter interface { - ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) - GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) + ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) + GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) } type crownJewelsExecutor struct{} diff --git a/lib/services/local/events.go b/lib/services/local/events.go index b906d264d580a..f831129b959ee 100644 --- a/lib/services/local/events.go +++ b/lib/services/local/events.go @@ -35,7 +35,6 @@ import ( kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1" machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/kubewaitingcontainer" "github.com/gravitational/teleport/lib/backend" @@ -2219,7 +2218,7 @@ func (p *staticHostUserParser) parse(event backend.Event) (types.Resource, error case types.OpDelete: return resourceHeader(event, types.KindStaticHostUser, types.V1, 0) case types.OpPut: - resource, err := services.UnmarshalProtoResource[*userprovisioningpb.StaticHostUser]( + resource, err := services.UnmarshalStaticHostUser( event.Item.Value, services.WithExpires(event.Item.Expires), services.WithRevision(event.Item.Revision), diff --git a/lib/services/local/statichostuser.go b/lib/services/local/statichostuser.go index 83eeead686f57..8cbb87bdac620 100644 --- a/lib/services/local/statichostuser.go +++ b/lib/services/local/statichostuser.go @@ -23,8 +23,8 @@ import ( "github.com/gravitational/trace" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/local/generic" @@ -36,7 +36,7 @@ const ( // StaticHostUserService manages host users that should be created on SSH nodes. type StaticHostUserService struct { - svc *generic.ServiceWrapper[*userprovisioningpb.StaticHostUser] + svc *generic.ServiceWrapper[*userprovisioning.StaticHostUser] } // NewStaticHostUserService creates a new static host user service. @@ -45,8 +45,8 @@ func NewStaticHostUserService(bk backend.Backend) (*StaticHostUserService, error bk, types.KindStaticHostUser, staticHostUserPrefix, - services.MarshalProtoResource[*userprovisioningpb.StaticHostUser], - services.UnmarshalProtoResource[*userprovisioningpb.StaticHostUser], + services.MarshalStaticHostUser, + services.UnmarshalStaticHostUser, ) if err != nil { return nil, trace.Wrap(err) @@ -57,7 +57,7 @@ func NewStaticHostUserService(bk backend.Backend) (*StaticHostUserService, error } // ListStaticHostUsers lists static host users. -func (s *StaticHostUserService) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) { +func (s *StaticHostUserService) ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) { out, nextToken, err := s.svc.ListResources(ctx, pageSize, pageToken) if err != nil { return nil, "", trace.Wrap(err) @@ -66,13 +66,13 @@ func (s *StaticHostUserService) ListStaticHostUsers(ctx context.Context, pageSiz } // GetStaticHostUser returns a static host user by name. -func (s *StaticHostUserService) GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) { +func (s *StaticHostUserService) GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) { out, err := s.svc.GetResource(ctx, name) return out, trace.Wrap(err) } // CreateStaticHostUser creates a static host user. -func (s *StaticHostUserService) CreateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (s *StaticHostUserService) CreateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { if err := services.ValidateStaticHostUser(in); err != nil { return nil, trace.Wrap(err) } @@ -81,7 +81,7 @@ func (s *StaticHostUserService) CreateStaticHostUser(ctx context.Context, in *us } // UpdateStaticHostUser updates a static host user. -func (s *StaticHostUserService) UpdateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (s *StaticHostUserService) UpdateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { if err := services.ValidateStaticHostUser(in); err != nil { return nil, trace.Wrap(err) } @@ -90,7 +90,7 @@ func (s *StaticHostUserService) UpdateStaticHostUser(ctx context.Context, in *us } // UpsertStaticHostUser upserts a static host user. -func (s *StaticHostUserService) UpsertStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) { +func (s *StaticHostUserService) UpsertStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) { if err := services.ValidateStaticHostUser(in); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/services/local/statichostuser_test.go b/lib/services/local/statichostuser_test.go index 65e1fb6f95209..c089e7156bb76 100644 --- a/lib/services/local/statichostuser_test.go +++ b/lib/services/local/statichostuser_test.go @@ -25,16 +25,15 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/mailgun/holster/v3/clock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/lib/backend/memory" "github.com/gravitational/teleport/lib/services" @@ -88,7 +87,7 @@ func TestGetStaticHostUser(t *testing.T) { name string key string assertErr assert.ErrorAssertionFunc - wantObj *userprovisioningpb.StaticHostUser + wantObj *userprovisioning.StaticHostUser }{ { name: "object does not exist", @@ -99,7 +98,7 @@ func TestGetStaticHostUser(t *testing.T) { }, { name: "success", - key: getStaticHostUser(0).GetMetadata().GetName(), + key: getStaticHostUser(0).GetMetadata().Name, assertErr: assert.NoError, wantObj: getStaticHostUser(0), }, @@ -113,8 +112,8 @@ func TestGetStaticHostUser(t *testing.T) { assert.Nil(t, obj) } else { cmpOpts := []cmp.Option{ - protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"), - protocmp.Transform(), + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}), + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), } require.Equal(t, "", cmp.Diff(tc.wantObj, obj, cmpOpts...)) } @@ -132,7 +131,7 @@ func TestUpdateStaticHostUser(t *testing.T) { expiry := timestamppb.New(clock.Now().Add(30 * time.Minute)) // Fetch the object from the backend so the revision is populated. - key := getStaticHostUser(0).GetMetadata().GetName() + key := getStaticHostUser(0).GetMetadata().Name obj, err := service.GetStaticHostUser(ctx, key) require.NoError(t, err) obj.Metadata.Expires = expiry @@ -184,7 +183,7 @@ func TestDeleteStaticHostUser(t *testing.T) { }, { name: "success", - key: getStaticHostUser(0).GetMetadata().GetName(), + key: getStaticHostUser(0).GetMetadata().Name, assertErr: require.NoError, }, } @@ -216,8 +215,8 @@ func TestListStaticHostUsers(t *testing.T) { for i := 0; i < count; i++ { cmpOpts := []cmp.Option{ - protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"), - protocmp.Transform(), + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}), + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), } require.Equal(t, "", cmp.Diff(getStaticHostUser(i), elements[i], cmpOpts...)) } @@ -225,7 +224,7 @@ func TestListStaticHostUsers(t *testing.T) { t.Run("paginated", func(t *testing.T) { // Fetch a paginated list of objects - elements := make([]*userprovisioningpb.StaticHostUser, 0) + elements := make([]*userprovisioning.StaticHostUser, 0) nextToken := "" for { out, token, err := service.ListStaticHostUsers(ctx, 2, nextToken) @@ -240,8 +239,8 @@ func TestListStaticHostUsers(t *testing.T) { for i := 0; i < count; i++ { cmpOpts := []cmp.Option{ - protocmp.IgnoreFields(&headerv1.Metadata{}, "revision"), - protocmp.Transform(), + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}), + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), } require.Equal(t, "", cmp.Diff(getStaticHostUser(i), elements[i], cmpOpts...)) } @@ -262,9 +261,11 @@ func getStaticHostUserService(t *testing.T) services.StaticHostUser { return service } -func getStaticHostUser(index int) *userprovisioningpb.StaticHostUser { +func getStaticHostUser(index int) *userprovisioning.StaticHostUser { name := fmt.Sprintf("obj%v", index) - return userprovisioning.NewStaticHostUser(name, &userprovisioningpb.StaticHostUserSpec{ + return userprovisioning.NewStaticHostUser(&headerv1.Metadata{ + Name: name, + }, userprovisioning.Spec{ Login: "alice", Groups: []string{"foo", "bar"}, Uid: "1234", diff --git a/lib/services/presets.go b/lib/services/presets.go index 631bcaa2ca80c..493d846939608 100644 --- a/lib/services/presets.go +++ b/lib/services/presets.go @@ -180,6 +180,7 @@ func NewPresetEditorRole() types.Role { types.NewRule(types.KindAccessGraphSettings, RW()), types.NewRule(types.KindSPIFFEFederation, RW()), types.NewRule(types.KindNotification, RW()), + types.NewRule(types.KindStaticHostUser, RW()), }, }, }, diff --git a/lib/services/resource.go b/lib/services/resource.go index bb384b582f9da..938458c740766 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -241,6 +241,8 @@ func ParseShortcut(in string) (string, error) { return types.KindPlugin, nil case types.KindAccessGraphSettings, "ags": return types.KindAccessGraphSettings, nil + case types.KindStaticHostUser, types.KindStaticHostUser + "s", "host_user", "host_users": + return types.KindStaticHostUser, nil } return "", trace.BadParameter("unsupported resource: %q - resources should be expressed as 'type/name', for example 'connector/github'", in) } diff --git a/lib/services/statichostuser.go b/lib/services/statichostuser.go index 6d6b9c362c86f..c1fe8f8c33b80 100644 --- a/lib/services/statichostuser.go +++ b/lib/services/statichostuser.go @@ -23,28 +23,67 @@ import ( "strconv" "github.com/gravitational/trace" + "google.golang.org/protobuf/types/known/timestamppb" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" + "github.com/gravitational/teleport/lib/utils" ) // StaticHostUserService manages host users that should be created on SSH nodes. type StaticHostUser interface { // ListStaticHostUsers lists static host users. - ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioningpb.StaticHostUser, string, error) + ListStaticHostUsers(ctx context.Context, pageSize int, pageToken string) ([]*userprovisioning.StaticHostUser, string, error) // GetStaticHostUser returns a static host user by name. - GetStaticHostUser(ctx context.Context, name string) (*userprovisioningpb.StaticHostUser, error) + GetStaticHostUser(ctx context.Context, name string) (*userprovisioning.StaticHostUser, error) // CreateStaticHostUser creates a static host user. - CreateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) + CreateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) // UpdateStaticHostUser updates a static host user. - UpdateStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) + UpdateStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) // UpsertStaticHostUser upserts a static host user. - UpsertStaticHostUser(ctx context.Context, in *userprovisioningpb.StaticHostUser) (*userprovisioningpb.StaticHostUser, error) + UpsertStaticHostUser(ctx context.Context, in *userprovisioning.StaticHostUser) (*userprovisioning.StaticHostUser, error) // DeleteStaticHostUser deletes a static host user. Note that this does not // remove any host users created on nodes from the resource. DeleteStaticHostUser(ctx context.Context, name string) error } +// MarshalStaticHostUser marshals the StaticHostUser object into a JSON byte array. +func MarshalStaticHostUser(hostUser *userprovisioning.StaticHostUser, opts ...MarshalOption) ([]byte, error) { + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + if !cfg.PreserveRevision { + copy := *hostUser + copy.GetMetadata().Revision = "" + hostUser = © + } + return utils.FastMarshal(hostUser) +} + +// UnmarshalStaticHostUser unmarshals the StaticHostUser object from a JSON +// byte array. +func UnmarshalStaticHostUser(data []byte, opts ...MarshalOption) (*userprovisioning.StaticHostUser, error) { + if len(data) == 0 { + return nil, trace.BadParameter("missing static host user data") + } + cfg, err := CollectOptions(opts) + if err != nil { + return nil, trace.Wrap(err) + } + var hostUser userprovisioning.StaticHostUser + if err := utils.FastUnmarshal(data, &hostUser); err != nil { + return nil, trace.BadParameter(err.Error()) + } + if cfg.Revision != "" { + hostUser.GetMetadata().Revision = cfg.Revision + } + if !cfg.Expires.IsZero() { + hostUser.GetMetadata().Expires = timestamppb.New(cfg.Expires) + } + return &hostUser, nil +} + func isValidUidOrGid(s string) bool { // No uid/gid is OK. if s == "" { @@ -58,25 +97,19 @@ func isValidUidOrGid(s string) bool { // ValidateStaticHostUser checks that required parameters are set for the // specified StaticHostUser. -func ValidateStaticHostUser(u *userprovisioningpb.StaticHostUser) error { +func ValidateStaticHostUser(u *userprovisioning.StaticHostUser) error { if u == nil { return trace.BadParameter("StaticHostUser is nil") } - if u.Metadata == nil { - return trace.BadParameter("Metadata is nil") - } if u.Metadata.Name == "" { return trace.BadParameter("missing name") } - if u.Spec == nil { - return trace.BadParameter("Spec is nil") - } if u.Spec.Login == "" { return trace.BadParameter("missing login") } if u.Spec.NodeLabels != nil { - for key, value := range u.Spec.NodeLabels.Values { - if key == types.Wildcard && !(len(value.Values) == 1 && value.Values[0] == types.Wildcard) { + for key, values := range u.Spec.NodeLabels { + if key == types.Wildcard && !(len(values) == 1 && values[0] == types.Wildcard) { return trace.BadParameter("selector *: is not supported") } } diff --git a/lib/services/statichostuser_test.go b/lib/services/statichostuser_test.go index 454e3e26efed4..23437198babaa 100644 --- a/lib/services/statichostuser_test.go +++ b/lib/services/statichostuser_test.go @@ -23,33 +23,17 @@ import ( "github.com/stretchr/testify/require" - userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v1" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/userprovisioning" - "github.com/gravitational/teleport/api/types/wrappers" ) func TestValidateStaticHostUser(t *testing.T) { t.Parallel() - nodeLabels := func(labels map[string]string) *wrappers.LabelValues { - if len(labels) == 0 { - return nil - } - values := &wrappers.LabelValues{ - Values: make(map[string]wrappers.StringValues, len(labels)), - } - for k, v := range labels { - values.Values[k] = wrappers.StringValues{ - Values: []string{v}, - } - } - return values - } - tests := []struct { name string - hostUser *userprovisioningpb.StaticHostUser + hostUser *userprovisioning.StaticHostUser assert require.ErrorAssertionFunc }{ { @@ -58,32 +42,27 @@ func TestValidateStaticHostUser(t *testing.T) { }, { name: "no name", - hostUser: userprovisioning.NewStaticHostUser("", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{}, userprovisioning.Spec{ Login: "alice", }), assert: require.Error, }, - { - name: "no spec", - hostUser: userprovisioning.NewStaticHostUser("alice_user", nil), - assert: require.Error, - }, { name: "missing login", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{}), + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{}), assert: require.Error, }, { name: "invalid node labels", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ Login: "alice", - NodeLabels: nodeLabels(map[string]string{types.Wildcard: "bar"}), + NodeLabels: types.Labels{types.Wildcard: {"bar"}}, }), assert: require.Error, }, { name: "invalid node labels expression", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ Login: "alice", NodeLabelsExpression: "foo bar xyz", }), @@ -91,45 +70,42 @@ func TestValidateStaticHostUser(t *testing.T) { }, { name: "valid wildcard labels", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ - Login: "alice", - NodeLabels: nodeLabels(map[string]string{ - "foo": types.Wildcard, - types.Wildcard: types.Wildcard, - }), + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ + Login: "alice", + NodeLabels: types.Labels{"foo": {types.Wildcard}, types.Wildcard: {types.Wildcard}}, }), assert: require.NoError, }, { name: "non-numeric uid", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ Login: "alice", Groups: []string{"foo", "bar"}, Uid: "abcd", Gid: "1234", - NodeLabels: nodeLabels(map[string]string{"foo": "bar"}), + NodeLabels: types.Labels{"foo": {"bar"}}, }), assert: require.Error, }, { name: "non-numeric gid", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ Login: "alice", Groups: []string{"foo", "bar"}, Uid: "1234", Gid: "abcd", - NodeLabels: nodeLabels(map[string]string{"foo": "bar"}), + NodeLabels: types.Labels{"foo": {"bar"}}, }), assert: require.Error, }, { name: "ok", - hostUser: userprovisioning.NewStaticHostUser("alice_user", &userprovisioningpb.StaticHostUserSpec{ + hostUser: userprovisioning.NewStaticHostUser(&headerv1.Metadata{Name: "alice_user"}, userprovisioning.Spec{ Login: "alice", Groups: []string{"foo", "bar"}, Uid: "1234", Gid: "5678", - NodeLabels: nodeLabels(map[string]string{"foo": "bar"}), + NodeLabels: types.Labels{"foo": {"bar"}}, NodeLabelsExpression: `labels["env"] == "staging" || labels["env"] == "test"`, }), assert: require.NoError, diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index d74fcf21cd418..d96d520a7dbc1 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/types/externalauditstorage" "github.com/gravitational/teleport/api/types/secreports" + "github.com/gravitational/teleport/api/types/userprovisioning" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/devicetrust" @@ -1733,3 +1734,34 @@ func (c *spiffeFederationCollection) writeText(w io.Writer, verbose bool) error _, err := t.AsBuffer().WriteTo(w) return trace.Wrap(err) } + +type staticHostUserCollection struct { + items []*userprovisioning.StaticHostUser +} + +func (c *staticHostUserCollection) resources() []types.Resource { + r := make([]types.Resource, 0, len(c.items)) + for _, resource := range c.items { + r = append(r, types.Resource153ToLegacy(resource)) + } + return r +} + +func (c *staticHostUserCollection) writeText(w io.Writer, verbose bool) error { + var rows [][]string + for _, item := range c.items { + rows = append(rows, []string{ + item.GetMetadata().Name, + item.Spec.Login, + strings.Join(item.Spec.Groups, ","), + item.Spec.Uid, + item.Spec.Gid, + printNodeLabels(item.Spec.NodeLabels), + }) + } + headers := []string{"Name", "Login", "Groups", "Uid", "Gid", "Node Labels"} + t := asciitable.MakeTable(headers, rows...) + t.SortRowsBy([]int{0}, true) + _, err := t.AsBuffer().WriteTo(w) + return trace.Wrap(err) +} diff --git a/tool/tctl/common/edit_command_test.go b/tool/tctl/common/edit_command_test.go index b42517ac70382..6a3c176369441 100644 --- a/tool/tctl/common/edit_command_test.go +++ b/tool/tctl/common/edit_command_test.go @@ -30,7 +30,9 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/constants" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/backend" @@ -73,6 +75,10 @@ func TestEditResources(t *testing.T) { kind: types.KindSessionRecordingConfig, edit: testEditSessionRecordingConfig, }, + { + kind: types.KindStaticHostUser, + edit: testEditStaticHostUser, + }, } for _, test := range tests { @@ -485,3 +491,45 @@ func testEditSAMLConnector(t *testing.T, clt *authclient.Client) { assert.Error(t, err, "stale connector was allowed to be updated") require.ErrorIs(t, err, backend.ErrIncorrectRevision, "expected an incorrect revision error, got %T", err) } + +func testEditStaticHostUser(t *testing.T, clt *authclient.Client) { + ctx := context.Background() + + expected := userprovisioning.NewStaticHostUser(&headerv1.Metadata{ + Name: "test-host-user", + }, userprovisioning.Spec{ + Login: "alice", + NodeLabels: types.Labels{ + "foo": {"bar"}, + }, + }) + created, err := clt.StaticHostUserClient().CreateStaticHostUser(ctx, expected) + require.NoError(t, err) + + editor := func(name string) error { + f, err := os.Create(name) + if err != nil { + return trace.Wrap(err, "opening file to edit") + } + + expected.GetMetadata().Revision = created.GetMetadata().Revision + expected.Spec.Login = "bob" + + collection := &staticHostUserCollection{items: []*userprovisioning.StaticHostUser{expected}} + return trace.NewAggregate(writeYAML(collection, f), f.Close()) + } + + _, err = runEditCommand(t, clt, []string{"edit", "host_user/test-host-user"}, withEditor(editor)) + require.NoError(t, err) + + actual, err := clt.StaticHostUserClient().GetStaticHostUser(ctx, expected.GetMetadata().Name) + require.NoError(t, err) + require.Empty(t, cmp.Diff(expected, actual, + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}), + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), + )) + + _, err = runEditCommand(t, clt, []string{"edit", "host_user/test-host-user"}, withEditor(editor)) + require.Error(t, err) + require.True(t, trace.IsCompareFailed(err), "unexpected error: %v", err) +} diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 3193cc28902aa..79f9423f869e6 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -59,6 +59,7 @@ import ( "github.com/gravitational/teleport/api/types/externalauditstorage" "github.com/gravitational/teleport/api/types/installers" "github.com/gravitational/teleport/api/types/secreports" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/defaults" @@ -166,6 +167,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec types.KindAccessGraphSettings: rc.upsertAccessGraphSettings, types.KindPlugin: rc.createPlugin, types.KindSPIFFEFederation: rc.createSPIFFEFederation, + types.KindStaticHostUser: rc.createStaticHostUser, } rc.UpdateHandlers = map[ResourceKind]ResourceCreateHandler{ types.KindUser: rc.updateUser, @@ -181,6 +183,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec types.KindVnetConfig: rc.updateVnetConfig, types.KindAccessGraphSettings: rc.updateAccessGraphSettings, types.KindPlugin: rc.updatePlugin, + types.KindStaticHostUser: rc.updateStaticHostUser, } rc.config = config @@ -1419,6 +1422,39 @@ func (rc *ResourceCommand) createServerInfo(ctx context.Context, client *authcli return nil } +func (rc *ResourceCommand) createStaticHostUser(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error { + hostUser, err := services.UnmarshalStaticHostUser(resource.Raw) + if err != nil { + return trace.Wrap(err) + } + c := client.StaticHostUserClient() + if rc.force { + if _, err := c.UpsertStaticHostUser(ctx, hostUser); err != nil { + return trace.Wrap(err) + } + fmt.Printf("static host user %q has been updated\n", hostUser.GetMetadata().Name) + } else { + if _, err := c.CreateStaticHostUser(ctx, hostUser); err != nil { + return trace.Wrap(err) + } + fmt.Printf("static host user %q has been created\n", hostUser.GetMetadata().Name) + } + + return nil +} + +func (rc *ResourceCommand) updateStaticHostUser(ctx context.Context, client *authclient.Client, resource services.UnknownResource) error { + hostUser, err := services.UnmarshalStaticHostUser(resource.Raw) + if err != nil { + return trace.Wrap(err) + } + if _, err := client.StaticHostUserClient().UpdateStaticHostUser(ctx, hostUser); err != nil { + return trace.Wrap(err) + } + fmt.Printf("static host user %q has been updated\n", hostUser.GetMetadata().Name) + return nil +} + // Delete deletes resource by name func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client) (err error) { singletonResources := []string{ @@ -1812,6 +1848,11 @@ func (rc *ResourceCommand) Delete(ctx context.Context, client *authclient.Client return trace.Wrap(err) } fmt.Printf("SPIFFE federation %q has been deleted\n", rc.ref.Name) + case types.KindStaticHostUser: + if err := client.StaticHostUserClient().DeleteStaticHostUser(ctx, rc.ref.Name); err != nil { + return trace.Wrap(err) + } + fmt.Printf("static host user %q has been deleted\n", rc.ref.Name) default: return trace.BadParameter("deleting resources of type %q is not supported", rc.ref.Kind) } @@ -2929,6 +2970,31 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient } return &botInstanceCollection{items: instances}, nil + case types.KindStaticHostUser: + hostUserClient := client.StaticHostUserClient() + if rc.ref.Name != "" { + hostUser, err := hostUserClient.GetStaticHostUser(ctx, rc.ref.Name) + if err != nil { + return nil, trace.Wrap(err) + } + + return &staticHostUserCollection{items: []*userprovisioning.StaticHostUser{hostUser}}, nil + } + + var hostUsers []*userprovisioning.StaticHostUser + var nextToken string + for { + resp, token, err := hostUserClient.ListStaticHostUsers(ctx, 0, nextToken) + if err != nil { + return nil, trace.Wrap(err) + } + hostUsers = append(hostUsers, resp...) + if token == "" { + break + } + nextToken = token + } + return &staticHostUserCollection{items: hostUsers}, nil } return nil, trace.BadParameter("getting %q is not supported", rc.ref.String()) } diff --git a/tool/tctl/common/resource_command_test.go b/tool/tctl/common/resource_command_test.go index 43002008f28d1..ce2a47f24cf56 100644 --- a/tool/tctl/common/resource_command_test.go +++ b/tool/tctl/common/resource_command_test.go @@ -45,6 +45,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/discoveryconfig" "github.com/gravitational/teleport/api/types/header" + "github.com/gravitational/teleport/api/types/userprovisioning" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/integration/helpers" "github.com/gravitational/teleport/lib/auth/authclient" @@ -1405,6 +1406,10 @@ func TestCreateResources(t *testing.T) { kind: types.KindAppServer, create: testCreateAppServer, }, + { + kind: types.KindStaticHostUser, + create: testCreateStaticHostUser, + }, } for _, test := range tests { @@ -2205,6 +2210,70 @@ spec: require.NoError(t, err) } +func testCreateStaticHostUser(t *testing.T, clt *authclient.Client) { + // Ensure that our test user does not exist + resourceName := "alice_user" + resourceKey := types.KindStaticHostUser + "/" + resourceName + _, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"}) + require.Error(t, err) + require.True(t, trace.IsNotFound(err), "unexpected error: %v", err) + + const userYAML = `kind: static_host_user +version: v1 +metadata: + name: alice_user +spec: + login: alice + groups: + - foo + - bar + sudoers: [ + "abcd1234" + ] + uid: "1234" + gid: "5678" + node_labels: + foo: bar + node_labels_expression: 'labels["foo"] == labels["bar"]' +` + + // Create the host user + userYAMLPath := filepath.Join(t.TempDir(), "host_user.yaml") + require.NoError(t, os.WriteFile(userYAMLPath, []byte(userYAML), 0644)) + _, err = runResourceCommand(t, clt, []string{"create", userYAMLPath}) + require.NoError(t, err) + + // Fetch the user + buf, err := runResourceCommand(t, clt, []string{"get", resourceKey, "--format=json"}) + require.NoError(t, err) + hostUsers := mustDecodeJSON[[]*userprovisioning.StaticHostUser](t, buf) + require.Len(t, hostUsers, 1) + + var expected userprovisioning.StaticHostUser + require.NoError(t, yaml.Unmarshal([]byte(userYAML), &expected)) + + require.Empty(t, cmp.Diff( + []*userprovisioning.StaticHostUser{&expected}, + hostUsers, + cmpopts.IgnoreUnexported(headerv1.ResourceHeader{}, headerv1.Metadata{}), + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), + )) + + // Explicitly change the revision and try creating the user with and without + // the force flag. + expected.GetMetadata().Revision = uuid.NewString() + hostUserBytes, err := services.MarshalStaticHostUser(&expected, services.PreserveRevision()) + require.NoError(t, err) + require.NoError(t, os.WriteFile(userYAMLPath, hostUserBytes, 0644)) + + _, err = runResourceCommand(t, clt, []string{"create", userYAMLPath}) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err), "unexpected error: %v", err) + + _, err = runResourceCommand(t, clt, []string{"create", "-f", userYAMLPath}) + require.NoError(t, err) +} + func TestPluginResourceWrapper(t *testing.T) { tests := []struct { name string