Skip to content

Commit

Permalink
User Tasks: services and clients implementation (#46131)
Browse files Browse the repository at this point in the history
This PR adds the implementation for the User Tasks:
- services (backend+cache)
- clients (API + tctl)
- light validation to set up the path for later PRs
  • Loading branch information
marcoandredinis committed Sep 19, 2024
1 parent cc89802 commit ac080d7
Show file tree
Hide file tree
Showing 30 changed files with 1,593 additions and 0 deletions.
10 changes: 10 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/gravitational/teleport/api/client/secreport"
statichostuserclient "github.com/gravitational/teleport/api/client/statichostuser"
"github.com/gravitational/teleport/api/client/userloginstate"
usertaskapi "github.com/gravitational/teleport/api/client/usertask"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
Expand Down Expand Up @@ -87,6 +88,7 @@ import (
userloginstatev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
usertaskv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
userpreferencespb "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
Expand Down Expand Up @@ -4730,6 +4732,14 @@ func (c *Client) UserLoginStateClient() *userloginstate.Client {
return userloginstate.NewClient(userloginstatev1.NewUserLoginStateServiceClient(c.conn))
}

// UserTasksServiceClient returns a UserTask client.
// Clients connecting to older Teleport versions, still get a UserTask client
// when calling this method, but all RPCs will return "not implemented" errors
// (as per the default gRPC behavior).
func (c *Client) UserTasksServiceClient() *usertaskapi.Client {
return usertaskapi.NewClient(usertaskv1.NewUserTaskServiceClient(c.conn))
}

// GetCertAuthority retrieves a CA by type and domain.
func (c *Client) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) {
ca, err := c.TrustClient().GetCertAuthority(ctx, &trustpb.GetCertAuthorityRequest{
Expand Down
8 changes: 8 additions & 0 deletions api/client/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
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/v2"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
accesslistv1conv "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
Expand Down Expand Up @@ -99,6 +100,10 @@ func EventToGRPC(in types.Event) (*proto.Event, error) {
out.Resource = &proto.Event_StaticHostUserV2{
StaticHostUserV2: r,
}
case *usertasksv1.UserTask:
out.Resource = &proto.Event_UserTask{
UserTask: r,
}
default:
return nil, trace.BadParameter("resource type %T is not supported", r)
}
Expand Down Expand Up @@ -542,6 +547,9 @@ func EventFromGRPC(in *proto.Event) (*types.Event, error) {
} else if r := in.GetStaticHostUserV2(); r != nil {
out.Resource = types.Resource153ToLegacy(r)
return &out, nil
} else if r := in.GetUserTask(); r != nil {
out.Resource = types.Resource153ToLegacy(r)
return &out, nil
} else {
return nil, trace.BadParameter("received unsupported resource %T", in.Resource)
}
Expand Down
106 changes: 106 additions & 0 deletions api/client/usertask/usertask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 usertask

import (
"context"

"github.com/gravitational/trace"

usertaskv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
)

// Client is a client for the User Task API.
type Client struct {
grpcClient usertaskv1.UserTaskServiceClient
}

// NewClient creates a new User Task client.
func NewClient(grpcClient usertaskv1.UserTaskServiceClient) *Client {
return &Client{
grpcClient: grpcClient,
}
}

// ListUserTasks returns a list of User Tasks.
func (c *Client) ListUserTasks(ctx context.Context, pageSize int64, nextToken string) ([]*usertaskv1.UserTask, string, error) {
resp, err := c.grpcClient.ListUserTasks(ctx, &usertaskv1.ListUserTasksRequest{
PageSize: pageSize,
PageToken: nextToken,
})
if err != nil {
return nil, "", trace.Wrap(err)
}

return resp.UserTasks, resp.NextPageToken, nil
}

// CreateUserTask creates a new User Task.
func (c *Client) CreateUserTask(ctx context.Context, req *usertaskv1.UserTask) (*usertaskv1.UserTask, error) {
rsp, err := c.grpcClient.CreateUserTask(ctx, &usertaskv1.CreateUserTaskRequest{
UserTask: req,
})
if err != nil {
return nil, trace.Wrap(err)
}
return rsp, nil
}

// GetUserTask returns a User Task by name.
func (c *Client) GetUserTask(ctx context.Context, name string) (*usertaskv1.UserTask, error) {
rsp, err := c.grpcClient.GetUserTask(ctx, &usertaskv1.GetUserTaskRequest{
Name: name,
})
if err != nil {
return nil, trace.Wrap(err)
}
return rsp, nil
}

// UpdateUserTask updates an existing User Task.
func (c *Client) UpdateUserTask(ctx context.Context, req *usertaskv1.UserTask) (*usertaskv1.UserTask, error) {
rsp, err := c.grpcClient.UpdateUserTask(ctx, &usertaskv1.UpdateUserTaskRequest{
UserTask: req,
})
if err != nil {
return nil, trace.Wrap(err)
}
return rsp, nil
}

// UpsertUserTask upserts a User Task.
func (c *Client) UpsertUserTask(ctx context.Context, req *usertaskv1.UserTask) (*usertaskv1.UserTask, error) {
rsp, err := c.grpcClient.UpsertUserTask(ctx, &usertaskv1.UpsertUserTaskRequest{
UserTask: req,
})
if err != nil {
return nil, trace.Wrap(err)
}
return rsp, nil
}

// DeleteUserTask deletes a User Task.
func (c *Client) DeleteUserTask(ctx context.Context, name string) error {
_, err := c.grpcClient.DeleteUserTask(ctx, &usertaskv1.DeleteUserTaskRequest{
Name: name,
})
return trace.Wrap(err)
}

// DeleteAllUserTasks deletes all User Tasks.
// Not implemented. Added to satisfy the interface.
func (c *Client) DeleteAllUserTasks(_ context.Context) error {
return trace.NotImplemented("DeleteAllUserTasks is not implemented")
}
3 changes: 3 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,9 @@ const (
// KindIntegration is a connection to a 3rd party system API.
KindIntegration = "integration"

// KindUserTask is a task representing an issue with some other resource.
KindUserTask = "user_task"

// KindClusterMaintenanceConfig determines maintenance times for the cluster.
KindClusterMaintenanceConfig = "cluster_maintenance_config"

Expand Down
95 changes: 95 additions & 0 deletions api/types/usertasks/object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

package usertasks

import (
"github.com/gravitational/trace"

headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/types"
)

// NewUserTask creates a new UserTask object.
// It validates the object before returning it.
func NewUserTask(name string, spec *usertasksv1.UserTaskSpec) (*usertasksv1.UserTask, error) {
cj := &usertasksv1.UserTask{
Kind: types.KindUserTask,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: name,
},
Spec: spec,
}

if err := ValidateUserTask(cj); err != nil {
return nil, trace.Wrap(err)
}

return cj, nil
}

const (
// TaskTypeDiscoverEC2 identifies a User Tasks that is created
// when an auto-enrollment of an EC2 instance fails.
// UserTasks that have this Task Type must include the DiscoverEC2 field.
TaskTypeDiscoverEC2 = "discover-ec2"
)

// ValidateUserTask validates the UserTask object without modifying it.
func ValidateUserTask(uit *usertasksv1.UserTask) error {
switch {
case uit.GetKind() != types.KindUserTask:
return trace.BadParameter("invalid kind")
case uit.GetVersion() != types.V1:
return trace.BadParameter("invalid version")
case uit.GetSubKind() != "":
return trace.BadParameter("invalid sub kind, must be empty")
case uit.GetMetadata() == nil:
return trace.BadParameter("user task metadata is nil")
case uit.Metadata.GetName() == "":
return trace.BadParameter("user task name is empty")
case uit.GetSpec() == nil:
return trace.BadParameter("user task spec is nil")
case uit.GetSpec().Integration == "":
return trace.BadParameter("integration is required")
}

switch uit.Spec.TaskType {
case TaskTypeDiscoverEC2:
if err := validateDiscoverEC2TaskType(uit); err != nil {
return trace.Wrap(err)
}
default:
return trace.BadParameter("task type %q is not valid", uit.Spec.TaskType)
}

return nil
}

func validateDiscoverEC2TaskType(uit *usertasksv1.UserTask) error {
if uit.Spec.DiscoverEc2 == nil {
return trace.BadParameter("%s requires the discover_ec2 field", TaskTypeDiscoverEC2)
}
if uit.Spec.IssueType == "" {
return trace.BadParameter("issue type is required")
}

return nil
}
69 changes: 69 additions & 0 deletions api/types/usertasks/object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

package usertasks_test

import (
"testing"

"github.com/stretchr/testify/require"

headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/types/usertasks"
)

func TestValidateUserTask(t *testing.T) {
t.Parallel()

tests := []struct {
name string
task *usertasksv1.UserTask
wantErr require.ErrorAssertionFunc
}{
{
name: "NilUserTask",
task: nil,
wantErr: require.Error,
},
{
name: "ValidUserTask",
task: &usertasksv1.UserTask{
Kind: "user_task",
Version: "v1",
Metadata: &headerv1.Metadata{
Name: "test",
},
Spec: &usertasksv1.UserTaskSpec{
Integration: "my-integration",
TaskType: "discover-ec2",
IssueType: "failed to enroll ec2 instances",
DiscoverEc2: &usertasksv1.DiscoverEC2{},
},
},
wantErr: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := usertasks.ValidateUserTask(tt.task)
tt.wantErr(t, err)
})
}
}
2 changes: 2 additions & 0 deletions lib/auth/accesspoint/accesspoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type Config struct {
StaticHostUsers services.StaticHostUser
Trust services.Trust
UserGroups services.UserGroups
UserTasks services.UserTasks
UserLoginStates services.UserLoginStates
Users services.UsersService
WebSession types.WebSessionInterface
Expand Down Expand Up @@ -191,6 +192,7 @@ func NewCache(cfg Config) (*cache.Cache, error) {
Trust: cfg.Trust,
UserGroups: cfg.UserGroups,
UserLoginStates: cfg.UserLoginStates,
UserTasks: cfg.UserTasks,
Users: cfg.Users,
WebSession: cfg.WebSession,
WebToken: cfg.WebToken,
Expand Down
8 changes: 8 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
return nil, trace.Wrap(err)
}
}
if cfg.UserTasks == nil {
cfg.UserTasks, err = local.NewUserTasksService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.DiscoveryConfigs == nil {
cfg.DiscoveryConfigs, err = local.NewDiscoveryConfigService(cfg.Backend)
if err != nil {
Expand Down Expand Up @@ -429,6 +435,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
SessionTrackerService: cfg.SessionTrackerService,
ConnectionsDiagnostic: cfg.ConnectionsDiagnostic,
Integrations: cfg.Integrations,
UserTasks: cfg.UserTasks,
DiscoveryConfigs: cfg.DiscoveryConfigs,
Okta: cfg.Okta,
AccessLists: cfg.AccessLists,
Expand Down Expand Up @@ -629,6 +636,7 @@ type Services struct {
services.StatusInternal
services.Integrations
services.IntegrationsTokenGenerator
services.UserTasks
services.DiscoveryConfigs
services.Okta
services.AccessLists
Expand Down
Loading

0 comments on commit ac080d7

Please sign in to comment.