Skip to content

Commit

Permalink
Feature/multiple fqdns for s3 proxy (#18)
Browse files Browse the repository at this point in the history
This change allows configuration of the proxy to support multiple FQDNs (via FAKES3PP_S3_PROXY_FQDN)

* test: Almost e2e add coverage for ListObjectsV2

* feature: support providing multiple FQDNs for S3 Proxy

For use cases where an S3 proxy is reachable by multiple DNS names it should be possible to be aware of all of them. Given that a DNS name is used to determine whether virtual hosting is used or not.

* tests: multiple FQDNs for s3proxy test env

* test: ListOjbects against alternate FQDN

It should not matter which FQDN should be used. Add a test case that uses the alternative FQDN.

* refactor: cleanup logic to get FQDN proxy

---------

Co-authored-by: Peter Van Bouwel <peter.vanbouwel@vito.be>
  • Loading branch information
pvbouwel and Peter Van Bouwel authored Jan 7, 2025
1 parent a67b167 commit 300c4fd
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 28 deletions.
57 changes: 56 additions & 1 deletion cmd/almost-e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ func TestPolicyAllowAllInRegion1ConditionsOnRegionAreEnforced(t *testing.T) {
defer tearDown()
token := getSignedToken("mySubject", time.Minute * 20, AWSSessionTags{PrincipalTags: map[string][]string{"org": {"a"}}})
//Given the policy Manager that has our test policies
pm = *NewTestPolicyManagerAlmostE2EPolicies()
pm = *NewTestPolicyManagerAlmostE2EPolicies()
//Given credentials that use the policy that allow everything in Region1
creds := getCredentialsFromTestStsProxy(t, token, "my-session", testPolicyAllowAllInRegion1ARN)

Expand All @@ -402,4 +402,59 @@ func TestPolicyAllowAllInRegion1ConditionsOnRegionAreEnforced(t *testing.T) {
t.Errorf("Expected AccessDenied, got %s", err.ErrorCode())
}
}
}

func listTestBucketObjects(t *testing.T, region, prefix string, creds aws.Credentials) (*s3.ListObjectsV2Output, smithy.APIError){

client := getS3ClientAgainstS3Proxy(t, region, creds)

max1Sec, cancel := context.WithTimeout(context.Background(), 1000 * time.Second)

input := s3.ListObjectsV2Input{
Bucket: &testingBucketNameBackenddetails,
Prefix: &prefix,
}
defer cancel()
s3ListObjectsOutput, err := client.ListObjectsV2(max1Sec, &input)
if err != nil {
var oe smithy.APIError
if !errors.As(err, &oe) {
t.Errorf("Could not convert smity error")
t.FailNow()
}
return nil, oe
}
return s3ListObjectsOutput, nil
}

//Make sure that needleObjectKey exists in the object Listing objectsList
func assertObjectInBucketListing(t *testing.T, objectsList *s3.ListObjectsV2Output, needleObjectKey string) {
for _, s3Object := range objectsList.Contents {
if needleObjectKey == *s3Object.Key {
return
}
}
t.Errorf("Did not encounter %s in %v", needleObjectKey, objectsList)
}

func TestListingOfS3BucketHasExpectedObjects(t *testing.T) {
tearDown, getSignedToken := testingFixture(t)
defer tearDown()
token := getSignedToken("mySubject", time.Minute * 20, AWSSessionTags{PrincipalTags: map[string][]string{"org": {"a"}}})
//Given the policy Manager that has our test policies
pm = *NewTestPolicyManagerAlmostE2EPolicies()
//Given credentials that use the policy that allow everything in Region1
creds := getCredentialsFromTestStsProxy(t, token, "my-session", testPolicyAllowAllInRegion1ARN)

var prefix string= ""

//WHEN we get an object in region 1
listObjects, err := listTestBucketObjects(t, testRegion1, prefix, creds)
//THEN it should just succeed as any action is allowed
if err != nil {
t.Errorf("Could not get objects in bucket due to error %s", err)
}
//THEN it should report the known objects "region.txt" and "team.txt"
assertObjectInBucketListing(t, listObjects, "region.txt")
assertObjectInBucketListing(t, listObjects, "team.txt")
}
52 changes: 51 additions & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package cmd

import (
"crypto/rsa"
"errors"
"fmt"
"log/slog"
"os"
"strings"
"time"

"github.com/spf13/viper"
Expand Down Expand Up @@ -77,7 +80,9 @@ var envVarDefs = []envVarDef{
s3ProxyFQDN,
FAKES3PP_S3_PROXY_FQDN,
true,
"The fully qualified domain name of this S3 proxy server (e.g. localhost)",
`The fully qualified domain name(s) of this S3 proxy server (e.g. localhost).
You can specify multiple to allow access via multiple FQDNs but the first one will be used for generating pre-signed urls.
When specifying multiple they must be comma-separated.`,
[]string{proxys3},
},
{
Expand Down Expand Up @@ -227,6 +232,51 @@ func getMaxStsDuration() (time.Duration) {
return time.Second * time.Duration(getMaxStsDurationSeconds())
}

//The Fully Qualified Domain names for the S3 proxy
var s3ProxyFQDNs []string

//get all the FQDNs associated with the S3 Proxy
func getS3ProxyFQDNs() ([]string, error) {
if s3ProxyFQDNs == nil {
var tmpS3ProxyFQDNS []string
err := viper.UnmarshalKey(s3ProxyFQDN, &tmpS3ProxyFQDNS)
if err != nil {
return nil, err
}
s3ProxyFQDNs = make([]string, len(tmpS3ProxyFQDNS))
for i, tmpFQDN := range tmpS3ProxyFQDNS {
s3ProxyFQDNs[i] = strings.ToLower(tmpFQDN)
}
}
return s3ProxyFQDNs, nil
}

//Check whether a given hostname is one of the possible FQDNs.
func isAS3ProxyFQDN(hostname string) bool{
fqdns, err := getS3ProxyFQDNs()
if err != nil {
slog.Error("Could not get S3ProxyFQDNS", "error", err)
}
for _, fqdn := range fqdns {
if fqdn == strings.ToLower(hostname) {
return true
}
}
return false
}

//get the main FQDN associated with the S3 proxy
func getMainS3ProxyFQDN() (string, error) {
fqdns, err := getS3ProxyFQDNs()
if err != nil {
return "", err
}
if len(fqdns) == 0 {
return "", errors.New("no S3ProxyFQDN available")
}
return fqdns[0], nil
}

//Bind the environment variables for a command
func BindEnvVariables(cmd string) {
for _, evd := range envVarDefs {
Expand Down
7 changes: 3 additions & 4 deletions cmd/policy-iam-action.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"strings"

"github.com/micahhausler/aws-iam-policy/policy"
"github.com/spf13/viper"
)

type iamAction struct{
Expand Down Expand Up @@ -44,8 +43,8 @@ func makeS3ObjectArn(bucketName, objectKey string) string {
}

func getS3ObjectFromRequest(req *http.Request) (bucketName string, objectKey string, err error) {
fqdn := viper.GetString(s3ProxyFQDN)
if strings.HasPrefix(req.Host, fqdn) {
hostWithoutPort := strings.Split(req.Host, ":")[0]
if isAS3ProxyFQDN(hostWithoutPort) {
//Path-style request
if !strings.HasPrefix(req.URL.Path, "/") {
return "", "", fmt.Errorf("request uri did not start with '/': %s", req.RequestURI)
Expand All @@ -56,7 +55,7 @@ func getS3ObjectFromRequest(req *http.Request) (bucketName string, objectKey str
return bucketName, objectKey, nil
} else {
//Virtual hosting
return "", "", errors.New("virtual hosting requests not implemented")
return "", "", errors.New("virtual hosting not supported try path-style request")
}
}

Expand Down
33 changes: 28 additions & 5 deletions cmd/policy-iam-action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ func newStubJustReturnIamAction(ti *testing.T) handlerBuilderI {
return &testStub
}


func getAnonymousS3TestClient(t *testing.T) (client *s3.Client, ctx context.Context, cancel context.CancelFunc) {
func getAnonymousS3TestClientForEndpoint(t *testing.T, endpoint string) (client *s3.Client, ctx context.Context, cancel context.CancelFunc) {
cfg := getTestAwsConfig(t)

endpoint := getS3ProxyUrl()
client = s3.NewFromConfig(cfg, func (o *s3.Options) {
o.BaseEndpoint = aws.String(endpoint)
o.Region = "eu-central-1"
Expand All @@ -70,8 +68,15 @@ func getAnonymousS3TestClient(t *testing.T) (client *s3.Client, ctx context.Cont
return
}

func runListObjectsV2AndReturnError(t *testing.T) error {
client, max1Sec, cancel := getAnonymousS3TestClient(t)

func getAnonymousS3TestClient(t *testing.T) (client *s3.Client, ctx context.Context, cancel context.CancelFunc) {
endpoint := getS3ProxyUrl()

return getAnonymousS3TestClientForEndpoint(t, endpoint)
}

func runListObjectsV2AndReturnErrorForEndpoint(t *testing.T, endpoint string) error {
client, max1Sec, cancel := getAnonymousS3TestClientForEndpoint(t, endpoint)

input := s3.ListObjectsV2Input{
Bucket: &testBucketName,
Expand All @@ -84,6 +89,15 @@ func runListObjectsV2AndReturnError(t *testing.T) error {
return err
}

func runListObjectsV2AndReturnError(t *testing.T) error {
return runListObjectsV2AndReturnErrorForEndpoint(t, getS3ProxyUrl())
}

//run listObjectsV2 but use alternate FQDN that is known by S3Proxy.
func runListObjectsV2AndReturnErrorAlternateEndpoint(t *testing.T) error {
return runListObjectsV2AndReturnErrorForEndpoint(t, "localhost2")
}

var listobjectv2_test_prefix string = "my-prefix"

func runListObjectsV2WithPrefixAndReturnError(t *testing.T) error {
Expand Down Expand Up @@ -283,6 +297,15 @@ func getApiAndIAMActionTestCases() ([]apiAndIAMActionTestCase) {
}),
},
},
{
ApiAction: "ListObjectsV2",
ApiCall: runListObjectsV2AndReturnErrorAlternateEndpoint,
ExpectedActions: []iamAction{
newIamAction(IAMActionS3ListBucket, testBucketARN, nil).addContext(contextType{
IAMConditionS3Prefix: policy.NewConditionValueString(true, listobjectv2_test_prefix),
}),
},
},
{
ApiAction: "PutObject",
ApiCall: runPutObjectAndReturnError,
Expand Down
6 changes: 5 additions & 1 deletion cmd/presign.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ func PreSignRequestWithServerCreds(req *http.Request, exiryInSeconds int, signin


func PreSignRequestForGet(bucket, key, region string, signingTime time.Time, expirySeconds int) (string, error) {
url := fmt.Sprintf("https://%s:%d/%s/%s", viper.Get(s3ProxyFQDN), viper.GetInt(s3ProxyPort), bucket, key)
mainS3ProxyFQDN, err := getMainS3ProxyFQDN()
if err != nil {
return "", fmt.Errorf("could not get main S3ProxyFQDN: %s", err)
}
url := fmt.Sprintf("https://%s:%d/%s/%s", mainS3ProxyFQDN, viper.GetInt(s3ProxyPort), bucket, key)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("error when creating a request context for url: %s", err)
Expand Down
22 changes: 19 additions & 3 deletions cmd/proxys3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ func TestValidPreSignWithServerCreds(t *testing.T) {
}
}

func getMainS3ProxyFQDNForTest(t *testing.T) string {
mainS3ProxyFQDN, err := getMainS3ProxyFQDN()
if err != nil {
t.Errorf("COuld not get Main S3 Proxy FQDN: %s", err)
t.FailNow()
}
return mainS3ProxyFQDN
}

func TestValidPreSignWithTempCreds(t *testing.T) {
//Given valid server config
BindEnvVariables("proxys3")
Expand All @@ -74,9 +83,8 @@ func TestValidPreSignWithTempCreds(t *testing.T) {
SessionToken: "Incredibly secure",
}


//Given we have a valid signed URI valid for 1 second
url := fmt.Sprintf("https://%s:%d/%s/%s", viper.Get(s3ProxyFQDN), viper.GetInt(s3ProxyPort), "bucket", "key")
url := fmt.Sprintf("https://%s:%d/%s/%s", getMainS3ProxyFQDNForTest(t), viper.GetInt(s3ProxyPort), "bucket", "key")
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
t.Errorf("error when creating a request context for url: %s", err)
Expand Down Expand Up @@ -168,9 +176,17 @@ func setupSuiteProxyS3(t *testing.T, handlerBuilder handlerBuilderI) (func(t *te
}
}

func getS3ProxyUrlWithoutPort() string {
mainS3ProxyFQDN, err := getMainS3ProxyFQDN()
if err != nil {
panic(fmt.Errorf("Could not get main S3 ProxyFQDN panic as this is from test code only, %s", mainS3ProxyFQDN))
}
return fmt.Sprintf("%s://%s", getProxyProtocol(), mainS3ProxyFQDN)
}

//Get the fully qualified URL to the S3 Proxy
func getS3ProxyUrl() string {
return fmt.Sprintf("%s:%d/", getProxyUrlWithoutPort(), viper.GetInt(s3ProxyPort))
return fmt.Sprintf("%s:%d/", getS3ProxyUrlWithoutPort(), viper.GetInt(s3ProxyPort))
}


Expand Down
4 changes: 2 additions & 2 deletions cmd/proxysts.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ var proxystsCmd = &cobra.Command{
func getProxyProtocol() string {
secure := viper.GetBool(secure)
if secure{
slog.Info("Got proxy protocol", "procotol", "https", "secure", secure)
slog.Debug("Got proxy protocol", "procotol", "https", "secure", secure)
return "https"
} else {
slog.Info("Got proxy protocol", "procotol", "http", "secure", secure)
slog.Debug("Got proxy protocol", "procotol", "http", "secure", secure)
return "http"
}
}
Expand Down
13 changes: 3 additions & 10 deletions cmd/proxysts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,12 @@ func getTestAwsConfig(t *testing.T) (aws.Config) {
return cfg
}

func getProxyUrlWithoutPort() string {
secure := viper.GetBool(secure)
var protocol string
if secure {
protocol = "https"
} else {
protocol = "http"
}
return fmt.Sprintf("%s://%s", protocol, viper.GetString(stsProxyFQDN))
func getStsProxyUrlWithoutPort() string {
return fmt.Sprintf("%s://%s", getProxyProtocol(), viper.GetString(stsProxyFQDN))
}

func getStsProxyUrl() string {
return fmt.Sprintf("%s:%d/", getProxyUrlWithoutPort(), viper.GetInt(stsProxyPort))
return fmt.Sprintf("%s:%d/", getStsProxyUrlWithoutPort(), viper.GetInt(stsProxyPort))
}

func assumeRoleWithWebIdentityAgainstTestStsProxy(t *testing.T, token, roleSessionName, roleArn string) (*sts.AssumeRoleWithWebIdentityOutput, error) {
Expand Down
2 changes: 1 addition & 1 deletion etc/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FAKES3PP_S3_PROXY_FQDN=localhost
FAKES3PP_S3_PROXY_FQDN=localhost,localhost2
FAKES3PP_S3_PROXY_PORT=8443
FAKES3PP_S3_PROXY_KEY_FILE=../etc/key.pem
FAKES3PP_S3_PROXY_CERT_FILE=../etc/cert.pem
Expand Down

0 comments on commit 300c4fd

Please sign in to comment.