From df7dd63c63c85f562cd294251a5d1838d5afac53 Mon Sep 17 00:00:00 2001 From: Robi Nino Date: Tue, 3 Oct 2023 11:58:19 +0300 Subject: [PATCH] New Platform Access Token Command (#2193) --- access_test.go | 179 +++++++++++++++- artifactory/cli.go | 23 +- artifactory_test.go | 2 +- docs/artifactory/accesstokencreate/help.go | 8 +- docs/general/token/help.go | 14 ++ general/token/cli.go | 112 ++++++++++ go.mod | 8 +- go.sum | 8 +- main.go | 26 ++- main_test.go | 2 +- utils/accesstoken/utils.go | 15 ++ utils/cliutils/cli_consts.go | 2 +- utils/cliutils/commandsflags.go | 232 ++++++++++++++------- 13 files changed, 512 insertions(+), 119 deletions(-) create mode 100644 docs/general/token/help.go create mode 100644 general/token/cli.go create mode 100644 utils/accesstoken/utils.go diff --git a/access_test.go b/access_test.go index c0a3d3ff5..299167a1b 100644 --- a/access_test.go +++ b/access_test.go @@ -3,33 +3,86 @@ package main import ( "encoding/base64" "encoding/json" + "fmt" "github.com/jfrog/jfrog-cli-core/v2/common/commands" - coreenvsetup "github.com/jfrog/jfrog-cli-core/v2/general/envsetup" + coreEnvSetup "github.com/jfrog/jfrog-cli-core/v2/general/envsetup" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli/utils/tests" "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/httpclient" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/httputils" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" + "net/http" "testing" ) +var ( + accessDetails *config.ServerDetails + accessCli *tests.JfrogCli + accessHttpDetails httputils.HttpClientDetails +) + func initAccessTest(t *testing.T) { if !*tests.TestAccess { t.Skip("Skipping Access test. To run Access test add the '-test.access=true' option.") } } +func initAccessCli() { + if accessCli != nil { + return + } + accessCli = tests.NewJfrogCli(execMain, "jfrog", authenticateAccess()) +} + +func InitAccessTests() { + initArtifactoryCli() + initAccessCli() + cleanUpOldBuilds() + cleanUpOldRepositories() + cleanUpOldUsers() + tests.AddTimestampToGlobalVars() + createRequiredRepos() + cleanArtifactoryTest() +} + +func authenticateAccess() string { + *tests.JfrogUrl = clientUtils.AddTrailingSlashIfNeeded(*tests.JfrogUrl) + accessDetails = &config.ServerDetails{ + AccessUrl: *tests.JfrogUrl + tests.AccessEndpoint} + + cred := fmt.Sprintf("--url=%s", *tests.JfrogUrl) + if *tests.JfrogAccessToken != "" { + accessDetails.AccessToken = *tests.JfrogAccessToken + cred += fmt.Sprintf(" --access-token=%s", accessDetails.AccessToken) + } else { + accessDetails.User = *tests.JfrogUser + accessDetails.Password = *tests.JfrogPassword + cred += fmt.Sprintf(" --user=%s --password=%s", accessDetails.User, accessDetails.Password) + } + + accessAuth, err := accessDetails.CreateAccessAuthConfig() + if err != nil { + coreutils.ExitOnErr(err) + } + accessHttpDetails = accessAuth.CreateHttpClientDetails() + return cred +} + func TestSetupInvitedUser(t *testing.T) { initAccessTest(t) - tempDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) defer createTempDirCallback() setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, tempDirPath) defer setEnvCallBack() - serverDetails := &config.ServerDetails{Url: *tests.JfrogUrl, AccessToken: *tests.JfrogAccessToken} - encodedCred := encodeConnectionDetails(serverDetails, t) - setupCmd := coreenvsetup.NewEnvSetupCommand().SetEncodedConnectionDetails(encodedCred) + setupServerDetails := &config.ServerDetails{Url: *tests.JfrogUrl, AccessToken: *tests.JfrogAccessToken} + encodedCred := encodeConnectionDetails(setupServerDetails, t) + setupCmd := coreEnvSetup.NewEnvSetupCommand().SetEncodedConnectionDetails(encodedCred) suffix := setupCmd.SetupAndConfigServer() assert.Empty(t, suffix) configs, err := config.GetAllServersConfigs() @@ -54,7 +107,7 @@ func TestRefreshableAccessTokens(t *testing.T) { initAccessTest(t) server := &config.ServerDetails{Url: *tests.JfrogUrl, AccessToken: *tests.JfrogAccessToken} - err := coreenvsetup.GenerateNewLongTermRefreshableAccessToken(server) + err := coreEnvSetup.GenerateNewLongTermRefreshableAccessToken(server) assert.NoError(t, err) assert.NotEmpty(t, server.RefreshToken) configCmd := commands.NewConfigCommand(commands.AddOrEdit, tests.ServerId).SetDetails(server).SetInteractive(false) @@ -115,3 +168,115 @@ func getAccessTokensFromConfig(t *testing.T, serverId string) (accessToken, refr } return details.AccessToken, details.RefreshToken, nil } + +const ( + userScope = "applied-permissions/user" + defaultExpiry = 31536000 +) + +var atcTestCases = []struct { + name string + args []string + shouldExpire bool + expectedExpiry uint + expectedScope string + expectedRefreshable bool + expectedReference bool +}{ + { + name: "default", + args: []string{"atc"}, + shouldExpire: true, + expectedExpiry: defaultExpiry, + expectedScope: userScope, + expectedRefreshable: false, + expectedReference: false, + }, + { + name: "explicit user, no expiry", + args: []string{"atc", auth.ExtractUsernameFromAccessToken(*tests.JfrogAccessToken), "--expiry=0"}, + shouldExpire: false, + expectedExpiry: 0, + expectedScope: userScope, + expectedRefreshable: false, + expectedReference: false, + }, + { + name: "refreshable, admin", + args: []string{"atc", "--refreshable", "--grant-admin"}, + shouldExpire: true, + expectedExpiry: defaultExpiry, + expectedScope: "applied-permissions/admin", + expectedRefreshable: true, + expectedReference: false, + }, + { + name: "reference, custom scope, custom expiry", + args: []string{"atc", "--reference", "--scope=system:metrics:r", "--expiry=123456"}, + shouldExpire: true, + expectedExpiry: 123456, + expectedScope: "system:metrics:r", + expectedRefreshable: false, + expectedReference: true, + }, + { + name: "groups, description", + args: []string{"atc", "--groups=group1,group2", "--description=description"}, + shouldExpire: true, + expectedExpiry: defaultExpiry, + expectedScope: "applied-permissions/groups:group1,group2", + expectedRefreshable: false, + expectedReference: false, + }, +} + +func TestAccessTokenCreate(t *testing.T) { + initAccessTest(t) + if *tests.JfrogAccessToken == "" { + t.Skip("access token create command only supports authorization with access token, but a token is not provided. Skipping...") + } + + for _, test := range atcTestCases { + t.Run(test.name, func(t *testing.T) { + var token auth.CreateTokenResponseData + output := accessCli.RunCliCmdWithOutput(t, test.args...) + assert.NoError(t, json.Unmarshal([]byte(output), &token)) + defer revokeToken(t, token.TokenId) + + if test.shouldExpire { + assert.EqualValues(t, test.expectedExpiry, *token.ExpiresIn) + } else { + assert.Nil(t, token.ExpiresIn) + } + assert.NotEmpty(t, token.AccessToken) + assert.Equal(t, test.expectedScope, token.Scope) + assertNotEmptyIfExpected(t, test.expectedRefreshable, token.RefreshToken) + assertNotEmptyIfExpected(t, test.expectedReference, token.ReferenceToken) + + // Try pinging Artifactory with the new token. + assert.NoError(t, tests.NewJfrogCli(execMain, "jfrog rt", + "--url="+*tests.JfrogUrl+tests.ArtifactoryEndpoint+" --access-token="+token.AccessToken).Exec("ping")) + }) + } +} + +func assertNotEmptyIfExpected(t *testing.T, expected bool, output string) { + if expected { + assert.NotEmpty(t, output) + } else { + assert.Empty(t, output) + } +} + +func revokeToken(t *testing.T, tokenId string) { + if tokenId == "" { + return + } + + client, err := httpclient.ClientBuilder().Build() + assert.NoError(t, err) + + resp, _, err := client.SendDelete(*tests.JfrogUrl+"access/api/v1/tokens/"+tokenId, nil, accessHttpDetails, "") + assert.NoError(t, err) + assert.NoError(t, errorutils.CheckResponseStatus(resp, http.StatusOK)) +} diff --git a/artifactory/cli.go b/artifactory/cli.go index 1e02ab521..12a639464 100644 --- a/artifactory/cli.go +++ b/artifactory/cli.go @@ -3,6 +3,7 @@ package artifactory import ( "errors" "fmt" + "github.com/jfrog/jfrog-cli/utils/accesstoken" "os" "strconv" "strings" @@ -860,13 +861,13 @@ func GetCommands() []cli.Command { { Name: "access-token-create", Aliases: []string{"atc"}, - Flags: cliutils.GetCommandFlags(cliutils.AccessTokenCreate), + Flags: cliutils.GetCommandFlags(cliutils.ArtifactoryAccessTokenCreate), Usage: accesstokencreate.GetDescription(), HelpName: corecommon.CreateUsage("rt atc", accesstokencreate.GetDescription(), accesstokencreate.Usage), UsageText: accesstokencreate.GetArguments(), ArgsUsage: common.CreateEnvVars(), BashComplete: corecommon.CreateBashCompletionFunc(), - Action: accessTokenCreateCmd, + Action: artifactoryAccessTokenCreateCmd, }, { Name: "transfer-settings", @@ -2199,7 +2200,7 @@ func groupDeleteCmd(c *cli.Context) error { return commands.Exec(groupDeleteCmd) } -func accessTokenCreateCmd(c *cli.Context) error { +func artifactoryAccessTokenCreateCmd(c *cli.Context) error { if c.NArg() > 1 { return cliutils.WrongNumberOfArgumentsHandler(c) } @@ -2208,20 +2209,16 @@ func accessTokenCreateCmd(c *cli.Context) error { if err != nil { return err } - // If the username is provided as an argument, then it is used when creating the token. - // If not, then the configured username (or the value of the --user option) is used. - var userName string - if c.NArg() > 0 { - userName = c.Args().Get(0) - } else { - userName = serverDetails.GetUser() - } - expiry, err := cliutils.GetIntFlagValue(c, "expiry", cliutils.TokenExpiry) + + username := accesstoken.GetSubjectUsername(c, serverDetails) + expiry, err := cliutils.GetIntFlagValue(c, cliutils.Expiry, cliutils.ArtifactoryTokenExpiry) if err != nil { return err } accessTokenCreateCmd := generic.NewAccessTokenCreateCommand() - accessTokenCreateCmd.SetUserName(userName).SetServerDetails(serverDetails).SetRefreshable(c.Bool("refreshable")).SetExpiry(expiry).SetGroups(c.String("groups")).SetAudience(c.String("audience")).SetGrantAdmin(c.Bool("grant-admin")) + accessTokenCreateCmd.SetUserName(username).SetServerDetails(serverDetails). + SetRefreshable(c.Bool(cliutils.Refreshable)).SetExpiry(expiry).SetGroups(c.String(cliutils.Groups)). + SetAudience(c.String(cliutils.Audience)).SetGrantAdmin(c.Bool(cliutils.GrantAdmin)) err = commands.Exec(accessTokenCreateCmd) if err != nil { return err diff --git a/artifactory_test.go b/artifactory_test.go index 1ec968a82..4330130a7 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -5223,7 +5223,7 @@ func TestArtifactoryReplicationCreate(t *testing.T) { cleanArtifactoryTest() } -func TestAccessTokenCreate(t *testing.T) { +func TestArtifactoryAccessTokenCreate(t *testing.T) { initArtifactoryTest(t, "") buffer, _, previousLog := coretests.RedirectLogOutputToBuffer() diff --git a/docs/artifactory/accesstokencreate/help.go b/docs/artifactory/accesstokencreate/help.go index 22f57c693..f51c8e6f0 100644 --- a/docs/artifactory/accesstokencreate/help.go +++ b/docs/artifactory/accesstokencreate/help.go @@ -1,12 +1,12 @@ package accesstokencreate -var Usage = []string{"rt atc", "rt atc "} +var Usage = []string{"rt atc", "rt atc "} func GetDescription() string { - return "Creates an access token. By default an user-scoped token will be created, unless the --groups and/or --grant-admin options are specified." + return "Creates an Artifactory access token. By default an user-scoped token will be created, unless the --groups and/or --grant-admin options are specified." } func GetArguments() string { - return ` user name - The user name for which this token is created. If not specified, the token will be created for the current user.` + return ` username + The username for which this token is created. If not specified, the token will be created for the current user.` } diff --git a/docs/general/token/help.go b/docs/general/token/help.go new file mode 100644 index 000000000..601a1665d --- /dev/null +++ b/docs/general/token/help.go @@ -0,0 +1,14 @@ +package token + +var Usage = []string{"atc", "atc "} + +func GetDescription() string { + return `Creates an access token. + By default, an user-scoped token will be created. + Administrator may provide the scope explicitly with '--scope', or implicitly with '--groups', '--grant-admin'.` +} + +func GetArguments() string { + return ` username + The username for which this token is created. If not specified, the token will be created for the current user.` +} diff --git a/general/token/cli.go b/general/token/cli.go new file mode 100644 index 000000000..748a0f0a1 --- /dev/null +++ b/general/token/cli.go @@ -0,0 +1,112 @@ +package token + +import ( + "errors" + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/common/commands" + generic "github.com/jfrog/jfrog-cli-core/v2/general/token" + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli/utils/accesstoken" + "github.com/jfrog/jfrog-cli/utils/cliutils" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/urfave/cli" + "strconv" +) + +func AccessTokenCreateCmd(c *cli.Context) error { + if c.NArg() > 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + + serverDetails, err := createPlatformDetailsByFlags(c) + if err != nil { + return err + } + + if err = assertAccessTokenAvailable(serverDetails); err != nil { + return err + } + + if err = assertScopeOptions(c); err != nil { + return err + } + + username := accesstoken.GetSubjectUsername(c, serverDetails) + + expiry, err := getExpiry(c) + if err != nil { + return err + } + + accessTokenCreateCmd := generic.NewAccessTokenCreateCommand() + accessTokenCreateCmd. + SetServerDetails(serverDetails). + SetUsername(username). + SetProjectKey(c.String(cliutils.Project)). + SetGroups(c.String(cliutils.Groups)). + SetScope(c.String(cliutils.Scope)). + SetGrantAdmin(c.Bool(cliutils.GrantAdmin)). + SetExpiry(expiry). + SetRefreshable(c.Bool(cliutils.Refreshable)). + SetDescription(c.String(cliutils.Description)). + SetAudience(c.String(cliutils.Audience)). + SetIncludeReferenceToken(c.Bool(cliutils.Reference)) + err = commands.Exec(accessTokenCreateCmd) + if err != nil { + return err + } + resString, err := accessTokenCreateCmd.Response() + if err != nil { + return err + } + log.Output(clientUtils.IndentJson(resString)) + + return nil +} + +func createPlatformDetailsByFlags(c *cli.Context) (*coreConfig.ServerDetails, error) { + platformDetails, err := cliutils.CreateServerDetailsWithConfigOffer(c, true, cliutils.Platform) + if err != nil { + return nil, err + } + if platformDetails.Url == "" { + return nil, errors.New("platform URL is mandatory for access token creation") + } + return platformDetails, nil +} + +func getExpiry(c *cli.Context) (*uint, error) { + if !c.IsSet(cliutils.Expiry) { + return nil, nil + } + + expiryInt, err := strconv.Atoi(c.String(cliutils.Expiry)) + if err != nil { + return nil, cliutils.PrintHelpAndReturnError( + fmt.Sprintf("The '--%s' option must have a numeric value. ", cliutils.Expiry), c) + } + if expiryInt < 0 { + return nil, cliutils.PrintHelpAndReturnError( + fmt.Sprintf("The '--%s' option must be non-negative. ", cliutils.Expiry), c) + } + expiry := uint(expiryInt) + return &expiry, nil +} + +func assertScopeOptions(c *cli.Context) error { + if c.IsSet(cliutils.Scope) && (c.IsSet(cliutils.GrantAdmin) || c.IsSet(cliutils.Groups)) { + return cliutils.PrintHelpAndReturnError( + fmt.Sprintf("Scope can either be provided explicitly with '--%s', or implicitly with '--%s' and '--%s'. ", + cliutils.Scope, cliutils.GrantAdmin, cliutils.Groups), c) + } + return nil +} + +func assertAccessTokenAvailable(serverDetails *coreConfig.ServerDetails) error { + if serverDetails.AccessToken == "" { + return errorutils.CheckErrorf("authenticating with access token is currently mandatory for creating access tokens") + } + return nil +} diff --git a/go.mod b/go.mod index ca40ad934..3e4da02e4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/buger/jsonparser v1.1.1 github.com/go-git/go-git/v5 v5.9.0 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d - github.com/jfrog/build-info-go v1.9.10 + github.com/jfrog/build-info-go v1.9.11 github.com/jfrog/gofrog v1.3.0 github.com/jfrog/jfrog-cli-core/v2 v2.43.2 github.com/jfrog/jfrog-client-go v1.32.3 @@ -128,8 +128,10 @@ require ( replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230928084830-478bd49f5d3e -replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230928145640-1d74324157cb +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20231003083915-9469fc8de766 // replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.2.6-0.20230418122323-2bf299dd6d27 -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230928142526-622034e3f57b +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20231003083451-568b46797866 + +// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230831151231-e5e7bd035ddc diff --git a/go.sum b/go.sum index 7d02a3c9f..121ccb37d 100644 --- a/go.sum +++ b/go.sum @@ -242,10 +242,10 @@ github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230928145640-1d74324157cb h1:keIadueXz/3gsXPqAC0wFrgCRGU23qlQkZ1mtvOPn5U= -github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230928145640-1d74324157cb/go.mod h1:ALnxPIFo/8EsubSGcLjfwSUoJC+YlRMCqM4eXm1zq3w= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230928142526-622034e3f57b h1:WXxRXkWaxFRdMechZJ0pkzZWCI6HNqlxLzAfr3qXjP8= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230928142526-622034e3f57b/go.mod h1:AePTNv5H1YSGycxiL+1jXHCzqu3rCGruVP7S0N+BEEo= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20231003083915-9469fc8de766 h1:HqgXCK9umbVutqd7ZFlip6qPAasBBXMtF3UAYktXA/s= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20231003083915-9469fc8de766/go.mod h1:fnOBbz6OfIRX1UNlX7kaf7Q+3AJkHb1ZiAG2aKeY2Jk= +github.com/jfrog/jfrog-client-go v1.28.1-0.20231003083451-568b46797866 h1:0SWHyECx5QfCjQXf8hDzbyM94B78Dvzei7TvD9CpsCY= +github.com/jfrog/jfrog-client-go v1.28.1-0.20231003083451-568b46797866/go.mod h1:wtk8jhtdrlzYvo3LLIwOn0OrqoSm8J5TiMfZzHIwLe8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jszwec/csvutil v1.8.0 h1:G7vS2LGdpZZDH1HmHeNbxOaJ/ZnJlpwGFvOkTkJzzNk= diff --git a/main.go b/main.go index 7304e0906..8cf9155c0 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,6 @@ package main import ( "fmt" - "github.com/jfrog/jfrog-cli/lifecycle" - "golang.org/x/exp/slices" - "os" - "runtime" - "sort" - "strings" - "github.com/agnivade/levenshtein" corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" setupcore "github.com/jfrog/jfrog-cli-core/v2/general/envsetup" @@ -23,10 +16,13 @@ import ( "github.com/jfrog/jfrog-cli/docs/common" "github.com/jfrog/jfrog-cli/docs/general/cisetup" loginDocs "github.com/jfrog/jfrog-cli/docs/general/login" + tokenDocs "github.com/jfrog/jfrog-cli/docs/general/token" cisetupcommand "github.com/jfrog/jfrog-cli/general/cisetup" "github.com/jfrog/jfrog-cli/general/envsetup" "github.com/jfrog/jfrog-cli/general/login" "github.com/jfrog/jfrog-cli/general/project" + "github.com/jfrog/jfrog-cli/general/token" + "github.com/jfrog/jfrog-cli/lifecycle" "github.com/jfrog/jfrog-cli/missioncontrol" "github.com/jfrog/jfrog-cli/pipelines" "github.com/jfrog/jfrog-cli/plugins" @@ -38,6 +34,11 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" clientlog "github.com/jfrog/jfrog-client-go/utils/log" "github.com/urfave/cli" + "golang.org/x/exp/slices" + "os" + "runtime" + "sort" + "strings" ) const commandHelpTemplate string = `{{.HelpName}}{{if .UsageText}} @@ -276,6 +277,17 @@ func getCommands() []cli.Command { Category: otherCategory, Action: login.LoginCmd, }, + { + Name: "access-token-create", + Aliases: []string{"atc"}, + Flags: cliutils.GetCommandFlags(cliutils.AccessTokenCreate), + Usage: tokenDocs.GetDescription(), + HelpName: corecommon.CreateUsage("atc", tokenDocs.GetDescription(), tokenDocs.Usage), + UsageText: tokenDocs.GetArguments(), + ArgsUsage: common.CreateEnvVars(), + BashComplete: corecommon.CreateBashCompletionFunc(), + Action: token.AccessTokenCreateCmd, + }, } allCommands := append(slices.Clone(cliNameSpaces), utils.GetPlugins()...) allCommands = append(allCommands, scan.GetCommands()...) diff --git a/main_test.go b/main_test.go index 96efc1218..41805010a 100644 --- a/main_test.go +++ b/main_test.go @@ -71,7 +71,7 @@ func setupIntegrationTests() { InitXrayTests() } if *tests.TestAccess { - InitArtifactoryTests() + InitAccessTests() } if *tests.TestTransfer { InitTransferTests() diff --git a/utils/accesstoken/utils.go b/utils/accesstoken/utils.go new file mode 100644 index 000000000..f5ab0fc42 --- /dev/null +++ b/utils/accesstoken/utils.go @@ -0,0 +1,15 @@ +package accesstoken + +import ( + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/urfave/cli" +) + +// If the username is provided as an argument, then it is used when creating the token. +// If not, then the configured username (or the value of the --user option) is used. +func GetSubjectUsername(c *cli.Context, serverDetails *coreConfig.ServerDetails) string { + if c.NArg() > 0 { + return c.Args().Get(0) + } + return serverDetails.GetUser() +} diff --git a/utils/cliutils/cli_consts.go b/utils/cliutils/cli_consts.go index 96597a3a8..2b88e08a5 100644 --- a/utils/cliutils/cli_consts.go +++ b/utils/cliutils/cli_consts.go @@ -28,7 +28,7 @@ const ( Retries = 3 RetryWaitMilliSecs = 0 Threads = 3 - TokenExpiry = 3600 + ArtifactoryTokenExpiry = 3600 DefaultLicenseCount = 1 LatestCliVersionCheckInterval = time.Hour * 6 diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 4de652c35..ef823ad63 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -76,16 +76,17 @@ const ( RepoDelete = "repo-delete" ReplicationDelete = "replication-delete" PermissionTargetDelete = "permission-target-delete" - AccessTokenCreate = "access-token-create" - UserCreate = "user-create" - UsersCreate = "users-create" - UsersDelete = "users-delete" - GroupCreate = "group-create" - GroupAddUsers = "group-add-users" - GroupDelete = "group-delete" - TransferConfig = "transfer-config" - TransferConfigMerge = "transfer-config-merge" - passphrase = "passphrase" + // #nosec G101 -- False positive - no hardcoded credentials. + ArtifactoryAccessTokenCreate = "artifactory-access-token-create" + UserCreate = "user-create" + UsersCreate = "users-create" + UsersDelete = "users-delete" + GroupCreate = "group-create" + GroupAddUsers = "group-add-users" + GroupDelete = "group-delete" + TransferConfig = "transfer-config" + TransferConfigMerge = "transfer-config-merge" + passphrase = "passphrase" // Distribution's Command Keys ReleaseBundleV1Create = "release-bundle-v1-create" @@ -135,9 +136,13 @@ const ( ReleaseBundlePromote = "release-bundle-promote" ReleaseBundleDistribute = "release-bundle-distribute" + // Access Token Create commands keys + AccessTokenCreate = "access-token-create" + // *** Artifactory Commands' flags *** // Base flags url = "url" + platformUrl = "platform-url" user = "user" password = "password" accessToken = "access-token" @@ -281,7 +286,7 @@ const ( envInclude = "env-include" envExclude = "env-exclude" buildUrl = "build-url" - project = "project" + Project = "project" // Unique build-add-dependencies flags badPrefix = "bad-" @@ -394,12 +399,35 @@ const ( Replace = "replace" Admin = "admin" + // Mutual *-access-token-create flags + Groups = "groups" + GrantAdmin = "grant-admin" + Expiry = "expiry" + Refreshable = "refreshable" + Audience = "audience" + + // Unique artifactory-access-token-create flags + artifactoryAccessTokenCreatePrefix = "rt-atc-" + rtAtcGroups = artifactoryAccessTokenCreatePrefix + Groups + rtAtcGrantAdmin = artifactoryAccessTokenCreatePrefix + GrantAdmin + rtAtcExpiry = artifactoryAccessTokenCreatePrefix + Expiry + rtAtcRefreshable = artifactoryAccessTokenCreatePrefix + Refreshable + rtAtcAudience = artifactoryAccessTokenCreatePrefix + Audience + // Unique access-token-create flags - groups = "groups" - grantAdmin = "grant-admin" - expiry = "expiry" - refreshable = "refreshable" - audience = "audience" + accessTokenCreatePrefix = "atc-" + atcProject = accessTokenCreatePrefix + Project + Scope = "scope" + atcScope = accessTokenCreatePrefix + Scope + Description = "description" + atcDescription = accessTokenCreatePrefix + Description + Reference = "reference" + atcReference = accessTokenCreatePrefix + Reference + atcGroups = accessTokenCreatePrefix + Groups + atcGrantAdmin = accessTokenCreatePrefix + GrantAdmin + atcExpiry = accessTokenCreatePrefix + Expiry + atcRefreshable = accessTokenCreatePrefix + Refreshable + atcAudience = accessTokenCreatePrefix + Audience // Unique Xray Flags for upload/publish commands xrayScan = "scan" @@ -549,7 +577,7 @@ const ( lifecyclePrefix = "lc-" lcUrl = lifecyclePrefix + url lcSync = lifecyclePrefix + Sync - lcProject = lifecyclePrefix + project + lcProject = lifecyclePrefix + Project Builds = "builds" lcBuilds = lifecyclePrefix + Builds ReleaseBundles = "release-bundles" @@ -566,6 +594,10 @@ const ( var flagsMap = map[string]cli.Flag{ // Common commands flags + platformUrl: cli.StringFlag{ + Name: url, + Usage: "[Optional] JFrog platform URL.` `", + }, user: cli.StringFlag{ Name: user, Usage: "[Optional] JFrog username.` `", @@ -908,8 +940,8 @@ var flagsMap = map[string]cli.Flag{ Name: buildUrl, Usage: "[Optional] Can be used for setting the CI server build URL in the build-info.` `", }, - project: cli.StringFlag{ - Name: project, + Project: cli.StringFlag{ + Name: Project, Usage: "[Optional] JFrog Artifactory project key.` `", }, bpDryRun: cli.BoolFlag{ @@ -1141,26 +1173,26 @@ var flagsMap = map[string]cli.Flag{ Name: vars, Usage: "[Optional] List of variables in the form of \"key1=value1;key2=value2;...\" to be replaced in the template. In the template, the variables should be used as follows: ${key1}.` `", }, - groups: cli.StringFlag{ - Name: groups, + rtAtcGroups: cli.StringFlag{ + Name: Groups, Usage: "[Default: *] A list of comma-separated groups for the access token to be associated with. " + "Specify * to indicate that this is a 'user-scoped token', i.e., the token provides the same access privileges that the current subject has, and is therefore evaluated dynamically. " + "A non-admin user can only provide a scope that is a subset of the groups to which he belongs` `", }, - grantAdmin: cli.BoolFlag{ - Name: grantAdmin, + rtAtcGrantAdmin: cli.BoolFlag{ + Name: GrantAdmin, Usage: "[Default: false] Set to true to provide admin privileges to the access token. This is only available for administrators.` `", }, - expiry: cli.StringFlag{ - Name: expiry, - Usage: "[Default: " + strconv.Itoa(TokenExpiry) + "] The time in seconds for which the token will be valid. To specify a token that never expires, set to zero. Non-admin can only set a value that is equal to or less than the default 3600.` `", + rtAtcExpiry: cli.StringFlag{ + Name: Expiry, + Usage: "[Default: " + strconv.Itoa(ArtifactoryTokenExpiry) + "] The time in seconds for which the token will be valid. To specify a token that never expires, set to zero. Non-admin may only set a value that is equal to or less than the default 3600.` `", }, - refreshable: cli.BoolFlag{ - Name: refreshable, + rtAtcRefreshable: cli.BoolFlag{ + Name: Refreshable, Usage: "[Default: false] Set to true if you'd like the token to be refreshable. A refresh token will also be returned in order to be used to generate a new token once it expires.` `", }, - audience: cli.StringFlag{ - Name: audience, + rtAtcAudience: cli.StringFlag{ + Name: Audience, Usage: "[Optional] A space-separate list of the other Artifactory instances or services that should accept this token identified by their Artifactory Service IDs, as obtained by the 'jfrog rt curl api/system/service_id' command.` `", }, usersCreateCsv: cli.StringFlag{ @@ -1603,7 +1635,7 @@ var flagsMap = map[string]cli.Flag{ Usage: "[Default: false] Set to true to run synchronously.` `", }, lcProject: cli.StringFlag{ - Name: project, + Name: Project, Usage: "[Optional] Project key associated with the Release Bundle version.` `", }, lcBuilds: cli.StringFlag{ @@ -1640,6 +1672,45 @@ var flagsMap = map[string]cli.Flag{ Usage: "Default: false] [npm] when set, the Contextual Analysis scan also uses the code of the project dependencies to determine the applicability of the vulnerability.", Hidden: true, }, + atcProject: cli.StringFlag{ + Name: Project, + Usage: "[Optional] The project for which this token is created. Enter the project name on which you want to apply this token.` `", + }, + atcGrantAdmin: cli.BoolFlag{ + Name: GrantAdmin, + Usage: "[Default: false] Set to true to provide admin privileges to the access token. This is only available for administrators.` `", + }, + atcGroups: cli.StringFlag{ + Name: Groups, + Usage: "[Optional] A list of comma-separated groups for the access token to be associated with. " + + "This is only available for administrators.` `", + }, + atcScope: cli.StringFlag{ + Name: Scope, + Usage: "[Optional] The scope of access that the token provides. This is only available for administrators.` `", + }, + atcExpiry: cli.StringFlag{ + Name: Expiry, + Usage: "[Optional] The amount of time, in seconds, it would take for the token to expire. Must be non-negative." + + "If not provided, the platform default will be used. To specify a token that never expires, set to zero. " + + "Non-admin may only set a value that is equal or lower than the platform default that was set by an administrator (1 year by default).` `", + }, + atcRefreshable: cli.BoolFlag{ + Name: Refreshable, + Usage: "[Default: false] Set to true if you'd like the token to be refreshable. A refresh token will also be returned in order to be used to generate a new token once it expires.` `", + }, + atcDescription: cli.StringFlag{ + Name: Description, + Usage: "[Optional] Free text token description. Useful for filtering and managing tokens. Limited to 1024 characters.` `", + }, + atcAudience: cli.StringFlag{ + Name: Audience, + Usage: "[Optional] A space-separated list of the other instances or services that should accept this token identified by their Service-IDs.` `", + }, + atcReference: cli.BoolFlag{ + Name: Reference, + Usage: "[Default: false] Generate a Reference Token (alias to Access Token) in addition to the full token (available from Artifactory 7.38.10)` `", + }, } var commandFlags = map[string][]string{ @@ -1658,7 +1729,7 @@ var commandFlags = map[string][]string{ url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, uploadTargetProps, ClientCertKeyPath, specFlag, specVars, buildName, buildNumber, module, uploadExclusions, deb, uploadRecursive, uploadFlat, uploadRegexp, retries, retryWaitTime, dryRun, uploadExplode, symlinks, includeDirs, - failNoOp, threads, uploadSyncDeletes, syncDeletesQuiet, InsecureTls, detailedSummary, project, + failNoOp, threads, uploadSyncDeletes, syncDeletesQuiet, InsecureTls, detailedSummary, Project, uploadAnt, uploadArchive, }, Download: { @@ -1666,74 +1737,74 @@ var commandFlags = map[string][]string{ ClientCertKeyPath, specFlag, specVars, buildName, buildNumber, module, exclusions, sortBy, sortOrder, limit, offset, downloadRecursive, downloadFlat, build, includeDeps, excludeArtifacts, minSplit, splitCount, retries, retryWaitTime, dryRun, downloadExplode, bypassArchiveInspection, validateSymlinks, bundle, publicGpgKey, includeDirs, - downloadProps, downloadExcludeProps, failNoOp, threads, archiveEntries, downloadSyncDeletes, syncDeletesQuiet, InsecureTls, detailedSummary, project, + downloadProps, downloadExcludeProps, failNoOp, threads, archiveEntries, downloadSyncDeletes, syncDeletesQuiet, InsecureTls, detailedSummary, Project, skipChecksum, }, Move: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, specFlag, specVars, exclusions, sortBy, sortOrder, limit, offset, moveRecursive, moveFlat, dryRun, build, includeDeps, excludeArtifacts, moveProps, moveExcludeProps, failNoOp, threads, archiveEntries, - InsecureTls, retries, retryWaitTime, project, + InsecureTls, retries, retryWaitTime, Project, }, Copy: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, specFlag, specVars, exclusions, sortBy, sortOrder, limit, offset, copyRecursive, copyFlat, dryRun, build, includeDeps, excludeArtifacts, bundle, copyProps, copyExcludeProps, failNoOp, threads, - archiveEntries, InsecureTls, retries, retryWaitTime, project, + archiveEntries, InsecureTls, retries, retryWaitTime, Project, }, Delete: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, specFlag, specVars, exclusions, sortBy, sortOrder, limit, offset, deleteRecursive, dryRun, build, includeDeps, excludeArtifacts, deleteQuiet, deleteProps, deleteExcludeProps, failNoOp, threads, archiveEntries, - InsecureTls, retries, retryWaitTime, project, + InsecureTls, retries, retryWaitTime, Project, }, Search: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, specFlag, specVars, exclusions, sortBy, sortOrder, limit, offset, searchRecursive, build, includeDeps, excludeArtifacts, count, bundle, includeDirs, searchProps, searchExcludeProps, failNoOp, archiveEntries, - InsecureTls, searchTransitive, retries, retryWaitTime, project, searchInclude, + InsecureTls, searchTransitive, retries, retryWaitTime, Project, searchInclude, }, Properties: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, specFlag, specVars, exclusions, sortBy, sortOrder, limit, offset, propsRecursive, build, includeDeps, excludeArtifacts, bundle, includeDirs, failNoOp, threads, archiveEntries, propsProps, propsExcludeProps, - InsecureTls, retries, retryWaitTime, project, + InsecureTls, retries, retryWaitTime, Project, }, BuildPublish: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, buildUrl, bpDryRun, - envInclude, envExclude, InsecureTls, project, bpDetailedSummary, + envInclude, envExclude, InsecureTls, Project, bpDetailedSummary, }, BuildAppend: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, buildUrl, bpDryRun, - envInclude, envExclude, InsecureTls, project, + envInclude, envExclude, InsecureTls, Project, }, BuildAddDependencies: { - specFlag, specVars, uploadExclusions, badRecursive, badRegexp, badDryRun, project, badFromRt, serverId, badModule, + specFlag, specVars, uploadExclusions, badRecursive, badRegexp, badDryRun, Project, badFromRt, serverId, badModule, }, BuildAddGit: { - configFlag, serverId, project, + configFlag, serverId, Project, }, BuildCollectEnv: { - project, + Project, }, BuildDockerCreate: { buildName, buildNumber, module, url, user, password, accessToken, sshPassphrase, sshKeyPath, - serverId, imageFile, project, + serverId, imageFile, Project, }, OcStartBuild: { - buildName, buildNumber, module, project, serverId, ocStartBuildRepo, + buildName, buildNumber, module, Project, serverId, ocStartBuildRepo, }, BuildScanLegacy: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, fail, InsecureTls, - project, + Project, }, BuildPromote: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, Status, comment, - sourceRepo, includeDependencies, copyFlag, failFast, bprDryRun, bprProps, InsecureTls, project, + sourceRepo, includeDependencies, copyFlag, failFast, bprDryRun, bprProps, InsecureTls, Project, }, BuildDiscard: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, maxDays, maxBuilds, - excludeBuilds, deleteArtifacts, bdiAsync, InsecureTls, project, + excludeBuilds, deleteArtifacts, bdiAsync, InsecureTls, Project, }, GitLfsClean: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, refs, glcRepo, glcDryRun, @@ -1747,21 +1818,21 @@ var commandFlags = map[string][]string{ deployIvyDesc, ivyDescPattern, ivyArtifactsPattern, }, Mvn: { - buildName, buildNumber, deploymentThreads, InsecureTls, project, detailedSummary, xrayScan, xrOutput, + buildName, buildNumber, deploymentThreads, InsecureTls, Project, detailedSummary, xrayScan, xrOutput, }, Gradle: { - buildName, buildNumber, deploymentThreads, project, detailedSummary, xrayScan, xrOutput, + buildName, buildNumber, deploymentThreads, Project, detailedSummary, xrayScan, xrOutput, }, Docker: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, serverId, skipLogin, threads, detailedSummary, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, }, DockerPush: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, serverId, skipLogin, threads, detailedSummary, }, DockerPull: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, serverId, skipLogin, }, DockerPromote: { @@ -1770,54 +1841,54 @@ var commandFlags = map[string][]string{ }, ContainerPush: { buildName, buildNumber, module, url, user, password, accessToken, sshPassphrase, sshKeyPath, - serverId, skipLogin, threads, project, detailedSummary, + serverId, skipLogin, threads, Project, detailedSummary, }, ContainerPull: { buildName, buildNumber, module, url, user, password, accessToken, sshPassphrase, sshKeyPath, - serverId, skipLogin, project, + serverId, skipLogin, Project, }, NpmConfig: { global, serverIdResolve, serverIdDeploy, repoResolve, repoDeploy, }, NpmInstallCi: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, NpmPublish: { - buildName, buildNumber, module, project, npmDetailedSummary, xrayScan, xrOutput, + buildName, buildNumber, module, Project, npmDetailedSummary, xrayScan, xrOutput, }, YarnConfig: { global, serverIdResolve, repoResolve, }, Yarn: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, NugetConfig: { global, serverIdResolve, repoResolve, nugetV2, }, Nuget: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, DotnetConfig: { global, serverIdResolve, repoResolve, nugetV2, }, Dotnet: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, GoConfig: { global, serverIdResolve, serverIdDeploy, repoResolve, repoDeploy, }, GoPublish: { - url, user, password, accessToken, buildName, buildNumber, module, project, detailedSummary, goPublishExclusions, + url, user, password, accessToken, buildName, buildNumber, module, Project, detailedSummary, goPublishExclusions, }, Go: { - buildName, buildNumber, module, project, noFallback, + buildName, buildNumber, module, Project, noFallback, }, TerraformConfig: { global, serverIdDeploy, repoDeploy, }, Terraform: { namespace, provider, tag, exclusions, - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, TransferConfig: { Force, Verbose, IncludeRepos, ExcludeRepos, SourceWorkingDir, TargetWorkingDir, PreChecks, @@ -1836,19 +1907,19 @@ var commandFlags = map[string][]string{ global, serverIdResolve, repoResolve, }, PipInstall: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, PipenvConfig: { global, serverIdResolve, repoResolve, }, PipenvInstall: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, PoetryConfig: { global, serverIdResolve, repoResolve, }, Poetry: { - buildName, buildNumber, module, project, + buildName, buildNumber, module, Project, }, ReleaseBundleV1Create: { distUrl, user, password, accessToken, serverId, specFlag, specVars, targetProps, @@ -1886,9 +1957,14 @@ var commandFlags = map[string][]string{ url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, deleteQuiet, }, - AccessTokenCreate: { + ArtifactoryAccessTokenCreate: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, - ClientCertKeyPath, groups, grantAdmin, expiry, refreshable, audience, + ClientCertKeyPath, rtAtcGroups, rtAtcGrantAdmin, rtAtcExpiry, rtAtcRefreshable, rtAtcAudience, + }, + AccessTokenCreate: { + platformUrl, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, ClientCertPath, ClientCertKeyPath, + atcProject, atcGrantAdmin, atcGroups, atcScope, atcExpiry, + atcRefreshable, atcDescription, atcAudience, atcReference, }, UserCreate: { url, user, password, accessToken, sshPassphrase, sshKeyPath, serverId, @@ -1939,37 +2015,37 @@ var commandFlags = map[string][]string{ curationOutput, workingDirs, curationThreads, }, Audit: { - xrUrl, user, password, accessToken, serverId, InsecureTls, project, watches, repoPath, licenses, xrOutput, ExcludeTestDeps, + xrUrl, user, password, accessToken, serverId, InsecureTls, Project, watches, repoPath, licenses, xrOutput, ExcludeTestDeps, useWrapperAudit, DepType, RequirementsFile, fail, ExtendedTable, workingDirs, Mvn, Gradle, Npm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, }, AuditMvn: { - xrUrl, user, password, accessToken, serverId, InsecureTls, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, useWrapperAudit, + xrUrl, user, password, accessToken, serverId, InsecureTls, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, useWrapperAudit, }, AuditGradle: { - xrUrl, user, password, accessToken, serverId, ExcludeTestDeps, useWrapperAudit, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, + xrUrl, user, password, accessToken, serverId, ExcludeTestDeps, useWrapperAudit, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, }, AuditNpm: { - xrUrl, user, password, accessToken, serverId, DepType, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, + xrUrl, user, password, accessToken, serverId, DepType, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, }, AuditGo: { - xrUrl, user, password, accessToken, serverId, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, + xrUrl, user, password, accessToken, serverId, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, }, AuditPip: { - xrUrl, user, password, accessToken, serverId, RequirementsFile, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, + xrUrl, user, password, accessToken, serverId, RequirementsFile, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, }, AuditPipenv: { - xrUrl, user, password, accessToken, serverId, project, watches, repoPath, licenses, xrOutput, ExtendedTable, + xrUrl, user, password, accessToken, serverId, Project, watches, repoPath, licenses, xrOutput, ExtendedTable, }, XrScan: { xrUrl, user, password, accessToken, serverId, specFlag, threads, scanRecursive, scanRegexp, scanAnt, - project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, + Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, }, DockerScan: { // Flags added here should be also added to Docker command - serverId, project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, + serverId, Project, watches, repoPath, licenses, xrOutput, fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, }, BuildScan: { - xrUrl, user, password, accessToken, serverId, project, vuln, xrOutput, fail, ExtendedTable, rescan, + xrUrl, user, password, accessToken, serverId, Project, vuln, xrOutput, fail, ExtendedTable, rescan, }, // Mission Control's commands McConfig: {