Skip to content

Commit

Permalink
Add global parameter identity-uri
Browse files Browse the repository at this point in the history
Added a new global parameter to override the identity server uri. This
makes it easier to use the UiPath services with an external identity
token provider. It was possible before to override the identity server
uri by setting the UIPATH_IDENTITY_URI environment variable but not by
providing a global CLI parameter.

Refactored the code so that the CLI parameter, config value and
environment variable parsing is done in the command_builder like all the
other parameters and removed the env variable parsing from the bearer
authenticator.
  • Loading branch information
thschmitt committed May 9, 2024
1 parent 4978601 commit 5e62403
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 101 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ You can either pass global arguments as CLI parameters, set an env variable or s
| `--uri` | `UIPATH_URI` | `uri` | `https://cloud.uipath.com` | URL override |
| `--organization` | `UIPATH_ORGANIZATION` | `string` | | Organization name |
| `--tenant` | `UIPATH_TENANT` | `string` | | Tenant name |
| `--identity-uri` | `UIPATH_IDENTITY_URI` | `uri` | `https://cloud.uipath.com/identity_` | URL override for identity calls |
| | `UIPATH_CLIENT_ID` | `string` | | Client Id |
| | `UIPATH_CLIENT_SECRET` | `string` | | Client Secret |
| | `UIPATH_PAT` | `string` | | Personal Access Token |
Expand Down
16 changes: 10 additions & 6 deletions auth/authenticator_context.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package auth

import "net/url"

// AuthenticatorContext provides information required for authenticating requests.
type AuthenticatorContext struct {
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
Debug bool `json:"debug"`
Insecure bool `json:"insecure"`
Request AuthenticatorRequest `json:"request"`
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
IdentityUri url.URL `json:"identityUri"`
Debug bool `json:"debug"`
Insecure bool `json:"insecure"`
Request AuthenticatorRequest `json:"request"`
}

func NewAuthenticatorContext(
authType string,
config map[string]interface{},
identityUri url.URL,
debug bool,
insecure bool,
request AuthenticatorRequest) *AuthenticatorContext {
return &AuthenticatorContext{authType, config, debug, insecure, request}
return &AuthenticatorContext{authType, config, identityUri, debug, insecure, request}
}
26 changes: 2 additions & 24 deletions auth/bearer_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package auth

import (
"fmt"
"net/url"
"os"

"github.com/UiPath/uipathcli/cache"
)

const ClientIdEnvVarName = "UIPATH_CLIENT_ID"
const ClientSecretEnvVarName = "UIPATH_CLIENT_SECRET" //nolint // This is not a secret but just the env variable name
const IdentityUriEnvVarName = "UIPATH_IDENTITY_URI"

// The BearerAuthenticator calls the identity token-endpoint to retrieve a JWT bearer token.
// It requires clientId and clientSecret.
Expand All @@ -26,21 +24,9 @@ func (a BearerAuthenticator) Auth(ctx AuthenticatorContext) AuthenticatorResult
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid bearer authenticator configuration: %w", err))
}
identityBaseUri := config.IdentityUri
if identityBaseUri == nil {
requestUrl, err := url.Parse(ctx.Request.URL)
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid request url '%s': %w", ctx.Request.URL, err))
}
identityBaseUri, err = url.Parse(fmt.Sprintf("%s://%s/identity_", requestUrl.Scheme, requestUrl.Host))
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid identity url '%s': %w", ctx.Request.URL, err))
}
}

identityClient := newIdentityClient(a.cache)
tokenRequest := newTokenRequest(
*identityBaseUri,
config.IdentityUri,
config.GrantType,
config.Scopes,
config.ClientId,
Expand Down Expand Up @@ -85,15 +71,7 @@ func (a BearerAuthenticator) getConfig(ctx AuthenticatorContext) (*bearerAuthent
if err != nil {
return nil, err
}
var uri *url.URL
uriString, err := a.parseRequiredString(ctx.Config, "uri", os.Getenv(IdentityUriEnvVarName))
if err == nil {
uri, err = url.Parse(uriString)
if err != nil {
return nil, fmt.Errorf("Error parsing identity uri: %w", err)
}
}
return newBearerAuthenticatorConfig(grantType, scopes, clientId, clientSecret, properties, uri), nil
return newBearerAuthenticatorConfig(grantType, scopes, clientId, clientSecret, properties, ctx.IdentityUri), nil
}

func (a BearerAuthenticator) parseProperties(config map[string]interface{}) (map[string]string, error) {
Expand Down
4 changes: 2 additions & 2 deletions auth/bearer_authenticator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type bearerAuthenticatorConfig struct {
ClientId string
ClientSecret string
Properties map[string]string
IdentityUri *url.URL
IdentityUri url.URL
}

func newBearerAuthenticatorConfig(
Expand All @@ -17,6 +17,6 @@ func newBearerAuthenticatorConfig(
clientId string,
clientSecret string,
properties map[string]string,
identityUri *url.URL) *bearerAuthenticatorConfig {
identityUri url.URL) *bearerAuthenticatorConfig {
return &bearerAuthenticatorConfig{grantType, scopes, clientId, clientSecret, properties, identityUri}
}
83 changes: 24 additions & 59 deletions auth/oauth_authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -44,7 +44,7 @@ func TestOAuthAuthenticatorPreservesExistingHeaders(t *testing.T) {
"my-header": "my-value",
}
request := NewAuthenticatorRequest("http:/localhost", headers)
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -63,7 +63,7 @@ func TestOAuthAuthenticatorInvalidRequestUrl(t *testing.T) {
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("://invalid", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -72,30 +72,14 @@ func TestOAuthAuthenticatorInvalidRequestUrl(t *testing.T) {
}
}

func TestOAuthAuthenticatorInvalidIdentityUrl(t *testing.T) {
config := map[string]interface{}{
"clientId": "my-client-id",
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("INVALID-URL", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
if result.Error != `Invalid identity url 'INVALID-URL': parse ":///identity_": missing protocol scheme` {
t.Errorf("Expected error with invalid request url, but got: %v", result.Error)
}
}

func TestOAuthAuthenticatorInvalidConfig(t *testing.T) {
config := map[string]interface{}{
"clientId": 1,
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -109,9 +93,9 @@ func TestOAuthFlowIdentityFails(t *testing.T) {
ResponseStatus: 400,
ResponseBody: "Invalid token request",
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)

Expand All @@ -126,9 +110,9 @@ func TestOAuthFlowSuccessful(t *testing.T) {
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)

Expand All @@ -142,42 +126,14 @@ func TestOAuthFlowSuccessful(t *testing.T) {
}
}

func TestOAuthFlowWithCustomIdentityUri(t *testing.T) {
identityServerFake := identityServerFake{
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
config := map[string]interface{}{
"clientId": newClientId(),
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
"uri": identityUrl.String() + "/identity_",
}
request := NewAuthenticatorRequest("no-url", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)

loginUrl, resultChannel := callAuthenticator(*context)
performLogin(loginUrl, t)

result := <-resultChannel
if result.Error != "" {
t.Errorf("Expected no error when performing oauth flow, but got: %v", result.Error)
}
authorizationHeader := result.RequestHeader["Authorization"]
if authorizationHeader != "Bearer my-access-token" {
t.Errorf("Expected JWT bearer token in authorization header, but got: %v", authorizationHeader)
}
}

func TestOAuthFlowIsCached(t *testing.T) {
identityServerFake := identityServerFake{
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)
<-resultChannel
Expand Down Expand Up @@ -217,9 +173,9 @@ func TestShowsSuccessfullyLoggedInPage(t *testing.T) {
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, _ := callAuthenticator(context)
responseBody := performLogin(loginUrl, t)

Expand Down Expand Up @@ -283,17 +239,26 @@ func callAuthenticator(context AuthenticatorContext) (url.URL, chan Authenticato
return *url, resultChannel
}

func createAuthContext(identityUrl url.URL) AuthenticatorContext {
func createAuthContext(baseUrl url.URL) AuthenticatorContext {
config := map[string]interface{}{
"clientId": newClientId(),
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest(fmt.Sprintf("%s://%s", identityUrl.Scheme, identityUrl.Host), map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
identityUrl := createIdentityUrl(baseUrl.Host)
request := NewAuthenticatorRequest(fmt.Sprintf("%s://%s", baseUrl.Scheme, baseUrl.Host), map[string]string{})
context := NewAuthenticatorContext("login", config, identityUrl, false, false, *request)
return *context
}

func createIdentityUrl(hostName string) url.URL {
if hostName == "" {
hostName = "localhost"
}
identityUrl, _ := url.Parse(fmt.Sprintf("http://%s//identity_", hostName))
return *identityUrl
}

func performLogin(loginUrl url.URL, t *testing.T) string {
redirectUri := loginUrl.Query().Get("redirect_uri")
state := loginUrl.Query().Get("state")
Expand Down
40 changes: 40 additions & 0 deletions commandline/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const insecureFlagName = "insecure"
const debugFlagName = "debug"
const profileFlagName = "profile"
const uriFlagName = "uri"
const identityUriFlagName = "identity-uri"
const organizationFlagName = "organization"
const tenantFlagName = "tenant"
const helpFlagName = "help"
Expand All @@ -41,6 +42,7 @@ var predefinedFlags = []string{
debugFlagName,
profileFlagName,
uriFlagName,
identityUriFlagName,
organizationFlagName,
tenantFlagName,
helpFlagName,
Expand Down Expand Up @@ -220,6 +222,32 @@ func (b CommandBuilder) createBaseUri(operation parser.Operation, config config.
return builder.Uri(), nil
}

func (b CommandBuilder) createIdentityUri(context *cli.Context, config config.Config, baseUri url.URL) (*url.URL, error) {
uri := context.String(identityUriFlagName)
if uri != "" {
identityUri, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("Error parsing %s argument: %w", identityUriFlagName, err)
}
return identityUri, nil
}

value := config.Auth.Config["uri"]
uri, valid := value.(string)
if valid && uri != "" {
identityUri, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("Error parsing identity uri config: %w", err)
}
return identityUri, nil
}
identityUri, err := url.Parse(fmt.Sprintf("%s://%s/identity_", baseUri.Scheme, baseUri.Host))
if err != nil {
return nil, fmt.Errorf("Error parsing identity uri: %w", err)
}
return identityUri, nil
}

func (b CommandBuilder) parseUriArgument(context *cli.Context) (*url.URL, error) {
uriFlag := context.String(uriFlagName)
if uriFlag == "" {
Expand Down Expand Up @@ -363,6 +391,11 @@ func (b CommandBuilder) createOperationCommand(operation parser.Operation) *cli.
}
insecure := context.Bool(insecureFlagName) || config.Insecure
debug := context.Bool(debugFlagName) || config.Debug
identityUri, err := b.createIdentityUri(context, *config, baseUri)
if err != nil {
return err
}

executionContext := executor.NewExecutionContext(
organization,
tenant,
Expand All @@ -375,6 +408,7 @@ func (b CommandBuilder) createOperationCommand(operation parser.Operation) *cli.
config.Auth,
insecure,
debug,
*identityUri,
operation.Plugin)

if wait != "" {
Expand Down Expand Up @@ -875,6 +909,12 @@ func (b CommandBuilder) CreateDefaultFlags(hidden bool) []cli.Flag {
Value: "",
Hidden: hidden,
},
&cli.StringFlag{
Name: identityUriFlagName,
Usage: "Identity Server URI",
EnvVars: []string{"UIPATH_IDENTITY_URI"},
Hidden: hidden,
},
b.VersionFlag(hidden),
}
}
Expand Down
4 changes: 2 additions & 2 deletions commandline/parameter_formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package commandline

import (
"fmt"
"slices"
"sort"
"strings"

"github.com/UiPath/uipathcli/parser"
Expand Down Expand Up @@ -68,7 +68,7 @@ func (f parameterFormatter) descriptionFields(parameter parser.Parameter) []inte

func (f parameterFormatter) usageExample(parameter parser.Parameter) string {
parameters := f.collectUsageParameters(parameter, "")
slices.Sort(parameters)
sort.Strings(parameters)

builder := strings.Builder{}
for _, value := range parameters {
Expand Down
4 changes: 3 additions & 1 deletion executor/execution_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ExecutionContext struct {
AuthConfig config.AuthConfig
Insecure bool
Debug bool
IdentityUri url.URL
Plugin plugin.CommandPlugin
}

Expand All @@ -37,6 +38,7 @@ func NewExecutionContext(
authConfig config.AuthConfig,
insecure bool,
debug bool,
identityUri url.URL,
plugin plugin.CommandPlugin) *ExecutionContext {
return &ExecutionContext{organization, tenant, method, uri, route, contentType, input, parameters, authConfig, insecure, debug, plugin}
return &ExecutionContext{organization, tenant, method, uri, route, contentType, input, parameters, authConfig, insecure, debug, identityUri, plugin}
}
Loading

0 comments on commit 5e62403

Please sign in to comment.