mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
refactor: move ecr authorization token action to domain
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ecr"
|
||||
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
)
|
||||
|
||||
// AuthorizationTokenIssuer is the narrow credential capability the ECR AWS
|
||||
// action needs. ECR auth owns the concrete ES256 issuer; the AWS adapter owns
|
||||
// its AWS JSON response projection.
|
||||
type AuthorizationTokenIssuer interface {
|
||||
Mint(ecrauth.Principal) (token string, expiresAt time.Time, err error)
|
||||
}
|
||||
|
||||
// GetAuthorizationToken mints an ECR registry token for the canonical
|
||||
// SigV4-authenticated principal. The returned token is base64("AWS:<jwt>"),
|
||||
// which Docker replays as Basic auth to the ECR /v2 registry endpoint.
|
||||
func GetAuthorizationToken(issuer AuthorizationTokenIssuer, endpoint RepositoryEndpoint, principal ecrauth.Principal) (*ecr.GetAuthorizationTokenOutput, error) {
|
||||
if issuer == nil {
|
||||
return nil, errors.New(awserrors.ErrorNotImplemented)
|
||||
}
|
||||
token, expiresAt, err := issuer.Mint(principal)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
authToken := base64.StdEncoding.EncodeToString([]byte("AWS:" + token))
|
||||
return &ecr.GetAuthorizationTokenOutput{
|
||||
AuthorizationData: []*ecr.AuthorizationData{{
|
||||
AuthorizationToken: aws.String(authToken),
|
||||
ProxyEndpoint: aws.String("https://" + endpoint.RegistryURIHost(principal.AccountID)),
|
||||
ExpiresAt: aws.Time(expiresAt),
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package awsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeAuthorizationTokenIssuer struct {
|
||||
got ecrauth.Principal
|
||||
token string
|
||||
expiresAt time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
func (i *fakeAuthorizationTokenIssuer) Mint(principal ecrauth.Principal) (string, time.Time, error) {
|
||||
i.got = principal
|
||||
return i.token, i.expiresAt, i.err
|
||||
}
|
||||
|
||||
func TestGetAuthorizationToken_ProjectsMintedCredential(t *testing.T) {
|
||||
expiresAt := time.Date(2026, 10, 2, 3, 4, 5, 0, time.UTC)
|
||||
issuer := &fakeAuthorizationTokenIssuer{token: "signed-token", expiresAt: expiresAt}
|
||||
principal := ecrauth.Principal{
|
||||
AccountID: "123456789012", ARN: "arn:aws:iam::123456789012:user/dev",
|
||||
Type: "user", AccessKeyID: "AKIAEXAMPLE",
|
||||
}
|
||||
|
||||
out, err := GetAuthorizationToken(issuer, RepositoryEndpoint{
|
||||
Region: "ap-southeast-2", ServicesDomain: "spinifex.test", RegistryPort: "9999",
|
||||
}, principal)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.AuthorizationData, 1)
|
||||
data := out.AuthorizationData[0]
|
||||
assert.Equal(t, principal, issuer.got)
|
||||
assert.Equal(t, "https://123456789012.dkr.ecr.ap-southeast-2.spinifex.test:9999", *data.ProxyEndpoint)
|
||||
assert.Equal(t, expiresAt, *data.ExpiresAt)
|
||||
decoded, err := base64.StdEncoding.DecodeString(*data.AuthorizationToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "AWS:signed-token", string(decoded))
|
||||
}
|
||||
|
||||
func TestGetAuthorizationToken_RefusesUnavailableIssuer(t *testing.T) {
|
||||
principal := ecrauth.Principal{AccountID: "123456789012"}
|
||||
_, err := GetAuthorizationToken(nil, RepositoryEndpoint{}, principal)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorNotImplemented, awserrors.ValidErrorCodeFromError(err))
|
||||
|
||||
_, err = GetAuthorizationToken(&fakeAuthorizationTokenIssuer{err: errors.New("signer unavailable")}, RepositoryEndpoint{}, principal)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, awserrors.ValidErrorCodeFromError(err))
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ecr"
|
||||
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
|
||||
awsapi "github.com/mulgadc/spinifex/spinifex/domains/ecr/awsapi"
|
||||
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
|
||||
)
|
||||
|
||||
// handleGetAuthorizationToken mints a self-contained ES256 ECR token for the
|
||||
// SigV4-authenticated caller and returns it in the AWS GetAuthorizationToken
|
||||
// shape. The token is base64("AWS:<jwt>"); docker stores it and replays it as
|
||||
// Basic auth against the /v2 registry, where the auth bridge verifies it.
|
||||
// handleGetAuthorizationToken adapts an authenticated gateway identity into
|
||||
// the canonical ECR credential principal. The AWS token response is owned by
|
||||
// the ECR action adapter, while canonical IAM/STS ARN construction remains a
|
||||
// generic gateway identity concern.
|
||||
func (gw *GatewayConfig) handleGetAuthorizationToken(w http.ResponseWriter, r *http.Request) error {
|
||||
if gw.ECRTokenIssuer == nil {
|
||||
return errors.New(awserrors.ErrorNotImplemented)
|
||||
@@ -42,26 +39,17 @@ func (gw *GatewayConfig) handleGetAuthorizationToken(w http.ResponseWriter, r *h
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
token, expiresAt, err := gw.ECRTokenIssuer.Mint(ecrauth.Principal{
|
||||
output, err := awsapi.GetAuthorizationToken(gw.ECRTokenIssuer, gw.ecrRepositoryEndpoint(), ecrauth.Principal{
|
||||
AccountID: accountID,
|
||||
ARN: callerARN,
|
||||
Type: principalType,
|
||||
AccessKeyID: accessKey,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("GetAuthorizationToken: mint failed", "err", err)
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
slog.Error("GetAuthorizationToken: action failed", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
authToken := base64.StdEncoding.EncodeToString([]byte("AWS:" + token))
|
||||
proxyEndpoint := "https://" + gw.ecrRepositoryEndpoint().RegistryURIHost(accountID)
|
||||
|
||||
awsapi.WriteJSONResponse(w, &ecr.GetAuthorizationTokenOutput{
|
||||
AuthorizationData: []*ecr.AuthorizationData{{
|
||||
AuthorizationToken: aws.String(authToken),
|
||||
ProxyEndpoint: aws.String(proxyEndpoint),
|
||||
ExpiresAt: aws.Time(expiresAt),
|
||||
}},
|
||||
})
|
||||
awsapi.WriteJSONResponse(w, output)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user