- Delete backups/*.dump and all real credentials from README/docs
(prod MinIO creds, Casdoor passwords, server IP, admin password)
- All stack secrets now come from git-ignored .env (.env.example added);
docker-compose.prod.yml refuses to boot without them (${VAR:?})
- seed_casdoor.py: bcrypt password hashing, generated console/client secrets
- Harden infra/deploy-webhook: HMAC-verified Gitea webhook receiver
+ systemd unit + README (replaces unauthenticated deploy endpoint)
- docs/SECRET-MANAGEMENT.md: secret inventory + rotation checklist
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
"""Seed Casdoor (dev) — org `inventory`, app `inventory-app`, users.
|
|
|
|
Idempotent, best-effort helper for local development. Run AFTER the stack
|
|
is up (casdoor on http://localhost:8080, admin/123):
|
|
|
|
# from the host (needs python3):
|
|
python tools/seed_casdoor.py
|
|
|
|
# or from inside the api container:
|
|
# docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py
|
|
|
|
Casdoor >= v1.9x contract: /api/login expects a JSON body with type="login"
|
|
(not the old form-encoded type=login-normal) and returns a session cookie
|
|
used for the management REST API. The application is created with the
|
|
`password` grant type — that is what the backend's POST /auth/login uses
|
|
for the in-page login flow.
|
|
"""
|
|
import http.cookiejar
|
|
import json
|
|
import os
|
|
import secrets
|
|
import urllib.request
|
|
|
|
import bcrypt
|
|
|
|
BASE = os.environ.get("CASDOOR_BASE", "http://localhost:8080")
|
|
ADMIN = os.environ.get("CASDOOR_ADMIN", "admin")
|
|
# Console admin password — MUST be overridden outside local dev (see
|
|
# docs/SECRET-MANAGEMENT.md); the historical default "123" is gone.
|
|
ADMIN_PW = os.environ.get("CASDOOR_ADMIN_PW", "")
|
|
if not ADMIN_PW:
|
|
ADMIN_PW = secrets.token_urlsafe(12)
|
|
print(f"[seed] generated Casdoor console admin password: {ADMIN_PW}")
|
|
|
|
ORG = "inventory"
|
|
APP = "inventory-app"
|
|
CLIENT_ID = "inventory-app-client"
|
|
CLIENT_SECRET = os.environ.get("CASDOOR_CLIENT_SECRET", "")
|
|
if not CLIENT_SECRET:
|
|
CLIENT_SECRET = secrets.token_urlsafe(24)
|
|
print(f"[seed] generated Casdoor client secret: {CLIENT_SECRET}")
|
|
REDIRECT_URI = "http://localhost/login" # Traefik edge (v1.3); unused by the in-page flow
|
|
|
|
# One password for all seeded dev users — override with CASDOOR_SEED_PASSWORD;
|
|
# stored as a bcrypt hash (passwordType "bcrypt"), never plaintext.
|
|
SEED_PASSWORD = os.environ.get("CASDOOR_SEED_PASSWORD", "123456")
|
|
|
|
USERS = [
|
|
# (name, first_name, last_name, email)
|
|
("razaei", "آقای", "رضایی", "razaei@sample.ir"),
|
|
("mousavi", "آقای", "موسوی", "mousavi@sample.ir"),
|
|
("mohaghegh", "آقای", "محقق", "mohaghegh@sample.ir"),
|
|
("tester", "کاربر", "تست", "tester@sample.ir"),
|
|
]
|
|
|
|
|
|
def _request(method: str, path: str, body=None, cookies=None):
|
|
url = BASE + path
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
req = urllib.request.Request(url, data=data, method=method)
|
|
req.add_header("Content-Type", "application/json")
|
|
jar = cookies if cookies is not None else http.cookiejar.CookieJar()
|
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
try:
|
|
with opener.open(req, timeout=20) as resp:
|
|
return resp.status, json.loads(resp.read() or b"{}")
|
|
except Exception as exc:
|
|
return 0, {"error": str(exc)}
|
|
|
|
|
|
def _login():
|
|
"""Casdoor admin login (JSON body, type=login) -> session cookie jar."""
|
|
jar = http.cookiejar.CookieJar()
|
|
status, body = _request("POST", "/api/login", {
|
|
"type": "login",
|
|
"organization": "built-in",
|
|
"username": ADMIN,
|
|
"password": ADMIN_PW,
|
|
"application": "app-built-in",
|
|
}, cookies=jar)
|
|
if body.get("status") != "ok":
|
|
print("admin login failed:", status, body.get("msg", body.get("error", "")))
|
|
return None
|
|
return jar
|
|
|
|
|
|
def _bcrypt(pw: str) -> str:
|
|
return bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt()).decode("ascii")
|
|
|
|
|
|
def main():
|
|
cookies = _login()
|
|
if cookies is None:
|
|
return
|
|
|
|
# 1) organization (bcrypt — passwords are never stored plaintext)
|
|
status, body = _request("POST", "/api/add-organization", {
|
|
"owner": "admin",
|
|
"name": ORG,
|
|
"displayName": "سازمان تدارکات",
|
|
"passwordType": "bcrypt",
|
|
}, cookies)
|
|
print("add-organization:", status, body.get("msg", body.get("error", "")))
|
|
|
|
# 2) application — `password` grant powers the in-page login (backend
|
|
# POST /auth/login); authorization_code keeps Casdoor's own flows working.
|
|
status, body = _request("POST", "/api/add-application", {
|
|
"owner": "admin",
|
|
"name": APP,
|
|
"displayName": "سامانه تدارکات و انبار",
|
|
"organization": ORG,
|
|
"clientId": CLIENT_ID,
|
|
"clientSecret": CLIENT_SECRET,
|
|
"redirectUris": [REDIRECT_URI],
|
|
"grantTypes": ["authorization_code", "refresh_token", "password"],
|
|
"signinMethods": [{"name": "Password"}],
|
|
"enableSignUp": False,
|
|
"tokenFormat": "JWT",
|
|
"expireInHours": 24,
|
|
}, cookies)
|
|
print("add-application:", status, body.get("msg", body.get("error", "")))
|
|
|
|
# 3) users (roles are NOT stored here — they live in the app's users table)
|
|
pw_hash = _bcrypt(SEED_PASSWORD)
|
|
for name, first, last, email in USERS:
|
|
status, body = _request("POST", "/api/add-user", {
|
|
"owner": ORG,
|
|
"name": name,
|
|
"displayName": f"{first} {last}",
|
|
"firstName": first,
|
|
"lastName": last,
|
|
"email": email,
|
|
"password": pw_hash,
|
|
"passwordType": "bcrypt",
|
|
"phone": "",
|
|
"type": "normal-user",
|
|
}, cookies)
|
|
print(f"add-user {name}:", status, body.get("msg", body.get("error", "")))
|
|
# Ensure passwordType even for already-existing users (this Casdoor
|
|
# version refuses login when both user and org passwordType are empty).
|
|
status, body = _request("POST", f"/api/update-user?id={ORG}/{name}&columns=password_type,password", {
|
|
"passwordType": "bcrypt",
|
|
"password": pw_hash,
|
|
}, cookies)
|
|
print(f"set-password-type {name}:", status, body.get("msg", body.get("error", "")))
|
|
|
|
print("\nDone. Casdoor console: http://localhost:8080")
|
|
print(f"Seeded users password: {SEED_PASSWORD} (change via CASDOOR_SEED_PASSWORD)")
|
|
print("Login in the app: http://localhost/login")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|