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:
Marco André Dinis
2022-12-21 14:34:36 +01:00
committed by GitHub
parent afd1fa7934
commit 69dc237fe2
26 changed files with 1786 additions and 1067 deletions
+28
View File
@@ -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:
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.
+13
View File
@@ -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))
+3
View File
@@ -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)
+5
View File
@@ -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
View File
@@ -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
+142
View File
@@ -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()
+1
View File
@@ -1575,6 +1575,7 @@ type ClientI interface {
services.Restrictions
services.Apps
services.Databases
services.DatabaseServices
services.Kubernetes
services.WindowsDesktops
WebService
+10 -28
View File
@@ -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 {
+127
View File
@@ -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()
+3
View File
@@ -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
+93
View File
@@ -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 = &copy
}
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)
}
+80
View File
@@ -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]
`
+61
View File
@@ -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)
}
+45
View File
@@ -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) {
+1 -1
View File
@@ -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()
+3
View File
@@ -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)
}
+9 -1
View File
@@ -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))
+1
View File
@@ -31,6 +31,7 @@ type Services interface {
Restrictions
Apps
Databases
DatabaseServices
Kubernetes
AppSession
SnowflakeSession
+3
View File
@@ -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
}
+3
View File
@@ -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))
+70
View File
@@ -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()
+24
View File
@@ -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)
+27
View File
@@ -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.