The PM panel (PmPanelPage -> iframe /pm/) needs the project-management platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM, branch feature/project-management-platform. Update via: git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Bootstrap the PM→ERP service account (one-off, idempotent).
|
|
|
|
1. Creates Casdoor user `pm-service` (org `inventory`) via the Casdoor admin API.
|
|
2. Performs a password grant so ERP auto-provisions the users row.
|
|
3. Promotes that row to role `admin` (read-only across all projects —
|
|
the ERP inventory router lets admin read without a project scope).
|
|
|
|
Usage (from the pm-api container):
|
|
PM_ERP_SERVICE_PASSWORD=... python -m tools.seed_erp_service
|
|
"""
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import logging
|
|
import os
|
|
import urllib.request
|
|
|
|
from app.config import settings
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
log = logging.getLogger("pm.seed_erp")
|
|
|
|
ORG = settings.ERP_CASDOOR_ORGANIZATION
|
|
USERNAME = settings.ERP_SERVICE_USERNAME
|
|
PASSWORD = settings.ERP_SERVICE_PASSWORD or os.environ.get("PM_ERP_SERVICE_PASSWORD", "pm-service-2026")
|
|
EMAIL = f"{USERNAME}@sample.ir"
|
|
ERP_DSN = "postgresql://postgres:postgres@procurement-postgres:5432/procurement"
|
|
|
|
|
|
def _post(url: str, body: dict, headers: dict | None = None) -> dict:
|
|
req = urllib.request.Request(
|
|
url,
|
|
data=json.dumps(body).encode(),
|
|
headers={"Content-Type": "application/json", **(headers or {})},
|
|
method="POST",
|
|
)
|
|
resp = urllib.request.urlopen(req, timeout=15)
|
|
return json.loads(resp.read())
|
|
|
|
|
|
def casdoor_admin_cookie() -> str:
|
|
resp = urllib.request.urlopen(urllib.request.Request(
|
|
f"{settings.ERP_CASDOOR_ENDPOINT}/api/login",
|
|
data=json.dumps({
|
|
"type": "login", "organization": "built-in",
|
|
"username": "admin", "password": "123", "application": "app-built-in",
|
|
}).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
), timeout=15)
|
|
data = json.loads(resp.read())
|
|
if data.get("status") != "ok":
|
|
raise SystemExit(f"casdoor admin login failed: {data}")
|
|
return (resp.headers.get("Set-Cookie") or "").split(";")[0]
|
|
|
|
|
|
async def main() -> None:
|
|
# 1) casdoor user (idempotent — casdoor rejects duplicates, that's fine)
|
|
cookie = casdoor_admin_cookie()
|
|
for path, body in [
|
|
("/api/add-user", {
|
|
"owner": ORG, "name": USERNAME, "displayName": "PM Service",
|
|
"firstName": "PM", "lastName": "Service", "email": EMAIL,
|
|
"password": PASSWORD, "passwordType": "plain", "type": "normal-user",
|
|
}),
|
|
(f"/api/update-user?id={ORG}/{USERNAME}&columns=password_type", {"passwordType": "plain"}),
|
|
]:
|
|
try:
|
|
result = _post(f"{settings.ERP_CASDOOR_ENDPOINT}{path}", body, {"Cookie": cookie})
|
|
log.info("%s → %s", path.split("?")[0], result.get("msg", result.get("status")))
|
|
except Exception as exc: # noqa: BLE001
|
|
log.warning("%s failed: %s", path.split("?")[0], exc)
|
|
|
|
# 2) password grant (PM never verifies this token — it just forwards it)
|
|
grant = _post(f"{settings.ERP_CASDOOR_ENDPOINT}/api/login/oauth/access_token", {
|
|
"grant_type": "password",
|
|
"client_id": settings.ERP_CASDOOR_CLIENT_ID,
|
|
"client_secret": settings.ERP_CASDOOR_CLIENT_SECRET,
|
|
"username": USERNAME,
|
|
"password": PASSWORD,
|
|
})
|
|
token = grant.get("access_token")
|
|
if not token:
|
|
raise SystemExit(f"password grant failed: {grant}")
|
|
claims = json.loads(base64.urlsafe_b64decode(token.split(".")[1] + "=" * (-len(token.split(".")[1]) % 4)))
|
|
casdoor_id = str(claims.get("sub") or claims.get("id") or "")
|
|
log.info("grant ok, casdoor_id=%s", casdoor_id[:8])
|
|
|
|
# trigger ERP auto-provisioning
|
|
me_req = urllib.request.Request(
|
|
f"{settings.ERP_BASE_URL}/api/v1/users/me",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
try:
|
|
urllib.request.urlopen(me_req, timeout=15).read()
|
|
log.info("ERP provisioning call ok")
|
|
except Exception as exc: # noqa: BLE001
|
|
log.warning("provisioning call failed (row may already exist): %s", exc)
|
|
|
|
# 3) promote in ERP DB (direct SQL — this service account is PM infrastructure)
|
|
import asyncpg
|
|
|
|
conn = await asyncpg.connect(ERP_DSN)
|
|
try:
|
|
updated = await conn.execute(
|
|
"UPDATE users SET role='admin' WHERE casdoor_id=$1", casdoor_id
|
|
)
|
|
log.info("ERP role promotion: %s", updated)
|
|
finally:
|
|
await conn.close()
|
|
log.info("service account ready: %s / ***", USERNAME)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|