Fix routing conflicts: move admin endpoints to reserved paths
This commit is contained in:
@@ -32,6 +32,6 @@ ENV APP_HOST=0.0.0.0 \
|
||||
FLASK_DEBUG=0
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:5000/healthz', timeout=2)"
|
||||
CMD python -c "import requests; requests.get('http://localhost:5000/myfsio/health', timeout=2)"
|
||||
|
||||
CMD ["./docker-entrypoint.sh"]
|
||||
|
||||
16
README.md
16
README.md
@@ -149,19 +149,13 @@ All endpoints require AWS Signature Version 4 authentication unless using presig
|
||||
| `POST` | `/<bucket>/<key>?uploadId=X` | Complete multipart upload |
|
||||
| `DELETE` | `/<bucket>/<key>?uploadId=X` | Abort multipart upload |
|
||||
|
||||
### Presigned URLs
|
||||
### Bucket Policies (S3-compatible)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/presign/<bucket>/<key>` | Generate presigned URL |
|
||||
|
||||
### Bucket Policies
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/bucket-policy/<bucket>` | Get bucket policy |
|
||||
| `PUT` | `/bucket-policy/<bucket>` | Set bucket policy |
|
||||
| `DELETE` | `/bucket-policy/<bucket>` | Delete bucket policy |
|
||||
| `GET` | `/<bucket>?policy` | Get bucket policy |
|
||||
| `PUT` | `/<bucket>?policy` | Set bucket policy |
|
||||
| `DELETE` | `/<bucket>?policy` | Delete bucket policy |
|
||||
|
||||
### Versioning
|
||||
|
||||
@@ -175,7 +169,7 @@ All endpoints require AWS Signature Version 4 authentication unless using presig
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/healthz` | Health check endpoint |
|
||||
| `GET` | `/myfsio/health` | Health check endpoint |
|
||||
|
||||
## IAM & Access Control
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ def create_app(
|
||||
return render_template("404.html"), 404
|
||||
return error
|
||||
|
||||
@app.get("/healthz")
|
||||
@app.get("/myfsio/health")
|
||||
def healthcheck() -> Dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -590,6 +590,7 @@ def _generate_presigned_url(
|
||||
bucket_name: str,
|
||||
object_key: str,
|
||||
expires_in: int,
|
||||
api_base_url: str | None = None,
|
||||
) -> str:
|
||||
region = current_app.config["AWS_REGION"]
|
||||
service = current_app.config["AWS_SERVICE"]
|
||||
@@ -610,7 +611,7 @@ def _generate_presigned_url(
|
||||
}
|
||||
canonical_query = _encode_query_params(query_params)
|
||||
|
||||
api_base = current_app.config.get("API_BASE_URL")
|
||||
api_base = api_base_url or current_app.config.get("API_BASE_URL")
|
||||
if api_base:
|
||||
parsed = urlparse(api_base)
|
||||
host = parsed.netloc
|
||||
@@ -940,6 +941,7 @@ def _maybe_handle_bucket_subresource(bucket_name: str) -> Response | None:
|
||||
"notification": _bucket_notification_handler,
|
||||
"logging": _bucket_logging_handler,
|
||||
"uploads": _bucket_uploads_handler,
|
||||
"policy": _bucket_policy_handler,
|
||||
}
|
||||
requested = [key for key in handlers if key in request.args]
|
||||
if not requested:
|
||||
@@ -2642,9 +2644,9 @@ def _list_parts(bucket_name: str, object_key: str) -> Response:
|
||||
return _xml_response(root)
|
||||
|
||||
|
||||
@s3_api_bp.route("/bucket-policy/<bucket_name>", methods=["GET", "PUT", "DELETE"])
|
||||
@limiter.limit("30 per minute")
|
||||
def bucket_policy_handler(bucket_name: str) -> Response:
|
||||
def _bucket_policy_handler(bucket_name: str) -> Response:
|
||||
if request.method not in {"GET", "PUT", "DELETE"}:
|
||||
return _method_not_allowed(["GET", "PUT", "DELETE"])
|
||||
principal, error = _require_principal()
|
||||
if error:
|
||||
return error
|
||||
@@ -2676,51 +2678,6 @@ def bucket_policy_handler(bucket_name: str) -> Response:
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
@s3_api_bp.post("/presign/<bucket_name>/<path:object_key>")
|
||||
@limiter.limit("45 per minute")
|
||||
def presign_object(bucket_name: str, object_key: str):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
method = str(payload.get("method", "GET")).upper()
|
||||
allowed_methods = {"GET", "PUT", "DELETE"}
|
||||
if method not in allowed_methods:
|
||||
return _error_response("InvalidRequest", "Method must be GET, PUT, or DELETE", 400)
|
||||
try:
|
||||
expires = int(payload.get("expires_in", 900))
|
||||
except (TypeError, ValueError):
|
||||
return _error_response("InvalidRequest", "expires_in must be an integer", 400)
|
||||
expires = max(1, min(expires, 7 * 24 * 3600))
|
||||
action = "read" if method == "GET" else ("delete" if method == "DELETE" else "write")
|
||||
principal, error = _require_principal()
|
||||
if error:
|
||||
return error
|
||||
try:
|
||||
_authorize_action(principal, bucket_name, action, object_key=object_key)
|
||||
except IamError as exc:
|
||||
return _error_response("AccessDenied", str(exc), 403)
|
||||
storage = _storage()
|
||||
if not storage.bucket_exists(bucket_name):
|
||||
return _error_response("NoSuchBucket", "Bucket does not exist", 404)
|
||||
if action != "write":
|
||||
try:
|
||||
storage.get_object_path(bucket_name, object_key)
|
||||
except StorageError:
|
||||
return _error_response("NoSuchKey", "Object not found", 404)
|
||||
secret = _iam().secret_for_key(principal.access_key)
|
||||
url = _generate_presigned_url(
|
||||
principal=principal,
|
||||
secret_key=secret,
|
||||
method=method,
|
||||
bucket_name=bucket_name,
|
||||
object_key=object_key,
|
||||
expires_in=expires,
|
||||
)
|
||||
current_app.logger.info(
|
||||
"Presigned URL generated",
|
||||
extra={"bucket": bucket_name, "key": object_key, "method": method},
|
||||
)
|
||||
return jsonify({"url": url, "method": method, "expires_in": expires})
|
||||
|
||||
|
||||
@s3_api_bp.route("/<bucket_name>", methods=["HEAD"])
|
||||
@limiter.limit("100 per minute")
|
||||
def head_bucket(bucket_name: str) -> Response:
|
||||
|
||||
62
app/ui.py
62
app/ui.py
@@ -36,6 +36,7 @@ from .extensions import limiter, csrf
|
||||
from .iam import IamError
|
||||
from .kms import KMSManager
|
||||
from .replication import ReplicationManager, ReplicationRule
|
||||
from .s3_api import _generate_presigned_url
|
||||
from .secret_store import EphemeralSecretStore
|
||||
from .storage import ObjectStorage, StorageError
|
||||
|
||||
@@ -1135,42 +1136,43 @@ def object_presign(bucket_name: str, object_key: str):
|
||||
principal = _current_principal()
|
||||
payload = request.get_json(silent=True) or {}
|
||||
method = str(payload.get("method", "GET")).upper()
|
||||
allowed_methods = {"GET", "PUT", "DELETE"}
|
||||
if method not in allowed_methods:
|
||||
return jsonify({"error": "Method must be GET, PUT, or DELETE"}), 400
|
||||
action = "read" if method == "GET" else ("delete" if method == "DELETE" else "write")
|
||||
try:
|
||||
_authorize_ui(principal, bucket_name, action, object_key=object_key)
|
||||
except IamError as exc:
|
||||
return jsonify({"error": str(exc)}), 403
|
||||
|
||||
try:
|
||||
expires = int(payload.get("expires_in", 900))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"error": "expires_in must be an integer"}), 400
|
||||
expires = max(1, min(expires, 7 * 24 * 3600))
|
||||
storage = _storage()
|
||||
if not storage.bucket_exists(bucket_name):
|
||||
return jsonify({"error": "Bucket does not exist"}), 404
|
||||
if action != "write":
|
||||
try:
|
||||
storage.get_object_path(bucket_name, object_key)
|
||||
except StorageError:
|
||||
return jsonify({"error": "Object not found"}), 404
|
||||
secret = _iam().secret_for_key(principal.access_key)
|
||||
api_base = current_app.config.get("API_BASE_URL") or "http://127.0.0.1:5000"
|
||||
api_base = api_base.rstrip("/")
|
||||
encoded_key = quote(object_key, safe="/")
|
||||
url = f"{api_base}/presign/{bucket_name}/{encoded_key}"
|
||||
|
||||
parsed_api = urlparse(api_base)
|
||||
headers = _api_headers()
|
||||
headers["X-Forwarded-Host"] = parsed_api.netloc or "127.0.0.1:5000"
|
||||
headers["X-Forwarded-Proto"] = parsed_api.scheme or "http"
|
||||
headers["X-Forwarded-For"] = request.remote_addr or "127.0.0.1"
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=5)
|
||||
except requests.RequestException as exc:
|
||||
return jsonify({"error": f"API unavailable: {exc}"}), 502
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
text = response.text or ""
|
||||
if text.strip().startswith("<"):
|
||||
import xml.etree.ElementTree as ET
|
||||
try:
|
||||
root = ET.fromstring(text)
|
||||
message = root.findtext(".//Message") or root.findtext(".//Code") or "Unknown S3 error"
|
||||
body = {"error": message}
|
||||
except ET.ParseError:
|
||||
body = {"error": text or "API returned an empty response"}
|
||||
else:
|
||||
body = {"error": text or "API returned an empty response"}
|
||||
return jsonify(body), response.status_code
|
||||
url = _generate_presigned_url(
|
||||
principal=principal,
|
||||
secret_key=secret,
|
||||
method=method,
|
||||
bucket_name=bucket_name,
|
||||
object_key=object_key,
|
||||
expires_in=expires,
|
||||
api_base_url=api_base,
|
||||
)
|
||||
current_app.logger.info(
|
||||
"Presigned URL generated",
|
||||
extra={"bucket": bucket_name, "key": object_key, "method": method},
|
||||
)
|
||||
return jsonify({"url": url, "method": method, "expires_in": expires})
|
||||
|
||||
|
||||
@ui_bp.get("/buckets/<bucket_name>/objects/<path:object_key>/metadata")
|
||||
|
||||
28
docs.md
28
docs.md
@@ -122,7 +122,7 @@ With these volumes attached you can rebuild/restart the container without losing
|
||||
|
||||
### Versioning
|
||||
|
||||
The repo now tracks a human-friendly release string inside `app/version.py` (see the `APP_VERSION` constant). Edit that value whenever you cut a release. The constant flows into Flask as `APP_VERSION` and is exposed via `GET /healthz`, so you can monitor deployments or surface it in UIs.
|
||||
The repo now tracks a human-friendly release string inside `app/version.py` (see the `APP_VERSION` constant). Edit that value whenever you cut a release. The constant flows into Flask as `APP_VERSION` and is exposed via `GET /myfsio/health`, so you can monitor deployments or surface it in UIs.
|
||||
|
||||
## 3. Configuration Reference
|
||||
|
||||
@@ -277,14 +277,14 @@ The application automatically trusts these headers to generate correct presigned
|
||||
### Version Checking
|
||||
|
||||
The application version is tracked in `app/version.py` and exposed via:
|
||||
- **Health endpoint:** `GET /healthz` returns JSON with `version` field
|
||||
- **Health endpoint:** `GET /myfsio/health` returns JSON with `version` field
|
||||
- **Metrics dashboard:** Navigate to `/ui/metrics` to see the running version in the System Status card
|
||||
|
||||
To check your current version:
|
||||
|
||||
```bash
|
||||
# API health endpoint
|
||||
curl http://localhost:5000/healthz
|
||||
curl http://localhost:5000/myfsio/health
|
||||
|
||||
# Or inspect version.py directly
|
||||
cat app/version.py | grep APP_VERSION
|
||||
@@ -377,7 +377,7 @@ docker run -d \
|
||||
myfsio:latest
|
||||
|
||||
# 5. Verify health
|
||||
curl http://localhost:5000/healthz
|
||||
curl http://localhost:5000/myfsio/health
|
||||
```
|
||||
|
||||
### Version Compatibility Checks
|
||||
@@ -502,7 +502,7 @@ docker run -d \
|
||||
myfsio:0.1.3 # specify previous version tag
|
||||
|
||||
# 3. Verify
|
||||
curl http://localhost:5000/healthz
|
||||
curl http://localhost:5000/myfsio/health
|
||||
```
|
||||
|
||||
#### Emergency Config Restore
|
||||
@@ -528,7 +528,7 @@ For production environments requiring zero downtime:
|
||||
APP_PORT=5001 UI_PORT=5101 python run.py &
|
||||
|
||||
# 2. Health check new instance
|
||||
curl http://localhost:5001/healthz
|
||||
curl http://localhost:5001/myfsio/health
|
||||
|
||||
# 3. Update load balancer to route to new ports
|
||||
|
||||
@@ -544,7 +544,7 @@ After any update, verify functionality:
|
||||
|
||||
```bash
|
||||
# 1. Health check
|
||||
curl http://localhost:5000/healthz
|
||||
curl http://localhost:5000/myfsio/health
|
||||
|
||||
# 2. Login to UI
|
||||
open http://localhost:5100/ui
|
||||
@@ -588,7 +588,7 @@ APP_PID=$!
|
||||
|
||||
# Wait and health check
|
||||
sleep 5
|
||||
if curl -f http://localhost:5000/healthz; then
|
||||
if curl -f http://localhost:5000/myfsio/health; then
|
||||
echo "Update successful!"
|
||||
else
|
||||
echo "Health check failed, rolling back..."
|
||||
@@ -860,7 +860,7 @@ A request is allowed only if:
|
||||
### Editing via CLI
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:5000/bucket-policy/test \
|
||||
curl -X PUT "http://127.0.0.1:5000/test?policy" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Access-Key: ..." -H "X-Secret-Key: ..." \
|
||||
-d '{
|
||||
@@ -923,9 +923,8 @@ Drag files directly onto the objects table to upload them to the current bucket
|
||||
## 6. Presigned URLs
|
||||
|
||||
- Trigger from the UI using the **Presign** button after selecting an object.
|
||||
- Or call `POST /presign/<bucket>/<key>` with JSON `{ "method": "GET", "expires_in": 900 }`.
|
||||
- Supported methods: `GET`, `PUT`, `DELETE`; expiration must be `1..604800` seconds.
|
||||
- The service signs requests using the caller’s IAM credentials and enforces bucket policies both when issuing and when the presigned URL is used.
|
||||
- The service signs requests using the caller's IAM credentials and enforces bucket policies both when issuing and when the presigned URL is used.
|
||||
- Legacy share links have been removed; presigned URLs now handle both private and public workflows.
|
||||
|
||||
### Multipart Upload Example
|
||||
@@ -1314,10 +1313,9 @@ GET /<bucket> # List objects
|
||||
PUT /<bucket>/<key> # Upload object
|
||||
GET /<bucket>/<key> # Download object
|
||||
DELETE /<bucket>/<key> # Delete object
|
||||
POST /presign/<bucket>/<key> # Generate SigV4 URL
|
||||
GET /bucket-policy/<bucket> # Fetch policy
|
||||
PUT /bucket-policy/<bucket> # Upsert policy
|
||||
DELETE /bucket-policy/<bucket> # Delete policy
|
||||
GET /<bucket>?policy # Fetch policy
|
||||
PUT /<bucket>?policy # Upsert policy
|
||||
DELETE /<bucket>?policy # Delete policy
|
||||
GET /<bucket>?quota # Get bucket quota
|
||||
PUT /<bucket>?quota # Set bucket quota (admin only)
|
||||
```
|
||||
|
||||
@@ -375,11 +375,8 @@ curl -X PUT {{ api_base }}/demo/notes.txt \
|
||||
-H "X-Secret-Key: <secret_key>" \
|
||||
--data-binary @notes.txt
|
||||
|
||||
curl -X POST {{ api_base }}/presign/demo/notes.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Access-Key: <access_key>" \
|
||||
-H "X-Secret-Key: <secret_key>" \
|
||||
-d '{"method":"GET", "expires_in": 900}'
|
||||
# Presigned URLs are generated via the UI
|
||||
# Use the "Presign" button in the object browser
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -437,13 +434,8 @@ curl -X POST {{ api_base }}/presign/demo/notes.txt \
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GET/PUT/DELETE</td>
|
||||
<td><code>/bucket-policy/<bucket></code></td>
|
||||
<td>Fetch, upsert, or remove a bucket policy.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/presign/<bucket>/<key></code></td>
|
||||
<td>Generate SigV4 URLs for GET/PUT/DELETE with custom expiry.</td>
|
||||
<td><code>/<bucket>?policy</code></td>
|
||||
<td>Fetch, upsert, or remove a bucket policy (S3-compatible).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -542,17 +534,16 @@ s3.complete_multipart_upload(
|
||||
)</code></pre>
|
||||
|
||||
<h3 class="h6 text-uppercase text-muted mt-4">Presigned URLs for Sharing</h3>
|
||||
<pre class="mb-0"><code class="language-bash"># Generate a download link valid for 15 minutes
|
||||
curl -X POST "{{ api_base }}/presign/mybucket/photo.jpg" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Access-Key: <key>" -H "X-Secret-Key: <secret>" \
|
||||
-d '{"method": "GET", "expires_in": 900}'
|
||||
<pre class="mb-0"><code class="language-text"># Generate presigned URLs via the UI:
|
||||
# 1. Navigate to your bucket in the object browser
|
||||
# 2. Select the object you want to share
|
||||
# 3. Click the "Presign" button
|
||||
# 4. Choose method (GET/PUT/DELETE) and expiration time
|
||||
# 5. Copy the generated URL
|
||||
|
||||
# Generate an upload link (PUT) valid for 1 hour
|
||||
curl -X POST "{{ api_base }}/presign/mybucket/upload.bin" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Access-Key: <key>" -H "X-Secret-Key: <secret>" \
|
||||
-d '{"method": "PUT", "expires_in": 3600}'</code></pre>
|
||||
# Supported options:
|
||||
# - Method: GET (download), PUT (upload), DELETE (remove)
|
||||
# - Expiration: 1 second to 7 days (604800 seconds)</code></pre>
|
||||
</div>
|
||||
</article>
|
||||
<article id="replication" class="card shadow-sm docs-section">
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
def test_bucket_and_object_lifecycle(client, signer):
|
||||
headers = signer("PUT", "/photos")
|
||||
response = client.put("/photos", headers=headers)
|
||||
@@ -105,7 +102,7 @@ def test_request_id_header_present(client, signer):
|
||||
|
||||
|
||||
def test_healthcheck_returns_status(client):
|
||||
response = client.get("/healthz")
|
||||
response = client.get("/myfsio/health")
|
||||
data = response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert data["status"] == "ok"
|
||||
@@ -117,36 +114,20 @@ def test_missing_credentials_denied(client):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_presign_and_bucket_policies(client, signer):
|
||||
# Create bucket and object
|
||||
def test_bucket_policies_deny_reads(client, signer):
|
||||
import json
|
||||
|
||||
headers = signer("PUT", "/docs")
|
||||
assert client.put("/docs", headers=headers).status_code == 200
|
||||
|
||||
|
||||
headers = signer("PUT", "/docs/readme.txt", body=b"content")
|
||||
assert client.put("/docs/readme.txt", headers=headers, data=b"content").status_code == 200
|
||||
|
||||
# Generate presigned GET URL and follow it
|
||||
json_body = {"method": "GET", "expires_in": 120}
|
||||
# Flask test client json parameter automatically sets Content-Type and serializes body
|
||||
# But for signing we need the body bytes.
|
||||
import json
|
||||
body_bytes = json.dumps(json_body).encode("utf-8")
|
||||
headers = signer("POST", "/presign/docs/readme.txt", headers={"Content-Type": "application/json"}, body=body_bytes)
|
||||
|
||||
response = client.post(
|
||||
"/presign/docs/readme.txt",
|
||||
headers=headers,
|
||||
json=json_body,
|
||||
)
|
||||
headers = signer("GET", "/docs/readme.txt")
|
||||
response = client.get("/docs/readme.txt", headers=headers)
|
||||
assert response.status_code == 200
|
||||
presigned_url = response.get_json()["url"]
|
||||
parts = urlsplit(presigned_url)
|
||||
presigned_path = f"{parts.path}?{parts.query}"
|
||||
download = client.get(presigned_path)
|
||||
assert download.status_code == 200
|
||||
assert download.data == b"content"
|
||||
assert response.data == b"content"
|
||||
|
||||
# Attach a deny policy for GETs
|
||||
policy = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
@@ -160,29 +141,26 @@ def test_presign_and_bucket_policies(client, signer):
|
||||
],
|
||||
}
|
||||
policy_bytes = json.dumps(policy).encode("utf-8")
|
||||
headers = signer("PUT", "/bucket-policy/docs", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/bucket-policy/docs", headers=headers, json=policy).status_code == 204
|
||||
|
||||
headers = signer("GET", "/bucket-policy/docs")
|
||||
fetched = client.get("/bucket-policy/docs", headers=headers)
|
||||
headers = signer("PUT", "/docs?policy", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/docs?policy", headers=headers, json=policy).status_code == 204
|
||||
|
||||
headers = signer("GET", "/docs?policy")
|
||||
fetched = client.get("/docs?policy", headers=headers)
|
||||
assert fetched.status_code == 200
|
||||
assert fetched.get_json()["Version"] == "2012-10-17"
|
||||
|
||||
# Reads are now denied by bucket policy
|
||||
headers = signer("GET", "/docs/readme.txt")
|
||||
denied = client.get("/docs/readme.txt", headers=headers)
|
||||
assert denied.status_code == 403
|
||||
|
||||
# Presign attempts are also denied
|
||||
json_body = {"method": "GET", "expires_in": 60}
|
||||
body_bytes = json.dumps(json_body).encode("utf-8")
|
||||
headers = signer("POST", "/presign/docs/readme.txt", headers={"Content-Type": "application/json"}, body=body_bytes)
|
||||
response = client.post(
|
||||
"/presign/docs/readme.txt",
|
||||
headers=headers,
|
||||
json=json_body,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
headers = signer("DELETE", "/docs?policy")
|
||||
assert client.delete("/docs?policy", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/docs/readme.txt")
|
||||
assert client.delete("/docs/readme.txt", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/docs")
|
||||
assert client.delete("/docs", headers=headers).status_code == 204
|
||||
|
||||
|
||||
def test_trailing_slash_returns_xml(client):
|
||||
@@ -193,9 +171,11 @@ def test_trailing_slash_returns_xml(client):
|
||||
|
||||
|
||||
def test_public_policy_allows_anonymous_list_and_read(client, signer):
|
||||
import json
|
||||
|
||||
headers = signer("PUT", "/public")
|
||||
assert client.put("/public", headers=headers).status_code == 200
|
||||
|
||||
|
||||
headers = signer("PUT", "/public/hello.txt", body=b"hi")
|
||||
assert client.put("/public/hello.txt", headers=headers, data=b"hi").status_code == 200
|
||||
|
||||
@@ -221,10 +201,9 @@ def test_public_policy_allows_anonymous_list_and_read(client, signer):
|
||||
},
|
||||
],
|
||||
}
|
||||
import json
|
||||
policy_bytes = json.dumps(policy).encode("utf-8")
|
||||
headers = signer("PUT", "/bucket-policy/public", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/bucket-policy/public", headers=headers, json=policy).status_code == 204
|
||||
headers = signer("PUT", "/public?policy", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/public?policy", headers=headers, json=policy).status_code == 204
|
||||
|
||||
list_response = client.get("/public")
|
||||
assert list_response.status_code == 200
|
||||
@@ -236,18 +215,20 @@ def test_public_policy_allows_anonymous_list_and_read(client, signer):
|
||||
|
||||
headers = signer("DELETE", "/public/hello.txt")
|
||||
assert client.delete("/public/hello.txt", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/bucket-policy/public")
|
||||
assert client.delete("/bucket-policy/public", headers=headers).status_code == 204
|
||||
|
||||
|
||||
headers = signer("DELETE", "/public?policy")
|
||||
assert client.delete("/public?policy", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/public")
|
||||
assert client.delete("/public", headers=headers).status_code == 204
|
||||
|
||||
|
||||
def test_principal_dict_with_object_get_only(client, signer):
|
||||
import json
|
||||
|
||||
headers = signer("PUT", "/mixed")
|
||||
assert client.put("/mixed", headers=headers).status_code == 200
|
||||
|
||||
|
||||
headers = signer("PUT", "/mixed/only.txt", body=b"ok")
|
||||
assert client.put("/mixed/only.txt", headers=headers, data=b"ok").status_code == 200
|
||||
|
||||
@@ -270,10 +251,9 @@ def test_principal_dict_with_object_get_only(client, signer):
|
||||
},
|
||||
],
|
||||
}
|
||||
import json
|
||||
policy_bytes = json.dumps(policy).encode("utf-8")
|
||||
headers = signer("PUT", "/bucket-policy/mixed", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/bucket-policy/mixed", headers=headers, json=policy).status_code == 204
|
||||
headers = signer("PUT", "/mixed?policy", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/mixed?policy", headers=headers, json=policy).status_code == 204
|
||||
|
||||
assert client.get("/mixed").status_code == 403
|
||||
allowed = client.get("/mixed/only.txt")
|
||||
@@ -282,18 +262,20 @@ def test_principal_dict_with_object_get_only(client, signer):
|
||||
|
||||
headers = signer("DELETE", "/mixed/only.txt")
|
||||
assert client.delete("/mixed/only.txt", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/bucket-policy/mixed")
|
||||
assert client.delete("/bucket-policy/mixed", headers=headers).status_code == 204
|
||||
|
||||
|
||||
headers = signer("DELETE", "/mixed?policy")
|
||||
assert client.delete("/mixed?policy", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/mixed")
|
||||
assert client.delete("/mixed", headers=headers).status_code == 204
|
||||
|
||||
|
||||
def test_bucket_policy_wildcard_resource_allows_object_get(client, signer):
|
||||
import json
|
||||
|
||||
headers = signer("PUT", "/test")
|
||||
assert client.put("/test", headers=headers).status_code == 200
|
||||
|
||||
|
||||
headers = signer("PUT", "/test/vid.mp4", body=b"video")
|
||||
assert client.put("/test/vid.mp4", headers=headers, data=b"video").status_code == 200
|
||||
|
||||
@@ -314,10 +296,9 @@ def test_bucket_policy_wildcard_resource_allows_object_get(client, signer):
|
||||
},
|
||||
],
|
||||
}
|
||||
import json
|
||||
policy_bytes = json.dumps(policy).encode("utf-8")
|
||||
headers = signer("PUT", "/bucket-policy/test", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/bucket-policy/test", headers=headers, json=policy).status_code == 204
|
||||
headers = signer("PUT", "/test?policy", headers={"Content-Type": "application/json"}, body=policy_bytes)
|
||||
assert client.put("/test?policy", headers=headers, json=policy).status_code == 204
|
||||
|
||||
listing = client.get("/test")
|
||||
assert listing.status_code == 403
|
||||
@@ -327,10 +308,10 @@ def test_bucket_policy_wildcard_resource_allows_object_get(client, signer):
|
||||
|
||||
headers = signer("DELETE", "/test/vid.mp4")
|
||||
assert client.delete("/test/vid.mp4", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/bucket-policy/test")
|
||||
assert client.delete("/bucket-policy/test", headers=headers).status_code == 204
|
||||
|
||||
|
||||
headers = signer("DELETE", "/test?policy")
|
||||
assert client.delete("/test?policy", headers=headers).status_code == 204
|
||||
|
||||
headers = signer("DELETE", "/test")
|
||||
assert client.delete("/test", headers=headers).status_code == 204
|
||||
|
||||
|
||||
Reference in New Issue
Block a user