Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
31 changes: 20 additions & 11 deletions cmd/auth/seturl/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ package seturl

import (
"github.com/datarobot/cli/internal/auth"
"github.com/datarobot/cli/internal/cli"
"github.com/datarobot/cli/internal/config"
"github.com/datarobot/cli/internal/log"
"github.com/datarobot/cli/internal/telemetry"
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "set-url [url]",
Short: "🌐 Configure your DataRobot environment URL.",
Use: "set-url [url]",
SilenceErrors: true,
SilenceUsage: true,
Short: "🌐 Configure your DataRobot environment URL.",
Long: `Configure your DataRobot environment URL with an interactive selection.

This command helps you choose the correct DataRobot environment:
Expand All @@ -34,28 +38,33 @@ This command helps you choose the correct DataRobot environment:
• Custom/On-Premise: Your organization's DataRobot URL

💡 If you're unsure, check the URL you use to log in to DataRobot in your browser.`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var url string
if len(args) > 0 {
url = args[0]
}

// An explicit arg that won't validate is the user's to fix; report it
// rather than silently dropping into the interactive picker.
if url != "" {
err := config.SetURLToConfig(url)
if err == nil {
_ = auth.WriteConfigFileSilent()
_ = auth.EnsureAuthenticatedE(cmd, args)
if err := config.SetURLToConfig(url); err != nil {
log.Error(err.Error())

return
return cli.ErrSilent
}
}

urlChanged := auth.SetURLAction()
_ = auth.WriteConfigFileSilent()
_ = auth.EnsureAuthenticatedE(cmd, args)

return nil
}

if urlChanged {
if auth.SetURLAction() {
_ = auth.WriteConfigFileSilent()
_ = auth.EnsureAuthenticatedE(cmd, args)
}

return nil
},
}

Expand Down
32 changes: 32 additions & 0 deletions cmd/auth/seturl/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// 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 seturl

import (
"testing"

"github.com/datarobot/cli/internal/cli"
"github.com/stretchr/testify/require"
)

// An explicit arg with an unsupported scheme errors out instead of falling
// through to the interactive picker.
func TestSetURLRejectsNonHTTPSchemeArg(t *testing.T) {
cmd := Cmd()

err := cmd.RunE(cmd, []string{"ftp://app.datarobot.com"})

require.ErrorIs(t, err, cli.ErrSilent)
}
4 changes: 4 additions & 0 deletions docs/commands/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ dr auth set-url [url]

- `url` (optional) - DataRobot instance URL. For example: `https://app.datarobot.com`

A bare host like `app.datarobot.com` is accepted and defaults to `https`. A URL whose
scheme is not `http` or `https` is rejected: `dr auth set-url ftp://host` prints
`unsupported URL scheme "ftp", use https://` and exits non-zero.

**Interactive mode:**

If you run `dr auth set-url` without providing a URL, the CLI shows a picker. Move with
Expand Down
7 changes: 1 addition & 6 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,7 @@ func ValidateEndpoint(endpoint string) error {
return err
}

// Checked here, not in SchemeHostOnly, which set-url and export share.
if scheme, _, _ := strings.Cut(baseURL, "://"); scheme != "http" && scheme != "https" {
return fmt.Errorf("unsupported URL scheme %q, use https://", scheme)
}

return nil
return config.RequireHTTPScheme(baseURL)
}

// ReportEnvCredentialsError writes a classified explanation of why an
Expand Down
15 changes: 15 additions & 0 deletions internal/config/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package config

import (
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
Expand Down Expand Up @@ -59,6 +60,16 @@ func SchemeHostOnly(longURL string) (string, error) {
return parsedURL.String(), nil
}

// RequireHTTPScheme rejects a normalized base URL whose scheme is not http or
// https. SchemeHostOnly stays scheme-agnostic (export and GetBaseURL share it).
func RequireHTTPScheme(baseURL string) error {
if scheme, _, _ := strings.Cut(baseURL, "://"); scheme != "http" && scheme != "https" {
return fmt.Errorf("unsupported URL scheme %q, use https://", scheme)
}
Comment thread
cursor[bot] marked this conversation as resolved.

return nil
}

func GetBaseURL() string {
if endpoint := viper.GetString(DataRobotURL); endpoint != "" {
if newURL, err := SchemeHostOnly(endpoint); err == nil {
Expand Down Expand Up @@ -187,6 +198,10 @@ func SetURLToConfig(newURL string) error {
return err
}

if err := RequireHTTPScheme(newURL); err != nil {
return err
}
Comment thread
chasdr marked this conversation as resolved.

viper.Set(DataRobotURL, newURL+DRAPIURLSuffix)

return nil
Expand Down
43 changes: 43 additions & 0 deletions internal/config/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ func (suite *APITestSuite) TestSetURLToConfig() {
input: "not a url",
expectError: true,
},
{
name: "http scheme is accepted",
input: "http://localhost:8080",
expectedURL: "http://localhost:8080/api/v2",
},
{
name: "ftp scheme is rejected",
input: "ftp://app.datarobot.com",
expectError: true,
},
{
name: "file scheme is rejected",
input: "file://host/etc/passwd",
expectError: true,
},
}

for _, tc := range tests {
Expand Down Expand Up @@ -251,3 +266,31 @@ func TestRedactSecretFields_CoversTheOtherNames(t *testing.T) {
assert.NotContains(t, out, "hunter2", "field %q", field)
}
}

// RequireHTTPScheme expects an already-normalized base URL (SchemeHostOnly runs
// first), so a scheme-less string is rejected, not defaulted.
func TestRequireHTTPScheme(t *testing.T) {
tests := []struct {
name string
baseURL string
wantErr string
}{
{"https accepted", "https://app.datarobot.com", ""},
{"http accepted", "http://localhost:8080", ""},
{"ftp rejected", "ftp://app.datarobot.com", `unsupported URL scheme "ftp"`},
{"file rejected", "file://host", `unsupported URL scheme "file"`},
{"scheme-less rejected", "app.datarobot.com", "unsupported URL scheme"},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := RequireHTTPScheme(tc.baseURL)

if tc.wantErr == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tc.wantErr)
}
})
}
}
Loading