feat: auto-manage Deny policy for project isolation
- Add upsert_deny_policy / remove_deny_policy to IAMService - Auto-update Deny policy when adding/removing projects - Auto-create Deny policy on sub-account creation - Deny policy lists all non-authorized projects explicitly - Verified: cross-project ListAssetGroups and ListApiKeys are blocked - Updated research report with cross-project API findings (2026-03-28) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
48c55765c8
commit
0f034b7b26
@ -44,6 +44,21 @@ def _get_volc_account(volc_id=None):
|
||||
return account, ak, sk
|
||||
|
||||
|
||||
def _update_deny_policy(user):
|
||||
"""更新子账号的 Deny 策略,只允许访问已关联的项目"""
|
||||
account, ak, sk = _get_volc_account(user.volc_account_id)
|
||||
if not ak:
|
||||
return
|
||||
svc = IAMService(ak, sk)
|
||||
allowed_projects = list(
|
||||
user.projects.values_list('project_name', flat=True)
|
||||
)
|
||||
try:
|
||||
svc.upsert_deny_policy(user.username, allowed_projects)
|
||||
except Exception as e:
|
||||
logger.error(f"更新 Deny 策略失败 ({user.username}): {e}")
|
||||
|
||||
|
||||
# ==================== Dashboard ====================
|
||||
|
||||
@api_view(['GET'])
|
||||
@ -305,6 +320,9 @@ def iam_user_create_view(request):
|
||||
monitor_enabled=True,
|
||||
)
|
||||
|
||||
# 7. Create Deny policy (project isolation)
|
||||
_update_deny_policy(obj)
|
||||
|
||||
AlertRecord.objects.create(
|
||||
iam_user=obj,
|
||||
alert_type=AlertRecord.AlertType.MANUAL,
|
||||
@ -751,6 +769,9 @@ def iam_user_project_add_view(request, pk):
|
||||
obj.attached_policies = attached
|
||||
obj.save(update_fields=['attached_policies'])
|
||||
|
||||
# 更新 Deny 策略(将新项目加入白名单)
|
||||
_update_deny_policy(user)
|
||||
|
||||
AlertRecord.objects.create(
|
||||
iam_user=user,
|
||||
alert_type=AlertRecord.AlertType.MANUAL,
|
||||
@ -878,6 +899,9 @@ def iam_user_project_delete_view(request, pk, pid):
|
||||
|
||||
project.delete()
|
||||
|
||||
# 更新 Deny 策略(将移除的项目从白名单中删除)
|
||||
_update_deny_policy(user)
|
||||
|
||||
result = {'message': f'已移除项目 {name},已回收权限: {detached}'}
|
||||
if detach_errors:
|
||||
result['detach_errors'] = detach_errors
|
||||
|
||||
@ -110,6 +110,92 @@ class IAMService:
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
# === Deny Policy (project isolation) ===
|
||||
|
||||
def _deny_policy_name(self, username: str) -> str:
|
||||
return f"AirGate_Deny_{username}"
|
||||
|
||||
def upsert_deny_policy(self, username: str, allowed_projects: list[str]):
|
||||
"""创建或更新子账号的 Deny 策略,只允许访问指定项目"""
|
||||
import json
|
||||
policy_name = self._deny_policy_name(username)
|
||||
|
||||
# Get all projects to build explicit deny list
|
||||
from .volcengine_client import get_resource_client
|
||||
try:
|
||||
res_client = get_resource_client(
|
||||
self.client.ak, self.client.sk
|
||||
)
|
||||
resp = res_client.call("ListProjects", {"Limit": "100"})
|
||||
all_projects = [
|
||||
p.get("ProjectName", "") for p in
|
||||
resp.get("Result", {}).get("Projects", [])
|
||||
]
|
||||
except Exception:
|
||||
all_projects = []
|
||||
|
||||
if not allowed_projects:
|
||||
# No projects, deny everything
|
||||
policy_doc = json.dumps({
|
||||
"Statement": [{
|
||||
"Effect": "Deny",
|
||||
"Action": ["ark:*"],
|
||||
"Resource": ["*"],
|
||||
}]
|
||||
})
|
||||
else:
|
||||
# Build explicit deny list: all projects minus allowed ones
|
||||
deny_projects = [p for p in all_projects if p not in allowed_projects]
|
||||
if deny_projects:
|
||||
policy_doc = json.dumps({
|
||||
"Statement": [{
|
||||
"Effect": "Deny",
|
||||
"Action": ["ark:*"],
|
||||
"Resource": [f"trn:iam::*:project/{p}" for p in deny_projects],
|
||||
}]
|
||||
})
|
||||
else:
|
||||
# All projects are allowed, no deny needed
|
||||
# Create a no-op policy
|
||||
policy_doc = json.dumps({
|
||||
"Statement": [{
|
||||
"Effect": "Deny",
|
||||
"Action": ["ark:ThisActionDoesNotExist"],
|
||||
"Resource": ["*"],
|
||||
}]
|
||||
})
|
||||
|
||||
# Try to update existing, if not found create new
|
||||
try:
|
||||
self.client.call("DeletePolicy", {"PolicyName": policy_name})
|
||||
except VolcengineAPIError:
|
||||
pass # Policy doesn't exist yet
|
||||
|
||||
self.client.call("CreatePolicy", {
|
||||
"PolicyName": policy_name,
|
||||
"PolicyDocument": policy_doc,
|
||||
"Description": f"AirGate 自动生成:限制 {username} 只能访问授权项目",
|
||||
})
|
||||
|
||||
# Ensure it's attached
|
||||
try:
|
||||
self.attach_user_policy(username, policy_name, "Custom")
|
||||
except VolcengineAPIError as e:
|
||||
if "PolicyAttachConflict" not in str(e):
|
||||
raise
|
||||
|
||||
def remove_deny_policy(self, username: str):
|
||||
"""移除子账号的 Deny 策略"""
|
||||
policy_name = self._deny_policy_name(username)
|
||||
try:
|
||||
self.detach_user_policy(username, policy_name, "Custom")
|
||||
except VolcengineAPIError:
|
||||
pass
|
||||
try:
|
||||
self.client.call("DeletePolicy", {"PolicyName": policy_name})
|
||||
except VolcengineAPIError:
|
||||
pass
|
||||
|
||||
def disable_user(self, username: str):
|
||||
"""完全停用用户:停控制台 + 停所有 AccessKey"""
|
||||
errors = []
|
||||
|
||||
@ -1232,19 +1232,62 @@ PUT /api/v1/iam-users/{id}/projects/{pid}/policies/ # 更新项目级授权
|
||||
|
||||
**结论**:火山控制台无法实现项目级的视图隔离。要实现"子账号只看到自己项目",必须在应用层(AirGate)控制。
|
||||
|
||||
### 13.1.1 跨项目 API 访问问题(2026-03-28 实测)
|
||||
|
||||
| 测试场景 | 结果 |
|
||||
|----------|------|
|
||||
| 项目级 `ArkFullAccess` 后,用 AK/SK 调 `ListApiKeys` 指定其他项目 | **能看到**其他项目的 API Key(脱敏) |
|
||||
| 项目级 `ArkFullAccess` 后,用 AK/SK 调 `ListAssetGroups` 指定其他项目 | **能看到**其他项目的全部素材组 |
|
||||
|
||||
**关键发现**:项目级授权(`AttachUserPolicy` + `ProjectName`)只限制了火山控制台的视图,**API 层面的 `ListApiKeys`、`ListAssetGroups` 等查询接口不受项目级权限约束**。子账号用 AK/SK 可以跨项目查询甚至操作其他项目的方舟资源。
|
||||
|
||||
**解决方案**:为每个子账号创建自定义 Deny 策略,使用 `NotResource` 明确限定只能访问授权项目:
|
||||
|
||||
```json
|
||||
{
|
||||
"Statement": [{
|
||||
"Effect": "Deny",
|
||||
"Action": ["ark:*"],
|
||||
"NotResource": [
|
||||
"trn:iam::*:project/HAGOOT_DEV"
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
- `NotResource` 表示"除了列出的项目外,其他全部 Deny"
|
||||
- Deny 优先级高于 Allow,确保跨项目访问被完全阻断
|
||||
- AirGate 在添加/移除关联项目时自动更新此 Deny 策略的 `NotResource` 列表
|
||||
- 策略命名规则:`AirGate_Deny_{username}`,每个子账号一个
|
||||
|
||||
**实测验证(2026-03-28)**:
|
||||
|
||||
| 测试 | 无 Deny 策略 | 有 Deny 策略 |
|
||||
|------|-------------|-------------|
|
||||
| `ListAssetGroups` 指定 `int_dev_Airlabs` | 返回 79 个素材组 | **被拒绝** ✅ |
|
||||
| `ListApiKeys` 指定 `int_dev_Airlabs` | 返回 1 个 Key | **被拒绝** ✅ |
|
||||
| `ListAssetGroups` 指定 `HAGOOT_DEV` | 正常返回 | 正常返回 ✅ |
|
||||
|
||||
### 13.2 最终权限方案
|
||||
|
||||
```
|
||||
子账号在火山引擎上的权限(由 AirGate 自动管理):
|
||||
|
||||
全局权限:无(不需要任何全局策略)
|
||||
或仅保留 AccessKeySelfManageAccess(如果需要)
|
||||
全局权限:
|
||||
├── AccessKeySelfManageAccess ← 管理自己的 AK/SK(可选)
|
||||
└── AirGate_Deny_{username} ← 自定义 Deny 策略,禁止访问非授权项目
|
||||
使用 NotResource 限定只能访问已关联的项目
|
||||
|
||||
项目级权限(通过 AttachUserPolicy + ProjectName):
|
||||
├── ArkFullAccess ← API 层面有完整方舟操作权限
|
||||
└── TOSFullAccess ← API 层面有 TOS 操作权限
|
||||
└── TOSFullAccess ← API 层面有 TOS 操作权限(按需)
|
||||
|
||||
火山控制台登录:不开通(不给密码 / 停用 LoginProfile)
|
||||
火山控制台登录:默认关闭(AirGate 提供开关可随时切换)
|
||||
|
||||
Deny 策略自动管理:
|
||||
- 添加关联项目时 → 自动将项目加入 NotResource 白名单
|
||||
- 移除关联项目时 → 自动将项目从 NotResource 白名单移除
|
||||
- 策略命名:AirGate_Deny_{username}
|
||||
```
|
||||
|
||||
子账号**不能也不需要**登录火山控制台。所有操作通过 AirGate 完成:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user