Skip to content

Commit

Permalink
Add support to check if the selected org is Tanzu Application platfor…
Browse files Browse the repository at this point in the history
…m enabled

- Added support to show warning to users if the CSP token received through login doesn't contain the necessary TAP scopes. The TAP scopes are fetched from the CLI central configuration file and can be modified without releasing new CLI version.
- User can skip the TAP scopes validation on "tanzu" context using the environment variable "TANZU_CLI_SKIP_TAP_SCOPES_VALIDATION_ON_TANZU_CONTEXT"
- Updated the local test central repo with updated central config file

Signed-off-by: Prem Kumar Kalle <[email protected]>
  • Loading branch information
prkalle committed Apr 18, 2024
1 parent 067a1b6 commit 25d0884
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 3 deletions.
Binary file modified hack/central-repo/registry-content.bz2
Binary file not shown.
4 changes: 4 additions & 0 deletions hack/central-repo/upload-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ cli.core.cli_recommended_versions:
- version: v1.2.2
- version: v1.1.1
- version: v0.90.0
cli.core.tanzu_application_platform_scopes:
- scope: tap:viewer
- scope: tap:admin
- scope: tap:member
cli.core.some-string: "the meaning of life, the universe, and everything"
cli.core.some-int: 42
cli.core.some-bool: true
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/utils/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func DeleteContextFromKubeConfig(kubeconfigPath, context string) error {
}

if shouldWarn {
log.Warningf("warning: this removed your active context, use \"kubectl config use-context\" to select a different one")
log.Warningf("WARNING: this removed your active context, use \"kubectl config use-context\" to select a different one")
}

return nil
Expand Down
11 changes: 10 additions & 1 deletion pkg/centralconfig/central_config_yaml_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import (
"gopkg.in/yaml.v3"
)

// KeyNotFoundError represents an error when the key is not found in the central configuration.
type KeyNotFoundError struct {
Key string
}

func (e *KeyNotFoundError) Error() string {
return fmt.Sprintf("key '%s' not found in central config", e.Key)
}

type centralConfigYamlReader struct {
// configFile is the path to the central config file.
configFile string
Expand Down Expand Up @@ -54,7 +63,7 @@ func (c *centralConfigYamlReader) GetCentralConfigEntry(key string, out interfac
return err
}
if !ok {
return fmt.Errorf("key %s not found in central config", key)
return &KeyNotFoundError{Key: key}
}

return nil
Expand Down
17 changes: 17 additions & 0 deletions pkg/command/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,9 +662,26 @@ func globalTanzuLogin(c *configtypes.Context, generateContextNameFunc func(orgNa
// format
fmt.Fprintln(os.Stderr)
log.Successf("Successfully logged into '%s' organization and created a tanzu context", orgName)

// log warning message if the TAP scopes are retrieved successfully and validation failed
valid, err := validateTokenForTAPScopes(claims, nil)
if err == nil && !valid {
logTanzuInvalidOrgWarningMessage(orgName)
}
return nil
}

func logTanzuInvalidOrgWarningMessage(orgName string) {
warnMsg := `WARNING: While authenticated to organization '%s', there are insufficient permissions to access
the Tanzu Application Platform service. Please verify that the organization authenticated into is
correct and supports the Tanzu Application Platform, and that your account has been granted access
to the service as well.
`
fmt.Fprintln(os.Stderr)
log.Warningf(warnMsg, orgName)
fmt.Fprintln(os.Stderr)
}

// updateTanzuContextMetadata updates the context additional metadata
func updateTanzuContextMetadata(c *configtypes.Context, orgID, orgName string) error {
exists, err := config.ContextExists(c.Name)
Expand Down
91 changes: 91 additions & 0 deletions pkg/command/org_validation_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package command

import (
"fmt"
"os"
"strconv"
"strings"

"github.com/pkg/errors"

"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
"github.com/vmware-tanzu/tanzu-plugin-runtime/log"

"github.com/vmware-tanzu/tanzu-cli/pkg/auth/csp"
"github.com/vmware-tanzu/tanzu-cli/pkg/centralconfig"
cliconfig "github.com/vmware-tanzu/tanzu-cli/pkg/config"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
)

const centralConfigTanzuApplicationPlatformScopesKey = "cli.core.tanzu_application_platform_scopes"

type tapScope struct {
Scope string `yaml:"scope" json:"scope"`
}

type tapScopesGetter func() ([]string, error)

// validateTokenForTAPScopes validates if the token claims contains at least one of the TAP scopes listed in
// the central configuration. If the central configuration doesn't have any TAP scopes listed, it will return success.
// It will skip the validation and return success if TANZU_CLI_SKIP_TANZU_CONTEXT_TAP_SCOPES_VALIDATION environment
// variable is set to true
func validateTokenForTAPScopes(claims *csp.Claims, scopesGetter tapScopesGetter) (bool, error) {
if skipTAPScopeValidation, _ := strconv.ParseBool(os.Getenv(constants.SkipTAPScopesValidationOnTanzuContext)); skipTAPScopeValidation {
return true, nil
}
if scopesGetter == nil {
scopesGetter = getTAPScopesFromCentralConfig
}
tapScopes, err := scopesGetter()
if err != nil {
log.V(7).Error(err, "error retrieving TAP scopes from the central config")
return false, errors.Wrap(err, "error retrieving TAP scopes from the central config")
}

// validate only if we get the permissions/scopes from central configuration
if len(tapScopes) == 0 {
return true, nil
}

for _, tapScope := range tapScopes {
for _, perm := range claims.Permissions {
tapScopeSuffix := fmt.Sprintf("/%s", tapScope)
if strings.HasSuffix(perm, tapScopeSuffix) {
return true, nil
}
}
}

return false, nil
}

func getTAPScopesFromCentralConfig() ([]string, error) {
// We will get the central configuration from the default discovery source
discoverySource, err := config.GetCLIDiscoverySource(cliconfig.DefaultStandaloneDiscoveryName)
if err != nil {
return nil, err
}

// Get the Tanzu Application Platform scopes from the central configuration
reader := centralconfig.NewCentralConfigReader(discoverySource)
var tapScopes []tapScope
err = reader.GetCentralConfigEntry(centralConfigTanzuApplicationPlatformScopesKey, &tapScopes)
if err != nil {
// If the key is not found in the central config, it does not return an error because some central repositories
// may choose not to have a central config file.
var keyNotFoundError *centralconfig.KeyNotFoundError
if errors.As(err, &keyNotFoundError) {
return nil, nil
}
return nil, err
}
// extract the scope names
var scopeNames []string
for _, ts := range tapScopes {
scopeNames = append(scopeNames, ts.Scope)
}
return scopeNames, nil
}
68 changes: 68 additions & 0 deletions pkg/command/org_validation_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package command

import (
"errors"
"testing"

"github.com/vmware-tanzu/tanzu-cli/pkg/auth/csp"
)

func TestValidateTokenForTanzuScopes(t *testing.T) {
tests := []struct {
name string
claims *csp.Claims
scopesGetter tapScopesGetter
expectedBool bool
expectedErr error
}{
{
name: "When the central config does not have any TAP scopes listed, it should return success.",
claims: &csp.Claims{Permissions: []string{}},
scopesGetter: func() ([]string, error) { return []string{}, nil },
expectedBool: true,
expectedErr: nil,
},
{
name: "When the token has at least one of the specified TAP scopes listed in central config, it should return success",
claims: &csp.Claims{Permissions: []string{"external/UID-123-567/matching-scope", "csp:org_admin", "csp:developer"}},
scopesGetter: func() ([]string, error) {
return []string{"matching-scope", "matching-another-scope"}, nil
},
expectedBool: true,
expectedErr: nil,
},
{
name: "When the token lacks at least one of the specified TAP scopes listed in the central config, it should return an error",
claims: &csp.Claims{Permissions: []string{"external/UID-123-567/non-matching-scope", "csp:org_member", "csp:software_installer"}},
scopesGetter: func() ([]string, error) {
return []string{"matching-scope", "matching-another-scope"}, nil
},
expectedBool: false,
expectedErr: nil,
},
{
name: "It should return an error if fetching the TAP scopes from the central config fails",
claims: &csp.Claims{},
scopesGetter: func() ([]string, error) {
return nil, errors.New("error retrieving TAP scopes")
},
expectedBool: false,
expectedErr: errors.New("error retrieving TAP scopes form the central config: error retrieving TAP scopes"),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actualBool, actualErr := validateTokenForTAPScopes(tc.claims, tc.scopesGetter)
if actualBool != tc.expectedBool {
t.Errorf("Test case %s failed: Expected bool value %t, but got %t", tc.name, tc.expectedBool, actualBool)
}
if actualErr != nil && tc.expectedErr != nil && actualErr.Error() != tc.expectedErr.Error() {
t.Errorf("Test case %s failed: Expected error %v, but got %v", tc.name, tc.expectedErr, actualErr)
}
})
}
}
4 changes: 4 additions & 0 deletions pkg/constants/env_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ const (
// in the plugin-group are available in the database or not.
// Note: THIS SHOULD ONLY BE USED FOR TEST AND NON PRODUCTION ENVIRONMENTS.
SkipPluginGroupVerificationOnPublish = "TANZU_CLI_SKIP_PLUGIN_GROUP_VERIFICATION_ON_PUBLISH"

//SkipTAPScopesValidationOnTanzuContext skips the TAP scopes validation on the token acquired while creating "tanzu"

Check failure on line 75 in pkg/constants/env_variables.go

View workflow job for this annotation

GitHub Actions / Build

commentFormatting: put a space between `//` and comment text (gocritic)
// context using tanzu login or tanzu context create command
SkipTAPScopesValidationOnTanzuContext = "TANZU_CLI_SKIP_TAP_SCOPES_VALIDATION_ON_TANZU_CONTEXT"
)
2 changes: 1 addition & 1 deletion pkg/pluginmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ func InitializePlugin(plugin *cli.PluginInfo) error {
// the plugin does not implement post-install command. Ignoring the
// errors if the command does not exist for a particular plugin.
if err != nil && !strings.Contains(string(b), "unknown command") {
log.Warningf("Warning: Failed to initialize plugin '%q' after installation. %v", plugin.Name, string(b))
log.Warningf("WARNING: Failed to initialize plugin '%q' after installation. %v", plugin.Name, string(b))
}

return nil
Expand Down

0 comments on commit 25d0884

Please sign in to comment.