Files
teleport/lib/web/databases.go
T
Gavin Frazar 9723fd096f cleanup after db ca split (#41351)
* remove database client CA event filter injection

* remove tctl database client CA fetch fallback

* remove sqlserver database client CA fetch fallback

* remove database client CA migration

* cleanup tests and mocks
2024-05-09 19:51:27 +00:00

442 lines
13 KiB
Go

/*
* Teleport
* Copyright (C) 2023 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 web
import (
"context"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net"
"net/http"
"net/url"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/tlsutils"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/services"
dbiam "github.com/gravitational/teleport/lib/srv/db/common/iam"
"github.com/gravitational/teleport/lib/web/scripts"
"github.com/gravitational/teleport/lib/web/ui"
)
// createDatabaseRequest contains the necessary basic information to create a database.
// Database here is the database resource, containing information to a real database (protocol, uri)
type createDatabaseRequest struct {
Name string `json:"name,omitempty"`
Labels []ui.Label `json:"labels,omitempty"`
Protocol string `json:"protocol,omitempty"`
URI string `json:"uri,omitempty"`
AWSRDS *awsRDS `json:"awsRds,omitempty"`
}
type awsRDS struct {
AccountID string `json:"accountId,omitempty"`
ResourceID string `json:"resourceId,omitempty"`
Subnets []string `json:"subnets,omitempty"`
VPCID string `json:"vpcId,omitempty"`
}
func (r *createDatabaseRequest) checkAndSetDefaults() error {
if r.Name == "" {
return trace.BadParameter("missing database name")
}
if r.Protocol == "" {
return trace.BadParameter("missing protocol")
}
if r.URI == "" {
return trace.BadParameter("missing uri")
}
if r.AWSRDS != nil {
if r.AWSRDS.ResourceID == "" {
return trace.BadParameter("missing aws rds field resource id")
}
if r.AWSRDS.AccountID == "" {
return trace.BadParameter("missing aws rds field account id")
}
if len(r.AWSRDS.Subnets) == 0 {
return trace.BadParameter("missing aws rds field subnets")
}
if r.AWSRDS.VPCID == "" {
return trace.BadParameter("missing aws rds field vpc id")
}
}
return nil
}
// handleDatabaseCreate creates a database's metadata.
func (h *Handler) handleDatabaseCreate(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
var req *createDatabaseRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
if err := req.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
database, err := getNewDatabaseResource(*req)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}
if err := clt.CreateDatabase(r.Context(), database); err != nil {
if trace.IsAlreadyExists(err) {
return nil, trace.AlreadyExists("failed to create database (%q already exists), please use another name", req.Name)
}
return nil, trace.Wrap(err)
}
accessChecker, err := sctx.GetUserAccessChecker()
if err != nil {
return nil, trace.Wrap(err)
}
dbNames, dbUsers, err := getDatabaseUsersAndNames(accessChecker)
if err != nil {
return nil, trace.Wrap(err)
}
return ui.MakeDatabase(database, dbUsers, dbNames, false /* requiresRequest */), nil
}
// updateDatabaseRequest contains some updatable fields of a database resource.
type updateDatabaseRequest struct {
CACert *string `json:"caCert,omitempty"`
Labels []ui.Label `json:"labels,omitempty"`
URI string `json:"uri,omitempty"`
AWSRDS *awsRDS `json:"awsRds,omitempty"`
}
func (r *updateDatabaseRequest) checkAndSetDefaults() error {
if r.CACert != nil {
if *r.CACert == "" {
return trace.BadParameter("missing CA certificate data")
}
if _, err := tlsutils.ParseCertificatePEM([]byte(*r.CACert)); err != nil {
return trace.BadParameter("could not parse provided CA as X.509 PEM certificate")
}
}
// These fields can't be empty if set.
if r.AWSRDS != nil {
if r.AWSRDS.ResourceID == "" {
return trace.BadParameter("missing aws rds field resource id")
}
if r.AWSRDS.AccountID == "" {
return trace.BadParameter("missing aws rds field account id")
}
}
if r.CACert == nil && r.AWSRDS == nil && r.Labels == nil && r.URI == "" {
return trace.BadParameter("missing fields to update the database")
}
return nil
}
// handleDatabaseUpdate updates the database
func (h *Handler) handleDatabaseUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
databaseName := p.ByName("database")
if databaseName == "" {
return nil, trace.BadParameter("a database name is required")
}
var req *updateDatabaseRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
if err := req.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}
database, err := clt.GetDatabase(r.Context(), databaseName)
if err != nil {
return nil, trace.Wrap(err)
}
savedOrNewCaCert := database.GetCA()
if req.CACert != nil {
savedOrNewCaCert = *req.CACert
}
savedOrNewAWSRDS := awsRDS{
AccountID: database.GetAWS().AccountID,
ResourceID: database.GetAWS().RDS.ResourceID,
}
if req.AWSRDS != nil {
savedOrNewAWSRDS = awsRDS{
AccountID: req.AWSRDS.AccountID,
ResourceID: req.AWSRDS.ResourceID,
}
}
savedOrNewURI := req.URI
if len(savedOrNewURI) == 0 {
savedOrNewURI = database.GetURI()
}
savedLabels := database.GetStaticLabels()
// Make a new database to reset the check and set defaulted fields.
database, err = getNewDatabaseResource(createDatabaseRequest{
Name: databaseName,
Protocol: database.GetProtocol(),
URI: savedOrNewURI,
Labels: req.Labels,
AWSRDS: &savedOrNewAWSRDS,
})
if err != nil {
return nil, trace.Wrap(err)
}
database.SetCA(savedOrNewCaCert)
if len(req.Labels) == 0 {
database.SetStaticLabels(savedLabels)
}
if err := clt.UpdateDatabase(r.Context(), database); err != nil {
return nil, trace.Wrap(err)
}
return ui.MakeDatabase(database, nil /* dbUsers */, nil /* dbNames */, false /* requiresRequest */), nil
}
// databaseIAMPolicyResponse is the response type for handleDatabaseGetIAMPolicy.
type databaseIAMPolicyResponse struct {
// Type is the type of the IAM policy.
Type string `json:"type"`
// AWS contains the IAM policy for AWS-hosted databases.
AWS *databaseIAMPolicyAWS `json:"aws,omitempty"`
}
// databaseIAMPolicyAWS contains IAM policy for AWS-hosted databases.
type databaseIAMPolicyAWS struct {
// PolicyDocument is the AWS IAM policy document.
PolicyDocument string `json:"policy_document"`
// Placeholders are placeholders found in the policy document.
Placeholders []string `json:"placeholders,omitempty"`
}
// handleDatabaseGetIAMPolicy returns the required IAM policy for database.
func (h *Handler) handleDatabaseGetIAMPolicy(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
databaseName := p.ByName("database")
if databaseName == "" {
return nil, trace.BadParameter("missing database name")
}
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}
database, err := fetchDatabaseWithName(r.Context(), clt, r, databaseName)
if err != nil {
return nil, trace.Wrap(err)
}
switch {
case database.IsAWSHosted():
policy, placeholders, err := dbiam.GetAWSPolicyDocument(database)
if err != nil {
return nil, trace.Wrap(err)
}
policyJSON, err := json.Marshal(policy)
if err != nil {
return nil, trace.Wrap(err)
}
return &databaseIAMPolicyResponse{
Type: "aws",
AWS: &databaseIAMPolicyAWS{
PolicyDocument: string(policyJSON),
Placeholders: placeholders,
},
}, nil
default:
return nil, trace.BadParameter("IAM policy not supported for database type %q", database.GetType())
}
}
func (h *Handler) sqlServerConfigureADScriptHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
tokenStr := p.ByName("token")
if err := validateJoinToken(tokenStr); err != nil {
return "", trace.Wrap(err)
}
dbAddress := r.URL.Query().Get("uri")
if err := services.ValidateSQLServerURI(dbAddress); err != nil {
return "", trace.BadParameter("invalid database address: %v", err)
}
// verify that the token exists
if _, err := h.GetProxyClient().GetToken(r.Context(), tokenStr); err != nil {
return "", trace.BadParameter("invalid token")
}
proxyServers, err := h.GetProxyClient().GetProxies()
if err != nil {
return "", trace.Wrap(err)
}
if len(proxyServers) == 0 {
return "", trace.NotFound("no proxy servers found")
}
clusterName, err := h.GetProxyClient().GetDomainName(r.Context())
if err != nil {
return nil, trace.Wrap(err)
}
certAuthority, err := h.GetProxyClient().GetCertAuthority(
r.Context(),
types.CertAuthID{Type: types.DatabaseClientCA, DomainName: clusterName},
false,
)
if err != nil {
return nil, trace.Wrap(err)
}
caCRL, err := h.GetProxyClient().GenerateCertAuthorityCRL(r.Context(), types.DatabaseClientCA)
if err != nil {
return nil, trace.Wrap(err)
}
if len(certAuthority.GetActiveKeys().TLS) != 1 {
return nil, trace.BadParameter("expected one TLS key pair, got %v", len(certAuthority.GetActiveKeys().TLS))
}
keyPair := certAuthority.GetActiveKeys().TLS[0]
block, _ := pem.Decode(keyPair.Cert)
if block == nil {
return nil, trace.BadParameter("no PEM data in CA data")
}
// Split host and port so we can escape domain characters.
dbHost, dbPort, err := net.SplitHostPort(dbAddress)
if err != nil {
return nil, trace.Wrap(err)
}
httplib.SetScriptHeaders(w.Header())
w.WriteHeader(http.StatusOK)
err = scripts.DatabaseAccessSQLServerConfigureScript.Execute(w, scripts.DatabaseAccessSQLServerConfigureParams{
CACertPEM: string(keyPair.Cert),
CACertSHA1: fmt.Sprintf("%X", sha1.Sum(block.Bytes)),
CACertBase64: base64.StdEncoding.EncodeToString(createCertificateBlob(block.Bytes)),
CRLPEM: string(encodeCRLPEM(caCRL)),
ProxyPublicAddr: proxyServers[0].GetPublicAddr(),
ProvisionToken: tokenStr,
DBAddress: net.JoinHostPort(url.QueryEscape(dbHost), dbPort),
})
return nil, trace.Wrap(err)
}
// fetchDatabaseWithName fetch a database with provided database name.
func fetchDatabaseWithName(ctx context.Context, clt resourcesAPIGetter, r *http.Request, databaseName string) (types.Database, error) {
resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{
Limit: defaults.MaxIterationLimit,
ResourceType: types.KindDatabaseServer,
PredicateExpression: fmt.Sprintf(`name == "%s"`, databaseName),
UseSearchAsRoles: r.URL.Query().Get("searchAsRoles") == "yes",
})
if err != nil {
return nil, trace.Wrap(err)
}
servers, err := types.ResourcesWithLabels(resp.Resources).AsDatabaseServers()
if err != nil {
return nil, trace.Wrap(err)
}
switch len(servers) {
case 0:
return nil, trace.NotFound("database %q not found", databaseName)
default:
return servers[0].GetDatabase(), nil
}
}
func getNewDatabaseResource(req createDatabaseRequest) (*types.DatabaseV3, error) {
labels := make(map[string]string)
for _, label := range req.Labels {
labels[label.Name] = label.Value
}
dbSpec := types.DatabaseSpecV3{
Protocol: req.Protocol,
URI: req.URI,
}
if req.AWSRDS != nil {
dbSpec.AWS = types.AWS{
AccountID: req.AWSRDS.AccountID,
RDS: types.RDS{
ResourceID: req.AWSRDS.ResourceID,
Subnets: req.AWSRDS.Subnets,
VPCID: req.AWSRDS.VPCID,
},
}
}
database, err := types.NewDatabaseV3(
types.Metadata{
Name: req.Name,
Labels: labels,
}, dbSpec)
if err != nil {
return nil, trace.Wrap(err)
}
database.SetOrigin(types.OriginDynamic)
return database, nil
}
// encodeCRLPEM takes DER encoded CRL and encodes into PEM.
func encodeCRLPEM(contents []byte) []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: contents,
})
}