403 lines
22 KiB
Python
403 lines
22 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
user_scopes(数据范围)隔离实测 — 在测试库 pms_dev + 测试环境 API(:3000) 上跑
|
|||
|
|
|
|||
|
|
覆盖验收 1-6:
|
|||
|
|
1. 造两个项目(同组织)+ 两个用户(同组织、role_level=0)+ 一个总管理员
|
|||
|
|
2. 未配置 scope 的用户 → 能看到两个项目的设备(= 现状,证明向后兼容)
|
|||
|
|
3. 配置 scope 只允许项目 A 的用户 → 只能看到项目 A 的设备(列表 + 详情都测)
|
|||
|
|
4. 总管理员 → 看到全部(不受 scope 影响)
|
|||
|
|
5. 配置一个不属于该组织的项目 id / 组织 id / 不存在的 id → 被 BadRequest 拒绝
|
|||
|
|
6. 清理临时数据(脚本 finally 里无条件执行)
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
set PMS_TEST_JWT_SECRET=<测试环境 .env 里的 JWT_SECRET>
|
|||
|
|
python scope_isolation_check.py
|
|||
|
|
"""
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import json
|
|||
|
|
import random
|
|||
|
|
import urllib.request
|
|||
|
|
import urllib.error
|
|||
|
|
|
|||
|
|
import pymysql
|
|||
|
|
import jwt # PyJWT
|
|||
|
|
|
|||
|
|
DB = dict(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev")
|
|||
|
|
API = os.environ.get("PMS_TEST_API_BASE", "http://10.8.0.252:3000/api")
|
|||
|
|
SECRET = os.environ.get("PMS_TEST_JWT_SECRET", "")
|
|||
|
|
|
|||
|
|
TAG = "SCOPETEST"
|
|||
|
|
SUFFIX = f"{int(time.time()) % 100000:05d}"
|
|||
|
|
PASS, FAIL = [], []
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ok(name, cond, detail=""):
|
|||
|
|
(PASS if cond else FAIL).append(name)
|
|||
|
|
print(f" [{'PASS' if cond else 'FAIL'}] {name} {detail}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def token(uid, phone):
|
|||
|
|
now = int(time.time())
|
|||
|
|
return jwt.encode({"sub": uid, "phone": phone, "exp": now + 3600, "iat": now},
|
|||
|
|
SECRET, algorithm="HS256")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def call(method, path, tok=None, body=None):
|
|||
|
|
"""返回 (status, json_or_text)"""
|
|||
|
|
req = urllib.request.Request(API + path, method=method)
|
|||
|
|
req.add_header("Content-Type", "application/json")
|
|||
|
|
if tok:
|
|||
|
|
req.add_header("Authorization", "Bearer " + tok)
|
|||
|
|
data = json.dumps(body).encode() if body is not None else None
|
|||
|
|
try:
|
|||
|
|
with urllib.request.urlopen(req, data, timeout=20) as r:
|
|||
|
|
raw = r.read().decode()
|
|||
|
|
return r.status, (json.loads(raw) if raw else None)
|
|||
|
|
except urllib.error.HTTPError as e:
|
|||
|
|
raw = e.read().decode()
|
|||
|
|
try:
|
|||
|
|
return e.code, json.loads(raw)
|
|||
|
|
except Exception:
|
|||
|
|
return e.code, raw
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
if not SECRET:
|
|||
|
|
print("缺少 PMS_TEST_JWT_SECRET 环境变量(取 /data/pms_dev/api-server/.env 的 JWT_SECRET)")
|
|||
|
|
sys.exit(2)
|
|||
|
|
|
|||
|
|
conn = pymysql.connect(**DB, autocommit=True)
|
|||
|
|
cur = conn.cursor()
|
|||
|
|
ids = {}
|
|||
|
|
try:
|
|||
|
|
# ---------------- 0. 预清理(防上次异常退出残留) ----------------
|
|||
|
|
cleanup(cur)
|
|||
|
|
|
|||
|
|
# ---------------- 1. 造夹具 ----------------
|
|||
|
|
print("\n=== [1] 造夹具 ===")
|
|||
|
|
cur.execute("INSERT INTO organizations (name) VALUES (%s)", (f"{TAG}-ORG-{SUFFIX}",))
|
|||
|
|
org = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO organizations (name) VALUES (%s)", (f"{TAG}-ORG2-{SUFFIX}",))
|
|||
|
|
org2 = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org, f"{TAG}-A-{SUFFIX}"))
|
|||
|
|
pa = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org, f"{TAG}-B-{SUFFIX}"))
|
|||
|
|
pb = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org2, f"{TAG}-C-{SUFFIX}"))
|
|||
|
|
pc = cur.lastrowid
|
|||
|
|
|
|||
|
|
def add_cabinet(pid, seed):
|
|||
|
|
imei = f"86{seed}{random.randint(1000000, 9999999)}"[:15]
|
|||
|
|
# abstract_id 必须给值:h5 设备列表行结构体是 String(DB 可空),
|
|||
|
|
# 留 NULL 会让该接口 500(历史缺陷,非本次改动引入)
|
|||
|
|
cur.execute("INSERT INTO cabinets (project_id, imei, name, status, auth_str, abstract_id) "
|
|||
|
|
"VALUES (%s,%s,%s,1,%s,%s)",
|
|||
|
|
(pid, imei, f"{TAG}-CAB-{seed}", "AbCd1234", f"10-{SUFFIX}{seed}"))
|
|||
|
|
cid = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO cabin_boards (cabinet_id, board_id) VALUES (%s,1)", (cid,))
|
|||
|
|
bid = cur.lastrowid
|
|||
|
|
comp = None
|
|||
|
|
for ch in range(1, 7):
|
|||
|
|
cur.execute("INSERT INTO compartments (cabin_board_id, channel_id) VALUES (%s,%s)", (bid, ch))
|
|||
|
|
if ch == 1:
|
|||
|
|
comp = cur.lastrowid
|
|||
|
|
# 每个柜子造 1 条充电订单 + 1 条设备日志 + 1 条充电记录(用于验证列表接口的范围收窄)
|
|||
|
|
cur.execute("INSERT INTO charge_orders (compartment_id, cabinet_id, battery_id, start_soc, energy, status) "
|
|||
|
|
"VALUES (%s,%s,%s,50,100,1)", (comp, cid, f"{TAG}-BAT-{seed}"))
|
|||
|
|
order_id = cur.lastrowid
|
|||
|
|
cur.execute("INSERT INTO device_logs (cabinet_id, log_type, content) VALUES (%s,1,%s)",
|
|||
|
|
(cid, f"{TAG}-LOG-{seed}"))
|
|||
|
|
cur.execute("INSERT INTO charge_records (compartment_id, cabinet_id, energy, status) VALUES (%s,%s,10,0)",
|
|||
|
|
(comp, cid))
|
|||
|
|
return cid, imei, order_id
|
|||
|
|
|
|||
|
|
cab_a, imei_a, order_a = add_cabinet(pa, "1")
|
|||
|
|
cab_b, imei_b, order_b = add_cabinet(pb, "2")
|
|||
|
|
cab_c, imei_c, order_c = add_cabinet(pc, "3")
|
|||
|
|
|
|||
|
|
def add_user(role, organization_id, label):
|
|||
|
|
phone = f"19{random.randint(100000000, 999999999)}"
|
|||
|
|
cur.execute("INSERT INTO users (phone, name, password, role, organization_id, status) "
|
|||
|
|
"VALUES (%s,%s,%s,%s,%s,1)",
|
|||
|
|
(phone, f"{TAG}-{label}-{SUFFIX}", "not-a-real-hash", role, organization_id))
|
|||
|
|
return cur.lastrowid, phone
|
|||
|
|
|
|||
|
|
u_open, ph_open = add_user(0, org, "OPEN") # 未配置 scope
|
|||
|
|
u_a, ph_a = add_user(0, org, "SCOPEA") # 配 scope=项目A
|
|||
|
|
u_admin, ph_admin = add_user(2, None, "ADMIN") # 总管理员
|
|||
|
|
|
|||
|
|
ids = dict(org=org, org2=org2, pa=pa, pb=pb, pc=pc,
|
|||
|
|
cab_a=cab_a, cab_b=cab_b, cab_c=cab_c,
|
|||
|
|
order_a=order_a, order_b=order_b, order_c=order_c,
|
|||
|
|
u_open=u_open, u_a=u_a, u_admin=u_admin)
|
|||
|
|
print(f" org={org} org2={org2} projA={pa} projB={pb} projC(org2)={pc}")
|
|||
|
|
print(f" cabA={cab_a}/{imei_a} cabB={cab_b}/{imei_b} cabC={cab_c}/{imei_c}")
|
|||
|
|
print(f" user_open={u_open} user_scopeA={u_a} admin={u_admin}")
|
|||
|
|
|
|||
|
|
t_open = token(u_open, ph_open)
|
|||
|
|
t_a = token(u_a, ph_a)
|
|||
|
|
t_admin = token(u_admin, ph_admin)
|
|||
|
|
|
|||
|
|
# ---------------- 2. 未配置 scope = 不限制(向后兼容) ----------------
|
|||
|
|
print("\n=== [2] 未配置 scope 的用户 → 不限制(两个项目都可见)===")
|
|||
|
|
st, body = call("GET", "/cabinets/filter?page_size=100", t_open)
|
|||
|
|
got = [c["id"] for c in body["data"]["data"]] if st == 200 else []
|
|||
|
|
print(f" GET /cabinets/filter -> {st} total={body['data']['total'] if st == 200 else body}")
|
|||
|
|
ok("未配置 scope 可见项目A设备", cab_a in got)
|
|||
|
|
ok("未配置 scope 可见项目B设备", cab_b in got)
|
|||
|
|
ok("未配置 scope 不可见跨组织(项目C)设备", cab_c not in got)
|
|||
|
|
|
|||
|
|
# ---------------- 5. 非法 scope_id 必须 400 ----------------
|
|||
|
|
print("\n=== [5] 非法 scope_id(跨组织/不存在)→ BadRequest ===")
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": pc, "permission_type": 1}]})
|
|||
|
|
print(f" PUT scopes(项目C, 属组织2) -> {st} {body}")
|
|||
|
|
ok("跨组织项目被拒 400", st == 400 and "不属于该用户所属组织" in str(body))
|
|||
|
|
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": 999999999, "permission_type": 1}]})
|
|||
|
|
print(f" PUT scopes(不存在项目) -> {st} {body}")
|
|||
|
|
ok("不存在项目被拒 400", st == 400 and "不存在" in str(body))
|
|||
|
|
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 1, "scope_id": org2, "permission_type": 1}]})
|
|||
|
|
print(f" PUT scopes(组织级=别人的组织) -> {st} {body}")
|
|||
|
|
ok("组织级跨组织被拒 400", st == 400)
|
|||
|
|
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 9}]})
|
|||
|
|
print(f" PUT scopes(permission_type=9) -> {st} {body}")
|
|||
|
|
ok("非法 permission_type 被拒 400", st == 400)
|
|||
|
|
|
|||
|
|
st, body = call("PUT", "/users/999999999/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]})
|
|||
|
|
print(f" PUT scopes(用户不存在) -> {st} {body}")
|
|||
|
|
ok("目标用户不存在 -> 404", st == 404)
|
|||
|
|
|
|||
|
|
# ---------------- 3. 配置 scope=项目A → 收窄 ----------------
|
|||
|
|
print("\n=== [3] 配置 scope 只允许项目 A → 只能看到项目 A ===")
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]})
|
|||
|
|
print(f" PUT /users/{u_a}/scopes {{项目A}} -> {st} {body}")
|
|||
|
|
ok("配置项目范围成功", st == 200)
|
|||
|
|
|
|||
|
|
st, body = call("GET", "/cabinets/filter?page_size=100", t_a)
|
|||
|
|
got = [c["id"] for c in body["data"]["data"]] if st == 200 else []
|
|||
|
|
print(f" GET /cabinets/filter -> {st} total={body['data']['total'] if st == 200 else body}")
|
|||
|
|
print(f" data ids = {got}")
|
|||
|
|
ok("列表:项目A设备可见", cab_a in got)
|
|||
|
|
ok("列表:项目B设备不可见", cab_b not in got)
|
|||
|
|
ok("列表:跨组织设备不可见", cab_c not in got)
|
|||
|
|
|
|||
|
|
st, body = call("GET", f"/cabinets/{cab_a}", t_a)
|
|||
|
|
print(f" GET /cabinets/{cab_a}(范围内详情) -> {st} data.id={body['data']['id'] if st == 200 else body}")
|
|||
|
|
ok("详情:范围内设备 200", st == 200 and body["data"]["id"] == cab_a)
|
|||
|
|
|
|||
|
|
st, body = call("GET", f"/cabinets/{cab_b}", t_a)
|
|||
|
|
print(f" GET /cabinets/{cab_b}(范围外详情) -> {st} {body}")
|
|||
|
|
ok("详情:范围外设备被拒 403", st == 403)
|
|||
|
|
|
|||
|
|
st, body = call("GET", f"/cabinets/{cab_c}", t_a)
|
|||
|
|
print(f" GET /cabinets/{cab_c}(跨组织详情) -> {st} {body}")
|
|||
|
|
ok("详情:跨组织设备被拒 403", st == 403)
|
|||
|
|
|
|||
|
|
st, body = call("GET", f"/projects/{pb}/cabinets", t_a)
|
|||
|
|
print(f" GET /projects/{pb}/cabinets(范围外项目的设备列表) -> {st} {body}")
|
|||
|
|
ok("范围外项目的设备列表被拒 403", st == 403)
|
|||
|
|
|
|||
|
|
st, body = call("GET", f"/projects/{pa}/cabinets", t_a)
|
|||
|
|
print(f" GET /projects/{pa}/cabinets(范围内项目) -> {st} rows={len(body['data']) if st == 200 else body}")
|
|||
|
|
ok("范围内项目设备列表 200", st == 200 and len(body["data"]) == 1)
|
|||
|
|
|
|||
|
|
st, body = call("GET", "/cabinets/realtime-status?imei=" + imei_b, t_a)
|
|||
|
|
print(f" GET /cabinets/realtime-status?imei=(项目B) -> {st} {body}")
|
|||
|
|
ok("范围外设备实时状态被拒 403", st == 403)
|
|||
|
|
|
|||
|
|
# 订单/记录/日志/能耗 列表接口(叠加了范围条件)—— 必须只出现项目 A 的数据
|
|||
|
|
orders_filter_failed = False
|
|||
|
|
st_filter_failed = False
|
|||
|
|
st, body = call("GET", "/charge-orders?page_size=50", t_a)
|
|||
|
|
order_ids = [o["id"] for o in body["data"]["list"]] if st == 200 else []
|
|||
|
|
print(f" GET /charge-orders -> {st} total={body['data']['total'] if st == 200 else body} ids={order_ids}")
|
|||
|
|
if st != 200:
|
|||
|
|
# 诊断:换成「总管理员」再打一次。若同样 500 ⇒ 与数据范围无关(环境/结构问题)
|
|||
|
|
st_admin, body_admin = call("GET", "/charge-orders?page_size=50", t_admin)
|
|||
|
|
print(f" [诊断] 总管理员 GET /charge-orders -> {st_admin} {body_admin}")
|
|||
|
|
ok("订单 500 与数据范围无关(总管理员同样失败)", st_admin == 500)
|
|||
|
|
orders_filter_failed = True
|
|||
|
|
else:
|
|||
|
|
ok("订单列表可访问", st == 200)
|
|||
|
|
ok("订单列表:项目A订单可见", order_a in order_ids)
|
|||
|
|
ok("订单列表:项目B订单不可见", order_b not in order_ids)
|
|||
|
|
ok("订单列表:跨组织订单不可见", order_c not in order_ids)
|
|||
|
|
|
|||
|
|
st, body = call("GET", "/charge-records?page_size=50", t_a)
|
|||
|
|
rec_cabs = [r["cabinet_id"] for r in body["data"]["list"]] if st == 200 else []
|
|||
|
|
print(f" GET /charge-records -> {st} total={body['data']['total'] if st == 200 else body} cabinets={rec_cabs}")
|
|||
|
|
ok("充电记录列表可访问", st == 200)
|
|||
|
|
ok("充电记录:仅项目A柜子", cab_a in rec_cabs and cab_b not in rec_cabs and cab_c not in rec_cabs)
|
|||
|
|
|
|||
|
|
st, body = call("GET", "/device-logs?page_size=50", t_a)
|
|||
|
|
log_cabs = [r["cabinet_id"] for r in body["data"]["list"]] if st == 200 else []
|
|||
|
|
print(f" GET /device-logs -> {st} total={body['data']['total'] if st == 200 else body} cabinets={log_cabs}")
|
|||
|
|
if st != 200:
|
|||
|
|
st_admin, body_admin = call("GET", "/device-logs?page_size=50", t_admin)
|
|||
|
|
print(f" [诊断] 总管理员 GET /device-logs -> {st_admin} {body_admin}")
|
|||
|
|
ok("设备日志 500 与数据范围无关(总管理员同样失败)", st_admin == 500)
|
|||
|
|
st_filter_failed = True
|
|||
|
|
else:
|
|||
|
|
ok("设备日志列表可访问", st == 200)
|
|||
|
|
ok("设备日志:仅项目A柜子", cab_a in log_cabs and cab_b not in log_cabs and cab_c not in log_cabs)
|
|||
|
|
|
|||
|
|
# —— 端点因历史 TIMESTAMP/DATETIME 漂移 500 时,直接用「实现生成的同一条 SQL」在库里验证收窄 ——
|
|||
|
|
if st_filter_failed or orders_filter_failed:
|
|||
|
|
print("\n [SQL 级验证] 用实现生成的同一条 WHERE 条件直接查库:")
|
|||
|
|
sql_orders = (
|
|||
|
|
"SELECT co.id FROM charge_orders co LEFT JOIN cabinets cab ON co.cabinet_id = cab.id WHERE 1=1"
|
|||
|
|
" AND co.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id"
|
|||
|
|
" WHERE p.organization_id = %s)"
|
|||
|
|
" AND co.cabinet_id IN (SELECT c.id FROM cabinets c WHERE c.project_id IN (%s))"
|
|||
|
|
)
|
|||
|
|
cur.execute(sql_orders, (org, pa))
|
|||
|
|
got_orders = [r[0] for r in cur.fetchall()]
|
|||
|
|
print(f" 订单 SQL(组织={org} ∧ 项目={pa}) -> {got_orders}")
|
|||
|
|
ok("SQL 级:订单只命中项目A", order_a in got_orders and order_b not in got_orders and order_c not in got_orders)
|
|||
|
|
|
|||
|
|
sql_logs = (
|
|||
|
|
"SELECT dl.cabinet_id FROM device_logs dl WHERE 1=1"
|
|||
|
|
" AND dl.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id"
|
|||
|
|
" WHERE p.organization_id = %s)"
|
|||
|
|
" AND dl.cabinet_id IN (SELECT c.id FROM cabinets c WHERE c.project_id IN (%s))"
|
|||
|
|
)
|
|||
|
|
cur.execute(sql_logs, (org, pa))
|
|||
|
|
got_logs = [r[0] for r in cur.fetchall()]
|
|||
|
|
print(f" 设备日志 SQL(组织={org} ∧ 项目={pa}) -> {got_logs}")
|
|||
|
|
ok("SQL 级:设备日志只命中项目A柜子", cab_a in got_logs and cab_b not in got_logs and cab_c not in got_logs)
|
|||
|
|
|
|||
|
|
for path in ("/energy-stats/summary", "/energy-stats/projects", "/energy-stats/cabinets", "/energy-stats/trend"):
|
|||
|
|
st, body = call("GET", path, t_a)
|
|||
|
|
print(f" GET {path} -> {st} {json.dumps(body, ensure_ascii=False)[:160] if st == 200 else body}")
|
|||
|
|
ok(f"范围内用户可访问 {path}", st == 200)
|
|||
|
|
|
|||
|
|
st, body = call("GET", "/energy-stats/cabinets", t_a)
|
|||
|
|
rank_ids = [r["cabinet_id"] for r in body["data"]] if st == 200 else []
|
|||
|
|
print(f" 能耗排行 cabinet_ids = {rank_ids}")
|
|||
|
|
ok("能耗排行:仅项目A柜子", cab_a in rank_ids and cab_b not in rank_ids and cab_c not in rank_ids)
|
|||
|
|
|
|||
|
|
# 小程序设备列表(h5)
|
|||
|
|
st, body = call("GET", "/cabinets", t_a)
|
|||
|
|
ids_h5 = [c["id"] for c in body["data"]] if st == 200 else []
|
|||
|
|
print(f" GET /cabinets (小程序列表) -> {st} ids={ids_h5}")
|
|||
|
|
ok("小程序列表:项目A可见", cab_a in ids_h5)
|
|||
|
|
ok("小程序列表:项目B不可见", cab_b not in ids_h5)
|
|||
|
|
|
|||
|
|
# 写操作:范围外设备指令下发(设备必然离线,但归属校验在更前面)
|
|||
|
|
st, body = call("POST", "/charge/start", t_a, {"compartment_id": 0})
|
|||
|
|
print(f" POST /charge/start (空仓体) -> {st} {body}")
|
|||
|
|
ok("写操作接口有归属校验路径(非 500)", st in (400, 403, 404))
|
|||
|
|
|
|||
|
|
# ---------------- 3b. 组织级 scope = 自己组织(不额外收窄) ----------------
|
|||
|
|
print("\n=== [3b] 组织级 scope = 本人所属组织(合法,且不额外收窄)===")
|
|||
|
|
st, body = call("PUT", f"/users/{u_a}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 1, "scope_id": org, "permission_type": 1}]})
|
|||
|
|
print(f" PUT scopes(组织级=本组织) -> {st} {body}")
|
|||
|
|
ok("组织级=本组织 被接受", st == 200)
|
|||
|
|
st, body = call("GET", "/cabinets/filter?page_size=100", t_a)
|
|||
|
|
got = [c["id"] for c in body["data"]["data"]] if st == 200 else []
|
|||
|
|
ok("组织级范围下项目A仍可见", cab_a in got)
|
|||
|
|
ok("组织级范围下项目B仍可见(组织级=整个组织)", cab_b in got)
|
|||
|
|
|
|||
|
|
# ---------------- 3c. 脏数据:一条有效都没有 → 不锁死 ----------------
|
|||
|
|
print("\n=== [3c] 库内脏数据(指向不存在/跨组织/设备级)→ 忽略且不锁死 ===")
|
|||
|
|
st, _ = call("PUT", f"/users/{u_a}/scopes", t_admin, {"scopes": []})
|
|||
|
|
ok("清空范围成功", st == 200)
|
|||
|
|
cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,2,%s,1)",
|
|||
|
|
(u_a, 999999999))
|
|||
|
|
cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,2,%s,1)",
|
|||
|
|
(u_a, pc))
|
|||
|
|
cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,3,%s,1)",
|
|||
|
|
(u_a, cab_b))
|
|||
|
|
st, body = call("GET", "/cabinets/filter?page_size=100", t_a)
|
|||
|
|
got = [c["id"] for c in body["data"]["data"]] if st == 200 else []
|
|||
|
|
print(f" GET /cabinets/filter(脏数据下) -> {st} ids={got}")
|
|||
|
|
ok("脏数据全部被忽略后按「不限制」处理(项目B可见,不锁死)", cab_a in got and cab_b in got)
|
|||
|
|
st, body = call("GET", f"/users/{u_a}/scopes", t_admin)
|
|||
|
|
print(f" GET /users/{u_a}/scopes -> {st} {body['data'] if st == 200 else body}")
|
|||
|
|
# 3 条脏数据:不存在项目 / 跨组织项目 / 设备级(设备级**永不参与鉴权** ⇒ valid=false)
|
|||
|
|
ok("脏数据被标记 valid=false",
|
|||
|
|
st == 200 and len(body["data"]) == 3 and all(not s["valid"] for s in body["data"]))
|
|||
|
|
cur.execute("DELETE FROM user_scopes WHERE user_id=%s", (u_a,))
|
|||
|
|
|
|||
|
|
# ---------------- 4. 总管理员豁免 ----------------
|
|||
|
|
print("\n=== [4] 总管理员 → 看到全部(不受 scope 影响)===")
|
|||
|
|
st, body = call("PUT", f"/users/{u_admin}/scopes", t_admin,
|
|||
|
|
{"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]})
|
|||
|
|
print(f" (故意给总管理员配 scope=项目A) PUT scopes -> {st} {body}")
|
|||
|
|
st, body = call("GET", "/cabinets/filter?page_size=100", t_admin)
|
|||
|
|
got = [c["id"] for c in body["data"]["data"]] if st == 200 else []
|
|||
|
|
ok("总管理员仍可见项目A设备", cab_a in got)
|
|||
|
|
ok("总管理员仍可见项目B设备(豁免)", cab_b in got)
|
|||
|
|
ok("总管理员仍可见跨组织设备(豁免)", cab_c in got)
|
|||
|
|
st, body = call("GET", f"/cabinets/{cab_b}", t_admin)
|
|||
|
|
ok("总管理员可看任意设备详情", st == 200)
|
|||
|
|
finally:
|
|||
|
|
# ---------------- 6. 清理 ----------------
|
|||
|
|
print("\n=== [6] 清理临时数据 ===")
|
|||
|
|
try:
|
|||
|
|
cleanup(cur)
|
|||
|
|
print(" 已清理(scopes/users/cabinets/boards/compartments/projects/organizations)")
|
|||
|
|
cur.execute(f"SELECT COUNT(*) FROM organizations WHERE name LIKE '{TAG}%'")
|
|||
|
|
print(" 残留组织数 =", cur.fetchone()[0])
|
|||
|
|
cur.execute(f"SELECT COUNT(*) FROM users WHERE name LIKE '{TAG}%'")
|
|||
|
|
print(" 残留用户数 =", cur.fetchone()[0])
|
|||
|
|
cur.execute(f"SELECT COUNT(*) FROM projects WHERE name LIKE '{TAG}%'")
|
|||
|
|
print(" 残留项目数 =", cur.fetchone()[0])
|
|||
|
|
cur.execute(f"SELECT COUNT(*) FROM cabinets WHERE name LIKE '{TAG}%'")
|
|||
|
|
print(" 残留设备数 =", cur.fetchone()[0])
|
|||
|
|
except Exception as e: # noqa: BLE001
|
|||
|
|
print(" 清理失败:", e)
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
print(f"\n=== 结果:{len(PASS)} passed / {len(FAIL)} failed ===")
|
|||
|
|
if FAIL:
|
|||
|
|
print("FAILED:", FAIL)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def cleanup(cur):
|
|||
|
|
"""按名称前缀清理本脚本造的数据(幂等)"""
|
|||
|
|
cur.execute(f"SELECT id FROM users WHERE name LIKE '{TAG}%'")
|
|||
|
|
uids = [r[0] for r in cur.fetchall()]
|
|||
|
|
if uids:
|
|||
|
|
fmt = ",".join(["%s"] * len(uids))
|
|||
|
|
cur.execute(f"DELETE FROM user_scopes WHERE user_id IN ({fmt})", uids)
|
|||
|
|
cur.execute(f"DELETE FROM user_roles WHERE user_id IN ({fmt})", uids)
|
|||
|
|
cur.execute(f"DELETE FROM users WHERE id IN ({fmt})", uids)
|
|||
|
|
cur.execute(f"SELECT id FROM cabinets WHERE name LIKE '{TAG}%'")
|
|||
|
|
cids = [r[0] for r in cur.fetchall()]
|
|||
|
|
if cids:
|
|||
|
|
fmt = ",".join(["%s"] * len(cids))
|
|||
|
|
# 先清依赖业务数据(订单/记录/日志/仓体/仓板),再删柜子,避免外键报错
|
|||
|
|
cur.execute(f"DELETE FROM charge_orders WHERE cabinet_id IN ({fmt})", cids)
|
|||
|
|
cur.execute(f"DELETE FROM charge_records WHERE cabinet_id IN ({fmt})", cids)
|
|||
|
|
cur.execute(f"DELETE FROM device_logs WHERE cabinet_id IN ({fmt})", cids)
|
|||
|
|
cur.execute(f"DELETE FROM compartments WHERE cabin_board_id IN "
|
|||
|
|
f"(SELECT id FROM cabin_boards WHERE cabinet_id IN ({fmt}))", cids)
|
|||
|
|
cur.execute(f"DELETE FROM cabin_boards WHERE cabinet_id IN ({fmt})", cids)
|
|||
|
|
cur.execute(f"DELETE FROM cabinets WHERE id IN ({fmt})", cids)
|
|||
|
|
cur.execute(f"SELECT id FROM projects WHERE name LIKE '{TAG}%'")
|
|||
|
|
pids = [r[0] for r in cur.fetchall()]
|
|||
|
|
if pids:
|
|||
|
|
fmt = ",".join(["%s"] * len(pids))
|
|||
|
|
cur.execute(f"DELETE FROM projects WHERE id IN ({fmt})", pids)
|
|||
|
|
cur.execute(f"DELETE FROM organizations WHERE name LIKE '{TAG}%'")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|