diff --git a/auth/iam.go b/auth/iam.go index 11a40c9..0387ffb 100644 --- a/auth/iam.go +++ b/auth/iam.go @@ -76,6 +76,7 @@ var ( ) type Opts struct { + RootAccount Account Dir string LDAPServerURL string LDAPBindDN string @@ -114,20 +115,20 @@ func New(o *Opts) (IAMService, error) { switch { case o.Dir != "": - svc, err = NewInternal(o.Dir) + svc, err = NewInternal(o.RootAccount, o.Dir) fmt.Printf("initializing internal IAM with %q\n", o.Dir) case o.LDAPServerURL != "": - svc, err = NewLDAPService(o.LDAPServerURL, o.LDAPBindDN, o.LDAPPassword, + svc, err = NewLDAPService(o.RootAccount, o.LDAPServerURL, o.LDAPBindDN, o.LDAPPassword, o.LDAPQueryBase, o.LDAPAccessAtr, o.LDAPSecretAtr, o.LDAPRoleAtr, o.LDAPUserIdAtr, o.LDAPGroupIdAtr, o.LDAPObjClasses) fmt.Printf("initializing LDAP IAM with %q\n", o.LDAPServerURL) case o.S3Endpoint != "": - svc, err = NewS3(o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket, + svc, err = NewS3(o.RootAccount, o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket, o.S3Endpoint, o.S3DisableSSlVerfiy, o.S3Debug) fmt.Printf("initializing S3 IAM with '%v/%v'\n", o.S3Endpoint, o.S3Bucket) case o.VaultEndpointURL != "": - svc, err = NewVaultIAMService(o.VaultEndpointURL, o.VaultSecretStoragePath, + svc, err = NewVaultIAMService(o.RootAccount, o.VaultEndpointURL, o.VaultSecretStoragePath, o.VaultMountPath, o.VaultRootToken, o.VaultRoleId, o.VaultRoleSecret, o.VaultServerCert, o.VaultClientCert, o.VaultClientCertKey) fmt.Printf("initializing Vault IAM with %q\n", o.VaultEndpointURL) diff --git a/auth/iam_internal.go b/auth/iam_internal.go index facfd02..a42c5bd 100644 --- a/auth/iam_internal.go +++ b/auth/iam_internal.go @@ -40,7 +40,8 @@ type IAMServiceInternal struct { // IAM service. All account updates should be sent to a single // gateway instance if possible. sync.RWMutex - dir string + dir string + rootAcc Account } // UpdateAcctFunc accepts the current data and returns the new data to be stored @@ -54,9 +55,10 @@ type iAMConfig struct { var _ IAMService = &IAMServiceInternal{} // NewInternal creates a new instance for the Internal IAM service -func NewInternal(dir string) (*IAMServiceInternal, error) { +func NewInternal(rootAcc Account, dir string) (*IAMServiceInternal, error) { i := &IAMServiceInternal{ - dir: dir, + dir: dir, + rootAcc: rootAcc, } err := i.initIAM() @@ -70,6 +72,10 @@ func NewInternal(dir string) (*IAMServiceInternal, error) { // CreateAccount creates a new IAM account. Returns an error if the account // already exists. func (s *IAMServiceInternal) CreateAccount(account Account) error { + if account.Access == s.rootAcc.Access { + return ErrUserExists + } + s.Lock() defer s.Unlock() @@ -97,6 +103,10 @@ func (s *IAMServiceInternal) CreateAccount(account Account) error { // GetUserAccount retrieves account info for the requested user. Returns // ErrNoSuchUser if the account does not exist. func (s *IAMServiceInternal) GetUserAccount(access string) (Account, error) { + if access == s.rootAcc.Access { + return s.rootAcc, nil + } + s.RLock() defer s.RUnlock() diff --git a/auth/iam_ldap.go b/auth/iam_ldap.go index 962d9ab..91ddd64 100644 --- a/auth/iam_ldap.go +++ b/auth/iam_ldap.go @@ -31,11 +31,12 @@ type LdapIAMService struct { roleAtr string groupIdAtr string userIdAtr string + rootAcc Account } var _ IAMService = &LdapIAMService{} -func NewLDAPService(url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userIdAtr, groupIdAtr, objClasses string) (IAMService, error) { +func NewLDAPService(rootAcc Account, url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userIdAtr, groupIdAtr, objClasses string) (IAMService, error) { if url == "" || bindDN == "" || pass == "" || queryBase == "" || accAtr == "" || secAtr == "" || roleAtr == "" || userIdAtr == "" || groupIdAtr == "" || objClasses == "" { return nil, fmt.Errorf("required parameters list not fully provided") @@ -58,10 +59,14 @@ func NewLDAPService(url, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userI roleAtr: roleAtr, userIdAtr: userIdAtr, groupIdAtr: groupIdAtr, + rootAcc: rootAcc, }, nil } func (ld *LdapIAMService) CreateAccount(account Account) error { + if ld.rootAcc.Access == account.Access { + return ErrUserExists + } userEntry := ldap.NewAddRequest(fmt.Sprintf("%v=%v,%v", ld.accessAtr, account.Access, ld.queryBase), nil) userEntry.Attribute("objectClass", ld.objClasses) userEntry.Attribute(ld.accessAtr, []string{account.Access}) @@ -79,6 +84,9 @@ func (ld *LdapIAMService) CreateAccount(account Account) error { } func (ld *LdapIAMService) GetUserAccount(access string) (Account, error) { + if access == ld.rootAcc.Access { + return ld.rootAcc, nil + } searchRequest := ldap.NewSearchRequest( ld.queryBase, ldap.ScopeWholeSubtree, diff --git a/auth/iam_s3_object.go b/auth/iam_s3_object.go index d6eed43..2cee171 100644 --- a/auth/iam_s3_object.go +++ b/auth/iam_s3_object.go @@ -57,12 +57,13 @@ type IAMServiceS3 struct { endpoint string sslSkipVerify bool debug bool + rootAcc Account client *s3.Client } var _ IAMService = &IAMServiceS3{} -func NewS3(access, secret, region, bucket, endpoint string, sslSkipVerify, debug bool) (*IAMServiceS3, error) { +func NewS3(rootAcc Account, access, secret, region, bucket, endpoint string, sslSkipVerify, debug bool) (*IAMServiceS3, error) { if access == "" { return nil, fmt.Errorf("must provide s3 IAM service access key") } @@ -87,6 +88,7 @@ func NewS3(access, secret, region, bucket, endpoint string, sslSkipVerify, debug endpoint: endpoint, sslSkipVerify: sslSkipVerify, debug: debug, + rootAcc: rootAcc, } cfg, err := i.getConfig() @@ -106,6 +108,10 @@ func NewS3(access, secret, region, bucket, endpoint string, sslSkipVerify, debug } func (s *IAMServiceS3) CreateAccount(account Account) error { + if s.rootAcc.Access == account.Access { + return ErrUserExists + } + s.Lock() defer s.Unlock() @@ -124,6 +130,10 @@ func (s *IAMServiceS3) CreateAccount(account Account) error { } func (s *IAMServiceS3) GetUserAccount(access string) (Account, error) { + if access == s.rootAcc.Access { + return s.rootAcc, nil + } + s.RLock() defer s.RUnlock() @@ -242,7 +252,7 @@ func (s *IAMServiceS3) getAccounts() (iAMConfig, error) { }) if err != nil { // if the error is object not exists, - // init empty accounts stuct and return that + // init empty accounts struct and return that var nsk *types.NoSuchKey if errors.As(err, &nsk) { return iAMConfig{AccessAccounts: map[string]Account{}}, nil diff --git a/auth/iam_vault.go b/auth/iam_vault.go index b4f6fc2..44fabc3 100644 --- a/auth/iam_vault.go +++ b/auth/iam_vault.go @@ -30,11 +30,12 @@ type VaultIAMService struct { client *vault.Client reqOpts []vault.RequestOption secretStoragePath string + rootAcc Account } var _ IAMService = &VaultIAMService{} -func NewVaultIAMService(endpoint, secretStoragePath, mountPath, rootToken, roleID, roleSecret, serverCert, clientCert, clientCertKey string) (IAMService, error) { +func NewVaultIAMService(rootAcc Account, endpoint, secretStoragePath, mountPath, rootToken, roleID, roleSecret, serverCert, clientCert, clientCertKey string) (IAMService, error) { opts := []vault.ClientOption{ vault.WithAddress(endpoint), // set request timeout to 10 secs @@ -100,10 +101,14 @@ func NewVaultIAMService(endpoint, secretStoragePath, mountPath, rootToken, roleI client: client, reqOpts: reqOpts, secretStoragePath: secretStoragePath, + rootAcc: rootAcc, }, nil } func (vt *VaultIAMService) CreateAccount(account Account) error { + if vt.rootAcc.Access == account.Access { + return ErrUserExists + } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) _, err := vt.client.Secrets.KvV2Write(ctx, vt.secretStoragePath+"/"+account.Access, schema.KvV2WriteRequest{ Data: map[string]any{ @@ -125,6 +130,9 @@ func (vt *VaultIAMService) CreateAccount(account Account) error { } func (vt *VaultIAMService) GetUserAccount(access string) (Account, error) { + if vt.rootAcc.Access == access { + return vt.rootAcc, nil + } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) resp, err := vt.client.Secrets.KvV2Read(ctx, vt.secretStoragePath+"/"+access, vt.reqOpts...) cancel() diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index 2e735da..731bc79 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -579,6 +579,11 @@ func runGateway(ctx context.Context, be backend.Backend) error { } iam, err := auth.New(&auth.Opts{ + RootAccount: auth.Account{ + Access: rootUserAccess, + Secret: rootUserSecret, + Role: auth.RoleAdmin, + }, Dir: iamDir, LDAPServerURL: ldapURL, LDAPBindDN: ldapBindDN, diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 4321b7a..1be756d 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -415,6 +415,7 @@ func TestWORMProtection(s *S3Conf) { WORMProtection_object_lock_retention_governance_bypass_delete(s) WORMProtection_object_lock_retention_governance_bypass_delete_mul(s) WORMProtection_object_lock_legal_hold_locked(s) + WORMProtection_root_bypass_governance_retention_delete_object(s) } func TestFullFlow(s *S3Conf) { @@ -475,6 +476,7 @@ func TestIAM(s *S3Conf) { IAM_userplus_access_denied(s) IAM_userplus_CreateBucket(s) IAM_admin_ChangeBucketOwner(s) + IAM_ChangeBucketOwner_back_to_root(s) } func TestAccessControl(s *S3Conf) { @@ -766,6 +768,7 @@ func GetIntTests() IntTests { "WORMProtection_object_lock_retention_governance_bypass_delete": WORMProtection_object_lock_retention_governance_bypass_delete, "WORMProtection_object_lock_retention_governance_bypass_delete_mul": WORMProtection_object_lock_retention_governance_bypass_delete_mul, "WORMProtection_object_lock_legal_hold_locked": WORMProtection_object_lock_legal_hold_locked, + "WORMProtection_root_bypass_governance_retention_delete_object": WORMProtection_root_bypass_governance_retention_delete_object, "PutObject_overwrite_dir_obj": PutObject_overwrite_dir_obj, "PutObject_overwrite_file_obj": PutObject_overwrite_file_obj, "PutObject_dir_obj_with_data": PutObject_dir_obj_with_data, @@ -774,6 +777,7 @@ func GetIntTests() IntTests { "IAM_userplus_access_denied": IAM_userplus_access_denied, "IAM_userplus_CreateBucket": IAM_userplus_CreateBucket, "IAM_admin_ChangeBucketOwner": IAM_admin_ChangeBucketOwner, + "IAM_ChangeBucketOwner_back_to_root": IAM_ChangeBucketOwner_back_to_root, "AccessControl_default_ACL_user_access_denied": AccessControl_default_ACL_user_access_denied, "AccessControl_default_ACL_userplus_access_denied": AccessControl_default_ACL_userplus_access_denied, "AccessControl_default_ACL_admin_successful_access": AccessControl_default_ACL_admin_successful_access, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index a6626e5..ed74a6c 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -9037,6 +9037,65 @@ func WORMProtection_object_lock_legal_hold_locked(s *S3Conf) error { }, withLock()) } +func WORMProtection_root_bypass_governance_retention_delete_object(s *S3Conf) error { + testName := "WORMProtection_root_bypass_governance_retention_delete_object" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + if err := putObjects(s3client, []string{obj}, bucket); err != nil { + return err + } + + retDate := time.Now().Add(time.Hour * 48) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &retDate, + }, + }) + cancel() + if err != nil { + return err + } + + if err := checkWORMProtection(s3client, bucket, obj); err != nil { + return err + } + + policy := genPolicyDoc("Allow", fmt.Sprintf(`"%v"`, s.awsID), `["s3:BypassGovernanceRetention"]`, fmt.Sprintf(`"arn:aws:s3:::%v/*"`, bucket)) + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{ + Bucket: &bucket, + Policy: &policy, + }) + cancel() + if err != nil { + return err + } + + bypass := true + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + BypassGovernanceRetention: &bypass, + }) + cancel() + if err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock()) +} + // Access control tests (with bucket ACLs and Policies) func AccessControl_default_ACL_user_access_denied(s *S3Conf) error { testName := "AccessControl_default_ACL_user_access_denied" @@ -9558,6 +9617,33 @@ func IAM_admin_ChangeBucketOwner(s *S3Conf) error { }) } +func IAM_ChangeBucketOwner_back_to_root(s *S3Conf) error { + testName := "IAM_ChangeBucketOwner_back_to_root" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + usr := user{ + access: "grt1", + secret: "grt1secret", + role: "user", + } + + if err := createUsers(s, []user{usr}); err != nil { + return err + } + + // Change the bucket ownership to a random user + if err := changeBucketsOwner(s, []string{bucket}, usr.access); err != nil { + return err + } + + // Change the bucket ownership back to the root user + if err := changeBucketsOwner(s, []string{bucket}, s.awsID); err != nil { + return err + } + + return nil + }) +} + // Posix related tests func PutObject_overwrite_dir_obj(s *S3Conf) error { testName := "PutObject_overwrite_dir_obj"