from .db_logging import log_change def ensure_entity_active(cursor, entity_id): """ Ensure an entity exists and is active. Revoked entities are immutable. """ cursor.execute("SELECT status FROM entity WHERE id = %s", (entity_id,)) row = cursor.fetchone() if row is None: raise ValueError("Entity does not exist") if row["status"] != "active": raise ValueError("Entity is not active") def _validate_ca_reference_for_group(ca_reference): if ca_reference is None: raise ValueError("ca_reference is required for groups") if not isinstance(ca_reference, str): raise ValueError("ca_reference must be a string") if len(ca_reference) == 0: raise ValueError("ca_reference cannot be empty") if len(ca_reference) > 100: raise ValueError("ca_reference must be at most 100 characters") def is_creator(cursor, entity_id): """ Return True if the entity is a creator. A creator is: - an entity of type 'person' - with a row in property where property_name = 'creator' """ cursor.execute("SELECT type FROM entity WHERE id = %s", (entity_id,)) row = cursor.fetchone() if row is None: return False if row["type"] != "person": return False cursor.execute( "SELECT 1 FROM property WHERE id = %s AND property_name = %s", (entity_id, "creator"), ) return cursor.fetchone() is not None def ensure_creator(cursor, creator_id): """ Ensure creator_id exists, is active, and references a creator. A creator is a 'person' entity that has the 'creator' property. """ ensure_entity_active(cursor, creator_id) if not is_creator(cursor, creator_id): raise ValueError("creator_id must reference a creator") def insert_creator(cursor, name, public_key): """ Create a creator. Creators are persons with property 'creator' in the property table. """ cursor.execute( """ INSERT INTO entity (name, type, public_key, status, ca_reference) VALUES (%s, 'person', %s, 'active', NULL) RETURNING id """, (name, public_key), ) creator_id = cursor.fetchone()["id"] # Mark as creator via property table (schema: property(id, property_name)) cursor.execute( """ INSERT INTO property (id, property_name) VALUES (%s, %s) ON CONFLICT (id, property_name) DO NOTHING """, (creator_id, "creator"), ) log_change(cursor, f"Created creator entity {creator_id} with name {name}") return creator_id def enroll_person(cursor, name, public_key, creator_id): """ Enroll a new person under a creator. creator_id must refer to an active creator (person + 'creator' property). """ ensure_creator(cursor, creator_id) cursor.execute( """ INSERT INTO entity (name, type, public_key, creator, status, ca_reference) VALUES (%s, 'person', %s, %s, 'active', NULL) RETURNING id """, (name, public_key, creator_id), ) person_id = cursor.fetchone()["id"] log_change(cursor, f"Enrolled person {person_id} under creator {creator_id}") return person_id def create_group(cursor, name, public_key, creator_id, ca_reference): """ Create a group under a creator. creator_id must refer to an active creator (person + 'creator' property). Groups must define a non-empty ca_reference. """ ensure_creator(cursor, creator_id) _validate_ca_reference_for_group(ca_reference) cursor.execute( """ INSERT INTO entity (name, type, public_key, creator, status, ca_reference) VALUES (%s, 'group', %s, %s, 'active', %s) RETURNING id """, (name, public_key, creator_id, ca_reference), ) group_id = cursor.fetchone()["id"] log_change( cursor, f"Created group {group_id} under creator {creator_id} with ca_reference {ca_reference}", ) return group_id def get_entity(cursor, entity_id): cursor.execute("SELECT * FROM entity WHERE id = %s", (entity_id,)) return cursor.fetchone() def set_entity_status(cursor, entity_id, status, changed_by): """ Update entity status. Only active entities can change status. Once revoked, immutable. """ ensure_entity_active(cursor, entity_id) cursor.execute("UPDATE entity SET status = %s WHERE id = %s", (status, entity_id)) log_change(cursor, f"Set status of entity {entity_id} to {status} by {changed_by}") def set_symmetrical_key(cursor, entity_id, key_value, changed_by): ensure_entity_active(cursor, entity_id) cursor.execute( "UPDATE entity SET symmetrical_key = %s WHERE id = %s", (key_value, entity_id), ) log_change(cursor, f"Set symmetrical_key for entity {entity_id} by {changed_by}") def get_symmetrical_key(cursor, entity_id): cursor.execute("SELECT symmetrical_key FROM entity WHERE id = %s", (entity_id,)) row = cursor.fetchone() return row["symmetrical_key"] if row else None def set_entity_keys(cursor, entity_id, public_key, changed_by): ensure_entity_active(cursor, entity_id) cursor.execute( "UPDATE entity SET public_key = %s WHERE id = %s", (public_key, entity_id), ) log_change(cursor, f"Updated public key for entity {entity_id} by {changed_by}")