From 18fb8e32b30508faf7645240987dba2684021756 Mon Sep 17 00:00:00 2001 From: Ida Date: Fri, 17 Apr 2026 13:48:18 +0200 Subject: [PATCH] bugfix(CON-01) --- modules/datamodels/datamodelUam.py | 1 - modules/routes/routeDataConnections.py | 54 ++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/modules/datamodels/datamodelUam.py b/modules/datamodels/datamodelUam.py index 61e7c105..0c397a6e 100644 --- a/modules/datamodels/datamodelUam.py +++ b/modules/datamodels/datamodelUam.py @@ -191,7 +191,6 @@ class UserConnection(PowerOnModel): json_schema_extra={"frontend_type": "list", "frontend_readonly": True, "frontend_required": False, "label": "Gewährte Berechtigungen"}, ) - @computed_field @computed_field @property def connectionReference(self) -> str: diff --git a/modules/routes/routeDataConnections.py b/modules/routes/routeDataConnections.py index 73123988..290be722 100644 --- a/modules/routes/routeDataConnections.py +++ b/modules/routes/routeDataConnections.py @@ -427,14 +427,54 @@ def update_connection( detail=routeApiMsg("Connection not found") ) - # Update connection fields + # Merge incoming changes into a dict and re-validate via pydantic. + # Direct setattr() bypasses type coercion (PowerOnModel doesn't enable + # validate_assignment), which leaves enum fields as raw strings and + # later breaks .value access. Also filters out computed / unknown keys. + writableFields = set(UserConnection.model_fields.keys()) + previous = connection.model_dump() + merged = dict(previous) for field, value in connection_data.items(): - if hasattr(connection, field): - setattr(connection, field, value) - - # Update lastChecked timestamp using UTC timestamp - connection.lastChecked = getUtcTimestamp() - + if field in writableFields: + merged[field] = value + merged["lastChecked"] = getUtcTimestamp() + connection = UserConnection.model_validate(merged) + + # If this is a remote (non-local) connection and any identity-bearing + # field changed, the stored OAuth tokens no longer match the account. + # Force the user to reconnect: mark PENDING and revoke existing tokens. + identityFields = ("externalUsername", "externalEmail", "externalId", "authority") + authorityValue = ( + connection.authority.value + if hasattr(connection.authority, "value") + else str(connection.authority) + ) + isRemote = authorityValue != AuthAuthority.LOCAL.value + identityChanged = any( + previous.get(field) != merged.get(field) for field in identityFields + ) + if isRemote and identityChanged: + connection.status = ConnectionStatus.PENDING + connection.expiresAt = None + try: + existingTokens = interface.db.getRecordset( + Token, recordFilter={"connectionId": connectionId} + ) + for token in existingTokens: + interface.revokeTokenById( + token["id"], + revokedBy=currentUser.id, + reason="connection identity changed", + ) + logger.info( + f"Revoked {len(existingTokens)} token(s) for connection " + f"{connectionId} after identity change; reconnect required." + ) + except Exception as e: + logger.warning( + f"Failed to revoke tokens for connection {connectionId}: {str(e)}" + ) + # Update connection - models now handle timestamp serialization automatically interface.db.recordModify(UserConnection, connectionId, connection.model_dump())