feat: isolate households and add explicit ownership succession

This commit is contained in:
Codex
2026-10-04 10:43:17 +08:00
parent a3f4544ddf
commit 72a3252e25
15 changed files with 635 additions and 60 deletions
+41 -7
View File
@@ -30,7 +30,7 @@ class Accounts:
@staticmethod
def public(user):
return {k: v for k, v in user.items() if k not in ('salt', 'hash')} if user else None
return {k: v for k, v in user.items() if k not in ('salt', 'hash') and not k.startswith('_')} if user else None
def password(self, password):
if not isinstance(password, str) or not 8 <= len(password) <= 128:
@@ -50,6 +50,9 @@ class Accounts:
def save(self, data, actor):
a = self.a
with a.LOCK:
if actor and not a.HOUSEHOLDS.manager(actor):
raise a.Problem('需要家庭管理员权限', 403)
family = a.HOUSEHOLDS.prepare(actor, 'users', data)
old = a.get_object('users', data.get('id'))
if data.get('id') and not old:
raise a.Problem('账号不存在', 404)
@@ -61,29 +64,47 @@ class Accounts:
raise a.Problem('用户名已存在', 409)
role = data.get('role', 'member')
access = data.get('familyAccess', 'none')
if role not in ('admin', 'member') or access not in ('none', 'read', 'edit'):
if role not in ('admin', 'family_admin', 'member') or access not in ('none', 'read', 'edit'):
raise a.Problem('权限选项不正确')
if actor and actor['role'] != 'admin' and (role == 'admin' or (old and old['role'] == 'admin')):
raise a.Problem('家庭管理员不能创建或修改超级管理员', 403)
home = a.get_object('households', family)
if actor and actor['role'] != 'admin' and home.get('ownerId') and actor['id'] != home['ownerId']:
if (role == 'family_admin' and not old) or (old and old['id'] != actor['id'] and
(role == 'family_admin' or old['role'] == 'family_admin')):
raise a.Problem('请由一家之主或超级管理员管理家庭管理员账号', 403)
sites = data.get('siteIds', [])
if not isinstance(sites, list) or any(not isinstance(s, str) or not a.get_object('sites', s) for s in sites):
raise a.Problem('请选择有效空间')
for site in sites:
a.HOUSEHOLDS.owns(actor, a.get_object('sites', site))
ptz_control = data.get('ptzControl', old.get('ptzControl', False) if old else False)
if not isinstance(ptz_control, bool):
raise a.Problem('云台权限格式不正确')
disabled = data.get('disabled', False)
if not isinstance(disabled, bool):
raise a.Problem('账号状态不正确')
if old and uid in (home.get('ownerId'), home.get('successorId')) and (disabled or role != 'family_admin'):
raise a.Problem('请先交接家主或更换备用负责人,再停用或调整此账号')
if old and old['role'] == 'admin' and (role != 'admin' or disabled):
if not any(u['id'] != uid and u['role'] == 'admin' and not u['disabled'] for u in a.objects('users')):
raise a.Problem('至少保留一个启用的管理员')
if old and old['role'] == 'family_admin' and (role != 'family_admin' or disabled):
if not any(u['id'] != uid and u['role'] == 'family_admin' and not u['disabled']
for u in a.HOUSEHOLDS.objects(actor, 'users')):
raise a.Problem('至少保留一个启用的家庭管理员')
if actor and actor['id'] == uid and (disabled or role != actor['role']):
raise a.Problem('不能停用或降低当前登录账号的管理权限')
related = a.clean_text(data.get('relatedToId', ''), 32)
person = a.clean_text(data.get('personId', ''), 32)
if related and related != uid and not a.get_object('users', related):
raise a.Problem('关系参照账号不存在')
if related and related != uid:
a.HOUSEHOLDS.owns(actor, a.get_object('users', related))
if person:
if not a.get_object('people', person):
raise a.Problem('家谱人物不存在')
a.HOUSEHOLDS.owns(actor, a.get_object('people', person))
if any(u.get('personId') == person and u['id'] != uid for u in a.objects('users')):
raise a.Problem('这个人物已经关联其他账号')
password = data.get('password', '')
@@ -91,11 +112,11 @@ class Accounts:
raise a.Problem('密码格式不正确')
credentials = self.password(password) if password or not old else {k: old[k] for k in ('salt', 'hash')}
user = dict(id=uid, username=username, name=a.clean_text(data.get('name', username), 80, True),
role=role, disabled=disabled, siteIds=sorted(set(sites)), familyAccess=access, ptzControl=ptz_control,
role=role, disabled=disabled, familyId=family, siteIds=sorted(set(sites)), familyAccess=access, ptzControl=ptz_control,
personId=person, relatedToId=related,
relationship=a.clean_text(data.get('relationship', ''), 40), **credentials)
# Revoke sessions when credentials or authorization change, not for label edits.
if old and any(old.get(k) != user.get(k) for k in ('hash', 'role', 'siteIds', 'familyAccess', 'disabled', 'username', 'ptzControl')):
if old and any(old.get(k) != user.get(k) for k in ('hash', 'role', 'familyId', 'siteIds', 'familyAccess', 'disabled', 'username', 'ptzControl', 'personId')):
a.DB.execute('DELETE FROM sessions WHERE user_id=?', (uid,))
a.save_object('users', user)
a.audit('更新账号' if old else '建立账号', username)
@@ -128,13 +149,18 @@ class Accounts:
def camera(self, user, camera):
if not camera:
raise self.a.Problem('摄像头不存在', 404)
if user['role'] != 'admin' and camera.get('siteId') not in user['siteIds']:
if user['role'] != 'admin':
self.a.HOUSEHOLDS.owns(user, camera)
site = self.a.get_object('sites', camera.get('siteId'))
if site:
self.a.HOUSEHOLDS.owns(user, site)
if not self.a.HOUSEHOLDS.manager(user) and camera.get('siteId') not in user['siteIds']:
raise self.a.Problem('没有这个空间的访问权限', 403)
return camera
def control(self, user, camera):
self.camera(user, camera)
if user['role'] != 'admin' and not user.get('ptzControl', False):
if not self.a.HOUSEHOLDS.manager(user) and not user.get('ptzControl', False):
raise self.a.Problem('没有云台控制权限,请联系管理员授权', 403)
if not camera.get('enabled'):
raise self.a.Problem('摄像头已停用', 409)
@@ -142,7 +168,15 @@ class Accounts:
def state(self, user, state):
state['user'] = self.public(user)
if user['role'] == 'admin':
family = self.a.HOUSEHOLDS.scope(user)
state['familyId'] = family
state['households'] = self.a.HOUSEHOLDS.list(user)
for key in ('sites', 'assets', 'cameras', 'recorders'):
state[key] = [x for x in state[key] if self.a.HOUSEHOLDS.family_of(x) == family]
if user['role'] != 'admin':
state['scan'] = {}
state['storage'] = {}
if self.a.HOUSEHOLDS.manager(user):
return state
permitted = set(user['siteIds'])
state['sites'] = [s for s in state['sites'] if s['id'] in permitted]