Skip to content
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
2 changes: 2 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/sailpoint-oss/sailpoint-cli/cmd/ui_plugins"
"github.com/sailpoint-oss/sailpoint-cli/cmd/va"
"github.com/sailpoint-oss/sailpoint-cli/cmd/workflow"
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -75,6 +76,7 @@ func NewRootCommand() *cobra.Command {

root.PersistentFlags().StringVarP(&env, "env", "", "", "Environment to use for SailPoint CLI commands")
root.PersistentFlags().BoolVarP(&debug, "debug", "", false, "Enable debug logging")
root.PersistentFlags().StringVar(config.RuntimeAccessTokenVar(), "access-token", "", "Pre-minted Identity Security Cloud access token to send as a bearer token")
viper.BindPFlag("activeenvironment", root.PersistentFlags().Lookup("env"))
viper.BindPFlag("debug", root.PersistentFlags().Lookup("debug"))

Expand Down
37 changes: 37 additions & 0 deletions cmd/root/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ package root

import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"strings"
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
)

// Expected number of subcommands to `sail` root command
Expand Down Expand Up @@ -44,6 +48,39 @@ func TestNewRootCmd_noArgs(t *testing.T) {
}
}

func TestNewRootCmd_accessTokenFlag(t *testing.T) {
t.Setenv("SAIL_ACCESS_TOKEN", "")
t.Cleanup(func() { config.SetRuntimeAccessToken("") })

payload, err := json.Marshal(map[string]any{
"org": "acme",
"exp": time.Now().Add(time.Hour).Unix(),
})
if err != nil {
t.Fatalf("marshal claims: %v", err)
}
token := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256"}`)) + "." +
base64.RawURLEncoding.EncodeToString(payload) + "." +
base64.RawURLEncoding.EncodeToString([]byte("sig"))

cmd := NewRootCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"--access-token", "Bearer " + token})

if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}

got, err := config.GetBearerToken()
if err != nil {
t.Fatalf("GetBearerToken() error = %v", err)
}
if got != token {
t.Fatalf("GetBearerToken() = %q, want the access token passed on the command line", got)
}
}

func TestNewRootCmd_completionDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
41 changes: 38 additions & 3 deletions cmd/set/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,50 @@ The `set` command makes it easy to update configuration values for the SailPoint

## Authentication

Run the following command to set the current authentication method for the CLI.
The CLI can authenticate to Identity Security Cloud with a personal access token, OAuth, or a bearer token that another system has already minted.

> :warning: **Currently only Personal Access Token Authentication is supported**: OAuth possibly coming in the future!
### Personal access token

```shell
sail set auth pat
sail set pat
```

This will set the currently active authentication method to PAT.
PAT authentication exchanges a client ID and client secret at the OAuth token endpoint. You can also set `SAIL_BASE_URL`, `SAIL_CLIENT_ID`, and `SAIL_CLIENT_SECRET`.

### OAuth

```shell
sail set auth oauth
```

OAuth signs in through the Identity Security Cloud web portal.

### Bearer token

Use bearer authentication when you receive an access token instead of a client ID and client secret. That token is the `access_token` returned after a client ID and client secret are presented to the Identity Security Cloud OAuth token endpoint. The CLI sends it as `Authorization: Bearer <token>` on every request and does not call the OAuth token endpoint again.

For a deployment, or any other process that receives the token from another system:

```shell
export SAIL_BASE_URL="https://<tenant>.api.identitynow.com"
export SAIL_ACCESS_TOKEN="<access_token>"
```

`SAIL_ACCESS_TOKEN` selects bearer authentication for that process, including when another authentication method is configured. You can also pass the token for a single invocation:

```shell
sail --access-token "<access_token>" <command>
```

To store the token for the active environment:

```shell
sail set auth bearer
sail set bearer --token "<access_token>"
```

`sail set bearer` prompts for the token when `--token` is omitted. The value may be the raw access token or the full `Bearer <access_token>` header value. A stored token is kept in the system keyring, not in the config file, and is used for later commands until it expires. The CLI cannot refresh it. Mint a new token at the OAuth token endpoint and set it again.

## Debug

Expand Down
15 changes: 11 additions & 4 deletions cmd/set/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func PromptAuth() (string, error) {
items := []tui.Choice{
{Title: "PAT", Description: "Person Access Token - Single User PAT Configuration"},
{Title: "OAuth", Description: "OAuth2.0 Authentication - Sign in via the Identity Security Cloud web portal"},
{Title: "Bearer", Description: "Bearer Token - Use an access token already minted at the OAuth token endpoint"},
}

choice, err := tui.PromptList(items, "Choose an authentication method to configure")
Expand All @@ -26,9 +27,9 @@ func PromptAuth() (string, error) {
func newAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Set the currently active authentication mode (PAT, OAuth)",
Long: "\nSet the currently active authentication mode\n\nSupported Authentication Methods:\nPAT\nOAuth",
Example: "sail set auth pat | sail set auth oauth",
Short: "Set the currently active authentication mode (PAT, OAuth, bearer)",
Long: "\nSet the currently active authentication mode\n\nSupported Authentication Methods:\nPAT\nOAuth\nBearer - an access token already minted at the Identity Security Cloud OAuth token endpoint",
Example: "sail set auth pat | sail set auth oauth | sail set auth bearer",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var selection string
Expand All @@ -54,8 +55,14 @@ func newAuthCommand() *cobra.Command {
config.SetAuthType("oauth")
log.Info("Authentication method set to OAuth")

case "bearer":

config.SetAuthType("bearer")
log.Info("Authentication method set to bearer")
log.Info("Provide the minted access token with `sail set bearer`, SAIL_ACCESS_TOKEN, or --access-token")

default:
log.Warn("No selection made")
log.Warn("Unsupported authentication method. Choose pat, oauth, or bearer", "selection", selection)
}

return nil
Expand Down
51 changes: 51 additions & 0 deletions cmd/set/bearer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package set

import (
"github.com/charmbracelet/log"
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
"github.com/spf13/cobra"
)

func newBearerCommand() *cobra.Command {
var token string
cmd := &cobra.Command{
Use: "bearer",
Short: "Configure bearer token authentication for the currently active environment",
Long: "\nConfigure bearer token authentication for the CLI\n\n" +
"Use this when another system has already minted an Identity Security Cloud access token\n" +
"at the OAuth token endpoint. Pass that access_token instead of a client ID and client secret.\n" +
"The CLI sends it as a bearer token on every request and does not call the OAuth token endpoint.\n\n" +
"The token can also be supplied without storing it, for a deployment or a single command:\n\n" +
" export SAIL_ACCESS_TOKEN=\"<access_token>\"\n" +
" sail --access-token \"<access_token>\" <command>\n\n" +
"SAIL_ACCESS_TOKEN and --access-token select bearer authentication for that process.\n" +
"A stored token cannot be refreshed, because the client ID and secret are not available.\n" +
"Mint a new token when this one expires.",
Example: "sail set bearer --token <access_token>",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if token == "" {
entered, err := config.PromptForBearerToken()
if err != nil {
return err
}
token = entered
}

if err := config.SetBearerToken(token); err != nil {
return err
}

config.SetAuthType("bearer")
log.Info("Bearer token stored for the active environment")
log.Info("Authentication method set to bearer")

return nil
},
}

cmd.Flags().StringVarP(&token, "token", "t", "", "Minted Identity Security Cloud access token to send as a bearer token")

return cmd
}
46 changes: 46 additions & 0 deletions cmd/set/bearer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package set

import (
"io"
"strings"
"testing"

"github.com/sailpoint-oss/sailpoint-cli/internal/config"
"github.com/spf13/viper"
)

func TestAuthCommandSetsBearer(t *testing.T) {
viper.Reset()
t.Cleanup(viper.Reset)

cmd := newAuthCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"Bearer"})

if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}
if got := config.GetAuthType(); got != "bearer" {
t.Fatalf("GetAuthType() = %q, want bearer", got)
}
}

func TestBearerCommandRejectsUnmintedToken(t *testing.T) {
viper.Reset()
t.Cleanup(viper.Reset)
config.SetAuthType("pat")

cmd := newBearerCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"--token", "not-a-token"})

err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), "not a signed Identity Security Cloud access token") {
t.Fatalf("Execute() error = %v, want a rejected token", err)
}
if got := config.GetAuthType(); got != "pat" {
t.Fatalf("GetAuthType() = %q, want pat after a rejected token", got)
}
}
1 change: 1 addition & 0 deletions cmd/set/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func NewSetCmd(term terminal.Terminal) *cobra.Command {
newExportTemplateCommand(),
newSearchTemplateCommand(),
newPATCommand(term),
newBearerCommand(),
)

return cmd
Expand Down
Loading