Release v0.1.3 #4
@@ -11,17 +11,51 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|||||||
RESOURCE_PREFIX = "arn:aws:s3:::"
|
RESOURCE_PREFIX = "arn:aws:s3:::"
|
||||||
|
|
||||||
ACTION_ALIASES = {
|
ACTION_ALIASES = {
|
||||||
"s3:getobject": "read",
|
# List actions
|
||||||
"s3:getobjectversion": "read",
|
|
||||||
"s3:listbucket": "list",
|
"s3:listbucket": "list",
|
||||||
"s3:listallmybuckets": "list",
|
"s3:listallmybuckets": "list",
|
||||||
|
"s3:listbucketversions": "list",
|
||||||
|
"s3:listmultipartuploads": "list",
|
||||||
|
"s3:listparts": "list",
|
||||||
|
# Read actions
|
||||||
|
"s3:getobject": "read",
|
||||||
|
"s3:getobjectversion": "read",
|
||||||
|
"s3:getobjecttagging": "read",
|
||||||
|
"s3:getobjectversiontagging": "read",
|
||||||
|
"s3:getobjectacl": "read",
|
||||||
|
"s3:getbucketversioning": "read",
|
||||||
|
"s3:headobject": "read",
|
||||||
|
"s3:headbucket": "read",
|
||||||
|
# Write actions
|
||||||
"s3:putobject": "write",
|
"s3:putobject": "write",
|
||||||
"s3:createbucket": "write",
|
"s3:createbucket": "write",
|
||||||
|
"s3:putobjecttagging": "write",
|
||||||
|
"s3:putbucketversioning": "write",
|
||||||
|
"s3:createmultipartupload": "write",
|
||||||
|
"s3:uploadpart": "write",
|
||||||
|
"s3:completemultipartupload": "write",
|
||||||
|
"s3:abortmultipartupload": "write",
|
||||||
|
"s3:copyobject": "write",
|
||||||
|
# Delete actions
|
||||||
"s3:deleteobject": "delete",
|
"s3:deleteobject": "delete",
|
||||||
"s3:deleteobjectversion": "delete",
|
"s3:deleteobjectversion": "delete",
|
||||||
"s3:deletebucket": "delete",
|
"s3:deletebucket": "delete",
|
||||||
|
"s3:deleteobjecttagging": "delete",
|
||||||
|
# Share actions (ACL)
|
||||||
"s3:putobjectacl": "share",
|
"s3:putobjectacl": "share",
|
||||||
|
"s3:putbucketacl": "share",
|
||||||
|
"s3:getbucketacl": "share",
|
||||||
|
# Policy actions
|
||||||
"s3:putbucketpolicy": "policy",
|
"s3:putbucketpolicy": "policy",
|
||||||
|
"s3:getbucketpolicy": "policy",
|
||||||
|
"s3:deletebucketpolicy": "policy",
|
||||||
|
# Replication actions
|
||||||
|
"s3:getreplicationconfiguration": "replication",
|
||||||
|
"s3:putreplicationconfiguration": "replication",
|
||||||
|
"s3:deletereplicationconfiguration": "replication",
|
||||||
|
"s3:replicateobject": "replication",
|
||||||
|
"s3:replicatetags": "replication",
|
||||||
|
"s3:replicatedelete": "replication",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
39
app/iam.py
39
app/iam.py
@@ -15,7 +15,7 @@ class IamError(RuntimeError):
|
|||||||
"""Raised when authentication or authorization fails."""
|
"""Raised when authentication or authorization fails."""
|
||||||
|
|
||||||
|
|
||||||
S3_ACTIONS = {"list", "read", "write", "delete", "share", "policy"}
|
S3_ACTIONS = {"list", "read", "write", "delete", "share", "policy", "replication"}
|
||||||
IAM_ACTIONS = {
|
IAM_ACTIONS = {
|
||||||
"iam:list_users",
|
"iam:list_users",
|
||||||
"iam:create_user",
|
"iam:create_user",
|
||||||
@@ -26,22 +26,59 @@ IAM_ACTIONS = {
|
|||||||
ALLOWED_ACTIONS = (S3_ACTIONS | IAM_ACTIONS) | {"iam:*"}
|
ALLOWED_ACTIONS = (S3_ACTIONS | IAM_ACTIONS) | {"iam:*"}
|
||||||
|
|
||||||
ACTION_ALIASES = {
|
ACTION_ALIASES = {
|
||||||
|
# List actions
|
||||||
"list": "list",
|
"list": "list",
|
||||||
"s3:listbucket": "list",
|
"s3:listbucket": "list",
|
||||||
"s3:listallmybuckets": "list",
|
"s3:listallmybuckets": "list",
|
||||||
|
"s3:listbucketversions": "list",
|
||||||
|
"s3:listmultipartuploads": "list",
|
||||||
|
"s3:listparts": "list",
|
||||||
|
# Read actions
|
||||||
"read": "read",
|
"read": "read",
|
||||||
"s3:getobject": "read",
|
"s3:getobject": "read",
|
||||||
"s3:getobjectversion": "read",
|
"s3:getobjectversion": "read",
|
||||||
|
"s3:getobjecttagging": "read",
|
||||||
|
"s3:getobjectversiontagging": "read",
|
||||||
|
"s3:getobjectacl": "read",
|
||||||
|
"s3:getbucketversioning": "read",
|
||||||
|
"s3:headobject": "read",
|
||||||
|
"s3:headbucket": "read",
|
||||||
|
# Write actions
|
||||||
"write": "write",
|
"write": "write",
|
||||||
"s3:putobject": "write",
|
"s3:putobject": "write",
|
||||||
"s3:createbucket": "write",
|
"s3:createbucket": "write",
|
||||||
|
"s3:putobjecttagging": "write",
|
||||||
|
"s3:putbucketversioning": "write",
|
||||||
|
"s3:createmultipartupload": "write",
|
||||||
|
"s3:uploadpart": "write",
|
||||||
|
"s3:completemultipartupload": "write",
|
||||||
|
"s3:abortmultipartupload": "write",
|
||||||
|
"s3:copyobject": "write",
|
||||||
|
# Delete actions
|
||||||
"delete": "delete",
|
"delete": "delete",
|
||||||
"s3:deleteobject": "delete",
|
"s3:deleteobject": "delete",
|
||||||
|
"s3:deleteobjectversion": "delete",
|
||||||
"s3:deletebucket": "delete",
|
"s3:deletebucket": "delete",
|
||||||
|
"s3:deleteobjecttagging": "delete",
|
||||||
|
# Share actions (ACL)
|
||||||
"share": "share",
|
"share": "share",
|
||||||
"s3:putobjectacl": "share",
|
"s3:putobjectacl": "share",
|
||||||
|
"s3:putbucketacl": "share",
|
||||||
|
"s3:getbucketacl": "share",
|
||||||
|
# Policy actions
|
||||||
"policy": "policy",
|
"policy": "policy",
|
||||||
"s3:putbucketpolicy": "policy",
|
"s3:putbucketpolicy": "policy",
|
||||||
|
"s3:getbucketpolicy": "policy",
|
||||||
|
"s3:deletebucketpolicy": "policy",
|
||||||
|
# Replication actions
|
||||||
|
"replication": "replication",
|
||||||
|
"s3:getreplicationconfiguration": "replication",
|
||||||
|
"s3:putreplicationconfiguration": "replication",
|
||||||
|
"s3:deletereplicationconfiguration": "replication",
|
||||||
|
"s3:replicateobject": "replication",
|
||||||
|
"s3:replicatetags": "replication",
|
||||||
|
"s3:replicatedelete": "replication",
|
||||||
|
# IAM actions
|
||||||
"iam:listusers": "iam:list_users",
|
"iam:listusers": "iam:list_users",
|
||||||
"iam:createuser": "iam:create_user",
|
"iam:createuser": "iam:create_user",
|
||||||
"iam:deleteuser": "iam:delete_user",
|
"iam:deleteuser": "iam:delete_user",
|
||||||
|
|||||||
74
app/ui.py
74
app/ui.py
@@ -185,6 +185,7 @@ def inject_nav_state() -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"principal": principal,
|
"principal": principal,
|
||||||
"can_manage_iam": can_manage,
|
"can_manage_iam": can_manage,
|
||||||
|
"can_view_metrics": can_manage, # Only admins can view metrics
|
||||||
"csrf_token": generate_csrf,
|
"csrf_token": generate_csrf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +337,28 @@ def bucket_detail(bucket_name: str):
|
|||||||
except IamError:
|
except IamError:
|
||||||
can_manage_versioning = False
|
can_manage_versioning = False
|
||||||
|
|
||||||
|
# Check replication permission
|
||||||
|
can_manage_replication = False
|
||||||
|
if principal:
|
||||||
|
try:
|
||||||
|
_iam().authorize(principal, bucket_name, "replication")
|
||||||
|
can_manage_replication = True
|
||||||
|
except IamError:
|
||||||
|
can_manage_replication = False
|
||||||
|
|
||||||
|
# Check if user is admin (can configure replication settings, not just toggle)
|
||||||
|
is_replication_admin = False
|
||||||
|
if principal:
|
||||||
|
try:
|
||||||
|
_iam().authorize(principal, None, "iam:list_users")
|
||||||
|
is_replication_admin = True
|
||||||
|
except IamError:
|
||||||
|
is_replication_admin = False
|
||||||
|
|
||||||
# Replication info - don't compute sync status here (it's slow), let JS fetch it async
|
# Replication info - don't compute sync status here (it's slow), let JS fetch it async
|
||||||
replication_rule = _replication().get_rule(bucket_name)
|
replication_rule = _replication().get_rule(bucket_name)
|
||||||
connections = _connections().list()
|
# Load connections for admin, or for non-admin if there's an existing rule (to show target name)
|
||||||
|
connections = _connections().list() if (is_replication_admin or replication_rule) else []
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"bucket_detail.html",
|
"bucket_detail.html",
|
||||||
@@ -349,6 +369,8 @@ def bucket_detail(bucket_name: str):
|
|||||||
bucket_policy=bucket_policy,
|
bucket_policy=bucket_policy,
|
||||||
can_edit_policy=can_edit_policy,
|
can_edit_policy=can_edit_policy,
|
||||||
can_manage_versioning=can_manage_versioning,
|
can_manage_versioning=can_manage_versioning,
|
||||||
|
can_manage_replication=can_manage_replication,
|
||||||
|
is_replication_admin=is_replication_admin,
|
||||||
default_policy=default_policy,
|
default_policy=default_policy,
|
||||||
versioning_enabled=versioning_enabled,
|
versioning_enabled=versioning_enabled,
|
||||||
replication_rule=replication_rule,
|
replication_rule=replication_rule,
|
||||||
@@ -1171,17 +1193,52 @@ def delete_connection(connection_id: str):
|
|||||||
def update_bucket_replication(bucket_name: str):
|
def update_bucket_replication(bucket_name: str):
|
||||||
principal = _current_principal()
|
principal = _current_principal()
|
||||||
try:
|
try:
|
||||||
_authorize_ui(principal, bucket_name, "write")
|
_authorize_ui(principal, bucket_name, "replication")
|
||||||
except IamError as exc:
|
except IamError as exc:
|
||||||
flash(str(exc), "danger")
|
flash(str(exc), "danger")
|
||||||
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
||||||
|
|
||||||
|
# Check if user is admin (required for create/delete operations)
|
||||||
|
is_admin = False
|
||||||
|
try:
|
||||||
|
_iam().authorize(principal, None, "iam:list_users")
|
||||||
|
is_admin = True
|
||||||
|
except IamError:
|
||||||
|
is_admin = False
|
||||||
|
|
||||||
action = request.form.get("action")
|
action = request.form.get("action")
|
||||||
|
|
||||||
if action == "delete":
|
if action == "delete":
|
||||||
|
# Admin only - remove configuration entirely
|
||||||
|
if not is_admin:
|
||||||
|
flash("Only administrators can remove replication configuration", "danger")
|
||||||
|
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
||||||
_replication().delete_rule(bucket_name)
|
_replication().delete_rule(bucket_name)
|
||||||
flash("Replication disabled", "info")
|
flash("Replication configuration removed", "info")
|
||||||
|
elif action == "pause":
|
||||||
|
# Users can pause - just set enabled=False
|
||||||
|
rule = _replication().get_rule(bucket_name)
|
||||||
|
if rule:
|
||||||
|
rule.enabled = False
|
||||||
|
_replication().set_rule(rule)
|
||||||
|
flash("Replication paused", "info")
|
||||||
else:
|
else:
|
||||||
|
flash("No replication configuration to pause", "warning")
|
||||||
|
elif action == "resume":
|
||||||
|
# Users can resume - just set enabled=True
|
||||||
|
rule = _replication().get_rule(bucket_name)
|
||||||
|
if rule:
|
||||||
|
rule.enabled = True
|
||||||
|
_replication().set_rule(rule)
|
||||||
|
flash("Replication resumed", "success")
|
||||||
|
else:
|
||||||
|
flash("No replication configuration to resume", "warning")
|
||||||
|
elif action == "create":
|
||||||
|
# Admin only - create new configuration
|
||||||
|
if not is_admin:
|
||||||
|
flash("Only administrators can configure replication settings", "danger")
|
||||||
|
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
||||||
|
|
||||||
from .replication import REPLICATION_MODE_NEW_ONLY, REPLICATION_MODE_ALL
|
from .replication import REPLICATION_MODE_NEW_ONLY, REPLICATION_MODE_ALL
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -1208,6 +1265,8 @@ def update_bucket_replication(bucket_name: str):
|
|||||||
flash("Replication configured. Existing objects are being replicated in the background.", "success")
|
flash("Replication configured. Existing objects are being replicated in the background.", "success")
|
||||||
else:
|
else:
|
||||||
flash("Replication configured. Only new uploads will be replicated.", "success")
|
flash("Replication configured. Only new uploads will be replicated.", "success")
|
||||||
|
else:
|
||||||
|
flash("Invalid action", "danger")
|
||||||
|
|
||||||
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
return redirect(url_for("ui.bucket_detail", bucket_name=bucket_name, tab="replication"))
|
||||||
|
|
||||||
@@ -1217,7 +1276,7 @@ def get_replication_status(bucket_name: str):
|
|||||||
"""Async endpoint to fetch replication sync status without blocking page load."""
|
"""Async endpoint to fetch replication sync status without blocking page load."""
|
||||||
principal = _current_principal()
|
principal = _current_principal()
|
||||||
try:
|
try:
|
||||||
_authorize_ui(principal, bucket_name, "read")
|
_authorize_ui(principal, bucket_name, "replication")
|
||||||
except IamError:
|
except IamError:
|
||||||
return jsonify({"error": "Access denied"}), 403
|
return jsonify({"error": "Access denied"}), 403
|
||||||
|
|
||||||
@@ -1257,6 +1316,13 @@ def connections_dashboard():
|
|||||||
def metrics_dashboard():
|
def metrics_dashboard():
|
||||||
principal = _current_principal()
|
principal = _current_principal()
|
||||||
|
|
||||||
|
# Metrics are restricted to admin users
|
||||||
|
try:
|
||||||
|
_iam().authorize(principal, None, "iam:list_users")
|
||||||
|
except IamError:
|
||||||
|
flash("Access denied: Metrics require admin permissions", "danger")
|
||||||
|
return redirect(url_for("ui.buckets_overview"))
|
||||||
|
|
||||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||||
memory = psutil.virtual_memory()
|
memory = psutil.virtual_memory()
|
||||||
|
|
||||||
|
|||||||
69
docs.md
69
docs.md
@@ -102,6 +102,46 @@ The application automatically trusts these headers to generate correct presigned
|
|||||||
|
|
||||||
The API expects every request to include `X-Access-Key` and `X-Secret-Key` headers. The UI persists them in the Flask session after login.
|
The API expects every request to include `X-Access-Key` and `X-Secret-Key` headers. The UI persists them in the Flask session after login.
|
||||||
|
|
||||||
|
### Available IAM Actions
|
||||||
|
|
||||||
|
| Action | Description | AWS Aliases |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `list` | List buckets and objects | `s3:ListBucket`, `s3:ListAllMyBuckets`, `s3:ListBucketVersions`, `s3:ListMultipartUploads`, `s3:ListParts` |
|
||||||
|
| `read` | Download objects | `s3:GetObject`, `s3:GetObjectVersion`, `s3:GetObjectTagging`, `s3:HeadObject`, `s3:HeadBucket` |
|
||||||
|
| `write` | Upload objects, create buckets | `s3:PutObject`, `s3:CreateBucket`, `s3:CreateMultipartUpload`, `s3:UploadPart`, `s3:CompleteMultipartUpload`, `s3:AbortMultipartUpload`, `s3:CopyObject` |
|
||||||
|
| `delete` | Remove objects and buckets | `s3:DeleteObject`, `s3:DeleteObjectVersion`, `s3:DeleteBucket` |
|
||||||
|
| `share` | Manage ACLs | `s3:PutObjectAcl`, `s3:PutBucketAcl`, `s3:GetBucketAcl` |
|
||||||
|
| `policy` | Manage bucket policies | `s3:PutBucketPolicy`, `s3:GetBucketPolicy`, `s3:DeleteBucketPolicy` |
|
||||||
|
| `replication` | Configure and manage replication | `s3:GetReplicationConfiguration`, `s3:PutReplicationConfiguration`, `s3:ReplicateObject`, `s3:ReplicateTags`, `s3:ReplicateDelete` |
|
||||||
|
| `iam:list_users` | View IAM users | `iam:ListUsers` |
|
||||||
|
| `iam:create_user` | Create IAM users | `iam:CreateUser` |
|
||||||
|
| `iam:delete_user` | Delete IAM users | `iam:DeleteUser` |
|
||||||
|
| `iam:rotate_key` | Rotate user secrets | `iam:RotateAccessKey` |
|
||||||
|
| `iam:update_policy` | Modify user policies | `iam:PutUserPolicy` |
|
||||||
|
| `iam:*` | All IAM actions (admin wildcard) | — |
|
||||||
|
|
||||||
|
### Example Policies
|
||||||
|
|
||||||
|
**Full Control (admin):**
|
||||||
|
```json
|
||||||
|
[{"bucket": "*", "actions": ["list", "read", "write", "delete", "share", "policy", "replication", "iam:*"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Read-Only:**
|
||||||
|
```json
|
||||||
|
[{"bucket": "*", "actions": ["list", "read"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single Bucket Access (no listing other buckets):**
|
||||||
|
```json
|
||||||
|
[{"bucket": "user-bucket", "actions": ["read", "write", "delete"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bucket Access with Replication:**
|
||||||
|
```json
|
||||||
|
[{"bucket": "my-bucket", "actions": ["list", "read", "write", "delete", "replication"]}]
|
||||||
|
```
|
||||||
|
|
||||||
## 5. Bucket Policies & Presets
|
## 5. Bucket Policies & Presets
|
||||||
|
|
||||||
- **Storage**: Policies are persisted in `data/.myfsio.sys/config/bucket_policies.json` under `{"policies": {"bucket": {...}}}`.
|
- **Storage**: Policies are persisted in `data/.myfsio.sys/config/bucket_policies.json` under `{"policies": {"bucket": {...}}}`.
|
||||||
@@ -177,6 +217,19 @@ s3.complete_multipart_upload(
|
|||||||
|
|
||||||
MyFSIO supports **Site Replication**, allowing you to automatically copy new objects from one MyFSIO instance (Source) to another (Target). This is useful for disaster recovery, data locality, or backups.
|
MyFSIO supports **Site Replication**, allowing you to automatically copy new objects from one MyFSIO instance (Source) to another (Target). This is useful for disaster recovery, data locality, or backups.
|
||||||
|
|
||||||
|
### Permission Model
|
||||||
|
|
||||||
|
Replication uses a two-tier permission system:
|
||||||
|
|
||||||
|
| Role | Capabilities |
|
||||||
|
|------|--------------|
|
||||||
|
| **Admin** (users with `iam:*` permissions) | Create/delete replication rules, configure connections and target buckets |
|
||||||
|
| **Users** (with `replication` permission) | Enable/disable (pause/resume) existing replication rules |
|
||||||
|
|
||||||
|
> **Note:** The Replication tab is hidden for users without the `replication` permission on the bucket.
|
||||||
|
|
||||||
|
This separation allows administrators to pre-configure where data should replicate, while allowing authorized users to toggle replication on/off without accessing connection credentials.
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
- **Source Instance**: The MyFSIO instance where you upload files. It runs the replication worker.
|
- **Source Instance**: The MyFSIO instance where you upload files. It runs the replication worker.
|
||||||
@@ -253,13 +306,15 @@ Now, configure the primary instance to replicate to the target.
|
|||||||
- **Secret Key**: The secret you generated on the Target.
|
- **Secret Key**: The secret you generated on the Target.
|
||||||
- Click **Add Connection**.
|
- Click **Add Connection**.
|
||||||
|
|
||||||
3. **Enable Replication**:
|
3. **Enable Replication** (Admin):
|
||||||
- Navigate to **Buckets** and select the source bucket.
|
- Navigate to **Buckets** and select the source bucket.
|
||||||
- Switch to the **Replication** tab.
|
- Switch to the **Replication** tab.
|
||||||
- Select the `Secondary Site` connection.
|
- Select the `Secondary Site` connection.
|
||||||
- Enter the target bucket name (`backup-bucket`).
|
- Enter the target bucket name (`backup-bucket`).
|
||||||
- Click **Enable Replication**.
|
- Click **Enable Replication**.
|
||||||
|
|
||||||
|
Once configured, users with `replication` permission on this bucket can pause/resume replication without needing access to connection details.
|
||||||
|
|
||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
1. Upload a file to the source bucket.
|
1. Upload a file to the source bucket.
|
||||||
@@ -270,6 +325,18 @@ Now, configure the primary instance to replicate to the target.
|
|||||||
aws --endpoint-url http://target-server:5002 s3 ls s3://backup-bucket
|
aws --endpoint-url http://target-server:5002 s3 ls s3://backup-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Pausing and Resuming Replication
|
||||||
|
|
||||||
|
Users with the `replication` permission (but not admin rights) can pause and resume existing replication rules:
|
||||||
|
|
||||||
|
1. Navigate to the bucket's **Replication** tab.
|
||||||
|
2. If replication is **Active**, click **Pause Replication** to temporarily stop syncing.
|
||||||
|
3. If replication is **Paused**, click **Resume Replication** to continue syncing.
|
||||||
|
|
||||||
|
When paused, new objects uploaded to the source will not replicate until replication is resumed. Objects uploaded while paused will be replicated once resumed.
|
||||||
|
|
||||||
|
> **Note:** Only admins can create new replication rules, change the target connection/bucket, or delete rules entirely.
|
||||||
|
|
||||||
### Bidirectional Replication (Active-Active)
|
### Bidirectional Replication (Active-Active)
|
||||||
|
|
||||||
To set up two-way replication (Server A ↔ Server B):
|
To set up two-way replication (Server A ↔ Server B):
|
||||||
|
|||||||
@@ -51,22 +51,18 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('ui.buckets_overview') }}">Buckets</a>
|
<a class="nav-link" href="{{ url_for('ui.buckets_overview') }}">Buckets</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if can_manage_iam %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if not can_manage_iam %}nav-link-muted{% endif %}" href="{{ url_for('ui.iam_dashboard') }}">
|
<a class="nav-link" href="{{ url_for('ui.iam_dashboard') }}">IAM</a>
|
||||||
IAM
|
|
||||||
{% if not can_manage_iam %}<span class="badge ms-2 text-bg-warning">Restricted</span>{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if not can_manage_iam %}nav-link-muted{% endif %}" href="{{ url_for('ui.connections_dashboard') }}">
|
<a class="nav-link" href="{{ url_for('ui.connections_dashboard') }}">Connections</a>
|
||||||
Connections
|
|
||||||
{% if not can_manage_iam %}<span class="badge ms-2 text-bg-warning">Restricted</span>{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('ui.metrics_dashboard') }}">Metrics</a>
|
<a class="nav-link" href="{{ url_for('ui.metrics_dashboard') }}">Metrics</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if principal %}
|
{% if principal %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('ui.docs_page') }}">Docs</a>
|
<a class="nav-link" href="{{ url_for('ui.docs_page') }}">Docs</a>
|
||||||
|
|||||||
@@ -59,11 +59,13 @@
|
|||||||
Permissions
|
Permissions
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% if can_manage_replication %}
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link {{ 'active' if active_tab == 'replication' else '' }}" id="replication-tab" data-bs-toggle="tab" data-bs-target="#replication-pane" type="button" role="tab" aria-controls="replication-pane" aria-selected="{{ 'true' if active_tab == 'replication' else 'false' }}">
|
<button class="nav-link {{ 'active' if active_tab == 'replication' else '' }}" id="replication-tab" data-bs-toggle="tab" data-bs-target="#replication-pane" type="button" role="tab" aria-controls="replication-pane" aria-selected="{{ 'true' if active_tab == 'replication' else 'false' }}">
|
||||||
Replication
|
Replication
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
@@ -658,6 +660,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if can_manage_replication %}
|
||||||
<!-- Replication Tab -->
|
<!-- Replication Tab -->
|
||||||
<div class="tab-pane fade {{ 'show active' if active_tab == 'replication' else '' }}" id="replication-pane" role="tabpanel" aria-labelledby="replication-tab" tabindex="0">
|
<div class="tab-pane fade {{ 'show active' if active_tab == 'replication' else '' }}" id="replication-pane" role="tabpanel" aria-labelledby="replication-tab" tabindex="0">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
@@ -671,8 +674,8 @@
|
|||||||
<span class="fw-semibold">Replication Configuration</span>
|
<span class="fw-semibold">Replication Configuration</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if replication_rule %}
|
{% if replication_rule and replication_rule.enabled %}
|
||||||
<!-- Replication Enabled State -->
|
<!-- Replication Active State -->
|
||||||
<div class="alert alert-success d-flex align-items-center mb-4" role="alert">
|
<div class="alert alert-success d-flex align-items-center mb-4" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
|
||||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||||
@@ -830,17 +833,103 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Refresh
|
Refresh
|
||||||
</a>
|
</a>
|
||||||
|
<!-- Pause button - available to users with replication permission -->
|
||||||
|
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="hidden" name="action" value="pause">
|
||||||
|
<button type="submit" class="btn btn-outline-warning">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
Pause Replication
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% if is_replication_admin %}
|
||||||
|
<!-- Delete button - admin only -->
|
||||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#disableReplicationModal">
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#disableReplicationModal">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Disable Replication
|
Remove Configuration
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% elif replication_rule and not replication_rule.enabled %}
|
||||||
|
<!-- Replication Paused State -->
|
||||||
|
<div class="alert alert-warning d-flex align-items-center mb-4" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="flex-shrink-0 me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Replication Paused</strong> —
|
||||||
|
Replication is configured but currently paused. New uploads will not be replicated until resumed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Show target info even when paused -->
|
||||||
|
<h6 class="text-muted text-uppercase small mb-3">Replication Target</h6>
|
||||||
|
<div class="card border mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="rounded-circle bg-warning bg-opacity-10 d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-warning" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
|
||||||
|
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{% set target_conn = connections | selectattr("id", "equalto", replication_rule.target_connection_id) | first %}
|
||||||
|
<div class="fw-semibold">{{ target_conn.name if target_conn else 'Unknown Connection' }}</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z"/>
|
||||||
|
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/>
|
||||||
|
</svg>
|
||||||
|
{{ replication_rule.target_bucket }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-warning-subtle text-warning px-3 py-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
Paused
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<!-- Resume button -->
|
||||||
|
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="hidden" name="action" value="resume">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
|
||||||
|
</svg>
|
||||||
|
Resume Replication
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% if is_replication_admin %}
|
||||||
|
<!-- Delete button - admin only -->
|
||||||
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#disableReplicationModal">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
|
</svg>
|
||||||
|
Remove Configuration
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Replication Setup State -->
|
<!-- Replication Not Configured State -->
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div class="rounded-circle bg-body-tertiary d-inline-flex align-items-center justify-content-center mb-3" style="width: 64px; height: 64px;">
|
<div class="rounded-circle bg-body-tertiary d-inline-flex align-items-center justify-content-center mb-3" style="width: 64px; height: 64px;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
|
||||||
@@ -848,11 +937,16 @@
|
|||||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_replication_admin %}
|
||||||
<h5 class="mb-2">Set Up Replication</h5>
|
<h5 class="mb-2">Set Up Replication</h5>
|
||||||
<p class="text-muted mb-4">Automatically copy new objects from this bucket to a bucket in another S3-compatible service.</p>
|
<p class="text-muted mb-4">Automatically copy new objects from this bucket to a bucket in another S3-compatible service.</p>
|
||||||
|
{% else %}
|
||||||
|
<h5 class="mb-2">Replication Not Configured</h5>
|
||||||
|
<p class="text-muted mb-4">An administrator needs to configure replication settings for this bucket before you can enable it.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if connections %}
|
{% if is_replication_admin and connections %}
|
||||||
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}">
|
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="hidden" name="action" value="create">
|
<input type="hidden" name="action" value="create">
|
||||||
@@ -904,7 +998,7 @@
|
|||||||
Enable Replication
|
Enable Replication
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% elif is_replication_admin %}
|
||||||
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="flex-shrink-0 me-2 mt-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="flex-shrink-0 me-2 mt-1" viewBox="0 0 16 16">
|
||||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
@@ -969,6 +1063,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -509,7 +509,7 @@
|
|||||||
full: [
|
full: [
|
||||||
{
|
{
|
||||||
bucket: '*',
|
bucket: '*',
|
||||||
actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'iam:list_users', 'iam:*'],
|
actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'replication', 'iam:list_users', 'iam:*'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
readonly: [
|
readonly: [
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
full: [
|
full: [
|
||||||
{
|
{
|
||||||
bucket: '*',
|
bucket: '*',
|
||||||
actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'iam:list_users', 'iam:*'],
|
actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'replication', 'iam:list_users', 'iam:*'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
readonly: [
|
readonly: [
|
||||||
|
|||||||
Reference in New Issue
Block a user