#!/bin/bash # # Proof test for patches/v1.11.0/0003-owner-scoped-access.patch. # # sudo bash proof.sh /path/to/versitygw # # Starts a throwaway gateway from the given binary on 127.0.0.1:${PROOF_PORT:-7390}, # with its own internal IAM and posix root under ${PROOF_DIR:-/var/tmp/s3gw-owner-scope}, # drives every rule in the patch through real S3 requests, and removes the lot # again. Nothing installed is touched: no package, no unit, no config. Prints one # line per check and exits non-zero if any check fails. KEEP=1 leaves the gateway # directory (and its log) behind for a look. # # The accounts, all made by root through the admin API: # # po-owner uid 74990 userplus owns bucket "proof" # po-full uid 74990 user FULL_CONTROL grantee (an owner's key) # po-obj uid 74990 user in no ACL; one policy statement on one object # po-foreign uid 74991 user somebody else's key # # The UIDs are from leil-cluster's reserved test band (74990-74999); nothing # is chowned, so they only have to be distinct. set -uo pipefail VGW=${1:?usage: proof.sh /path/to/versitygw} HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) PORT=${PROOF_PORT:-7390} W=${PROOF_DIR:-/var/tmp/s3gw-owner-scope} export S3_ENDPOINT=http://127.0.0.1:$PORT S3_REGION=us-east-1 [ -x "$VGW" ] || { echo "FATAL: $VGW is not an executable" >&2; exit 2; } command -v python3 >/dev/null || { echo "FATAL: python3 is required" >&2; exit 2; } if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then echo "FATAL: something already listens on 127.0.0.1:$PORT (set PROOF_PORT)" >&2 exit 2 fi rm -rf "$W" && mkdir -p "$W/iam" "$W/data" "$W/tmp" || exit 2 secret() { python3 -c 'import secrets; print(secrets.token_hex(16))'; } declare -A KEY SECRET KEY[root]=proofroot for who in root po-owner po-full po-obj po-foreign; do [ "$who" = root ] || KEY[$who]=$who SECRET[$who]=$(secret) done "$VGW" --access "${KEY[root]}" --secret "${SECRET[root]}" --region "$S3_REGION" \ --port "127.0.0.1:$PORT" --iam-dir "$W/iam" posix "$W/data" >"$W/gateway.log" 2>&1 & GW=$! cleanup() { kill "$GW" 2>/dev/null wait "$GW" 2>/dev/null [ "${KEEP:-0}" = 1 ] && echo "kept $W" || rm -rf "$W" } trap cleanup EXIT for _ in $(seq 1 50); do (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null && break kill -0 "$GW" 2>/dev/null || { cat "$W/gateway.log"; echo "FATAL: the gateway exited" >&2; exit 2; } sleep 0.2 done echo "gateway $("$VGW" --version 2>&1 | head -1) on 127.0.0.1:$PORT, sha256 $(sha256sum "$VGW" | cut -c1-16)" admin() { ADMIN_ACCESS_KEY_ID=${KEY[root]} ADMIN_SECRET_ACCESS_KEY=${SECRET[root]} \ "$VGW" admin --region "$S3_REGION" --endpoint-url "$S3_ENDPOINT" "$@" } # req -- one request as ("anon" for none); prints the status req() { local who=$1; shift if [ "$who" = anon ]; then S3_KEY= S3_SECRET= python3 "$HERE/s3req.py" "$@" 2>"$W/tmp/err" else S3_KEY=${KEY[$who]} S3_SECRET=${SECRET[$who]} python3 "$HERE/s3req.py" "$@" 2>"$W/tmp/err" fi } PASS=0 FAIL=0 # check check() { local id=$1 want=$2 what=$3; shift 3 local got got=$(req "$@") if [ "$got" = "$want" ]; then PASS=$((PASS + 1)); printf 'PASS %-4s %s %s\n' "$id" "$got" "$what" else FAIL=$((FAIL + 1)); printf 'FAIL %-4s %s (want %s) %s [%s]\n' "$id" "${got:-none}" "$want" "$what" "$(cat "$W/tmp/err")" fi } # check_cmd check_cmd() { local id=$1 want=$2 what=$3; shift 3 local got=ok "$@" >"$W/tmp/err" 2>&1 || got=refused if [ "$got" = "$want" ]; then PASS=$((PASS + 1)); printf 'PASS %-4s %s %s\n' "$id" "$got" "$what" else FAIL=$((FAIL + 1)); printf 'FAIL %-4s %s (want %s) %s [%s]\n' "$id" "$got" "$want" "$what" "$(tr '\n' ' ' <"$W/tmp/err")" fi } doc() { printf '%s' "$2" >"$W/tmp/$1"; echo "$W/tmp/$1"; } policy() { doc policy.json "{\"Version\":\"2012-10-17\",\"Statement\":[$1]}"; } acl_body() { # acl_body doc acl.xml "po-owner$1$3" } BUCKET_RES='["arn:aws:s3:::proof","arn:aws:s3:::proof/*"]' S3NS='xmlns="http://s3.amazonaws.com/doc/2006-03-01/"' echo "--- setup (root, admin API)" check_cmd S1 ok "create-user po-owner" admin create-user --access po-owner --secret "${SECRET[po-owner]}" --role userplus --user-id 74990 --group-id 74990 check_cmd S2 ok "create-user po-full" admin create-user --access po-full --secret "${SECRET[po-full]}" --role user --user-id 74990 --group-id 74990 check_cmd S3 ok "create-user po-obj" admin create-user --access po-obj --secret "${SECRET[po-obj]}" --role user --user-id 74990 --group-id 74990 check_cmd S4 ok "create-user po-foreign" admin create-user --access po-foreign --secret "${SECRET[po-foreign]}" --role user --user-id 74991 --group-id 74991 check_cmd S5 ok "create-bucket proof --owner po-owner --grant-full-control po-full" \ admin create-bucket --owner po-owner --bucket proof --object-ownership BucketOwnerPreferred --grant-full-control po-full printf 'hello\n' >"$W/tmp/hello.txt" echo "--- a foreign key is refused as a grantee" check_cmd A1 refused "admin create-bucket --grant-full-control po-foreign (root)" \ admin create-bucket --owner po-owner --bucket proof-foreign --object-ownership BucketOwnerPreferred --grant-full-control po-foreign check A2 404 " ...and no bucket proof-foreign was made" root HEAD /proof-foreign check A3 403 "PutBucketAcl x-amz-grant-read: po-foreign, as the owner" po-owner PUT /proof acl -H "x-amz-grant-read: po-foreign" check A4 403 "PutBucketAcl x-amz-grant-read: po-foreign, as root" root PUT /proof acl -H "x-amz-grant-read: po-foreign" check A5 403 "PutBucketAcl body naming po-foreign, as root" root PUT /proof acl --body "$(acl_body po-foreign CanonicalUser FULL_CONTROL)" check A6 200 "PutBucketAcl x-amz-grant-full-control: po-full (owner's key)" po-owner PUT /proof acl -H "x-amz-grant-full-control: po-full" echo "--- a foreign key is refused as a principal" P=$(policy "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"po-foreign\"},\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::proof/*\"}") check B1 403 "PutBucketPolicy naming po-foreign, as the owner" po-owner PUT /proof policy --body "$P" check B2 403 "PutBucketPolicy naming po-foreign, as root" root PUT /proof policy --body "$P" echo "--- public access is read-only" P=$(policy "{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"s3:PutObject\",\"Resource\":\"arn:aws:s3:::proof/*\"}") check C1 403 "PutBucketPolicy allowing \"*\" s3:PutObject" po-owner PUT /proof policy --body "$P" check C2 403 "PutBucketAcl x-amz-acl: public-read-write" po-owner PUT /proof acl -H "x-amz-acl: public-read-write" check C3 403 "PutBucketAcl body granting all-users WRITE" po-owner PUT /proof acl --body "$(acl_body all-users Group WRITE)" check C4 200 "owner PutObject public/hello.txt" po-owner PUT /proof/public/hello.txt --body "$W/tmp/hello.txt" P=$(policy "{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":[\"s3:GetObject\",\"s3:ListBucket\"],\"Resource\":$BUCKET_RES}") check C5 204 "PutBucketPolicy allowing \"*\" s3:GetObject + s3:ListBucket" po-owner PUT /proof policy --body "$P" check C6 200 "anonymous GetObject public/hello.txt" anon GET /proof/public/hello.txt check C7 200 "anonymous ListBucket" anon GET /proof list-type=2 check C8 403 "anonymous PutObject" anon PUT /proof/public/anon.txt --body "$W/tmp/hello.txt" echo "--- the owner key keeps its bucket under a public-read-only policy" check D1 200 "owner PutObject" po-owner PUT /proof/owner/a.txt --body "$W/tmp/hello.txt" check D2 200 "owner GetObject" po-owner GET /proof/owner/a.txt check D3 204 "owner DeleteObject" po-owner DELETE /proof/owner/a.txt check D4 200 "owner GetBucketAcl" po-owner GET /proof acl echo "--- ...and under a policy that denies it" P=$(policy "{\"Effect\":\"Deny\",\"Principal\":{\"AWS\":\"po-owner\"},\"Action\":\"s3:*\",\"Resource\":$BUCKET_RES},{\"Effect\":\"Deny\",\"Principal\":\"*\",\"Action\":\"s3:PutObject\",\"Resource\":\"arn:aws:s3:::proof/*\"}") check E1 204 "PutBucketPolicy denying po-owner s3:* and \"*\" s3:PutObject" po-owner PUT /proof policy --body "$P" check E2 200 "owner PutObject" po-owner PUT /proof/owner/b.txt --body "$W/tmp/hello.txt" check E3 200 "owner GetObject" po-owner GET /proof/owner/b.txt check E4 200 "owner ListBucket" po-owner GET /proof list-type=2 check E5 204 "owner DeleteObject" po-owner DELETE /proof/owner/b.txt check E6 403 "FULL_CONTROL grantee PutObject: the Deny beats its ACL grant" po-full PUT /proof/full/b.txt --body "$W/tmp/hello.txt" check E7 200 "FULL_CONTROL grantee GetObject: the ACL still grants it" po-full GET /proof/public/hello.txt check E8 204 "owner DeleteBucketPolicy" po-owner DELETE /proof policy echo "--- a key for one object" P=$(policy "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"po-obj\"},\"Action\":[\"s3:GetObject\",\"s3:PutObject\",\"s3:DeleteObject\"],\"Resource\":\"arn:aws:s3:::proof/shared/report.txt\"}") check F1 204 "PutBucketPolicy for po-obj on shared/report.txt" po-owner PUT /proof policy --body "$P" check F2 200 "po-obj PutObject shared/report.txt" po-obj PUT /proof/shared/report.txt --body "$W/tmp/hello.txt" check F3 200 "po-obj GetObject shared/report.txt" po-obj GET /proof/shared/report.txt check F4 403 "po-obj GetObject public/hello.txt" po-obj GET /proof/public/hello.txt check F5 403 "po-obj PutObject shared/other.txt" po-obj PUT /proof/shared/other.txt --body "$W/tmp/hello.txt" check F6 403 "po-obj DeleteObject public/hello.txt" po-obj DELETE /proof/public/hello.txt check F7 403 "po-obj ListBucket" po-obj GET /proof list-type=2 check F8 403 "po-obj GetBucketAcl" po-obj GET /proof acl check F9 403 "po-obj PutBucketAcl" po-obj PUT /proof acl -H "x-amz-grant-read: po-obj" check F10 403 "po-obj GetBucketPolicy" po-obj GET /proof policy check F11 403 "po-obj PutBucketPolicy" po-obj PUT /proof policy --body "$P" check F12 204 "po-obj DeleteObject shared/report.txt" po-obj DELETE /proof/shared/report.txt echo "--- bucket settings are root's and admins' alone" OWN=$(doc own.xml "BucketOwnerPreferred") VER=$(doc ver.xml "Enabled") LCK=$(doc lck.xml "Enabled") # A FULL_CONTROL grant alone never reached these four; a policy did, so grant them outright. P=$(policy "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"po-full\"},\"Action\":[\"s3:PutBucketOwnershipControls\",\"s3:PutBucketVersioning\",\"s3:PutBucketObjectLockConfiguration\",\"s3:DeleteBucket\"],\"Resource\":\"arn:aws:s3:::proof\"}") check G0 204 "PutBucketPolicy granting po-full all four" po-owner PUT /proof policy --body "$P" check G1 403 "FULL_CONTROL grantee PutBucketOwnershipControls" po-full PUT /proof ownershipControls --body "$OWN" check G2 403 "FULL_CONTROL grantee DeleteBucketOwnershipControls" po-full DELETE /proof ownershipControls check G3 403 "FULL_CONTROL grantee PutBucketVersioning" po-full PUT /proof versioning --body "$VER" check G4 403 "FULL_CONTROL grantee PutObjectLockConfiguration" po-full PUT /proof object-lock --body "$LCK" check G5 403 "FULL_CONTROL grantee DeleteBucket" po-full DELETE /proof check G6 403 "owner PutBucketVersioning" po-owner PUT /proof versioning --body "$VER" check G7 403 "owner PutObjectLockConfiguration" po-owner PUT /proof object-lock --body "$LCK" check G8 403 "owner DeleteBucket" po-owner DELETE /proof check G9 204 "owner DeleteBucketPolicy" po-owner DELETE /proof policy check G10 200 "FULL_CONTROL grantee PutObject (its grant still works)" po-full PUT /proof/full/c.txt --body "$W/tmp/hello.txt" check G11 200 "root PutBucketOwnershipControls" root PUT /proof ownershipControls --body "$OWN" echo "--- an ACL with the public read grant writes back unchanged" check H1 200 "PutBucketAcl x-amz-acl: public-read" po-owner PUT /proof acl -H "x-amz-acl: public-read" check H2 200 "GetBucketAcl" po-owner GET /proof acl -o "$W/tmp/acl-read.xml" check H3 200 "PutBucketAcl with that body, unchanged" po-owner PUT /proof acl --body "$W/tmp/acl-read.xml" check H4 200 "GetBucketAcl again" po-owner GET /proof acl -o "$W/tmp/acl-again.xml" check_cmd H5 ok " ...and it still grants all-users READ" \ grep -q 'all-users.*READ' "$W/tmp/acl-again.xml" echo "--- cleanup (root)" for k in public/hello.txt full/c.txt; do req root DELETE "/proof/$k" >/dev/null; done check Z1 204 "root DeleteBucket" root DELETE /proof echo echo "$PASS passed, $FAIL failed" [ "$FAIL" = 0 ]