Skip to content
Merged
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
112 changes: 112 additions & 0 deletions internal/providerfactory/bypass_guard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package providerfactory

import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
azureProviderPkg = "github.com/LeanerCloud/cloud-commitments-go/providers/azure"
gcpProviderPkg = "github.com/LeanerCloud/cloud-commitments-go/providers/gcp"
providerPkg = "github.com/LeanerCloud/cloud-commitments-go/pkg/provider"
)

// bypassConstructors are the concrete constructors that skip the provider
// registry. Production code builds credentialed providers through this
// package instead (platform#41).
var bypassConstructors = map[string]map[string]bool{
azureProviderPkg: {"NewAzureProvider": true},
gcpProviderPkg: {"NewProvider": true, "NewProviderWithProject": true, "NewProviderWithCredentials": true},
}

// No non-test file under cmd/ or internal/ may call the concrete Azure/GCP
// provider constructors or set the deprecated ProviderConfig.Profile field.
func TestNoRegistryBypassInProductionCode(t *testing.T) {
root := filepath.Join("..", "..")
var violations []string
scanned := 0
for _, dir := range []string{"cmd", "internal"} {
err := filepath.WalkDir(filepath.Join(root, dir), func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
scanned++
violations = append(violations, registryBypasses(t, path)...)
return nil
})
require.NoError(t, err)
}
require.Greater(t, scanned, 100, "the walk must actually reach the production sources")
assert.Empty(t, violations,
"build credentialed Azure/GCP providers with providerfactory.NewAzure/NewGCP and use the typed ProviderConfig fields")
}

// registryBypasses returns one entry per bypassing call or deprecated
// Profile key in the file at path.
func registryBypasses(t *testing.T, path string) []string {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
require.NoError(t, err, path)

imports := map[string]string{} // local name -> import path
for _, imp := range f.Imports {
p, err := strconv.Unquote(imp.Path.Value)
require.NoError(t, err)
name := p[strings.LastIndex(p, "/")+1:]
if imp.Name != nil {
name = imp.Name.Name
}
imports[name] = p
}

var out []string
ast.Inspect(f, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.SelectorExpr:
if pkg := selectorPkg(node, imports); bypassConstructors[pkg][node.Sel.Name] {
out = append(out, fset.Position(node.Pos()).String()+": "+node.Sel.Name)
}
case *ast.CompositeLit:
if sel, ok := node.Type.(*ast.SelectorExpr); ok && sel.Sel.Name == "ProviderConfig" && selectorPkg(sel, imports) == providerPkg {
out = append(out, profileKeys(fset, node)...)
}
}
return true
})
return out
}

func selectorPkg(sel *ast.SelectorExpr, imports map[string]string) string {
id, ok := sel.X.(*ast.Ident)
if !ok {
return ""
}
return imports[id.Name]
}

func profileKeys(fset *token.FileSet, lit *ast.CompositeLit) []string {
var out []string
for _, el := range lit.Elts {
kv, ok := el.(*ast.KeyValueExpr)
if !ok {
continue
}
if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Profile" {
out = append(out, fset.Position(kv.Pos()).String()+": ProviderConfig.Profile")
}
}
return out
}
92 changes: 92 additions & 0 deletions internal/providerfactory/providerfactory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Package providerfactory builds credentialed cloud providers through the
// cloud-commitments-go provider registry, so the internal layer no longer
// calls the concrete Azure/GCP constructors directly (platform#41).
//
// Credentials go in the registry's typed slots (AzureTokenCredential,
// GCPTokenSource) and the account identity in the typed
// AzureSubscriptionID / GCPProjectID fields, never the deprecated Profile
// field.
package providerfactory

import (
"errors"
"fmt"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/LeanerCloud/cloud-commitments-go/pkg/provider"
"golang.org/x/oauth2"

// The provider packages register their factories with the global
// registry in init(); importing them here guarantees "azure" and "gcp"
// are registered in every binary that uses this package.
_ "github.com/LeanerCloud/cloud-commitments-go/providers/azure"
_ "github.com/LeanerCloud/cloud-commitments-go/providers/gcp"
)

// Registry names of the providers built here.
const (
azureName = "azure"
gcpName = "gcp"
)

// ErrMissingGCPProjectID is returned by NewGCP when the project ID is empty.
var ErrMissingGCPProjectID = errors.New("providerfactory: GCP project ID is required")

// ErrMissingGCPTokenSource is returned by NewGCP when the token source is nil.
var ErrMissingGCPTokenSource = errors.New("providerfactory: GCP token source is required")

// NewAzure returns an Azure provider pinned to subscriptionID that
// authenticates with cred, built through the global provider registry.
//
// A nil cred leaves the provider on its ambient-credential fallback, the
// same as installing a nil credential on a directly constructed provider.
func NewAzure(subscriptionID string, cred azcore.TokenCredential) (provider.Provider, error) {
return newAzure(provider.GetRegistry(), subscriptionID, cred)
}

// NewGCP returns a GCP provider for projectID that authenticates with ts,
// built through the global provider registry.
//
// projectID must be non-empty: the registry's GCP factory resolves an empty
// project ID by asking the cloud for the credential's default project, which
// the direct constructor never did and which could pin the provider to a
// project other than the account's. ts must be non-nil; callers in ADC mode
// use the ambient factory path instead.
func NewGCP(projectID string, ts oauth2.TokenSource) (provider.Provider, error) {
return newGCP(provider.GetRegistry(), projectID, ts)
}

func newAzure(reg *provider.Registry, subscriptionID string, cred azcore.TokenCredential) (provider.Provider, error) {
// A nil cred converts to a nil `any`, which the Azure factory treats as
// "no credential supplied".
return build(reg, azureName, &provider.ProviderConfig{
Name: azureName,
AzureSubscriptionID: subscriptionID,
AzureTokenCredential: cred,
})
}

func newGCP(reg *provider.Registry, projectID string, ts oauth2.TokenSource) (provider.Provider, error) {
if projectID == "" {
return nil, ErrMissingGCPProjectID
}
if ts == nil {
return nil, ErrMissingGCPTokenSource
}
return build(reg, gcpName, &provider.ProviderConfig{
Name: gcpName,
GCPProjectID: projectID,
GCPTokenSource: ts,
})
}

func build(reg *provider.Registry, name string, cfg *provider.ProviderConfig) (provider.Provider, error) {
prov, err := reg.GetProviderWithConfig(name, cfg)
if err != nil {
return nil, fmt.Errorf("providerfactory: build %s provider: %w", name, err)
}
if prov == nil {
return nil, fmt.Errorf("providerfactory: %s factory returned no provider", name)
}
return prov, nil
}
Loading
Loading