Skip to content

Commit

Permalink
Add Support for Oracle protocol (#23892)
Browse files Browse the repository at this point in the history
  • Loading branch information
smallinsky committed Apr 3, 2023
1 parent 6397af5 commit 0fb6843
Show file tree
Hide file tree
Showing 27 changed files with 752 additions and 51 deletions.
1 change: 1 addition & 0 deletions api/types/database.go
Expand Up @@ -511,6 +511,7 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
if err := d.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

for key := range d.Spec.DynamicLabels {
if !IsValidLabelKey(key) {
return trace.BadParameter("database %q invalid label key: %q", d.GetName(), key)
Expand Down
17 changes: 14 additions & 3 deletions api/utils/keypaths/keypaths.go
Expand Up @@ -60,6 +60,8 @@ const (
currentProfileFilename = "current-profile"
// profileFileExt is the suffix of a profile file.
profileFileExt = ".yaml"
// oracleWalletDirSuffix is the suffix of the oracle wallet database directory.
oracleWalletDirSuffix = "-wallet"
)

// Here's the file layout of all these keypaths.
Expand Down Expand Up @@ -88,9 +90,11 @@ const (
// │ ├── foo-db --> App access certs for user "foo"
// │ │ ├── root --> App access certs for cluster "root"
// │ │ │ ├── dbA-x509.pem --> TLS cert for database service "dbA"
// │ │ │ └── dbB-x509.pem --> TLS cert for database service "dbB"
// │ │ └── leaf --> App access certs for cluster "leaf"
// │ │ └── dbC-x509.pem --> TLS cert for database service "dbC"
// │ │ │ ├── dbB-x509.pem --> TLS cert for database service "dbB"
// │ │ │ └── dbC-wallet --> Oracle Client wallet Configuration directory.
// │ │ ├── leaf --> App access certs for cluster "leaf"
// │ │ │ └── dbC-x509.pem --> TLS cert for database service "dbC"
// │ │ └── proxy-localca.pem --> Self-signed TLS Routing local proxy CA
// │ ├── foo-kube --> Kubernetes certs for user "foo"
// │ | ├── root --> Kubernetes certs for Teleport cluster "root"
// │ | │ ├── kubeA-kubeconfig --> standalone kubeconfig for Kubernetes cluster "kubeA"
Expand Down Expand Up @@ -267,6 +271,13 @@ func DatabaseCertPath(baseDir, proxy, username, cluster, dbname string) string {
return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+fileExtTLSCert)
}

// DatabaseOracleWalletDirectory returns the path to the user's Oracle Wallet configuration directory.
// for the given proxy, cluster and database.
// <baseDir>/keys/<proxy>/<username>-db/<cluster>/dbname-wallet/
func DatabaseOracleWalletDirectory(baseDir, proxy, username, cluster, dbname string) string {
return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+oracleWalletDirSuffix)
}

// KubeDir returns the path to the user's kube directory
// for the given proxy.
//
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -157,6 +157,7 @@ require (
sigs.k8s.io/controller-runtime v0.14.1
sigs.k8s.io/controller-tools v0.11.1
sigs.k8s.io/yaml v1.3.0
software.sslmate.com/src/go-pkcs12 v0.2.0
)

// DO NOT UPDATE any of the following dependencies until the
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -1987,4 +1987,6 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
6 changes: 3 additions & 3 deletions lib/client/db/database_certificates.go
Expand Up @@ -41,8 +41,8 @@ type GenerateDatabaseCertificatesRequest struct {
IdentityFileWriter identityfile.ConfigWriter
TTL time.Duration
Key *client.Key
// JKSKeyStore is used to generate JKS keystore used for cassandra format.
JKSPassword string
// Password is used to generate JKS keystore used for cassandra format or Oracle wallet.
Password string
}

// GenerateDatabaseCertificates to be used by databases to set up mTLS authentication
Expand Down Expand Up @@ -123,7 +123,7 @@ func GenerateDatabaseCertificates(ctx context.Context, req GenerateDatabaseCerti
Format: req.OutputFormat,
OverwriteDestination: req.OutputCanOverwrite,
Writer: req.IdentityFileWriter,
JKSPassword: req.JKSPassword,
Password: req.Password,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
35 changes: 35 additions & 0 deletions lib/client/db/dbcmd/dbcmd.go
Expand Up @@ -69,6 +69,8 @@ const (
elasticsearchSQLBin = "elasticsearch-sql-cli"
// awsBin is the aws CLI program name.
awsBin = "aws"
// oracleBin is the Oracle CLI program name.
oracleBin = "sql"
)

// Execer is an abstraction of Go's exec module, as this one doesn't specify any interfaces.
Expand Down Expand Up @@ -192,6 +194,9 @@ func (c *CLICommandBuilder) GetConnectCommand() (*exec.Cmd, error) {

case defaults.ProtocolDynamoDB:
return c.getDynamoDBCommand()

case defaults.ProtocolOracle:
return c.getOracleCommand()
}

return nil, trace.BadParameter("unsupported database protocol: %v", c.db)
Expand Down Expand Up @@ -619,6 +624,36 @@ func (c *CLICommandBuilder) getDynamoDBCommand() (*exec.Cmd, error) {
return c.options.exe.Command(awsBin, args...), nil
}

type jdbcOracleThinConnection struct {
host string
port int
db string
tnsAdmin string
}

func (j *jdbcOracleThinConnection) ConnString() string {
return fmt.Sprintf(`jdbc:oracle:thin:@tcps://%s:%d/%s?TNS_ADMIN=%s`, j.host, j.port, j.db, j.tnsAdmin)
}

func (c *CLICommandBuilder) getOracleCommand() (*exec.Cmd, error) {
cs := jdbcOracleThinConnection{
host: c.host,
port: c.port,
db: c.db.Database,
tnsAdmin: c.profile.OracleWalletDir(c.profile.Cluster, c.db.ServiceName),
}
// Quote the address for printing as the address contains "?".
connString := cs.ConnString()
if c.options.printFormat {
connString = fmt.Sprintf(`'%s'`, connString)
}
args := []string{
"-L", // dont retry
connString,
}
return c.options.exe.Command(oracleBin, args...), nil
}

func (c *CLICommandBuilder) getElasticsearchAlternativeCommands() []CommandAlternative {
var commands []CommandAlternative
if c.isElasticsearchSQLBinAvailable() {
Expand Down
18 changes: 18 additions & 0 deletions lib/client/db/dbcmd/dbcmd_test.go
Expand Up @@ -585,6 +585,24 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) {
cmd: []string{"aws", "--endpoint", "http://localhost:12345/", "[dynamodb|dynamodbstreams|dax]", "<command>"},
wantErr: false,
},
{
name: "oracle",
dbProtocol: defaults.ProtocolOracle,
opts: []ConnectCommandFunc{WithLocalProxy("localhost", 12345, "")},
execer: &fakeExec{},
databaseName: "oracle01",
cmd: []string{"sql", "-L", "jdbc:oracle:thin:@tcps://localhost:12345/oracle01?TNS_ADMIN=/tmp/keys/example.com/bob-db/mysql-wallet"},
wantErr: false,
},
{
name: "Oracle with print format",
dbProtocol: defaults.ProtocolOracle,
opts: []ConnectCommandFunc{WithLocalProxy("localhost", 12345, ""), WithPrintFormat()},
execer: &fakeExec{},
databaseName: "oracle01",
cmd: []string{"sql", "-L", "'jdbc:oracle:thin:@tcps://localhost:12345/oracle01?TNS_ADMIN=/tmp/keys/example.com/bob-db/mysql-wallet'"},
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
115 changes: 115 additions & 0 deletions lib/client/db/oracle/config.go
@@ -0,0 +1,115 @@
/*
Copyright 2023 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 oracle

import (
"bytes"
"os"
"path/filepath"
"text/template"

"github.com/gravitational/trace"

"github.com/gravitational/teleport"
)

type jdbcSettings struct {
KeyStoreFile string
TrustStoreFile string
KeyStorePassword string
TrustStorePassword string
}

const jdbcPropertiesTemplateContent = `
javax.net.ssl.keyStore={{.KeyStoreFile}}
javax.net.ssl.trustStore={{.TrustStoreFile}}
javax.net.ssl.keyStorePassword={{.KeyStorePassword}}
javax.net.ssl.trustStorePassword={{.TrustStorePassword}}
javax.net.ssl.keyStoreType=jks
javax.net.ssl.trustStoreType=jks
oracle.net.authentication_services=TCPS
`

type tnsNamesORASettings struct {
ServiceName string
Host string
Port string
}

const sqlnetORATemplateContent = `
SSL_CLIENT_AUTHENTICATION = TRUE
SQLNET.AUTHENTICATION_SERVICES = (TCPS)
WALLET_LOCATION =
(SOURCE =
(METHOD = FILE)
(METHOD_DATA =
(DIRECTORY = {{.WalletDir}})
)
)
`

type sqlnetORASettings struct {
WalletDir string
}

const tnsnamesORATemplateContent = `
{{.ServiceName}} =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCPS)(HOST = {{.Host}})(PORT = {{.Port}}))
)
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = {{.ServiceName}})
)
(SECURITY =
(SSL_SERVER_CERT_DN = "CN=localhost")
)
)
`

var (
jdbcPropertiesTemplate = template.Must(template.New("").Parse(jdbcPropertiesTemplateContent))
sqlnetORATemplate = template.Must(template.New("").Parse(sqlnetORATemplateContent))
tnsnamesORATemplate = template.Must(template.New("").Parse(tnsnamesORATemplateContent))
)

func (c jdbcSettings) template() *template.Template { return jdbcPropertiesTemplate }
func (c sqlnetORASettings) template() *template.Template { return sqlnetORATemplate }
func (c tnsNamesORASettings) template() *template.Template { return tnsnamesORATemplate }

func (c jdbcSettings) configFilename() string { return "ojdbc.properties" }
func (c sqlnetORASettings) configFilename() string { return "sqlnet.ora" }
func (c tnsNamesORASettings) configFilename() string { return "tnsnames.ora" }

type templateSettings interface {
template() *template.Template
configFilename() string
}

func writeSettings(settings templateSettings, dir string) error {
var buff bytes.Buffer
if err := settings.template().Execute(&buff, settings); err != nil {
return trace.Wrap(err)
}
filePath := filepath.Join(dir, settings.configFilename())
if err := os.WriteFile(filePath, buff.Bytes(), teleport.FileMaskOwnerOnly); err != nil {
return trace.Wrap(err)
}
return nil
}

0 comments on commit 0fb6843

Please sign in to comment.