Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Oracle connections without wallet #49753

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 62 additions & 21 deletions lib/client/db/dbcmd/dbcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ const (
openSearchSQLBin = "opensearchsql"
// awsBin is the aws CLI program name.
awsBin = "aws"
// oracleBin is the Oracle CLI program name.
oracleBin = "sql"
// sqlclBin is the SQLcl program name (Oracle client).
sqlclBin = "sql"
// spannerBin is a Google Spanner interactive CLI program name.
spannerBin = "spanner-cli"
)
Expand Down Expand Up @@ -214,6 +214,7 @@ func (c *CLICommandBuilder) GetConnectCommand(ctx context.Context) (*exec.Cmd, e

case defaults.ProtocolClickHouseHTTP:
return c.getClickhouseHTTPCommand()

case defaults.ProtocolClickHouse:
return c.getClickhouseNativeCommand()

Expand All @@ -239,6 +240,8 @@ func (c *CLICommandBuilder) GetConnectCommandAlternatives(ctx context.Context) (
return c.getElasticsearchAlternativeCommands(), nil
case defaults.ProtocolOpenSearch:
return c.getOpenSearchAlternativeCommands(), nil
case defaults.ProtocolOracle:
return c.getOracleAlternativeCommands(), nil
}

cmd, err := c.GetConnectCommand(ctx)
Expand Down Expand Up @@ -780,38 +783,62 @@ func (c *CLICommandBuilder) getSpannerCommand() (*exec.Cmd, error) {
return cmd, nil
}

type jdbcOracleThinConnection struct {
host string
port int
db string
tnsAdmin string
func (c *CLICommandBuilder) getOracleTNSDescriptorString() string {
return fmt.Sprintf("/@(DESCRIPTION=(SDU=8000)(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=%s)(PORT=%d)))(CONNECT_DATA=(SERVICE_NAME=%s)))", c.host, c.port, c.db.Database)
}

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) getOracleDirectConnectionString() string {
return fmt.Sprintf("/@%s:%d/%s", c.host, c.port, c.db.Database)
}

func (c *CLICommandBuilder) getOracleCommand() (*exec.Cmd, error) {
func (c *CLICommandBuilder) getOracleJDBCConnectionString() string {
tnsAdminPath := c.profile.OracleWalletDir(c.tc.SiteName, c.db.ServiceName)
if runtime.GOOS == constants.WindowsOS {
tnsAdminPath = strings.ReplaceAll(tnsAdminPath, `\`, `\\`)
}
cs := jdbcOracleThinConnection{
host: c.host,
port: c.port,
db: c.db.Database,
tnsAdmin: tnsAdminPath,
}
// Quote the address for printing as the address contains "?".
connString := cs.ConnString()
connString := fmt.Sprintf(`jdbc:oracle:thin:@tcps://%s:%d/%s?TNS_ADMIN=%s`, c.host, c.port, c.db.Database, tnsAdminPath)
if c.options.printFormat {
connString = fmt.Sprintf(`'%s'`, connString)
}
args := []string{
"-L", // dont retry
connString,
return connString
}

func (c *CLICommandBuilder) getOracleCommand() (*exec.Cmd, error) {
alternatives := c.getOracleAlternativeCommands()
if len(alternatives) == 0 {
return nil, trace.BadParameter("no alternative commands found")
}
return exec.Command(oracleBin, args...), nil
return alternatives[0].Command, nil
}

func (c *CLICommandBuilder) getOracleAlternativeCommands() []CommandAlternative {
var commands []CommandAlternative

c.options.log.Debugf("Building Oracle commands.")
c.options.log.Debugf("Found servers with TCP support: %v.", c.options.oracle.hasTCPServers)
c.options.log.Debugf("All servers support TCP: %v.", c.options.oracle.canUseTCP)

c.options.log.Debugf("Connection strings:")
c.options.log.Debugf("- JDBC: %s", c.getOracleJDBCConnectionString())
if c.options.oracle.hasTCPServers {
c.options.log.Debugf("- TNS descriptor: %s", c.getOracleTNSDescriptorString())
c.options.log.Debugf("- Direct: %s", c.getOracleDirectConnectionString())
}
Tener marked this conversation as resolved.
Show resolved Hide resolved

const oneShotLogin = "-L"

commandTCP := exec.Command(sqlclBin, oneShotLogin, c.getOracleDirectConnectionString())
commandTCPS := exec.Command(sqlclBin, oneShotLogin, c.getOracleJDBCConnectionString())

if c.options.oracle.canUseTCP {
commands = append(commands, CommandAlternative{Description: "SQLcl", Command: commandTCP})
commands = append(commands, CommandAlternative{Description: "SQLcl (JDBC)", Command: commandTCPS})
} else {
commands = append(commands, CommandAlternative{Description: "SQLcl", Command: commandTCPS})
}

return commands
}

func (c *CLICommandBuilder) getElasticsearchAlternativeCommands() []CommandAlternative {
Expand Down Expand Up @@ -909,6 +936,7 @@ type connectionCommandOpts struct {
exe Execer
password string
gcp types.GCPCloudSQL
oracle oracleOpts
getDatabase GetDatabaseFunc
}

Expand Down Expand Up @@ -999,6 +1027,19 @@ func WithExecer(exe Execer) ConnectCommandFunc {
}
}

type oracleOpts struct {
canUseTCP bool
hasTCPServers bool
}

// WithOracleOpts configures Oracle-specific options.
func WithOracleOpts(canUseTCP bool, hasTCPServers bool) ConnectCommandFunc {
return func(opts *connectionCommandOpts) {
opts.oracle.canUseTCP = canUseTCP
opts.oracle.hasTCPServers = hasTCPServers
}
}

// WithGCP adds GCP metadata for the database command to access.
// TODO(greedy52) use GetDatabaseFunc instead.
func WithGCP(gcp types.GCPCloudSQL) ConnectCommandFunc {
Expand Down
40 changes: 35 additions & 5 deletions tool/tsh/common/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,11 @@ type localProxyConfig struct {
}

func createLocalProxyListener(addr string, route tlsca.RouteToDatabase, profile *client.ProfileStatus) (net.Listener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, trace.Wrap(err)
}

if route.Protocol == defaults.ProtocolOracle {
localCert, err := tls.LoadX509KeyPair(
profile.DatabaseLocalCAPath(),
Expand All @@ -685,14 +690,15 @@ func createLocalProxyListener(addr string, route tlsca.RouteToDatabase, profile
if err != nil {
return nil, trace.Wrap(err)
}
l, err := tls.Listen("tcp", addr, &tls.Config{
config := &tls.Config{
Certificates: []tls.Certificate{localCert},
ServerName: "localhost",
})
return l, trace.Wrap(err)
}

l = NewTLSMuxListener(l, config)
}
l, err := net.Listen("tcp", addr)
return l, trace.Wrap(err)

return l, nil
}

// prepareLocalProxyOptions created localProxyOpts needed to create local proxy from localProxyConfig.
Expand Down Expand Up @@ -789,6 +795,7 @@ func onDatabaseConnect(cf *CLIConf) error {
if opts, err = maybeAddGCPMetadata(cf.Context, tc, dbInfo, opts); err != nil {
return trace.Wrap(err)
}
opts = maybeAddOracleOptions(cf.Context, tc, dbInfo, opts)

bb := dbcmd.NewCmdBuilder(tc, profile, dbInfo.RouteToDatabase, rootClusterName, opts...)
cmd, err := bb.GetConnectCommand(cf.Context)
Expand Down Expand Up @@ -1109,6 +1116,29 @@ func getDatabase(ctx context.Context, tc *client.TeleportClient, name string) (t
return databases[0], nil
}

func getDatabaseServers(ctx context.Context, tc *client.TeleportClient, name string) ([]types.DatabaseServer, error) {
var databases []types.DatabaseServer

err := client.RetryWithRelogin(ctx, tc, func() error {
matchName := makeNamePredicate(name)

var err error
predicate := makePredicateConjunction(matchName, tc.PredicateExpression)
log.Debugf("Listing databases with predicate (%v) and labels %v", predicate, tc.Labels)

databases, err = tc.ListDatabaseServersWithFilters(ctx, &proto.ListResourcesRequest{
Namespace: tc.Namespace,
ResourceType: types.KindDatabaseServer,
PredicateExpression: predicate,
Labels: tc.Labels,
UseSearchAsRoles: tc.UseSearchAsRoles,
})
return trace.Wrap(err)
})

return databases, trace.Wrap(err)
}
Tener marked this conversation as resolved.
Show resolved Hide resolved

// getDatabaseByNameOrDiscoveredName fetches a database that unambiguously
// matches a given name or a discovered name label.
func getDatabaseByNameOrDiscoveredName(cf *CLIConf, tc *client.TeleportClient, activeRoutes []tlsca.RouteToDatabase) (types.Database, error) {
Expand Down
123 changes: 101 additions & 22 deletions tool/tsh/common/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"text/template"
"unicode"

"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"

"github.com/gravitational/teleport"
Expand Down Expand Up @@ -244,6 +245,7 @@ func onProxyCommandDB(cf *CLIConf) error {
if opts, err = maybeAddGCPMetadata(cf.Context, tc, dbInfo, opts); err != nil {
return trace.Wrap(err)
}
opts = maybeAddOracleOptions(cf.Context, tc, dbInfo, opts)

commands, err := dbcmd.NewCmdBuilder(tc, profile, dbInfo.RouteToDatabase, rootCluster,
opts...,
Expand Down Expand Up @@ -341,36 +343,112 @@ func maybeAddGCPMetadataTplArgs(ctx context.Context, tc *libclient.TeleportClien
}
}

func maybeAddOracleOptions(ctx context.Context, tc *libclient.TeleportClient, dbInfo *databaseInfo, opts []dbcmd.ConnectCommandFunc) []dbcmd.ConnectCommandFunc {
Tener marked this conversation as resolved.
Show resolved Hide resolved
// Skip for non-Oracle protocols.
if dbInfo.Protocol != defaults.ProtocolOracle {
return opts
}

// TODO(Tener): DELETE IN 20.0.0 - all agents should now contain improved Oracle engine.
// minimum version to support TCPS-less connection.
cutoffVersion := semver.Version{
Major: 17,
Minor: 2,
Patch: 0,
PreRelease: "",
}

devV17Version := semver.Version{
Major: 17,
Minor: 0,
Patch: 0,
PreRelease: "dev",
}

dbServers, err := getDatabaseServers(ctx, tc, dbInfo.ServiceName)
if err != nil {
// log, but treat this error as non-fatal.
log.Warnf("Error getting database servers: %s", err.Error())
return opts
}

var oldServers, newServers int
Tener marked this conversation as resolved.
Show resolved Hide resolved

for _, server := range dbServers {
ver, err := semver.NewVersion(server.GetTeleportVersion())
if err != nil {
log.Debugf("Failed to parse teleport version %q: %v", server.GetTeleportVersion(), err)
continue
}

if ver.Equal(devV17Version) {
newServers++
} else {
if ver.LessThan(cutoffVersion) {
oldServers++
} else {
newServers++
}
}
Tener marked this conversation as resolved.
Show resolved Hide resolved
}

log.Debugf("Agents for database %q with Oracle support: total %v, old %v, new %v.", dbInfo.ServiceName, len(dbServers), oldServers, newServers)

if oldServers > 0 {
log.Warnf("Detected database agents older than %v. For improved client support upgrade all database agents in your cluster to a newer version.", cutoffVersion)
}

opts = append(opts, dbcmd.WithOracleOpts(oldServers == 0, newServers > 0))
return opts
}

type templateCommandItem struct {
Description string
Command string
}

func chooseProxyCommandTemplate(templateArgs map[string]any, commands []dbcmd.CommandAlternative, dbInfo *databaseInfo) *template.Template {
// there is only one command, use plain template.
if len(commands) == 1 {
templateArgs["command"] = formatCommand(commands[0].Command)
switch dbInfo.Protocol {
case defaults.ProtocolOracle:
templateArgs["args"] = commands[0].Command.Args
return dbProxyOracleAuthTpl
case defaults.ProtocolSpanner:
templateArgs["databaseName"] = "<database>"
if dbInfo.Database != "" {
templateArgs["databaseName"] = dbInfo.Database
templateArgs["command"] = formatCommand(commands[0].Command)

// protocol-specific templates
if dbInfo.Protocol == defaults.ProtocolOracle {
// the JDBC connection string should always be found,
// but the order of commands is important as only the first command will actually be shown.
jdbcConnectionString := ""
ixFound := -1
for ix, cmd := range commands {
for _, arg := range cmd.Command.Args {
if strings.Contains(arg, "jdbc:oracle:") {
jdbcConnectionString = arg
ixFound = ix
}
}
return dbProxySpannerAuthTpl
}
templateArgs["jdbcConnectionString"] = jdbcConnectionString
templateArgs["canUseTCP"] = ixFound > 0
return dbProxyOracleAuthTpl
}

if dbInfo.Protocol == defaults.ProtocolSpanner {
templateArgs["databaseName"] = "<database>"
if dbInfo.Database != "" {
templateArgs["databaseName"] = dbInfo.Database
}
return dbProxySpannerAuthTpl
}

// there is only one command, use plain template.
if len(commands) == 1 {
return dbProxyAuthTpl
}

// multiple command options, use a different template.

var commandsArg []templateCommandItem
for _, cmd := range commands {
commandsArg = append(commandsArg, templateCommandItem{cmd.Description, formatCommand(cmd.Command)})
}

delete(templateArgs, "command")
templateArgs["commands"] = commandsArg
return dbProxyAuthMultiTpl
}
Expand Down Expand Up @@ -715,21 +793,22 @@ Or use the following JDBC connection string to connect with other GUI/CLI client
jdbc:cloudspanner://{{.address}}/projects/{{.gcpProject}}/instances/{{.gcpInstance}}/databases/{{.databaseName}};usePlainText=true
`))

// dbProxyOracleAuthTpl is the message that's printed for an authenticated db proxy.
var dbProxyOracleAuthTpl = template.Must(template.New("").Funcs(templateFunctions).Parse(
var dbProxyOracleAuthTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
` + dbProxyConnectAd + `
Use the following command to connect to the Oracle database server using CLI:
$ {{.command}}

or using following Oracle JDBC connection string in order to connect with other GUI/CLI clients:
{{- range $val := .args}}
{{- if contains $val "jdbc:oracle:"}}
{{$val}}
{{- end}}
{{- end}}
{{if .canUseTCP }}Other clients can use:
- a direct connection to {{.address}} without a username and password
- a custom JDBC connection string: {{.jdbcConnectionString}}

{{else }}You can also connect using Oracle JDBC connection string:
{{.jdbcConnectionString}}

Note: for improved client compatibility, upgrade your Teleport cluster. For details rerun this command with --debug.
{{- end }}
`))

// dbProxyAuthMultiTpl is the message that's printed for an authenticated db proxy if there are multiple command options.
Expand Down
Loading
Loading