Browse Source
fix S3 per-user-directory Policy (#6443)
fix S3 per-user-directory Policy (#6443)
* fix S3 per-user-directory Policy * Delete docker/config.json * add tests * remove logs * undo modifications of weed/shell/command_volume_balance.go * remove modifications of docker-compose * fix failing test --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>pull/6224/merge
Tom Crasset
4 days ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 404 additions and 18 deletions
-
21docker/compose/local-dev-compose.yml
-
274docker/test.py
-
5weed/command/iam.go
-
17weed/iamapi/iamapi_management_handlers.go
-
71weed/iamapi/iamapi_management_handlers_test.go
-
21weed/s3api/auth_credentials.go
-
13weed/s3api/s3_constants/header.go
@ -0,0 +1,274 @@ |
|||
#!/usr/bin/env python3 |
|||
# /// script |
|||
# requires-python = ">=3.12" |
|||
# dependencies = [ |
|||
# "boto3", |
|||
# ] |
|||
# /// |
|||
|
|||
import argparse |
|||
import json |
|||
import random |
|||
import string |
|||
import subprocess |
|||
from enum import Enum |
|||
from pathlib import Path |
|||
|
|||
import boto3 |
|||
|
|||
REGION_NAME = "us-east-1" |
|||
|
|||
|
|||
class Actions(str, Enum): |
|||
Get = "Get" |
|||
Put = "Put" |
|||
List = "List" |
|||
|
|||
|
|||
def get_user_dir(bucket_name, user, with_bucket=True): |
|||
if with_bucket: |
|||
return f"{bucket_name}/user-id-{user}" |
|||
|
|||
return f"user-id-{user}" |
|||
|
|||
|
|||
def create_power_user(): |
|||
power_user_key = "power_user_key" |
|||
power_user_secret = "power_user_secret" |
|||
command = f"s3.configure -apply -user poweruser -access_key {power_user_key} -secret_key {power_user_secret} -actions Admin" |
|||
print("Creating Power User...") |
|||
subprocess.run( |
|||
["docker", "exec", "-i", "seaweedfs-master-1", "weed", "shell"], |
|||
input=command, |
|||
text=True, |
|||
stdout=subprocess.PIPE, |
|||
) |
|||
print( |
|||
f"Power User created with key: {power_user_key} and secret: {power_user_secret}" |
|||
) |
|||
return power_user_key, power_user_secret |
|||
|
|||
|
|||
def create_bucket(s3_client, bucket_name): |
|||
print(f"Creating Bucket {bucket_name}...") |
|||
s3_client.create_bucket(Bucket=bucket_name) |
|||
print(f"Bucket {bucket_name} created.") |
|||
|
|||
|
|||
def upload_file(s3_client, bucket_name, user, file_path, custom_remote_path=None): |
|||
user_dir = get_user_dir(bucket_name, user, with_bucket=False) |
|||
if custom_remote_path: |
|||
remote_path = custom_remote_path |
|||
else: |
|||
remote_path = f"{user_dir}/{str(Path(file_path).name)}" |
|||
|
|||
print(f"Uploading {file_path} for {user}... on {user_dir}") |
|||
|
|||
s3_client.upload_file(file_path, bucket_name, remote_path) |
|||
print(f"File {file_path} uploaded for {user}.") |
|||
|
|||
|
|||
def create_user(iam_client, user): |
|||
print(f"Creating user {user}...") |
|||
response = iam_client.create_access_key(UserName=user) |
|||
print( |
|||
f"User {user} created with access key: {response['AccessKey']['AccessKeyId']}" |
|||
) |
|||
return response |
|||
|
|||
|
|||
def list_files(s3_client, bucket_name, path=None): |
|||
if path is None: |
|||
path = "" |
|||
print(f"Listing files of s3://{bucket_name}/{path}...") |
|||
try: |
|||
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=path) |
|||
if "Contents" in response: |
|||
for obj in response["Contents"]: |
|||
print(f"\t - {obj['Key']}") |
|||
else: |
|||
print("No files found.") |
|||
except Exception as e: |
|||
print(f"Error listing files: {e}") |
|||
|
|||
|
|||
def create_policy_for_user( |
|||
iam_client, user, bucket_name, actions=[Actions.Get, Actions.List] |
|||
): |
|||
print(f"Creating policy for {user} on {bucket_name}...") |
|||
policy_document = { |
|||
"Version": "2012-10-17", |
|||
"Statement": [ |
|||
{ |
|||
"Effect": "Allow", |
|||
"Action": [f"s3:{action.value}*" for action in actions], |
|||
"Resource": [ |
|||
f"arn:aws:s3:::{get_user_dir(bucket_name, user)}/*", |
|||
], |
|||
} |
|||
], |
|||
} |
|||
policy_name = f"{user}-{bucket_name}-full-access" |
|||
|
|||
policy_json = json.dumps(policy_document) |
|||
filepath = f"/tmp/{policy_name}.json" |
|||
with open(filepath, "w") as f: |
|||
f.write(json.dumps(policy_document, indent=2)) |
|||
|
|||
iam_client.put_user_policy( |
|||
PolicyName=policy_name, PolicyDocument=policy_json, UserName=user |
|||
) |
|||
print(f"Policy for {user} on {bucket_name} created.") |
|||
|
|||
|
|||
def main(): |
|||
parser = argparse.ArgumentParser(description="SeaweedFS S3 Test Script") |
|||
parser.add_argument( |
|||
"--s3-url", default="http://127.0.0.1:8333", help="S3 endpoint URL" |
|||
) |
|||
parser.add_argument( |
|||
"--iam-url", default="http://127.0.0.1:8111", help="IAM endpoint URL" |
|||
) |
|||
args = parser.parse_args() |
|||
|
|||
bucket_name = ( |
|||
f"test-bucket-{''.join(random.choices(string.digits + 'abcdef', k=8))}" |
|||
) |
|||
sentinel_file = "/tmp/SENTINEL" |
|||
with open(sentinel_file, "w") as f: |
|||
f.write("Hello World") |
|||
print(f"SENTINEL file created at {sentinel_file}") |
|||
|
|||
power_user_key, power_user_secret = create_power_user() |
|||
|
|||
admin_s3_client = get_s3_client(args, power_user_key, power_user_secret) |
|||
iam_client = get_iam_client(args, power_user_key, power_user_secret) |
|||
|
|||
create_bucket(admin_s3_client, bucket_name) |
|||
upload_file(admin_s3_client, bucket_name, "Alice", sentinel_file) |
|||
upload_file(admin_s3_client, bucket_name, "Bob", sentinel_file) |
|||
list_files(admin_s3_client, bucket_name) |
|||
|
|||
alice_user_info = create_user(iam_client, "Alice") |
|||
bob_user_info = create_user(iam_client, "Bob") |
|||
|
|||
alice_key = alice_user_info["AccessKey"]["AccessKeyId"] |
|||
alice_secret = alice_user_info["AccessKey"]["SecretAccessKey"] |
|||
bob_key = bob_user_info["AccessKey"]["AccessKeyId"] |
|||
bob_secret = bob_user_info["AccessKey"]["SecretAccessKey"] |
|||
|
|||
# Make sure Admin can read any files |
|||
list_files(admin_s3_client, bucket_name) |
|||
list_files( |
|||
admin_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Alice", with_bucket=False), |
|||
) |
|||
list_files( |
|||
admin_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Bob", with_bucket=False), |
|||
) |
|||
|
|||
# Create read policy for Alice and Bob |
|||
create_policy_for_user(iam_client, "Alice", bucket_name) |
|||
create_policy_for_user(iam_client, "Bob", bucket_name) |
|||
|
|||
alice_s3_client = get_s3_client(args, alice_key, alice_secret) |
|||
|
|||
# Make sure Alice can read her files |
|||
list_files( |
|||
alice_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Alice", with_bucket=False) + "/", |
|||
) |
|||
|
|||
# Make sure Bob can read his files |
|||
bob_s3_client = get_s3_client(args, bob_key, bob_secret) |
|||
list_files( |
|||
bob_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Bob", with_bucket=False) + "/", |
|||
) |
|||
|
|||
# Update policy to include write |
|||
create_policy_for_user(iam_client, "Alice", bucket_name, actions=[Actions.Put, Actions.Get, Actions.List]) # fmt: off |
|||
create_policy_for_user(iam_client, "Bob", bucket_name, actions=[Actions.Put, Actions.Get, Actions.List]) # fmt: off |
|||
|
|||
print("############################# Make sure Alice can write her files") |
|||
upload_file( |
|||
alice_s3_client, |
|||
bucket_name, |
|||
"Alice", |
|||
sentinel_file, |
|||
custom_remote_path=f"{get_user_dir(bucket_name, 'Alice', with_bucket=False)}/SENTINEL_by_Alice", |
|||
) |
|||
|
|||
|
|||
print("############################# Make sure Bob can write his files") |
|||
upload_file( |
|||
bob_s3_client, |
|||
bucket_name, |
|||
"Bob", |
|||
sentinel_file, |
|||
custom_remote_path=f"{get_user_dir(bucket_name, 'Bob', with_bucket=False)}/SENTINEL_by_Bob", |
|||
) |
|||
|
|||
|
|||
print("############################# Make sure Alice can read her new files") |
|||
list_files( |
|||
alice_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Alice", with_bucket=False) + "/", |
|||
) |
|||
|
|||
|
|||
print("############################# Make sure Bob can read his new files") |
|||
list_files( |
|||
bob_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Bob", with_bucket=False) + "/", |
|||
) |
|||
|
|||
|
|||
print("############################# Make sure Bob cannot read Alice's files") |
|||
list_files( |
|||
bob_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Alice", with_bucket=False) + "/", |
|||
) |
|||
|
|||
print("############################# Make sure Alice cannot read Bob's files") |
|||
|
|||
list_files( |
|||
alice_s3_client, |
|||
bucket_name, |
|||
get_user_dir(bucket_name, "Bob", with_bucket=False) + "/", |
|||
) |
|||
|
|||
|
|||
|
|||
def get_iam_client(args, access_key, secret_key): |
|||
iam_client = boto3.client( |
|||
"iam", |
|||
endpoint_url=args.iam_url, |
|||
region_name=REGION_NAME, |
|||
aws_access_key_id=access_key, |
|||
aws_secret_access_key=secret_key, |
|||
) |
|||
return iam_client |
|||
|
|||
|
|||
def get_s3_client(args, access_key, secret_key): |
|||
s3_client = boto3.client( |
|||
"s3", |
|||
endpoint_url=args.s3_url, |
|||
region_name=REGION_NAME, |
|||
aws_access_key_id=access_key, |
|||
aws_secret_access_key=secret_key, |
|||
) |
|||
return s3_client |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
@ -0,0 +1,71 @@ |
|||
package iamapi |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestGetActionsUserPath(t *testing.T) { |
|||
|
|||
policyDocument := PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []*Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Action: []string{ |
|||
"s3:Put*", |
|||
"s3:PutBucketAcl", |
|||
"s3:Get*", |
|||
"s3:GetBucketAcl", |
|||
"s3:List*", |
|||
"s3:Tagging*", |
|||
"s3:DeleteBucket*", |
|||
}, |
|||
Resource: []string{ |
|||
"arn:aws:s3:::shared/user-Alice/*", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
actions, _ := GetActions(&policyDocument) |
|||
|
|||
expectedActions := []string{ |
|||
"Write:shared/user-Alice/*", |
|||
"WriteAcp:shared/user-Alice/*", |
|||
"Read:shared/user-Alice/*", |
|||
"ReadAcp:shared/user-Alice/*", |
|||
"List:shared/user-Alice/*", |
|||
"Tagging:shared/user-Alice/*", |
|||
"DeleteBucket:shared/user-Alice/*", |
|||
} |
|||
assert.Equal(t, expectedActions, actions) |
|||
} |
|||
|
|||
func TestGetActionsWildcardPath(t *testing.T) { |
|||
|
|||
policyDocument := PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []*Statement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Action: []string{ |
|||
"s3:Get*", |
|||
"s3:PutBucketAcl", |
|||
}, |
|||
Resource: []string{ |
|||
"arn:aws:s3:::*", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
actions, _ := GetActions(&policyDocument) |
|||
|
|||
expectedActions := []string{ |
|||
"Read", |
|||
"WriteAcp", |
|||
} |
|||
assert.Equal(t, expectedActions, actions) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue