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
5 changes: 5 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ oss:
credential_mode: "static"
ecs_role_name: "CixingEcsOssUploadRole"
disable_imdsv1: true
assume_role_arn: ""
assume_role_session_name: ""
assume_role_external_id: ""
assume_role_sts_endpoint: ""
assume_role_session_duration: 1h
bucket: "change-me"
region: "cn-hongkong"
access_key_id: ""
Expand Down
7 changes: 7 additions & 0 deletions configs/config.local.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ oss:
credential_mode: "static"
ecs_role_name: "CixingEcsOssUploadRole"
disable_imdsv1: true
# For cross-account ECS RAM role access, set credential_mode to
# "ecs_ram_role_assume_role" and fill the target account role ARN below.
assume_role_arn: ""
assume_role_session_name: ""
assume_role_external_id: ""
assume_role_sts_endpoint: ""
assume_role_session_duration: 1h
bucket: "change-me"
region: "cn-hongkong"
access_key_id: ""
Expand Down
7 changes: 7 additions & 0 deletions configs/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ PG_DSN=postgres://postgres:postgres@localhost:5432/cixing?sslmode=disable
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
OSS_CREDENTIAL_MODE=
OSS_ECS_ROLE_NAME=
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ASSUME_ROLE_ARN=
OSS_ASSUME_ROLE_SESSION_NAME=
OSS_ASSUME_ROLE_EXTERNAL_ID=
OSS_ASSUME_ROLE_STS_ENDPOINT=
OSS_ASSUME_ROLE_SESSION_DURATION=
JWT_PRIVATE_KEY_PEM=
JWT_PUBLIC_KEY_PEM=
SMTP_PASSWORD=
52 changes: 31 additions & 21 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,27 @@ type RedisConfig struct {
}

type OSSConfig struct {
CredentialMode string `yaml:"credential_mode"`
ECSRoleName string `yaml:"ecs_role_name"`
DisableIMDSv1 bool `yaml:"disable_imdsv1"`
Bucket string `yaml:"bucket"`
Region string `yaml:"region"`
AccessKeyID string `yaml:"access_key_id"`
AccessKeySecret string `yaml:"access_key_secret"`
PublicEndpoint string `yaml:"public_endpoint"`
PublicEndpointIsCName bool `yaml:"public_endpoint_is_cname"`
InternalEndpoint string `yaml:"internal_endpoint"`
InternalEndpointIsCName bool `yaml:"internal_endpoint_is_cname"`
UseInternalEndpoint bool `yaml:"use_internal_endpoint"`
PutPresignExpire time.Duration `yaml:"put_presign_expire"`
GetPresignExpire time.Duration `yaml:"get_presign_expire"`
UploadPrefix string `yaml:"upload_prefix"`
Styles OSSStyleConfig `yaml:"styles"`
CredentialMode string `yaml:"credential_mode"`
ECSRoleName string `yaml:"ecs_role_name"`
DisableIMDSv1 bool `yaml:"disable_imdsv1"`
AssumeRoleARN string `yaml:"assume_role_arn"`
AssumeRoleSessionName string `yaml:"assume_role_session_name"`
AssumeRoleExternalID string `yaml:"assume_role_external_id"`
AssumeRoleSTSEndpoint string `yaml:"assume_role_sts_endpoint"`
AssumeRoleSessionDuration time.Duration `yaml:"assume_role_session_duration"`
Bucket string `yaml:"bucket"`
Region string `yaml:"region"`
AccessKeyID string `yaml:"access_key_id"`
AccessKeySecret string `yaml:"access_key_secret"`
PublicEndpoint string `yaml:"public_endpoint"`
PublicEndpointIsCName bool `yaml:"public_endpoint_is_cname"`
InternalEndpoint string `yaml:"internal_endpoint"`
InternalEndpointIsCName bool `yaml:"internal_endpoint_is_cname"`
UseInternalEndpoint bool `yaml:"use_internal_endpoint"`
PutPresignExpire time.Duration `yaml:"put_presign_expire"`
GetPresignExpire time.Duration `yaml:"get_presign_expire"`
UploadPrefix string `yaml:"upload_prefix"`
Styles OSSStyleConfig `yaml:"styles"`
}

type OSSStyleConfig struct {
Expand Down Expand Up @@ -107,11 +112,12 @@ func Default() Config {
DB: 0,
},
OSS: OSSConfig{
CredentialMode: "static",
ECSRoleName: "CixingEcsOssUploadRole",
PutPresignExpire: 15 * time.Minute,
GetPresignExpire: 10 * time.Minute,
UploadPrefix: "uploads/",
CredentialMode: "static",
ECSRoleName: "CixingEcsOssUploadRole",
AssumeRoleSessionDuration: time.Hour,
PutPresignExpire: 15 * time.Minute,
GetPresignExpire: 10 * time.Minute,
UploadPrefix: "uploads/",
Styles: OSSStyleConfig{
Card4x3: "card_4x3",
SquareSmall: "square_small",
Expand Down Expand Up @@ -139,6 +145,10 @@ func (c *Config) Normalize() {
c.OSS.Region = strings.TrimSpace(c.OSS.Region)
c.OSS.CredentialMode = strings.ToLower(strings.TrimSpace(c.OSS.CredentialMode))
c.OSS.ECSRoleName = strings.TrimSpace(c.OSS.ECSRoleName)
c.OSS.AssumeRoleARN = strings.TrimSpace(c.OSS.AssumeRoleARN)
c.OSS.AssumeRoleSessionName = strings.TrimSpace(c.OSS.AssumeRoleSessionName)
c.OSS.AssumeRoleExternalID = strings.TrimSpace(c.OSS.AssumeRoleExternalID)
c.OSS.AssumeRoleSTSEndpoint = strings.TrimSpace(c.OSS.AssumeRoleSTSEndpoint)
c.OSS.AccessKeyID = strings.TrimSpace(c.OSS.AccessKeyID)
c.OSS.AccessKeySecret = strings.TrimSpace(c.OSS.AccessKeySecret)
c.OSS.PublicEndpoint = strings.TrimSpace(c.OSS.PublicEndpoint)
Expand Down
11 changes: 9 additions & 2 deletions internal/config/env_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ const (
EnvRedisPassword = "REDIS_PASSWORD"
EnvRedisDB = "REDIS_DB"

EnvOSSAccessKeyID = "OSS_ACCESS_KEY_ID"
EnvOSSAccessKeySecret = "OSS_ACCESS_KEY_SECRET"
EnvOSSCredentialMode = "OSS_CREDENTIAL_MODE"
EnvOSSECSRoleName = "OSS_ECS_ROLE_NAME"
EnvOSSAccessKeyID = "OSS_ACCESS_KEY_ID"
EnvOSSAccessKeySecret = "OSS_ACCESS_KEY_SECRET"
EnvOSSAssumeRoleARN = "OSS_ASSUME_ROLE_ARN"
EnvOSSAssumeRoleSessionName = "OSS_ASSUME_ROLE_SESSION_NAME"
EnvOSSAssumeRoleExternalID = "OSS_ASSUME_ROLE_EXTERNAL_ID"
EnvOSSAssumeRoleSTSEndpoint = "OSS_ASSUME_ROLE_STS_ENDPOINT"
EnvOSSAssumeRoleSessionDuration = "OSS_ASSUME_ROLE_SESSION_DURATION"

EnvJWTPrivatePEM = "JWT_PRIVATE_KEY_PEM"
EnvJWTPublicPEM = "JWT_PUBLIC_KEY_PEM"
Expand Down
21 changes: 21 additions & 0 deletions internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"strconv"
"strings"
"time"

"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -75,8 +76,17 @@ func applyEnv(cfg *Config) error {
return err
}

setString(&cfg.OSS.CredentialMode, EnvOSSCredentialMode)
setString(&cfg.OSS.ECSRoleName, EnvOSSECSRoleName)
setString(&cfg.OSS.AccessKeyID, EnvOSSAccessKeyID)
setString(&cfg.OSS.AccessKeySecret, EnvOSSAccessKeySecret)
setString(&cfg.OSS.AssumeRoleARN, EnvOSSAssumeRoleARN)
setString(&cfg.OSS.AssumeRoleSessionName, EnvOSSAssumeRoleSessionName)
setString(&cfg.OSS.AssumeRoleExternalID, EnvOSSAssumeRoleExternalID)
setString(&cfg.OSS.AssumeRoleSTSEndpoint, EnvOSSAssumeRoleSTSEndpoint)
if err := setDuration(&cfg.OSS.AssumeRoleSessionDuration, EnvOSSAssumeRoleSessionDuration); err != nil {
return err
}
setPEM(&cfg.JWT.PrivateKeyPEM, EnvJWTPrivatePEM)
setPEM(&cfg.JWT.PublicKeyPEM, EnvJWTPublicPEM)

Expand Down Expand Up @@ -116,3 +126,14 @@ func setInt(dst *int, key string) error {
}
return nil
}

func setDuration(dst *time.Duration, key string) error {
if v := readEnv(key); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return fmt.Errorf("config: invalid duration for %s: %w", key, err)
}
*dst = d
}
return nil
}
90 changes: 90 additions & 0 deletions internal/config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
)

func TestLoadWith_UsesDefaultConfigFile(t *testing.T) {
Expand Down Expand Up @@ -75,6 +76,13 @@ jwt:
t.Setenv("SMTP_PASSWORD", "env-smtp")
t.Setenv("JWT_PRIVATE_KEY_PEM", "env-private")
t.Setenv("JWT_PUBLIC_KEY_PEM", "env-public")
t.Setenv("OSS_CREDENTIAL_MODE", "ecs_ram_role_assume_role")
t.Setenv("OSS_ECS_ROLE_NAME", "env-source-role")
t.Setenv("OSS_ASSUME_ROLE_ARN", "acs:ram::1234567890123456:role/env-target-role")
t.Setenv("OSS_ASSUME_ROLE_SESSION_NAME", "env-session")
t.Setenv("OSS_ASSUME_ROLE_EXTERNAL_ID", "env-external")
t.Setenv("OSS_ASSUME_ROLE_STS_ENDPOINT", "sts.cn-hongkong.aliyuncs.com")
t.Setenv("OSS_ASSUME_ROLE_SESSION_DURATION", "45m")

// These used to override config, but should no longer be applied.
t.Setenv("HTTP_ADDR", ":9999")
Expand Down Expand Up @@ -106,6 +114,27 @@ jwt:
if cfg.JWT.PublicKeyPEM != "env-public" {
t.Fatalf("JWT.PublicKeyPEM = %q, want env-public", cfg.JWT.PublicKeyPEM)
}
if cfg.OSS.CredentialMode != "ecs_ram_role_assume_role" {
t.Fatalf("OSS.CredentialMode = %q, want ecs_ram_role_assume_role", cfg.OSS.CredentialMode)
}
if cfg.OSS.ECSRoleName != "env-source-role" {
t.Fatalf("OSS.ECSRoleName = %q, want env-source-role", cfg.OSS.ECSRoleName)
}
if cfg.OSS.AssumeRoleARN != "acs:ram::1234567890123456:role/env-target-role" {
t.Fatalf("OSS.AssumeRoleARN = %q, want env target role arn", cfg.OSS.AssumeRoleARN)
}
if cfg.OSS.AssumeRoleSessionName != "env-session" {
t.Fatalf("OSS.AssumeRoleSessionName = %q, want env-session", cfg.OSS.AssumeRoleSessionName)
}
if cfg.OSS.AssumeRoleExternalID != "env-external" {
t.Fatalf("OSS.AssumeRoleExternalID = %q, want env-external", cfg.OSS.AssumeRoleExternalID)
}
if cfg.OSS.AssumeRoleSTSEndpoint != "sts.cn-hongkong.aliyuncs.com" {
t.Fatalf("OSS.AssumeRoleSTSEndpoint = %q, want sts.cn-hongkong.aliyuncs.com", cfg.OSS.AssumeRoleSTSEndpoint)
}
if cfg.OSS.AssumeRoleSessionDuration != 45*time.Minute {
t.Fatalf("OSS.AssumeRoleSessionDuration = %s, want 45m", cfg.OSS.AssumeRoleSessionDuration)
}
if cfg.HTTP.Addr != ":8088" {
t.Fatalf("HTTP.Addr = %q, want :8088", cfg.HTTP.Addr)
}
Expand All @@ -117,6 +146,67 @@ jwt:
}
}

func TestLoadWith_LoadsOSSAssumeRoleFields(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")
if err := os.WriteFile(configPath, []byte(`
oss:
credential_mode: " ECS_RAM_ROLE_ASSUME_ROLE "
ecs_role_name: " source-role "
assume_role_arn: " acs:ram::1234567890123456:role/target-role "
assume_role_session_name: " cixing-test "
assume_role_external_id: " external-id "
assume_role_sts_endpoint: " sts.cn-hongkong.aliyuncs.com "
assume_role_session_duration: 30m
`), 0o644); err != nil {
t.Fatalf("WriteFile(config.yaml) error = %v", err)
}

cfg, err := LoadWith(Options{ConfigFile: configPath, SkipValidate: true})
if err != nil {
t.Fatalf("LoadWith() error = %v", err)
}

if cfg.OSS.CredentialMode != "ecs_ram_role_assume_role" {
t.Fatalf("OSS.CredentialMode = %q, want ecs_ram_role_assume_role", cfg.OSS.CredentialMode)
}
if cfg.OSS.ECSRoleName != "source-role" {
t.Fatalf("OSS.ECSRoleName = %q, want source-role", cfg.OSS.ECSRoleName)
}
if cfg.OSS.AssumeRoleARN != "acs:ram::1234567890123456:role/target-role" {
t.Fatalf("OSS.AssumeRoleARN = %q, want trimmed target role arn", cfg.OSS.AssumeRoleARN)
}
if cfg.OSS.AssumeRoleSessionName != "cixing-test" {
t.Fatalf("OSS.AssumeRoleSessionName = %q, want cixing-test", cfg.OSS.AssumeRoleSessionName)
}
if cfg.OSS.AssumeRoleExternalID != "external-id" {
t.Fatalf("OSS.AssumeRoleExternalID = %q, want external-id", cfg.OSS.AssumeRoleExternalID)
}
if cfg.OSS.AssumeRoleSTSEndpoint != "sts.cn-hongkong.aliyuncs.com" {
t.Fatalf("OSS.AssumeRoleSTSEndpoint = %q, want sts.cn-hongkong.aliyuncs.com", cfg.OSS.AssumeRoleSTSEndpoint)
}
if cfg.OSS.AssumeRoleSessionDuration != 30*time.Minute {
t.Fatalf("OSS.AssumeRoleSessionDuration = %s, want 30m", cfg.OSS.AssumeRoleSessionDuration)
}
}

func TestLoadWith_InvalidOSSAssumeRoleSessionDurationEnv(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")
if err := os.WriteFile(configPath, []byte(`app: {}`), 0o644); err != nil {
t.Fatalf("WriteFile(config.yaml) error = %v", err)
}
t.Setenv("OSS_ASSUME_ROLE_SESSION_DURATION", "not-a-duration")

_, err := LoadWith(Options{ConfigFile: configPath, SkipValidate: true})
if err == nil {
t.Fatalf("LoadWith() error = nil, want invalid duration error")
}
if got := err.Error(); !strings.Contains(got, "OSS_ASSUME_ROLE_SESSION_DURATION") {
t.Fatalf("LoadWith() error = %q, want OSS_ASSUME_ROLE_SESSION_DURATION", got)
}
}

func TestConfigValidate_OnlyChecksHardRequirements(t *testing.T) {
cfg := Default()
cfg.OSS.Bucket = "bucket"
Expand Down
41 changes: 29 additions & 12 deletions internal/platform/oss/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,8 @@ func NewClients(cfg appcfg.OSSConfig) (*Clients, error) {
if strings.TrimSpace(cfg.Region) == "" {
return nil, fmt.Errorf("oss: region is required")
}
switch credentialMode(cfg) {
case "static":
if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.AccessKeySecret) == "" {
return nil, fmt.Errorf("oss: access key id/secret are required")
}
case "ecs_ram_role":
if strings.TrimSpace(cfg.ECSRoleName) == "" {
return nil, fmt.Errorf("oss: ecs role name is required for ecs_ram_role")
}
default:
return nil, fmt.Errorf("oss: unsupported credential mode %q", cfg.CredentialMode)
if err := validateCredentialsConfig(cfg); err != nil {
return nil, err
}

// 创建公共 client,给外部/前端使用
Expand Down Expand Up @@ -67,6 +58,32 @@ func NewClients(cfg appcfg.OSSConfig) (*Clients, error) {
return &Clients{Internal: internalClient, Public: publicClient}, nil
}

func validateCredentialsConfig(cfg appcfg.OSSConfig) error {
switch credentialMode(cfg) {
case credentialModeStatic:
if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.AccessKeySecret) == "" {
return fmt.Errorf("oss: access key id/secret are required")
}
case credentialModeECSRAMRole:
if strings.TrimSpace(cfg.ECSRoleName) == "" {
return fmt.Errorf("oss: ecs role name is required for ecs_ram_role")
}
case credentialModeECSRAMRoleAssumeRole:
if strings.TrimSpace(cfg.ECSRoleName) == "" {
return fmt.Errorf("oss: ecs role name is required for ecs_ram_role_assume_role")
}
if strings.TrimSpace(cfg.AssumeRoleARN) == "" {
return fmt.Errorf("oss: assume role arn is required for ecs_ram_role_assume_role")
}
if _, err := assumeRoleSessionDurationSeconds(cfg.AssumeRoleSessionDuration); err != nil {
return err
}
default:
return fmt.Errorf("oss: unsupported credential mode %q", cfg.CredentialMode)
}
return nil
}

// 判断是否需要不同的 client
func needsDedicatedInternalClient(publicOpts, internalOpts clientOptions) bool {
if internalOpts.useInternalEndpoint {
Expand Down Expand Up @@ -117,7 +134,7 @@ func newClient(cfg appcfg.OSSConfig, opts clientOptions) (*alioss.Client, string
func credentialMode(cfg appcfg.OSSConfig) string {
mode := strings.ToLower(strings.TrimSpace(cfg.CredentialMode))
if mode == "" {
return "static"
return credentialModeStatic
}
return mode
}
Expand Down
Loading
Loading