Skip to content

Commit c986c44

Browse files
committed
feat: basic implementation
Signed-off-by: Mike Nguyen <hey@mike.ee>
1 parent 6b4621f commit c986c44

File tree

15 files changed

+594
-185
lines changed

15 files changed

+594
-185
lines changed

.build-tools/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/dapr/components-contrib/build-tools
22

3-
go 1.24.1
3+
go 1.24.4
44

55
require (
66
github.com/dapr/components-contrib v0.0.0

common/aws/auth/auth.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-sdk-go-v2/aws"
6+
"github.com/aws/aws-sdk-go-v2/config"
7+
"github.com/dapr/kit/logger"
8+
)
9+
10+
type ProviderType int
11+
12+
const (
13+
StaticProviderTypeStatic ProviderType = iota
14+
StaticProviderTypeAssumeRole
15+
X509ProviderType
16+
ProviderTypeUnknown // Or default
17+
)
18+
19+
type Options struct {
20+
Logger logger.Logger
21+
Properties map[string]string
22+
23+
Region string `json:"region" mapstructure:"region" mapstructurealiases:"awsRegion"`
24+
AccessKey string `json:"accessKey" mapstructure:"accessKey"`
25+
SecretKey string `json:"secretKey" mapstructure:"secretKey"`
26+
SessionToken string `json:"sessionToken" mapstructure:"sessionToken"`
27+
AssumeRoleArn string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"`
28+
TrustAnchorArn string `json:"trustAnchorArn" mapstructure:"trustAnchorArn"`
29+
TrustProfileArn string `json:"trustProfileArn" mapstructure:"trustProfileArn"`
30+
31+
Endpoint string `json:"endpoint" mapstructure:"endpoint"`
32+
}
33+
34+
// CredentialProvider provides an interface for retrieving AWS credentials.
35+
type CredentialProvider interface {
36+
Retrieve(ctx context.Context) (aws.Credentials, error)
37+
Type() ProviderType
38+
}
39+
40+
func NewCredentialProvider(ctx context.Context, opts Options, configOpts []func(*config.LoadOptions) error) (CredentialProvider,
41+
error) {
42+
// TODO: Refactor this to search the opts structure for the right fields rather than the metadata map
43+
if isX509Auth(opts.Properties) {
44+
return newAuthX509(ctx, opts)
45+
}
46+
return newAuthStatic(ctx, opts, configOpts)
47+
}

common/aws/auth/auth_static.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"errors"
6+
"github.com/aws/aws-sdk-go-v2/aws"
7+
"github.com/aws/aws-sdk-go-v2/config"
8+
"github.com/aws/aws-sdk-go-v2/credentials"
9+
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
10+
"github.com/aws/aws-sdk-go-v2/service/sts"
11+
"github.com/dapr/kit/logger"
12+
)
13+
14+
type Static struct {
15+
ProviderType ProviderType
16+
Logger logger.Logger
17+
AccessKey string
18+
SecretKey string
19+
SessionToken string
20+
Region string
21+
Endpoint string
22+
AssumeRoleArn string
23+
24+
CredentialProvider aws.CredentialsProvider
25+
}
26+
27+
func (a *Static) Retrieve(ctx context.Context) (aws.Credentials, error) {
28+
if a.CredentialProvider == nil {
29+
return aws.Credentials{}, errors.New("credential provider is not set")
30+
}
31+
return a.CredentialProvider.Retrieve(ctx)
32+
}
33+
34+
func (a *Static) Type() ProviderType {
35+
return a.ProviderType
36+
}
37+
38+
func newAuthStatic(ctx context.Context, opts Options, configOpts []func(*config.LoadOptions) error) (CredentialProvider, error) {
39+
static := &Static{
40+
Logger: opts.Logger,
41+
AccessKey: opts.AccessKey,
42+
SecretKey: opts.SecretKey,
43+
SessionToken: opts.SessionToken,
44+
Region: opts.Region,
45+
Endpoint: opts.Endpoint,
46+
AssumeRoleArn: opts.AssumeRoleArn,
47+
}
48+
49+
switch {
50+
case static.AccessKey != "" && static.SecretKey != "" && static.SessionToken != "":
51+
static.ProviderType = StaticProviderTypeStatic
52+
static.CredentialProvider = credentials.NewStaticCredentialsProvider(opts.AccessKey, opts.SecretKey,
53+
opts.SessionToken)
54+
static.Logger.Debug("using static credentials provider")
55+
56+
case static.AssumeRoleArn != "":
57+
awsCfg, err := config.LoadDefaultConfig(ctx, configOpts...)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
stsSvc := sts.NewFromConfig(awsCfg)
63+
stsProvider := stscreds.NewAssumeRoleProvider(stsSvc, static.AssumeRoleArn)
64+
static.ProviderType = StaticProviderTypeAssumeRole
65+
static.CredentialProvider = stsProvider
66+
static.Logger.Debug("using AssumeRole credentials provider")
67+
68+
default:
69+
static.Logger.Debug("using an undefined credentials provider, this may lead to unexpected behavior")
70+
static.ProviderType = ProviderTypeUnknown
71+
}
72+
73+
return static, nil
74+
}

common/aws/auth/auth_static_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package auth

common/aws/auth/auth_x509.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/x509"
7+
"errors"
8+
"github.com/aws/aws-sdk-go-v2/aws"
9+
cryptopem "github.com/dapr/kit/crypto/pem"
10+
spiffecontext "github.com/dapr/kit/crypto/spiffe/context"
11+
"github.com/dapr/kit/logger"
12+
kitmd "github.com/dapr/kit/metadata"
13+
"github.com/mikeee/aws_credential_helper"
14+
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
15+
"time"
16+
)
17+
18+
func isX509Auth(m map[string]string) bool {
19+
tp := m["trustProfileArn"]
20+
ta := m["trustAnchorArn"]
21+
ar := m["assumeRoleArn"]
22+
return tp != "" && ta != "" && ar != ""
23+
}
24+
25+
type X509 struct {
26+
logger logger.Logger
27+
28+
x509Cert *x509.Certificate
29+
30+
region *string
31+
assumeRoleArn *string
32+
trustAnchorArn *string
33+
trustProfileArn *string
34+
35+
wrappedCredentialProvider *aws_credential_helper.CredentialProvider
36+
}
37+
38+
func (x *X509) Type() ProviderType {
39+
return X509ProviderType
40+
}
41+
42+
func (x *X509) RefreshX509(ctx context.Context) error {
43+
cert, _, signer, err := getCertAndSigner(ctx)
44+
if err != nil {
45+
return errors.New("failed to refresh X509 cert: " + err.Error())
46+
}
47+
x.x509Cert = cert
48+
if x.wrappedCredentialProvider != nil {
49+
if x.region == nil || x.assumeRoleArn == nil || x.trustAnchorArn == nil || x.trustProfileArn == nil {
50+
return errors.New("missing required fields: Region, AssumeRoleArn, TrustAnchorArn, TrustProfileArn")
51+
}
52+
53+
credentialProviderInput := aws_credential_helper.CredentialProviderInput{
54+
Region: *x.region,
55+
TrustProfileArn: *x.trustProfileArn,
56+
TrustAnchorArn: *x.trustAnchorArn,
57+
AssumeRoleArn: *x.assumeRoleArn,
58+
Signer: *signer,
59+
}
60+
61+
credentialProvider, err := aws_credential_helper.NewCredentialProvider(ctx, credentialProviderInput)
62+
if err != nil {
63+
return errors.New("failed to create new credential provider: " + err.Error())
64+
}
65+
66+
x.wrappedCredentialProvider = credentialProvider
67+
} else {
68+
// change signer
69+
x.wrappedCredentialProvider.ChangeSigner(*signer)
70+
}
71+
72+
return nil
73+
}
74+
75+
func (x *X509) Retrieve(ctx context.Context) (aws.Credentials, error) {
76+
// Check cert/svid expiry, if expired then refresh
77+
if isCertExpired(x.x509Cert) {
78+
if err := x.RefreshX509(ctx); err != nil {
79+
return aws.Credentials{}, errors.New("failed to refresh X509 cert: " + err.Error())
80+
}
81+
}
82+
return x.wrappedCredentialProvider.Retrieve(ctx)
83+
}
84+
85+
func getSpiffeX509Svid(ctx context.Context) (*x509svid.SVID, error) {
86+
svid, ok := spiffecontext.X509From(ctx)
87+
if !ok {
88+
return nil, errors.New("invalid SVID: no certs found")
89+
}
90+
91+
return svid.GetX509SVID()
92+
}
93+
94+
func marshalSvid(svid *x509svid.SVID) ([]byte, []byte, error) {
95+
if svid == nil {
96+
return nil, nil, errors.New("nil SVID")
97+
}
98+
99+
return svid.Marshal()
100+
}
101+
102+
func getCertAndSigner(ctx context.Context) (*x509.Certificate, *ecdsa.PrivateKey, *aws_credential_helper.Signer,
103+
error) {
104+
// obtain certs from spiffe via context
105+
x509Svid, err := getSpiffeX509Svid(ctx)
106+
if err != nil {
107+
panic("failed to get SPIFFE certs: " + err.Error())
108+
}
109+
110+
chain, key, err := marshalSvid(x509Svid)
111+
if err != nil {
112+
return nil, nil, nil, err
113+
}
114+
115+
certs, err := cryptopem.DecodePEMCertificatesChain(chain)
116+
if err != nil {
117+
return nil, nil, nil, err
118+
}
119+
120+
pkey, err := cryptopem.DecodePEMPrivateKey(key)
121+
if err != nil {
122+
return nil, nil, nil, err
123+
}
124+
125+
signer := aws_credential_helper.NewSigner(certs[0], pkey.(*ecdsa.PrivateKey))
126+
127+
return certs[0], pkey.(*ecdsa.PrivateKey), &signer, nil
128+
}
129+
130+
func isCertExpired(cert *x509.Certificate) bool {
131+
if cert == nil {
132+
return true
133+
}
134+
now := time.Now()
135+
return now.After(cert.NotAfter) || now.Before(cert.NotBefore)
136+
}
137+
138+
type awsRAOpts struct {
139+
AssumeRoleArn *string `json:"assumeRoleArn" mapstructure:"assumeRoleArn"`
140+
TrustAnchorArn *string `json:"trustAnchorArn" mapstructure:"trustAnchorArn"`
141+
TrustProfileArn *string `json:"trustProfileArn" mapstructure:"trustProfileArn"`
142+
}
143+
144+
func newAuthX509(ctx context.Context, opts Options) (*X509, error) {
145+
var authOpts awsRAOpts
146+
if err := kitmd.DecodeMetadata(opts.Properties, &authOpts); err != nil {
147+
return nil, errors.New("failed to decode metadata: " + err.Error())
148+
}
149+
150+
switch {
151+
case authOpts.AssumeRoleArn == nil:
152+
return nil, errors.New("missing required field: AssumeRoleArn")
153+
case authOpts.TrustAnchorArn == nil:
154+
return nil, errors.New("missing required field: TrustAnchorArn")
155+
case authOpts.TrustProfileArn == nil:
156+
return nil, errors.New("missing required field: TrustProfileArn")
157+
}
158+
159+
cert, _, signer, err := getCertAndSigner(ctx)
160+
if err != nil {
161+
return nil, errors.New("failed to get cert and signer: " + err.Error())
162+
}
163+
164+
// create credential provider
165+
166+
authInput := aws_credential_helper.CredentialProviderInput{
167+
Region: opts.Region,
168+
169+
AssumeRoleArn: *authOpts.AssumeRoleArn,
170+
TrustAnchorArn: *authOpts.TrustAnchorArn,
171+
TrustProfileArn: *authOpts.TrustProfileArn,
172+
173+
Signer: *signer,
174+
}
175+
176+
credentialProvider, err := aws_credential_helper.NewCredentialProvider(ctx, authInput)
177+
178+
// test credentialprovider
179+
cred, err := credentialProvider.Retrieve(ctx)
180+
if err != nil {
181+
return nil, errors.New("failed to retrieve credentials: " + err.Error())
182+
}
183+
184+
if cred.Expires.Before(time.Now()) {
185+
return nil, errors.New("credentials are not valid")
186+
}
187+
188+
return &X509{
189+
logger: nil,
190+
x509Cert: cert,
191+
wrappedCredentialProvider: credentialProvider,
192+
}, nil
193+
}

common/aws/aws.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package aws

common/aws/config.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-sdk-go-v2/aws"
6+
"github.com/aws/aws-sdk-go-v2/config"
7+
"github.com/dapr/components-contrib/common/aws/auth"
8+
)
9+
10+
type ConfigOption func(*ConfigOptions)
11+
12+
type ConfigOptions struct {
13+
CredentialProvider aws.CredentialsProvider
14+
}
15+
16+
// WithCredentialProvider allows for passing a custom credential provider,
17+
// this is not cached - wrap the credential provider with a NewCredentialCache if you wish.
18+
func WithCredentialProvider(provider aws.CredentialsProvider) func(*ConfigOptions) {
19+
return func(opts *ConfigOptions) {
20+
opts.CredentialProvider = provider
21+
}
22+
}
23+
24+
func loadConfigOptions(opts ...ConfigOption) *ConfigOptions {
25+
options := &ConfigOptions{}
26+
for _, opt := range opts {
27+
opt(options)
28+
}
29+
return options
30+
}
31+
32+
type ConfigLoadOptions []func(*config.LoadOptions) error
33+
34+
func NewConfig(ctx context.Context, authOptions auth.Options, opts ...ConfigOption) (aws.Config, error) {
35+
options := loadConfigOptions(opts...)
36+
37+
var configLoadOptions ConfigLoadOptions
38+
39+
// Deal with options
40+
switch {
41+
case authOptions.Endpoint != "":
42+
configLoadOptions = append(
43+
configLoadOptions,
44+
config.WithBaseEndpoint(authOptions.Endpoint),
45+
)
46+
47+
}
48+
49+
if options.CredentialProvider != nil {
50+
configLoadOptions = append(
51+
configLoadOptions,
52+
config.WithCredentialsProvider(options.CredentialProvider),
53+
)
54+
} else {
55+
credentialsProvider, err := auth.NewCredentialProvider(ctx, authOptions, configLoadOptions)
56+
if err != nil {
57+
return aws.Config{}, err
58+
} else if credentialsProvider.Type() != auth.ProviderTypeUnknown {
59+
configLoadOptions = append(
60+
configLoadOptions,
61+
config.WithCredentialsProvider(credentialsProvider),
62+
)
63+
}
64+
// else use the sdk default external config if possible
65+
}
66+
return config.LoadDefaultConfig(ctx, configLoadOptions...)
67+
}

common/aws/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package aws

0 commit comments

Comments
 (0)