mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
DatabaseService resource: client and server CRUD (#19469)
* DatabaseService resource: client and server CRUD In the context of Teleport Discover, we must be able to known if there's any DatabaseService available to proxy a given Database resource. If there's none available, we will offer a script for the user to run and install a DatabaseService which proxies the desired Database resource. By DatabaseService, we mean the process that Teleport binary manages when the `teleport.yaml` config has the following section: ```yaml db_service: enabled: "yes" ``` To accomplish this we are creating a new resource: DatabaseService. The UI will fetch all DatabaseServices and check if there's any ResourceMatcher that matches the DatabaseLabels. A previous PR created the DatabaseService resource. This PR creates all the necessary CRUD operations: - Add Service to manage DatabaseServices resources in the backend. - Add GetAll, Upsert, Delete and DeleteAll operations to Client. - Add DatabaseService support to ListResources. - Add WebAPI endpoint to list DatabaseServices using ListResources. The next PR will add the heartbeat mechanism to the DatabaseService process.
This commit is contained in:
@@ -2470,6 +2470,32 @@ func (c *Client) DeleteAllDatabases(ctx context.Context) error {
|
||||
return trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// UpsertDatabaseService creates or updates existing DatabaseService resource.
|
||||
func (c *Client) UpsertDatabaseService(ctx context.Context, service types.DatabaseService) (*types.KeepAlive, error) {
|
||||
serviceV1, ok := service.(*types.DatabaseServiceV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unsupported DatabaseService type %T", serviceV1)
|
||||
}
|
||||
keepAlive, err := c.grpc.UpsertDatabaseService(ctx, &proto.UpsertDatabaseServiceRequest{
|
||||
Service: serviceV1,
|
||||
}, c.callOpts...)
|
||||
|
||||
return keepAlive, trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// DeleteDatabaseService deletes a specific DatabaseService resource.
|
||||
func (c *Client) DeleteDatabaseService(ctx context.Context, name string) error {
|
||||
_, err := c.grpc.DeleteDatabaseService(ctx, &types.ResourceRequest{Name: name}, c.callOpts...)
|
||||
return trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// DeleteAllDatabaseServices deletes all DatabaseService resources.
|
||||
// If an error occurs, a partial delete may happen.
|
||||
func (c *Client) DeleteAllDatabaseServices(ctx context.Context) error {
|
||||
_, err := c.grpc.DeleteAllDatabaseServices(ctx, &proto.DeleteAllDatabaseServicesRequest{}, c.callOpts...)
|
||||
return trail.FromGRPC(err)
|
||||
}
|
||||
|
||||
// GetWindowsDesktopServices returns all registered windows desktop services.
|
||||
func (c *Client) GetWindowsDesktopServices(ctx context.Context) ([]types.WindowsDesktopService, error) {
|
||||
resp, err := c.grpc.GetWindowsDesktopServices(ctx, &emptypb.Empty{}, c.callOpts...)
|
||||
@@ -2696,6 +2722,8 @@ func (c *Client) ListResources(ctx context.Context, req proto.ListResourcesReque
|
||||
switch req.ResourceType {
|
||||
case types.KindDatabaseServer:
|
||||
resources[i] = respResource.GetDatabaseServer()
|
||||
case types.KindDatabaseService:
|
||||
resources[i] = respResource.GetDatabaseService()
|
||||
case types.KindAppServer:
|
||||
resources[i] = respResource.GetAppServer()
|
||||
case types.KindNode:
|
||||
|
||||
+927
-1015
File diff suppressed because it is too large
Load Diff
@@ -839,9 +839,6 @@ message DeleteAllDatabaseServersRequest {
|
||||
string Namespace = 1 [(gogoproto.jsontag) = "namespace"];
|
||||
}
|
||||
|
||||
// GetAllDatabaseServicesRequest is a request to fetch all DatabaseServices.
|
||||
message GetAllDatabaseServicesRequest {}
|
||||
|
||||
// DatabaseServiceV1List represents a list of DatabaseService resources.
|
||||
message DatabaseServiceV1List {
|
||||
// Services is a list of DatabaseService resources.
|
||||
@@ -1626,6 +1623,8 @@ message PaginatedResource {
|
||||
types.KubernetesServerV3 KubernetesServer = 7 [(gogoproto.jsontag) = "kubernetes_server,omitempty"];
|
||||
// WindowsDesktopService represents a WindowsDesktopServiceV3 resource.
|
||||
types.WindowsDesktopServiceV3 WindowsDesktopService = 8 [(gogoproto.jsontag) = "windows_desktop_service,omitempty"];
|
||||
// DatabaseService represents a DatabaseService resource.
|
||||
types.DatabaseServiceV1 DatabaseService = 9 [(gogoproto.jsontag) = "database_service,omitempty"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2213,10 +2212,8 @@ service AuthService {
|
||||
// DeleteAllDatabaseServers removes all registered database proxy servers.
|
||||
rpc DeleteAllDatabaseServers(DeleteAllDatabaseServersRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// GetAllDatabaseServices returns all DatabaseService resources.
|
||||
rpc GetAllDatabaseServices(GetAllDatabaseServicesRequest) returns (DatabaseServiceV1List);
|
||||
// UpsertDatabaseService registers a new DatabaseService.
|
||||
rpc UpsertDatabaseService(UpsertDatabaseServiceRequest) returns (google.protobuf.Empty);
|
||||
rpc UpsertDatabaseService(UpsertDatabaseServiceRequest) returns (types.KeepAlive);
|
||||
// DeleteDatabaseService removes the specified DatabaseService.
|
||||
rpc DeleteDatabaseService(types.ResourceRequest) returns (google.protobuf.Empty);
|
||||
// DeleteAllDatabaseServices removes all registered DatabaseServices.
|
||||
|
||||
@@ -165,6 +165,19 @@ func (r ResourcesWithLabels) AsDatabaseServers() ([]DatabaseServer, error) {
|
||||
return dbs, nil
|
||||
}
|
||||
|
||||
// AsDatabaseServices converts each resource into type DatabaseService.
|
||||
func (r ResourcesWithLabels) AsDatabaseServices() ([]DatabaseService, error) {
|
||||
services := make([]DatabaseService, len(r))
|
||||
for i, resource := range r {
|
||||
dbService, ok := resource.(DatabaseService)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("expected types.DatabaseService, got: %T", resource)
|
||||
}
|
||||
services[i] = dbService
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// AsWindowsDesktops converts each resource into type WindowsDesktop.
|
||||
func (r ResourcesWithLabels) AsWindowsDesktops() ([]WindowsDesktop, error) {
|
||||
desktops := make([]WindowsDesktop, 0, len(r))
|
||||
|
||||
@@ -70,6 +70,9 @@ type Announcer interface {
|
||||
CreateWindowsDesktop(context.Context, types.WindowsDesktop) error
|
||||
// UpdateWindowsDesktop updates a Windows desktop host.
|
||||
UpdateWindowsDesktop(context.Context, types.WindowsDesktop) error
|
||||
|
||||
// UpsertDatabaseService registers a DatabaseService.
|
||||
UpsertDatabaseService(context.Context, types.DatabaseService) (*types.KeepAlive, error)
|
||||
}
|
||||
|
||||
// accessPoint is an API interface implemented by a certificate authority (CA)
|
||||
|
||||
@@ -148,6 +148,9 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
|
||||
if cfg.Databases == nil {
|
||||
cfg.Databases = local.NewDatabasesService(cfg.Backend)
|
||||
}
|
||||
if cfg.DatabaseServices == nil {
|
||||
cfg.DatabaseServices = local.NewDatabaseServicesService(cfg.Backend)
|
||||
}
|
||||
if cfg.Kubernetes == nil {
|
||||
cfg.Kubernetes = local.NewKubernetesService(cfg.Backend)
|
||||
}
|
||||
@@ -230,6 +233,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
|
||||
Apps: cfg.Apps,
|
||||
Kubernetes: cfg.Kubernetes,
|
||||
Databases: cfg.Databases,
|
||||
DatabaseServices: cfg.DatabaseServices,
|
||||
IAuditLog: cfg.AuditLog,
|
||||
Events: cfg.Events,
|
||||
WindowsDesktops: cfg.WindowsDesktops,
|
||||
@@ -310,6 +314,7 @@ type Services struct {
|
||||
services.Apps
|
||||
services.Kubernetes
|
||||
services.Databases
|
||||
services.DatabaseServices
|
||||
services.WindowsDesktops
|
||||
services.SessionTrackerService
|
||||
services.Enforcer
|
||||
|
||||
+10
-16
@@ -1254,7 +1254,7 @@ func (a *ServerWithRoles) ListResources(ctx context.Context, req proto.ListResou
|
||||
// https://github.com/gravitational/teleport/pull/1224
|
||||
actionVerbs = []string{types.VerbList}
|
||||
|
||||
case types.KindDatabaseServer, types.KindAppServer, types.KindKubeService, types.KindKubeServer, types.KindWindowsDesktop, types.KindWindowsDesktopService:
|
||||
case types.KindDatabaseServer, types.KindDatabaseService, types.KindAppServer, types.KindKubeService, types.KindKubeServer, types.KindWindowsDesktop, types.KindWindowsDesktopService:
|
||||
|
||||
default:
|
||||
return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType)
|
||||
@@ -1337,6 +1337,8 @@ func (r resourceChecker) CanAccess(resource types.Resource) error {
|
||||
return r.CheckAccess(rr.GetCluster(), mfaParams)
|
||||
case types.DatabaseServer:
|
||||
return r.CheckAccess(rr.GetDatabase(), mfaParams)
|
||||
case types.DatabaseService:
|
||||
return r.CheckAccess(rr, mfaParams)
|
||||
case types.Database:
|
||||
return r.CheckAccess(rr, mfaParams)
|
||||
case types.Server:
|
||||
@@ -1433,7 +1435,7 @@ func (k *kubeChecker) canAccessKubernetes(server types.KubeServer) error {
|
||||
// newResourceAccessChecker creates a resourceAccessChecker for the provided resource type
|
||||
func (a *ServerWithRoles) newResourceAccessChecker(resource string) (resourceAccessChecker, error) {
|
||||
switch resource {
|
||||
case types.KindAppServer, types.KindDatabaseServer, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindNode:
|
||||
case types.KindAppServer, types.KindDatabaseServer, types.KindDatabaseService, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindNode:
|
||||
return &resourceChecker{AccessChecker: a.context.Checker}, nil
|
||||
case types.KindKubeService, types.KindKubeServer:
|
||||
return newKubeChecker(a.context), nil
|
||||
@@ -3861,20 +3863,12 @@ func (a *ServerWithRoles) DeleteAllDatabaseServers(ctx context.Context, namespac
|
||||
return a.authServer.DeleteAllDatabaseServers(ctx, namespace)
|
||||
}
|
||||
|
||||
// GetAllDatabaseServices returns all DatabaseService resources.
|
||||
func (a *ServerWithRoles) GetAllDatabaseServices(ctx context.Context) ([]types.DatabaseService, error) {
|
||||
if err := a.action(apidefaults.Namespace, types.KindDatabaseService, types.VerbList, types.VerbRead); err != nil {
|
||||
// UpsertDatabaseService creates or updates a new DatabaseService resource.
|
||||
func (a *ServerWithRoles) UpsertDatabaseService(ctx context.Context, service types.DatabaseService) (*types.KeepAlive, error) {
|
||||
if err := a.action(service.GetNamespace(), types.KindDatabaseService, types.VerbCreate, types.VerbUpdate); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return nil, trace.NotImplemented("no DatabaseService service")
|
||||
}
|
||||
|
||||
// UpsertDatabaseService creates or updates a new DatabaseService resource.
|
||||
func (a *ServerWithRoles) UpsertDatabaseService(ctx context.Context, service types.DatabaseService) error {
|
||||
if err := a.action(service.GetNamespace(), types.KindDatabaseService, types.VerbCreate, types.VerbUpdate); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.NotImplemented("no DatabaseService service")
|
||||
return a.authServer.UpsertDatabaseService(ctx, service)
|
||||
}
|
||||
|
||||
// DeleteAllDatabaseServices removes all DatabaseService resources.
|
||||
@@ -3882,7 +3876,7 @@ func (a *ServerWithRoles) DeleteAllDatabaseServices(ctx context.Context) error {
|
||||
if err := a.action(apidefaults.Namespace, types.KindDatabaseService, types.VerbList, types.VerbDelete); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.NotImplemented("no DatabaseService service")
|
||||
return a.authServer.DeleteAllDatabaseServices(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabaseService removes a specific DatabaseService resource.
|
||||
@@ -3890,7 +3884,7 @@ func (a *ServerWithRoles) DeleteDatabaseService(ctx context.Context, name string
|
||||
if err := a.action(apidefaults.Namespace, types.KindDatabaseService, types.VerbDelete); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return trace.NotImplemented("no DatabaseService service")
|
||||
return a.authServer.DeleteDatabaseService(ctx, name)
|
||||
}
|
||||
|
||||
// SignDatabaseCSR generates a client certificate used by proxy when talking
|
||||
|
||||
@@ -2362,6 +2362,148 @@ func TestGetAndList_KubernetesServers(t *testing.T) {
|
||||
require.Empty(t, cmp.Diff(testResources[:0], resp.Resources))
|
||||
}
|
||||
|
||||
func TestListDatabaseServices(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
srv := newTestTLSServer(t)
|
||||
|
||||
numInitialResources := 5
|
||||
|
||||
// Create test Database Services.
|
||||
for i := 0; i < numInitialResources; i++ {
|
||||
name := uuid.NewString()
|
||||
s, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: name,
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{name},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = srv.Auth().UpsertDatabaseService(ctx, s)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
listServicesResp, err := srv.Auth().ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
databaseServices, err := types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
|
||||
testResources := make([]types.ResourceWithLabels, len(databaseServices))
|
||||
for i, server := range databaseServices {
|
||||
testResources[i] = server
|
||||
}
|
||||
|
||||
// Create user, role, and client
|
||||
username := "user"
|
||||
user, role, err := CreateUserAndRole(srv.Auth(), username, nil)
|
||||
require.NoError(t, err)
|
||||
identity := TestUser(user.GetName())
|
||||
clt, err := srv.NewClient(identity)
|
||||
require.NoError(t, err)
|
||||
|
||||
// User is not allowed to list DatabseServices
|
||||
_, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.True(t, trace.IsAccessDenied(err), "expected access denied because role does not allow Read operations")
|
||||
|
||||
// Change the user's role to allow them to list DatabaseServices
|
||||
currentAllowRules := role.GetRules(types.Allow)
|
||||
role.SetRules(types.Allow, append(currentAllowRules, types.NewRule(types.KindDatabaseService, services.RO())))
|
||||
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
|
||||
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
usersViewDBServices, err := types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, usersViewDBServices, numInitialResources)
|
||||
|
||||
require.Empty(t, cmp.Diff(databaseServices, usersViewDBServices))
|
||||
|
||||
// User is not allowed to Upsert DatabaseServices
|
||||
extraDatabaseService, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: "extra",
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"extra"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = clt.UpsertDatabaseService(ctx, extraDatabaseService)
|
||||
require.True(t, trace.IsAccessDenied(err), "expected access denied because role does not allow Create/Update operations")
|
||||
|
||||
currentAllowRules = role.GetRules(types.Allow)
|
||||
role.SetRules(types.Allow, append(currentAllowRules, types.NewRule(types.KindDatabaseService, services.RW())))
|
||||
require.NoError(t, srv.Auth().UpsertRole(ctx, role))
|
||||
|
||||
_, err = clt.UpsertDatabaseService(ctx, extraDatabaseService)
|
||||
require.NoError(t, err)
|
||||
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
usersViewDBServices, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, usersViewDBServices, numInitialResources+1)
|
||||
|
||||
// User can also delete a single or multiple DatabaseServices because they have RW permissions now
|
||||
err = clt.DeleteDatabaseService(ctx, extraDatabaseService.GetName())
|
||||
require.NoError(t, err)
|
||||
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
usersViewDBServices, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, usersViewDBServices, numInitialResources)
|
||||
|
||||
// After removing all resources, we should have 0 resources being returned.
|
||||
err = clt.DeleteAllDatabaseServices(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: defaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
usersViewDBServices, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, usersViewDBServices)
|
||||
}
|
||||
|
||||
func TestListResources_NeedTotalCountFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -1575,6 +1575,7 @@ type ClientI interface {
|
||||
services.Restrictions
|
||||
services.Apps
|
||||
services.Databases
|
||||
services.DatabaseServices
|
||||
services.Kubernetes
|
||||
services.WindowsDesktops
|
||||
WebService
|
||||
|
||||
+10
-28
@@ -1135,43 +1135,18 @@ func (g *GRPCServer) DeleteAllDatabaseServers(ctx context.Context, req *proto.De
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// GetAllDatabaseServices returns all registered DatabaseServices.
|
||||
func (g *GRPCServer) GetAllDatabaseServices(ctx context.Context, _ *proto.GetAllDatabaseServicesRequest) (*proto.DatabaseServiceV1List, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
dbs, err := auth.GetAllDatabaseServices(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
ret := &proto.DatabaseServiceV1List{
|
||||
Services: make([]*types.DatabaseServiceV1, len(dbs)),
|
||||
}
|
||||
for i, db := range dbs {
|
||||
dbServiceV1, ok := db.(*types.DatabaseServiceV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unexpected DatabaseService type: %T", db)
|
||||
}
|
||||
ret.Services[i] = dbServiceV1
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// UpsertDatabaseService registers a new database service.
|
||||
func (g *GRPCServer) UpsertDatabaseService(ctx context.Context, req *proto.UpsertDatabaseServiceRequest) (*emptypb.Empty, error) {
|
||||
func (g *GRPCServer) UpsertDatabaseService(ctx context.Context, req *proto.UpsertDatabaseServiceRequest) (*types.KeepAlive, error) {
|
||||
auth, err := g.authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = auth.UpsertDatabaseService(ctx, req.Service)
|
||||
keepAlive, err := auth.UpsertDatabaseService(ctx, req.Service)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
return keepAlive, nil
|
||||
}
|
||||
|
||||
// DeleteDatabaseService removes the specified DatabaseService.
|
||||
@@ -3900,6 +3875,13 @@ func (g *GRPCServer) ListResources(ctx context.Context, req *proto.ListResources
|
||||
}
|
||||
|
||||
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseServer{DatabaseServer: database}}
|
||||
case types.KindDatabaseService:
|
||||
databaseService, ok := resource.(*types.DatabaseServiceV1)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("database service has invalid type %T", resource)
|
||||
}
|
||||
|
||||
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseService{DatabaseService: databaseService}}
|
||||
case types.KindAppServer:
|
||||
app, ok := resource.(*types.AppServerV3)
|
||||
if !ok {
|
||||
|
||||
@@ -1883,6 +1883,133 @@ func TestDatabasesCRUD(t *testing.T) {
|
||||
require.Len(t, out, 0)
|
||||
}
|
||||
|
||||
// TestDatabaseServicesCRUD tests DatabaseService resource operations.
|
||||
func TestDatabaseServicesCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
srv := newTestTLSServer(t)
|
||||
|
||||
clt, err := srv.NewClient(TestAdmin())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create two DatabaseServices.
|
||||
db1, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: "db1",
|
||||
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"prod"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
db2, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: "db2",
|
||||
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"stg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially we expect no DatabaseServices.
|
||||
listServicesResp, err := clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: apidefaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
out, err := types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
|
||||
// Create both DatabaseServices.
|
||||
_, err = clt.UpsertDatabaseService(ctx, db1)
|
||||
require.NoError(t, err)
|
||||
_, err = clt.UpsertDatabaseService(ctx, db2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch all DatabaseServices.
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: apidefaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.DatabaseService{db1, db2}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
||||
))
|
||||
|
||||
// Update a DatabaseService.
|
||||
db1.Spec.ResourceMatchers[0] = &types.DatabaseResourceMatcher{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"notprod"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = clt.UpsertDatabaseService(ctx, db1)
|
||||
require.NoError(t, err)
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: apidefaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.DatabaseService{db1, db2}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
||||
))
|
||||
|
||||
// Delete a DatabaseService.
|
||||
err = clt.DeleteDatabaseService(ctx, db1.GetName())
|
||||
require.NoError(t, err)
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: apidefaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.DatabaseService{db2}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
||||
))
|
||||
|
||||
// Try to delete a DatabaseService that doesn't exist.
|
||||
err = clt.DeleteDatabaseService(ctx, "doesnotexist")
|
||||
require.IsType(t, trace.NotFound(""), err)
|
||||
|
||||
// Delete all DatabaseServices.
|
||||
err = clt.DeleteAllDatabaseServices(ctx)
|
||||
require.NoError(t, err)
|
||||
listServicesResp, err = clt.ListResources(ctx,
|
||||
proto.ListResourcesRequest{
|
||||
ResourceType: types.KindDatabaseService,
|
||||
Limit: apidefaults.DefaultChunkSize,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
}
|
||||
|
||||
func TestListResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -135,6 +135,9 @@ type InitConfig struct {
|
||||
// Databases is a service that manages database resources.
|
||||
Databases services.Databases
|
||||
|
||||
// DatabaseServices is a service that manages DatabaseService resources.
|
||||
DatabaseServices services.DatabaseServices
|
||||
|
||||
// Status is a service that manages cluster status info.
|
||||
Status services.StatusInternal
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2022 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 services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// DatabaseServices defines an interface for managing DatabaseService resources.
|
||||
type DatabaseServices interface {
|
||||
// DeleteDatabasService removes the specified DatabaseService resource.
|
||||
DeleteDatabaseService(ctx context.Context, name string) error
|
||||
// DeleteAllDatabaseServices removes all DatabaseService resources.
|
||||
DeleteAllDatabaseServices(context.Context) error
|
||||
}
|
||||
|
||||
// MarshalDatabaseService marshals the DatabaseService resource to JSON.
|
||||
func MarshalDatabaseService(databaseService types.DatabaseService, opts ...MarshalOption) ([]byte, error) {
|
||||
if err := databaseService.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
switch databaseService := databaseService.(type) {
|
||||
case *types.DatabaseServiceV1:
|
||||
if !cfg.PreserveResourceID {
|
||||
// avoid modifying the original object
|
||||
// to prevent unexpected data races
|
||||
copy := *databaseService
|
||||
copy.SetResourceID(0)
|
||||
databaseService = ©
|
||||
}
|
||||
return utils.FastMarshal(databaseService)
|
||||
default:
|
||||
return nil, trace.BadParameter("unrecognized DatabaseService version %T", databaseService)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalDatabaseService unmarshals the DatabaseService resource from JSON.
|
||||
func UnmarshalDatabaseService(data []byte, opts ...MarshalOption) (types.DatabaseService, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, trace.BadParameter("missing DatabaseService data")
|
||||
}
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
var h types.ResourceHeader
|
||||
if err := utils.FastUnmarshal(data, &h); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
switch h.Version {
|
||||
case types.V1:
|
||||
var s types.DatabaseServiceV1
|
||||
if err := utils.FastUnmarshal(data, &s); err != nil {
|
||||
return nil, trace.BadParameter(err.Error())
|
||||
}
|
||||
if err := s.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if cfg.ID != 0 {
|
||||
s.SetResourceID(cfg.ID)
|
||||
}
|
||||
if !cfg.Expires.IsZero() {
|
||||
s.SetExpiry(cfg.Expires)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
return nil, trace.BadParameter("database service resource version %q is not supported", h.Version)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2022 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 services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// TestDatabaseServiceUnmarshal verifies that DatabaseService resource can be unmarshaled.
|
||||
func TestDatabaseServiceUnmarshal(t *testing.T) {
|
||||
expected, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: "test-database-service",
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"prod", "stg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
data, err := utils.ToJSON([]byte(dbServiceYAML))
|
||||
require.NoError(t, err)
|
||||
actual, err := UnmarshalDatabaseService(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestDatabaseServicenMarshal verifies a marshaled kubernetes resource resource can be unmarshaled back.
|
||||
func TestDatabaseServiceMarshal(t *testing.T) {
|
||||
expected, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: "test-database-service",
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"prod"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
data, err := MarshalDatabaseService(expected)
|
||||
require.NoError(t, err)
|
||||
actual, err := UnmarshalDatabaseService(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
var dbServiceYAML = `---
|
||||
kind: database_service
|
||||
version: v1
|
||||
metadata:
|
||||
name: test-database-service
|
||||
spec:
|
||||
resources:
|
||||
- labels:
|
||||
env: [prod, stg]
|
||||
`
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2022 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 local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
)
|
||||
|
||||
// DatabaseServicesService manages DatabaseService resources in the backend.
|
||||
type DatabaseServicesService struct {
|
||||
backend.Backend
|
||||
}
|
||||
|
||||
// NewDatabaseServicesService creates a new DatabaseServicesService.
|
||||
func NewDatabaseServicesService(backend backend.Backend) *DatabaseServicesService {
|
||||
return &DatabaseServicesService{Backend: backend}
|
||||
}
|
||||
|
||||
// DeleteDatabaseService removes the specified DatabaseService resource.
|
||||
func (s *DatabaseServicesService) DeleteDatabaseService(ctx context.Context, name string) error {
|
||||
err := s.Delete(ctx, backend.Key(databaseServicePrefix, name))
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return trace.NotFound("databaseService %q doesn't exist", name)
|
||||
}
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllDatabaseServices removes all DatabaseService resources.
|
||||
func (s *DatabaseServicesService) DeleteAllDatabaseServices(ctx context.Context) error {
|
||||
startKey := backend.Key(databaseServicePrefix)
|
||||
err := s.DeleteRange(ctx, startKey, backend.RangeEnd(startKey))
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
databaseServicePrefix = "databaseService"
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2022 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 local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/backend/memory"
|
||||
)
|
||||
|
||||
func TestDatabaseServices(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
clock := clockwork.NewFakeClock()
|
||||
|
||||
bk, err := memory.New(memory.Config{
|
||||
Context: ctx,
|
||||
Clock: clock,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
service := NewDatabaseServicesService(bk)
|
||||
presence := NewPresenceService(bk)
|
||||
|
||||
// Upsert some DatabaseServices.
|
||||
ds1, err := types.NewDatabaseServiceV1(
|
||||
types.Metadata{Name: "ds1"},
|
||||
types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"ds1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = presence.UpsertDatabaseService(ctx, ds1)
|
||||
require.NoError(t, err)
|
||||
|
||||
ds2, err := types.NewDatabaseServiceV1(
|
||||
types.Metadata{Name: "ds2"},
|
||||
types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{
|
||||
"env": []string{"ds2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = presence.UpsertDatabaseService(ctx, ds2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Replace the DS1
|
||||
initialResourceMatchers := ds1.GetResourceMatchers()
|
||||
initialResourceMatchers[0] = &types.DatabaseResourceMatcher{
|
||||
Labels: &types.Labels{"env": []string{"ds1", "ds2"}},
|
||||
}
|
||||
ds1.Spec.ResourceMatchers = initialResourceMatchers
|
||||
_, err = presence.UpsertDatabaseService(ctx, ds1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Remove one of the DatabaseServices
|
||||
err = service.DeleteDatabaseService(ctx, ds2.GetName())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Remove all of the DatabaseServices
|
||||
err = service.DeleteAllDatabaseServices(ctx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1508,6 +1508,9 @@ func (s *PresenceService) listResources(ctx context.Context, req proto.ListResou
|
||||
case types.KindDatabaseServer:
|
||||
keyPrefix = []string{dbServersPrefix, req.Namespace}
|
||||
unmarshalItemFunc = backendItemToDatabaseServer
|
||||
case types.KindDatabaseService:
|
||||
keyPrefix = []string{databaseServicePrefix}
|
||||
unmarshalItemFunc = backendItemToDatabaseService
|
||||
case types.KindAppServer:
|
||||
keyPrefix = []string{appServersPrefix, req.Namespace}
|
||||
unmarshalItemFunc = backendItemToApplicationServer
|
||||
@@ -1577,6 +1580,38 @@ func (s *PresenceService) listResources(ctx context.Context, req proto.ListResou
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpsertDatabaseService creates or updates (by name) a DatabaseService resource.
|
||||
func (s *PresenceService) UpsertDatabaseService(ctx context.Context, service types.DatabaseService) (*types.KeepAlive, error) {
|
||||
if err := service.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
value, err := services.MarshalDatabaseService(service)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
item := backend.Item{
|
||||
Key: backend.Key(databaseServicePrefix, service.GetName()),
|
||||
Value: value,
|
||||
Expires: service.Expiry(),
|
||||
ID: service.GetResourceID(),
|
||||
}
|
||||
lease, err := s.Put(ctx, item)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if service.Expiry().IsZero() {
|
||||
return &types.KeepAlive{}, nil
|
||||
}
|
||||
return &types.KeepAlive{
|
||||
Type: types.KeepAlive_DATABASE,
|
||||
LeaseID: lease.ID,
|
||||
Namespace: apidefaults.Namespace,
|
||||
Name: service.GetName(),
|
||||
HostID: service.GetName(),
|
||||
Expires: service.Expiry(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// listResourcesWithSort supports sorting by falling back to retrieving all resources
|
||||
// with GetXXXs, filter, and then fake pagination.
|
||||
func (s *PresenceService) listResourcesWithSort(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
|
||||
@@ -1727,6 +1762,16 @@ func backendItemToDatabaseServer(item backend.Item) (types.ResourceWithLabels, e
|
||||
)
|
||||
}
|
||||
|
||||
// backendItemToDatabaseService unmarshals `backend.Item` into a
|
||||
// `types.DatabaseService`, returning it as a `types.ResourceWithLabels`.
|
||||
func backendItemToDatabaseService(item backend.Item) (types.ResourceWithLabels, error) {
|
||||
return services.UnmarshalDatabaseService(
|
||||
item.Value,
|
||||
services.WithResourceID(item.ID),
|
||||
services.WithExpires(item.Expires),
|
||||
)
|
||||
}
|
||||
|
||||
// backendItemToApplicationServer unmarshals `backend.Item` into a
|
||||
// `types.AppServer`, returning it as a `types.ResourceWithLabels`.
|
||||
func backendItemToApplicationServer(item backend.Item) (types.ResourceWithLabels, error) {
|
||||
|
||||
@@ -130,7 +130,7 @@ func MatchResourceByFilters(resource types.ResourceWithLabels, filter MatchResou
|
||||
// the user is wanting to filter the contained resource ie. KubeClusters, Application, and Database.
|
||||
resourceKey := ResourceSeenKey{}
|
||||
switch filter.ResourceKind {
|
||||
case types.KindNode, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindKubernetesCluster:
|
||||
case types.KindNode, types.KindWindowsDesktop, types.KindWindowsDesktopService, types.KindKubernetesCluster, types.KindDatabaseService:
|
||||
specResource = resource
|
||||
resourceKey.name = specResource.GetName()
|
||||
|
||||
|
||||
@@ -233,4 +233,7 @@ type Presence interface {
|
||||
|
||||
// ListResoures returns a paginated list of resources.
|
||||
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
|
||||
|
||||
// UpsertDatabaseService updates an existing DatabaseService resource.
|
||||
UpsertDatabaseService(context.Context, types.DatabaseService) (*types.KeepAlive, error)
|
||||
}
|
||||
|
||||
@@ -2101,10 +2101,16 @@ func (set RoleSet) checkAccess(r AccessCheckable, mfa AccessMFAParams, matchers
|
||||
additionalDeniedMessage := ""
|
||||
|
||||
var getRoleLabels func(types.Role, types.RoleConditionType) types.Labels
|
||||
resourceHasLabels := true
|
||||
getEmptyLabels := func(r types.Role, rct types.RoleConditionType) types.Labels { return types.Labels{} }
|
||||
|
||||
switch r.GetKind() {
|
||||
case types.KindDatabase:
|
||||
getRoleLabels = types.Role.GetDatabaseLabels
|
||||
additionalDeniedMessage = "Confirm database user and name."
|
||||
case types.KindDatabaseService:
|
||||
resourceHasLabels = false
|
||||
getRoleLabels = getEmptyLabels
|
||||
case types.KindApp:
|
||||
getRoleLabels = types.Role.GetAppLabels
|
||||
case types.KindNode:
|
||||
@@ -2171,7 +2177,9 @@ func (set RoleSet) checkAccess(r AccessCheckable, mfa AccessMFAParams, matchers
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if !matchLabels {
|
||||
|
||||
// Deny only if the resourceHasLabels but there was no match.
|
||||
if resourceHasLabels && !matchLabels {
|
||||
if isDebugEnabled {
|
||||
errs = append(errs, trace.AccessDenied("role=%v, match(label=%v)",
|
||||
role.GetName(), labelsMessage))
|
||||
|
||||
@@ -31,6 +31,7 @@ type Services interface {
|
||||
Restrictions
|
||||
Apps
|
||||
Databases
|
||||
DatabaseServices
|
||||
Kubernetes
|
||||
AppSession
|
||||
SnowflakeSession
|
||||
|
||||
@@ -405,6 +405,9 @@ func (f *fakeAnnouncer) UpdateWindowsDesktop(ctx context.Context, s types.Window
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeAnnouncer) UpsertDatabaseService(ctx context.Context, s types.DatabaseService) (*types.KeepAlive, error) {
|
||||
return nil, trace.NotImplemented("not yet implemented")
|
||||
}
|
||||
func (f *fakeAnnouncer) NewKeepAliver(ctx context.Context) (types.KeepAliver, error) {
|
||||
return f, f.err
|
||||
}
|
||||
|
||||
@@ -596,6 +596,9 @@ func (h *Handler) bindDefaultEndpoints(challengeLimiter *limiter.RateLimiter) {
|
||||
h.GET("/webapi/sites/:site/databases/:database", h.WithClusterAuth(h.clusterDatabaseGet))
|
||||
h.GET("/webapi/sites/:site/databases/:database/iam/policy", h.WithClusterAuth(h.handleDatabaseGetIAMPolicy))
|
||||
|
||||
// DatabaseService handlers
|
||||
h.GET("/webapi/sites/:site/databaseservices", h.WithClusterAuth(h.clusterDatabaseServicesList))
|
||||
|
||||
// Kube access handlers.
|
||||
h.GET("/webapi/sites/:site/kubernetes", h.WithClusterAuth(h.clusterKubesGet))
|
||||
|
||||
|
||||
@@ -22,13 +22,16 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/roundtrip"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
awslib "github.com/gravitational/teleport/lib/cloud/aws"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
dbiam "github.com/gravitational/teleport/lib/srv/db/common/iam"
|
||||
"github.com/gravitational/teleport/lib/web/ui"
|
||||
)
|
||||
|
||||
func TestCreateDatabaseRequestParameters(t *testing.T) {
|
||||
@@ -264,6 +267,73 @@ func TestHandleDatabasesGetIAMPolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type listDatabaseServicesResp struct {
|
||||
Items []ui.DatabaseService `json:"items"`
|
||||
}
|
||||
|
||||
func TestHandleDatabaseServicesGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
user := "user"
|
||||
roleRODatabaseServices, err := types.NewRole(services.RoleNameForUser(user), types.RoleSpecV5{
|
||||
Allow: types.RoleConditions{
|
||||
Rules: []types.Rule{
|
||||
types.NewRule(types.KindDatabaseService,
|
||||
[]string{types.VerbRead, types.VerbList}),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
env := newWebPack(t, 1)
|
||||
proxy := env.proxies[0]
|
||||
pack := proxy.authPack(t, user, []types.Role{roleRODatabaseServices})
|
||||
|
||||
var listDBServicesResp listDatabaseServicesResp
|
||||
|
||||
// No DatabaseServices exist
|
||||
resp, err := pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "databaseservices"), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.Code())
|
||||
require.NoError(t, json.Unmarshal(resp.Bytes(), &listDBServicesResp))
|
||||
|
||||
require.Empty(t, listDBServicesResp.Items)
|
||||
|
||||
// Adding one DatabaseService
|
||||
dbServiceName := uuid.NewString()
|
||||
dbService001, err := types.NewDatabaseServiceV1(types.Metadata{
|
||||
Name: dbServiceName,
|
||||
}, types.DatabaseServiceSpecV1{
|
||||
ResourceMatchers: []*types.DatabaseResourceMatcher{
|
||||
{
|
||||
Labels: &types.Labels{"env": []string{"prod"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = env.server.Auth().UpsertDatabaseService(ctx, dbService001)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The API returns one DatabaseService.
|
||||
resp, err = pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "databaseservices"), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.Code())
|
||||
require.NoError(t, json.Unmarshal(resp.Bytes(), &listDBServicesResp))
|
||||
|
||||
dbServices := listDBServicesResp.Items
|
||||
require.Len(t, dbServices, 1)
|
||||
respDBService := dbServices[0]
|
||||
|
||||
require.Equal(t, respDBService.Name, dbServiceName)
|
||||
|
||||
require.Len(t, respDBService.ResourceMatchers, 1)
|
||||
respResourceMatcher := respDBService.ResourceMatchers[0]
|
||||
|
||||
require.Equal(t, respResourceMatcher.Labels, &types.Labels{"env": []string{"prod"}})
|
||||
}
|
||||
|
||||
func mustCreateDatabaseServer(t *testing.T, db *types.DatabaseV3) types.DatabaseServer {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -125,6 +125,30 @@ func (h *Handler) clusterDatabaseGet(w http.ResponseWriter, r *http.Request, p h
|
||||
return ui.MakeDatabase(database, dbUsers, dbNames), nil
|
||||
}
|
||||
|
||||
// clusterDatabaseServicesList returns a list of DatabaseServices (database agents) in a form the UI can present.
|
||||
func (h *Handler) clusterDatabaseServicesList(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
clt, err := ctx.GetUserClient(r.Context(), site)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
resp, err := listResources(clt, r, types.KindDatabaseService)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
servers, err := types.ResourcesWithLabels(resp.Resources).AsDatabaseServices()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return listResourcesGetResponse{
|
||||
Items: ui.MakeDatabaseServices(servers),
|
||||
StartKey: resp.NextKey,
|
||||
TotalCount: resp.TotalCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// clusterDesktopsGet returns a list of desktops in a form the UI can present.
|
||||
func (h *Handler) clusterDesktopsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
|
||||
clt, err := sctx.GetUserClient(r.Context(), site)
|
||||
|
||||
@@ -231,6 +231,33 @@ func MakeDatabases(databases []types.Database) []Database {
|
||||
return uiServers
|
||||
}
|
||||
|
||||
// DatabaseService describes a DatabaseService resource.
|
||||
type DatabaseService struct {
|
||||
// Name is the name of the database.
|
||||
Name string `json:"name"`
|
||||
// ResourceMatchers is a list of resource matchers of the DatabaseService.
|
||||
ResourceMatchers []*types.DatabaseResourceMatcher `json:"resource_matchers"`
|
||||
}
|
||||
|
||||
// MakeDatabaseService creates DatabaseService resource.
|
||||
func MakeDatabaseService(databaseService types.DatabaseService) DatabaseService {
|
||||
return DatabaseService{
|
||||
Name: databaseService.GetName(),
|
||||
ResourceMatchers: databaseService.GetResourceMatchers(),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeDatabaseServices creates database service objects.
|
||||
func MakeDatabaseServices(databaseServices []types.DatabaseService) []DatabaseService {
|
||||
dbServices := make([]DatabaseService, len(databaseServices))
|
||||
for i, database := range databaseServices {
|
||||
db := MakeDatabaseService(database)
|
||||
dbServices[i] = db
|
||||
}
|
||||
|
||||
return dbServices
|
||||
}
|
||||
|
||||
// Desktop describes a desktop to pass to the ui.
|
||||
type Desktop struct {
|
||||
// OS is the os of this desktop. Should be one of constants.WindowsOS, constants.LinuxOS, or constants.DarwinOS.
|
||||
|
||||
Reference in New Issue
Block a user