Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abee512866 | ||
|
|
af0dee05ec | ||
|
|
83d4892acb | ||
|
|
011bc918a8 | ||
|
|
c38c575d8e | ||
|
|
b1c1e49cc5 |
@@ -185,15 +185,15 @@ class H(BaseHTTPRequestHandler):
|
||||
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
|
||||
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
|
||||
if path in ('/api','/api/status'): return self.sendj(api_spec())
|
||||
if path in ('/api/base','/base'):
|
||||
if path in ('/api/base','/api/baseline','/base'):
|
||||
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
|
||||
if path in ('/api/ver','/ver'):
|
||||
if path in ('/api/ver','/api/versions','/ver'):
|
||||
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
|
||||
if path in ('/api/patch','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
|
||||
if path in ('/api/run','/run'):
|
||||
if path in ('/api/run','/api/runtime-status','/run'):
|
||||
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
|
||||
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
|
||||
if path in ('/api/action-monitor','/action-monitor'):
|
||||
@@ -211,11 +211,11 @@ class H(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
try:
|
||||
if path == '/api/branch': return self.branch(data)
|
||||
if path == '/api/file': return self.file(data)
|
||||
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
|
||||
if path in ('/api/file','/api/commit-file'): return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
if path == '/api/patch': return self.patch(data)
|
||||
if path == '/api/tag': return self.tag(data)
|
||||
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
|
||||
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
|
||||
if path == '/api/rollback': return self.rollback(data)
|
||||
if path == '/api/model': return self.model(data)
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""V1R3C0 User Portal RBAC Ops core services.
|
||||
|
||||
Pure application layer: no secrets, no external credentials.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
ROLES = {"user", "group_admin", "owner"}
|
||||
|
||||
@dataclass
|
||||
class PortalContext:
|
||||
user_id: str
|
||||
group_id: str | None
|
||||
role: str
|
||||
|
||||
def scope(self):
|
||||
if self.role == "owner":
|
||||
return "global"
|
||||
if self.role == "group_admin":
|
||||
return "group"
|
||||
return "self"
|
||||
|
||||
@dataclass
|
||||
class Approval:
|
||||
request_id: str
|
||||
request_type: str
|
||||
state: str = "pending"
|
||||
|
||||
@dataclass
|
||||
class Alert:
|
||||
alert_id: str
|
||||
level: str
|
||||
state: str = "open"
|
||||
|
||||
@dataclass
|
||||
class AuditEvent:
|
||||
action: str
|
||||
actor: str
|
||||
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
|
||||
class PortalRBACService:
|
||||
def authorize(self, ctx: PortalContext, target_scope: str):
|
||||
if ctx.role not in ROLES:
|
||||
return False
|
||||
if ctx.scope() == "global":
|
||||
return True
|
||||
return ctx.scope() == target_scope
|
||||
|
||||
def navigation(self, role: str):
|
||||
common = ["/", "/me", "/model", "/usage", "/quota", "/tasks", "/alerts", "/approvals", "/audit"]
|
||||
if role == "owner":
|
||||
return common + ["/owner/system", "/owner/gitops", "/owner/runtime", "/owner/security"]
|
||||
if role == "group_admin":
|
||||
return common + ["/group", "/group/users", "/group/quota", "/group/audit"]
|
||||
return common
|
||||
|
||||
class PortalAuditService:
|
||||
def append(self, action: str, actor: str):
|
||||
return AuditEvent(action=action, actor=actor)
|
||||
|
||||
class SecretPolicy:
|
||||
@staticmethod
|
||||
def redact(value: str):
|
||||
return "***REDACTED***" if value else value
|
||||
|
||||
__all__ = ["PortalContext", "PortalRBACService", "PortalAuditService", "SecretPolicy", "Approval", "Alert"]
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS portal_approvals (
|
||||
id TEXT PRIMARY KEY,
|
||||
request_type TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_alerts (
|
||||
id TEXT PRIMARY KEY,
|
||||
level TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'open',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_audit_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
+10
-20
@@ -1,22 +1,12 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"mode": "gitops-online",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"chatgpt_access": true,
|
||||
"codex_access": true,
|
||||
"updated_at": "2026-07-08T00:00:00Z"
|
||||
}
|
||||
|
||||
+7
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"active_version": "V1R2C2",
|
||||
"active_tag": "V1R2C2",
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"mode": "gitops-online",
|
||||
"version_policy": "VxRxCx",
|
||||
"update_mode": "git_patch_only",
|
||||
@@ -28,8 +28,8 @@
|
||||
"human_shortlinks_login_required": true,
|
||||
"autonomous_recovery_loop": true
|
||||
},
|
||||
"version": "V1R2C2",
|
||||
"baseline_tag": "V1R2C2-auto-recovery-baseline",
|
||||
"version": "V1R2C3",
|
||||
"baseline_tag": "V1R2C3",
|
||||
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"zip_update_mode": "deprecated",
|
||||
@@ -47,7 +47,7 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-07T19:45:00Z",
|
||||
"updated_at": "2026-07-08T00:00:00Z",
|
||||
"auth": {
|
||||
"primary_domain": "https://auth.cnnetmkt.top",
|
||||
"backup_domain": "https://auth.h.imruyi.top:8443",
|
||||
@@ -56,7 +56,7 @@
|
||||
"casdoor_application": "admin/aios-portal",
|
||||
"backup_domain_role": "standby-only"
|
||||
},
|
||||
"previous_version": "V1R2C1",
|
||||
"previous_version": "V1R2C2",
|
||||
"codex_gitops": {
|
||||
"config": "config/codex/gitops-online.json",
|
||||
"token_account": "codex-gitops-bot",
|
||||
@@ -96,14 +96,5 @@
|
||||
"owner_password_sync": "aios-gitea-owner-sync.timer",
|
||||
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"last_auth_portal_fix": {
|
||||
"version": "V1R1C4",
|
||||
"timestamp": "2026-07-06T11:16:23Z",
|
||||
"shortlinks": "redirect_to_login_protected_portal_gitops_section",
|
||||
"gitea_owner_sync": "enabled",
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/v1r3c0-user-portal-rbac-ops-20260708",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-07T18:32:29Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
from active_runtime.app.portal_rbac_ops import PortalContext, PortalRBACService, PortalAuditService, SecretPolicy
|
||||
|
||||
|
||||
def test_user_scope():
|
||||
ctx = PortalContext('u1', 'g1', 'user')
|
||||
assert PortalRBACService().authorize(ctx, 'self')
|
||||
|
||||
|
||||
def test_owner_scope():
|
||||
ctx = PortalContext('o1', None, 'owner')
|
||||
assert PortalRBACService().authorize(ctx, 'global')
|
||||
|
||||
|
||||
def test_navigation():
|
||||
assert '/owner/system' in PortalRBACService().navigation('owner')
|
||||
|
||||
|
||||
def test_audit_append():
|
||||
event = PortalAuditService().append('login', 'u1')
|
||||
assert event.action == 'login'
|
||||
|
||||
|
||||
def test_secret_redaction():
|
||||
assert SecretPolicy.redact('secret') == '***REDACTED***'
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "V1R2C3",
|
||||
"tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"release_reason": "GPT Actions ClientResponseError compatibility recovery",
|
||||
"validated_actions": [
|
||||
"getRuntimeStatus",
|
||||
"getBaseline",
|
||||
"listVersions",
|
||||
"listPatches",
|
||||
"getDiff",
|
||||
"createBranch",
|
||||
"createPatch",
|
||||
"commitFile"
|
||||
],
|
||||
"secret_plaintext_written": false,
|
||||
"created_at": "2026-07-08T00:00:00Z",
|
||||
"author": "openclaw"
|
||||
}
|
||||
Reference in New Issue
Block a user