Skip to content
Draft
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
16 changes: 14 additions & 2 deletions docs/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,25 @@ Required fields:
+-----------+-----------------------------------------------+-----------------------+-----------------------------------+
| Region | Region name | Yes | eu-central-1 |
+-----------+-----------------------------------------------+-----------------------+-----------------------------------+
| KeyId | API key ID | Yes | keyname123456789 |
| KeyId | API key ID | For static keys | keyname123456789 |
+-----------+-----------------------------------------------+-----------------------+-----------------------------------+
| KeySecret | API key secret | Yes | verysecret123 |
| KeySecret | API key secret | For static keys | verysecret123 |
+-----------+-----------------------------------------------+-----------------------+-----------------------------------+
| Endpoint | Custom endpoint. Leave blank for AWS S3. | Only for Backblaze B2 | s3.eu-central-001.backblazeb2.com |
+-----------+-----------------------------------------------+-----------------------+-----------------------------------+

To use an IAM role or the AWS SDK credential provider chain, leave **both**
``KeyId`` and ``KeySecret`` blank. Bucket and region are still required. This
works with the setup form, ``cloudconfig.yml``, or ``GOKAPI_AWS_BUCKET`` and
``GOKAPI_AWS_REGION`` environment variables. Providing only one key is invalid.

The SDK can obtain temporary credentials from an EC2 instance profile, an ECS
task role, EKS web identity, a shared credentials file, or standard AWS
``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and ``AWS_SESSION_TOKEN``
environment variables. Explicit Gokapi keys keep their existing precedence.
Grant the role access to the bucket and use **Test configuration** before
finishing setup. Gokapi does not write resolved role credentials to its config.

If you plan to use end-to-end encryption with cloud storage, configure your bucket's CORS rules to allow requests from your Gokapi URL.

.. _setup_encryption:
Expand Down
3 changes: 3 additions & 0 deletions internal/configuration/setup/Setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ func getCloudConfig(formObjects *[]jsonFormObject) (*cloudconfig.CloudConfig, er
if err != nil {
return nil, err
}
if (awsConfig.Aws.KeyId == "") != (awsConfig.Aws.KeySecret == "") {
return nil, errors.New("provide both S3 key fields, or leave both blank to use the AWS credential provider chain")
}
return &awsConfig, nil
}

Expand Down
21 changes: 21 additions & 0 deletions internal/configuration/setup/Setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,3 +757,24 @@ func TestIsErrorAddressAlreadyInUse(t *testing.T) {
test.IsEqualBool(t, isErrorAddressAlreadyInUse(err), false)
l.Close()
}

func TestCloudConfigCredentialModes(t *testing.T) {
for _, tc := range []struct {
key, secret string
valid bool
}{
{"", "", true}, {"key", "secret", true}, {"key", "", false}, {"", "secret", false},
} {
form := []jsonFormObject{
{Name: "storage_sel_proxy", Value: "proxy"},
{Name: "s3_bucket", Value: "bucket"}, {Name: "s3_region", Value: "region"},
{Name: "s3_api", Value: tc.key}, {Name: "s3_secret", Value: tc.secret},
{Name: "s3_endpoint", Value: ""},
}
config, err := getCloudConfig(&form)
test.IsEqualBool(t, err == nil, tc.valid)
if tc.valid {
test.IsEqualBool(t, config.Aws.IsAllProvided(), true)
}
}
}
12 changes: 10 additions & 2 deletions internal/configuration/setup/templates/setup.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,14 @@
<input type="text" class="form-control" id="s3_region" name="s3_region" placeholder="Region Name" required data-min="1" data-validate="validateMinLength">
</div><br><br>
<div class="col-sm-8">
<p>Leave both key fields blank to use the AWS credential provider chain (for example, an IAM role). Otherwise, provide both fields.</p>
<label for="s3_api">API Key:</label>
<input type="text" class="form-control" id="s3_api" name="s3_api" placeholder="API Key" required data-min="1" data-validate="validateMinLength">
<input type="text" class="form-control" id="s3_api" name="s3_api" placeholder="API Key (optional)" data-validate="validateS3Credentials">
</div><br><br>

<div class="col-sm-8">
<label for="s3_secret">API Key Secret:</label>
<input type="text" class="form-control" id="s3_secret" name="s3_secret" placeholder="API Key Secret" required data-min="1" data-validate="validateMinLength">
<input type="text" class="form-control" id="s3_secret" name="s3_secret" placeholder="API Key Secret (optional)" data-validate="validateS3Credentials">
</div><br><br>
<div class="col-sm-8">
<label for="s3_endpoint">Endpoint:</label>
Expand Down Expand Up @@ -918,6 +919,13 @@ function TestAWS(button, isManual) {
function parseAndSetDbString(input) {
}

function validateS3Credentials(el) {
return {
status: Boolean(document.getElementById("s3_api").value) === Boolean(document.getElementById("s3_secret").value),
msg: "Provide both key fields, or leave both blank to use the AWS credential provider chain"
};
}

function validateMinLength(el) {
let value = el.val();
let retValue = {};
Expand Down
3 changes: 1 addition & 2 deletions internal/environment/Environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ func parseFlags(result Environment) Environment {
func (e *Environment) IsAwsProvided() bool {
return e.AwsBucket != "" &&
e.AwsRegion != "" &&
e.AwsKeyId != "" &&
e.AwsKeySecret != ""
((e.AwsKeyId == "" && e.AwsKeySecret == "") || (e.AwsKeyId != "" && e.AwsKeySecret != ""))
}

// GetConfigPaths returns the config paths to config files and the directory containing the files. The following results are returned:
Expand Down
16 changes: 16 additions & 0 deletions internal/environment/Environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,19 @@ func TestGetConfigPaths(t *testing.T) {
test.IsEqualString(t, configFile, "test2")
test.IsEqualString(t, awsConfig, "test/cloudconfig.yml")
}

func TestAwsEnvironmentCredentialModes(t *testing.T) {
for _, tc := range []struct {
key, secret string
valid bool
}{
{"", "", true}, {"key", "secret", true}, {"key", "", false}, {"", "secret", false},
} {
t.Setenv("GOKAPI_AWS_BUCKET", "bucket")
t.Setenv("GOKAPI_AWS_REGION", "region")
t.Setenv("GOKAPI_AWS_KEY", tc.key)
t.Setenv("GOKAPI_AWS_KEY_SECRET", tc.secret)
env := New()
test.IsEqualBool(t, env.IsAwsProvided(), tc.valid)
}
}
3 changes: 1 addition & 2 deletions internal/models/AwsConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ type AwsConfig struct {
func (c *AwsConfig) IsAllProvided() bool {
return c.Bucket != "" &&
c.Region != "" &&
c.KeyId != "" &&
c.KeySecret != ""
((c.KeyId == "" && c.KeySecret == "") || (c.KeyId != "" && c.KeySecret != ""))
}
14 changes: 14 additions & 0 deletions internal/models/AwsConfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,17 @@ func TestIsAwsProvided(t *testing.T) {
}
test.IsEqualBool(t, config.IsAllProvided(), true)
}

func TestAwsCredentialModes(t *testing.T) {
for _, tc := range []struct {
key, secret string
valid bool
}{
{"", "", true}, {"key", "secret", true}, {"key", "", false}, {"", "secret", false},
} {
config := AwsConfig{Bucket: "bucket", Region: "region", KeyId: tc.key, KeySecret: tc.secret}
test.IsEqualBool(t, config.IsAllProvided(), tc.valid)
config.Region = ""
test.IsEqualBool(t, config.IsAllProvided(), false)
}
}
5 changes: 4 additions & 1 deletion internal/storage/filesystem/s3filesystem/aws/Aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ func IsValidLogin(config models.AwsConfig) (bool, error) {

func createSession() *session.Session {
s3Config := &aws.Config{
Credentials: credentials.NewStaticCredentials(awsConfig.KeyId, awsConfig.KeySecret, ""),
Endpoint: aws.String(awsConfig.Endpoint),
Region: aws.String(awsConfig.Region),
S3ForcePathStyle: aws.Bool(true),
}
// Leaving Credentials unset enables the SDK provider chain, including IAM roles.
if awsConfig.KeyId != "" || awsConfig.KeySecret != "" {
s3Config.Credentials = credentials.NewStaticCredentials(awsConfig.KeyId, awsConfig.KeySecret, "")
}
return session.Must(session.NewSession(s3Config))
}

Expand Down
36 changes: 36 additions & 0 deletions internal/storage/filesystem/s3filesystem/aws/Credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build !noaws && !awsmock

package aws

import (
"github.com/forceu/gokapi/internal/models"
"testing"
)

func TestSessionCredentials(t *testing.T) {
previous := awsConfig
t.Cleanup(func() { awsConfig = previous })
t.Setenv("AWS_ACCESS_KEY_ID", "environment-key")
t.Setenv("AWS_SECRET_ACCESS_KEY", "environment-secret")
t.Setenv("AWS_SESSION_TOKEN", "temporary-token")
t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
for _, static := range []bool{false, true} {
awsConfig = models.AwsConfig{Bucket: "bucket", Region: "eu-central-1"}
if static {
awsConfig.KeyId = "static-key"
awsConfig.KeySecret = "static-secret"
}
sess := createSession()
value, err := sess.Config.Credentials.Get()
if err != nil {
t.Fatal(err)
}
if static {
if value.AccessKeyID != "static-key" || value.SecretAccessKey != "static-secret" || value.SessionToken != "" {
t.Fatalf("static credentials not preserved: %s", value.ProviderName)
}
} else if value.AccessKeyID != "environment-key" || value.SecretAccessKey != "environment-secret" || value.SessionToken != "temporary-token" {
t.Fatalf("default credential chain not used: %s", value.ProviderName)
}
}
}