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
17 changes: 12 additions & 5 deletions access/aws/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,30 @@ package aws
import (
"encoding/json"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
"gopkg.in/nullstone-io/go-api-client.v0/types"
"time"
)

const (
DefaultAwsRegion = "us-east-1"
)

var (
assumeRoleSessionName = "nullstone-executor"
assumeRoleDuration = time.Hour
)

func ResolveConfig(assumerAwsConfig aws.Config, provider types.Provider, cfg types.ProviderConfig) (aws.Config, error) {
region := "us-east-1"
if cfg.Aws != nil && cfg.Aws.Region != "" {
region = cfg.Aws.Region
func ResolveConfig(assumerAwsConfig aws.Config, provider types.Provider, cfg *types.AwsProviderConfig, region string) (aws.Config, error) {
if region == "" {
region = DefaultAwsRegion
}
if cfg != nil && cfg.Region != "" {
region = cfg.Region
}

awsConfig := aws.Config{Region: region}
Expand Down
26 changes: 26 additions & 0 deletions access/gcp/assumer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gcp

import (
"context"
"encoding/base64"
"fmt"

"golang.org/x/oauth2/google"
)

type Assumer struct {
Credentials *google.Credentials
}

func AssumerFromBase64KeyFile(ctx context.Context, encoded string) (Assumer, error) {
base64Decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return Assumer{}, fmt.Errorf("error decoding gcp service account key: %w", err)
}

creds, err := google.CredentialsFromJSONWithType(ctx, base64Decoded, google.ServiceAccount, "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return Assumer{}, fmt.Errorf("error loading credentials from json: %w", err)
}
return Assumer{Credentials: creds}, nil
}
59 changes: 59 additions & 0 deletions access/gcp/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gcp

import (
"context"
"encoding/json"
"errors"
"fmt"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"
"gopkg.in/nullstone-io/go-api-client.v0/types"
)

var (
ErrMissingAuthTypeInGcpCredentials = errors.New(`missing "auth_type" in gcp credentials`)
ErrUnsupportedAuthTypeInGcpCredentials = errors.New(`unsupported "auth_type" in gcp credentials`)
ErrMissingEmailInServiceAccountOutput = errors.New(`missing "email" in output for service account`)
)

func ResolveTokenSource(ctx context.Context, assumer Assumer, provider types.Provider) (oauth2.TokenSource, error) {
gcpCreds := types.GcpCredentials{}
if err := json.Unmarshal(provider.Credentials, &gcpCreds); err != nil {
return nil, fmt.Errorf("invalid gcp credentials: %s", err)
}

switch gcpCreds.AuthType {
case "":
return nil, ErrMissingAuthTypeInGcpCredentials
case types.GcpAuthTypeServiceAccount:
return ResolveKeyTokenSource(ctx, gcpCreds.ServiceAccountKey, "https://www.googleapis.com/auth/cloud-platform")
case types.GcpAuthTypeServiceAccountImpersonation:
return ResolveImpersonationTokenSource(ctx, assumer, gcpCreds.Impersonation, "https://www.googleapis.com/auth/cloud-platform")
default:
return nil, ErrUnsupportedAuthTypeInGcpCredentials
}
}

func ResolveKeyTokenSource(ctx context.Context, a types.GcpServiceAccountKey, scopes ...string) (oauth2.TokenSource, error) {
keyFile, _ := json.Marshal(a)
cfg, err := google.JWTConfigFromJSON(keyFile, scopes...)
if err != nil {
return nil, fmt.Errorf("unable to read service account credentials json file: %w", err)
}
return cfg.TokenSource(ctx), nil
}

func ResolveImpersonationTokenSource(ctx context.Context, assumer Assumer, a types.GcpServiceAccountImpersonation, scopes ...string) (oauth2.TokenSource, error) {
if a.ServiceAccountEmail == "" {
return nil, ErrMissingEmailInServiceAccountOutput
}

// Create a token source that can impersonate the target service account
return impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
TargetPrincipal: a.ServiceAccountEmail,
Scopes: scopes,
}, option.WithTokenSource(assumer.Credentials.TokenSource))
}
3 changes: 1 addition & 2 deletions builtin/aws/aws-account/coster.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ type Coster struct {

func (c Coster) GetCosts(ctx context.Context, query infra_sdk.CostQuery) (*infra_sdk.CostResult, error) {
// Cost Explorer is global, use us-east-1 as the region to satisfy the aws sdk
providerConfig := types.ProviderConfig{Aws: &types.AwsProviderConfig{Region: "us-east-1"}}
awsConfig, err := aws.ResolveConfig(c.Assumer.AwsConfig(), c.Provider, providerConfig)
awsConfig, err := aws.ResolveConfig(c.Assumer.AwsConfig(), c.Provider, &types.AwsProviderConfig{Region: "us-east-1"}, "")
if err != nil {
return nil, fmt.Errorf("error resolving aws config: %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions builtin/aws/aws-account/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"

"github.com/nullstone-io/infra-sdk"
"github.com/nullstone-io/infra-sdk/access/aws"
"gopkg.in/nullstone-io/go-api-client.v0/types"
Expand Down Expand Up @@ -41,11 +42,11 @@ var (
type Scanner struct {
Assumer aws.Assumer
Provider types.Provider
ProviderConfig types.ProviderConfig
ProviderConfig *types.AwsProviderConfig
}

func (s Scanner) Scan(ctx context.Context) ([]infra_sdk.ScanResource, error) {
awsConfig, err := aws.ResolveConfig(s.Assumer.AwsConfig(), s.Provider, s.ProviderConfig)
awsConfig, err := aws.ResolveConfig(s.Assumer.AwsConfig(), s.Provider, s.ProviderConfig, "")
if err != nil {
return nil, fmt.Errorf("error resolving aws config: %w", err)
}
Expand Down
145 changes: 145 additions & 0 deletions builtin/aws/secret_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package aws

import (
"context"
"errors"
"fmt"
"strings"

"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
sm_types "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
infra_sdk "github.com/nullstone-io/infra-sdk"
"github.com/nullstone-io/infra-sdk/access/aws"
"gopkg.in/nullstone-io/go-api-client.v0/types"
)

var (
_ infra_sdk.SecretManager = SecretManager{}
)

type SecretManager struct {
Assumer aws.Assumer
Provider types.Provider
ProviderConfig *types.AwsProviderConfig
}

func (s SecretManager) List(ctx context.Context, location types.SecretLocation) ([]types.Secret, error) {
if s.ProviderConfig == nil || s.ProviderConfig.ProviderName == "" {
return nil, nil
}
client, err := s.smClient(location.AwsRegion)
if err != nil {
return nil, err
}

input := &secretsmanager.ListSecretsInput{}
out, err := client.ListSecrets(ctx, input)
if err != nil {
return nil, fmt.Errorf("error listing secrets: %w", err)
}
result := make([]types.Secret, 0)
for _, cur := range out.SecretList {
result = append(result, types.Secret{
Identity: s.secretIdentityFromAws(cur.ARN, cur.Name, cur.PrimaryRegion),
Metadata: map[string]any{
"description": cur.Description,
"tags": cur.Tags,
},
Value: "",
Redacted: true,
})
}
return result, nil
}

func (s SecretManager) Create(ctx context.Context, identity types.SecretIdentity, value string) (*types.Secret, error) {
if s.ProviderConfig == nil || s.ProviderConfig.ProviderName == "" {
return nil, nil
}
client, err := s.smClient(identity.AwsRegion)
if err != nil {
return nil, err
}

out, err := client.CreateSecret(ctx, &secretsmanager.CreateSecretInput{
Name: &identity.Name,
SecretString: &value,
})
if err != nil {
var ree *sm_types.ResourceExistsException
if errors.As(err, &ree) {
return nil, infra_sdk.ErrSecretAlreadyExists
}
return nil, fmt.Errorf("error creating secret: %w", err)
}

return &types.Secret{
Identity: s.secretIdentityFromAws(out.ARN, out.Name, &identity.AwsRegion),
Metadata: nil,
Value: "",
Redacted: false,
}, nil
}

func (s SecretManager) Update(ctx context.Context, identity types.SecretIdentity, value string) (*types.Secret, error) {
if s.ProviderConfig == nil || s.ProviderConfig.ProviderName == "" {
return nil, nil
}
client, err := s.smClient(identity.AwsRegion)
if err != nil {
return nil, err
}

out, err := client.UpdateSecret(ctx, &secretsmanager.UpdateSecretInput{
SecretId: &identity.Name,
SecretString: &value,
})
if err != nil {
var rnfe *sm_types.ResourceNotFoundException
if errors.As(err, &rnfe) {
return nil, infra_sdk.ErrDoesNotExist
}
return nil, fmt.Errorf("error updating secret: %w", err)
}

return &types.Secret{
Identity: s.secretIdentityFromAws(out.ARN, out.Name, &identity.AwsRegion),
Metadata: nil,
Value: "",
Redacted: false,
}, nil
}

func (s SecretManager) smClient(region string) (*secretsmanager.Client, error) {
awsConfig, err := aws.ResolveConfig(s.Assumer.AwsConfig(), s.Provider, s.ProviderConfig, region)
if err != nil {
return nil, fmt.Errorf("error resolving aws config: %w", err)
}
return secretsmanager.NewFromConfig(awsConfig), nil
}

func (s SecretManager) secretIdentityFromAws(secretArn *string, name *string, primaryRegion *string) types.SecretIdentity {
identity := types.SecretIdentity{
Name: unptr(name),
SecretLocation: types.SecretLocation{
Platform: types.SecretLocationPlatformAws,
AwsRegion: unptr(primaryRegion),
AwsAccountId: s.Provider.ProviderId,
},
}
if a, err := arn.Parse(unptr(secretArn)); err == nil {
identity.AwsRegion = a.Region
identity.AwsAccountId = a.AccountID
identity.Name = strings.TrimPrefix(a.Resource, "secret:")
}
return identity
}

func unptr[T any](t *T) T {
if t != nil {
return *t
}
var x T
return x
}
6 changes: 4 additions & 2 deletions builtin/discover_coster.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package builtin

import (
infra_sdk "github.com/nullstone-io/infra-sdk"
"github.com/nullstone-io/infra-sdk/access/aws"
aws_access "github.com/nullstone-io/infra-sdk/access/aws"
gcp_access "github.com/nullstone-io/infra-sdk/access/gcp"
aws_account "github.com/nullstone-io/infra-sdk/builtin/aws/aws-account"
"gopkg.in/nullstone-io/go-api-client.v0/types"
)

type CosterCreator struct {
AwsAssumer aws.Assumer
AwsAssumer aws_access.Assumer
GcpAssumer gcp_access.Assumer
}

func (s CosterCreator) NewMultiCoster(providers []types.Provider) (infra_sdk.MultiCoster, error) {
Expand Down
45 changes: 45 additions & 0 deletions builtin/discover_secret_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package builtin

import (
infra_sdk "github.com/nullstone-io/infra-sdk"
aws_access "github.com/nullstone-io/infra-sdk/access/aws"
gcp_access "github.com/nullstone-io/infra-sdk/access/gcp"
"github.com/nullstone-io/infra-sdk/builtin/aws"
"github.com/nullstone-io/infra-sdk/builtin/gcp"
"gopkg.in/nullstone-io/go-api-client.v0/types"
)

type SecretManagerCreator struct {
AwsAssumer aws_access.Assumer
GcpAssumer gcp_access.Assumer
}

func (c SecretManagerCreator) NewSecretManager(providers []types.Provider, providerConfig types.ProviderConfig) (infra_sdk.MultiSecretManager, error) {
mc := infra_sdk.MultiSecretManager{Managers: map[string]infra_sdk.SecretManager{}}
for _, cur := range providers {
manager, err := c.DiscoverSecretManager(cur, providerConfig)
if err != nil {
return mc, err
}
mc.Managers[cur.ProviderType] = manager
}
return mc, nil
}

func (c SecretManagerCreator) DiscoverSecretManager(provider types.Provider, providerConfig types.ProviderConfig) (infra_sdk.SecretManager, error) {
switch provider.ProviderType {
case "aws":
return aws.SecretManager{
Assumer: c.AwsAssumer,
Provider: provider,
ProviderConfig: providerConfig.Aws,
}, nil
case "gcp":
return gcp.SecretManager{
Assumer: c.GcpAssumer,
Provider: provider,
ProviderConfig: providerConfig.Gcp,
}, nil
}
return nil, nil
}
Loading