fix: invalidate tokens on permission change (medium), optimize create_user query, fix ws reconnect race
- update_permissions now calls blacklist_active_refresh_token and rotate_user_secret to immediately invalidate stale tokens - create_user uses returned id from tx.run_one instead of redundant SELECT - websocket reconnect explicitly closes old connection after token refresh to prevent onclose handler race condition
This commit is contained in:
+8
-4
@@ -160,13 +160,13 @@ def create_user(
|
|||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
with db.in_transaction() as tx:
|
with db.in_transaction() as tx:
|
||||||
tx.run_one(Q_INSERT_USER, (username, password_hash, jwt_secret))
|
user_id = tx.run_one(Q_INSERT_USER, (username, password_hash, jwt_secret))
|
||||||
if permissions:
|
if permissions:
|
||||||
for subsystem, level in permissions.items():
|
for subsystem, level in permissions.items():
|
||||||
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
|
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": db.query(Q_SELECT_USER_BY_NAME, (username,))[0]["id"],
|
"id": user_id,
|
||||||
"username": username,
|
"username": username,
|
||||||
"permissions": _get_permissions(username),
|
"permissions": _get_permissions(username),
|
||||||
}
|
}
|
||||||
@@ -202,9 +202,11 @@ def update_password(username: str, old_password: str, new_password: str) -> bool
|
|||||||
|
|
||||||
|
|
||||||
def update_permissions(username: str, permissions: dict[str, str]) -> None:
|
def update_permissions(username: str, permissions: dict[str, str]) -> None:
|
||||||
"""Update a user's permissions.
|
"""Update a user's permissions and invalidate all existing tokens.
|
||||||
|
|
||||||
Replaces all existing permissions with the provided mapping.
|
Replaces all existing permissions with the provided mapping. Rotates the
|
||||||
|
JWT secret so that permission changes take effect immediately — existing
|
||||||
|
tokens with stale permissions are no longer valid.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username: The username.
|
username: The username.
|
||||||
@@ -219,6 +221,8 @@ def update_permissions(username: str, permissions: dict[str, str]) -> None:
|
|||||||
tx.run(Q_DELETE_PERMISSIONS, (username,))
|
tx.run(Q_DELETE_PERMISSIONS, (username,))
|
||||||
for subsystem, level in permissions.items():
|
for subsystem, level in permissions.items():
|
||||||
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
|
tx.run(Q_UPSERT_PERMISSION, (username, subsystem, level))
|
||||||
|
blacklist_active_refresh_token(username)
|
||||||
|
rotate_user_secret(username)
|
||||||
|
|
||||||
|
|
||||||
def list_users() -> list[dict[str, Any]]:
|
def list_users() -> list[dict[str, Any]]:
|
||||||
|
|||||||
@@ -59,12 +59,14 @@ function _wsConnect() {
|
|||||||
|
|
||||||
if (_wsFailCount >= 3 && !_wsRefreshing) {
|
if (_wsFailCount >= 3 && !_wsRefreshing) {
|
||||||
_wsRefreshing = true;
|
_wsRefreshing = true;
|
||||||
|
const oldConn = _wsConn;
|
||||||
tryRefreshToken().then(ok => {
|
tryRefreshToken().then(ok => {
|
||||||
_wsRefreshing = false;
|
_wsRefreshing = false;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
_wsFailCount = 0;
|
_wsFailCount = 0;
|
||||||
_wsReconnectMs = 0;
|
_wsReconnectMs = 0;
|
||||||
_wsConn = null;
|
_wsConn = null;
|
||||||
|
if (oldConn) oldConn.close();
|
||||||
setTimeout(_wsConnect, 100);
|
setTimeout(_wsConnect, 100);
|
||||||
} else {
|
} else {
|
||||||
redirectLogin();
|
redirectLogin();
|
||||||
|
|||||||
Reference in New Issue
Block a user