From a1be210f589a7ef1d12b987942968cf431c8dac2 Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Thu, 26 Feb 2026 17:28:43 +0100 Subject: [PATCH] felt rettelser --- PROJECT_CONTEXT.md | 167 ++++++++++++++++++ ca_core/__pycache__/entity.cpython-313.pyc | Bin 4972 -> 5184 bytes ca_core/__pycache__/metadata.cpython-313.pyc | Bin 2451 -> 3369 bytes ca_core/__pycache__/property.cpython-313.pyc | Bin 1589 -> 4062 bytes ca_core/entity.py | 50 +++--- ca_core/metadata.py | 21 +++ ca_core/property.py | 78 +++++++- create_tables.sql | 18 +- create_tables.sql.old | 48 ----- tests/__pycache__/test_entity.cpython-313.pyc | Bin 5164 -> 6345 bytes tests/__pycache__/test_group.cpython-313.pyc | Bin 6805 -> 6898 bytes .../__pycache__/test_metadata.cpython-313.pyc | Bin 4303 -> 4893 bytes .../__pycache__/test_property.cpython-313.pyc | Bin 5353 -> 7017 bytes tests/test_entity.py | 19 +- tests/test_group.py | 11 +- tests/test_metadata.py | 6 + tests/test_property.py | 33 +++- 17 files changed, 354 insertions(+), 97 deletions(-) create mode 100644 PROJECT_CONTEXT.md delete mode 100644 create_tables.sql.old diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md new file mode 100644 index 0000000..c2bc6a0 --- /dev/null +++ b/PROJECT_CONTEXT.md @@ -0,0 +1,167 @@ +CA/PKI Backend Project Context +Stack + +Python 3 + psycopg (dict_row cursors) + +PostgreSQL database: ca + +Unit tests: unittest (python3 -m unittest discover) + +Database Schema (current assumptions) +entity + +id INT identity PK + +creation_ts TIMESTAMPTZ default now() + +creator INT FK → entity(id) (the entity that created this one; nullable) + +name VARCHAR(100) NOT NULL + +type VARCHAR(...) NOT NULL (e.g. person, group, device, alias) + +public_key VARCHAR(300) NOT NULL + +symmetrical_key VARCHAR(100) NULL + +status VARCHAR(...) NOT NULL default 'active' (values: 'active', 'revoked') + +expiration DATE NULL + +Index on entity(name) (and other indexes as needed) + +group_member + +group_id INT FK → entity(id) ON DELETE CASCADE + +member_id INT FK → entity(id) ON DELETE CASCADE + +role VARCHAR(10) + +PK (group_id, member_id) + +Index (member_id, group_id) + +Groups can contain any entity type, including other groups and devices. + +property + +Columns: (id INT FK → entity(id), property_name VARCHAR(100)) + +PK (id, property_name) + +Used for flags/roles such as "creator" + +metadata + +Intended “singleton row” table, enforced at application level + +Columns: name, comment, private_key, public_key + +log + +id SERIAL PK + +ts TIMESTAMPTZ default now() + +entry TEXT NOT NULL + +Every API mutation must log one row here. + +Core Business Rules + +Creators are not an entity type + +creator is a property (property_name='creator') on a person entity. + +insert_creator() creates a person entity and inserts the creator property. + +Revoked entities are immutable + +Any mutation on an entity requires ensure_entity_active(cursor, entity_id). + +Revoked entities cannot: + +Join groups or accept members + +Add/delete properties + +Change keys (public_key, symmetrical_key) + +Change status again + +Logging + +All changes to entity, group_member, property, metadata must call log_change(cursor, "...") + +Logging happens inside the same transaction (no extra commits). + +Python Modules (current structure) + +ca_core/entity.py + +Must provide: + +ensure_entity_active(cursor, entity_id) + +insert_creator(cursor, name, public_key) + +enroll_person(cursor, name, public_key, creator_id) + +create_group(cursor, name, public_key, creator_id) + +create_alias(cursor, target_entity_id) + +get_entity(cursor, entity_id) + +set_entity_status(cursor, entity_id, status, changed_by) (requires active entity) + +set_entity_keys(cursor, entity_id, public_key, changed_by) (active-only) + +set_symmetrical_key(cursor, entity_id, key, changed_by) (active-only) + +get_symmetrical_key(cursor, entity_id) + +ca_core/group_member.py + +Uses member_id (not person_id) + +Must prevent adding revoked groups/members (via ensure_entity_active) + +Logs add/remove membership + +ca_core/property.py + +Table is property(id, property_name) (NOT entity_id/name) + +Must reject mutations if entity revoked (immutability) + +Logs set/delete property + +ca_core/metadata.py + +Updates metadata fields and logs changes + +ca_core/db_logging.py + +log_change(cursor, message: str) inserts into log(entry) + +Tests + +tests/test_entity.py, tests/test_group.py, tests/test_property.py, tests/test_metadata.py + +Tests verify: + +Core behaviors (create, enroll, group membership, revoke immutability) + +Log entry is created for mutations (case-insensitive substring checks) + +Run via: + +python3 -m unittest discover + +Known gotchas + +Avoid naming a module logging.py (conflicts with stdlib). Use db_logging.py. + +Schema and code must stay aligned (e.g., property.id/property_name, group_member.member_id). diff --git a/ca_core/__pycache__/entity.cpython-313.pyc b/ca_core/__pycache__/entity.cpython-313.pyc index 9fe9993a1344f3597b1301e9147b1bae6635b3a2..78268d16113a92b23d119cbe06148eb604c51cad 100644 GIT binary patch delta 1401 zcmZ`(L2nyH6rS~3sl?K+O*)J{62X=*2~NSj2sp`@xnEl5!)wTvQ#));%6t{U56 zc1^(&a=38>4M-Lek`Iv(NQfd!J#pxfqa#-$GUmV$35l@n58%zZZe1uN`RUDj-}m0U znQ#5)-2H_9aCFp*U^RD}=6vu+{hkM7z8DPGe}>?fD2oa{gtoANh?T)Bk9`AgN&OfC z)RsK(S3G?$@i9J4Wb!obrYey57md8%7ZjCeIALde&LdaIm%PcHcF}+ zwUdQ>ju|%%W|RvCH7&}F_p2r|iu7iM(QQ_#?pn2}0jTbxN>$6I?-?{tEt{F;Z6=1R z#Rni4Q^=Rg6&GcccI}V;C<@5JGms}`Y+9;7#j}fxv~VlW@&(&qR#Ijo!0CET%Pd=V z9*)d>$&?HJ`G?)ry|S>paF(*=X(KFIHw#k_6gh9Nl^DCZ8uCIEH}ewH}Z9y(OV z4%AprkG3b*I{L-7cCqIh|7>Uf)n9yx17E^Tq&xcgwzhh{r+PoV&{3m}OC5E}&Aimn z(`_yNB(c;{r`^Qmj((-BT{)7Fc7g?4_dNvTulV=c${37eSFIEsIv)Q-p5PO5L`l(& z?D{&VBPUwxasZQQIAXl@E}6sl8z8?n`aGJ>|5E%gNZ?U(R?$r83zPMckD|87WJ2l##O%Sdx`>w4@9%$t+4L8@2ua8C4yblvDJS z1@|L{yfhN7Qz%>;qjyaE*6@WI6y|iN7q9*AD^UJ(1p*WS)74y!_bsM}r)RUu)$OAdmlV_XnJdn@vAJlQY%zyXA5{H_8=#4g_4R7Oy5q2!M&PnCQ<$XC_)OQIiSqw}=IW?|x_6g;J$h-7(f!8e-xDwAR2J yNf_hb&@#b=%48h#x+kx3kJ(mEb@Wxo; z2aydwksWvN;4Y#RJa`&-@EWm*6a^8F9!e*8@! z1p*=ir`Omn-3vaKU$FHsj}W>=PWdOjrlngIlU(*@>YLufh>ruav;L|hi4Xz8SwIHK zf*_GiZ-T`kLWCsRAuojk{s=H}l%^Q@ER6DfP>zy_SR_-SD9QOAp$Gc{akAs@q5`8h z1f;5b1{=C%6xAhMu3T4fHx=oDl;W|U)~+-;Erm56o>h9V3{_KFbXp5eCr-fC41{@+ zbc^X(CALw!TYDQ+w>);dLcJ*0ew`W(f<8uuz``#a3pT`RHEwYe) zoen5G5PJ{FTxeX%v3VpjoII1`@~q6{c!$YN?yIRxo*iUT z-fZ*rHl)InB@R!RZ;;{ zjTNh8t*NeS9>=w&V(ECvunI0#C>5JNSFBVt>4*JgqJ!c=faW301xO3)%m=P(E7st~ zK5^ph?2T-TdO@ar^$w*@kijc zOyo^2lsOh(c6fQ%yh JiS$PjKLPzt;J*L> diff --git a/ca_core/__pycache__/metadata.cpython-313.pyc b/ca_core/__pycache__/metadata.cpython-313.pyc index c13f1fa75854ebc06ead45493db3e41dd7bb42a1..69bac03f682f0ad503cee97d291a8950ebc58f24 100644 GIT binary patch delta 773 zcmZuuOHUI~6rSnKVCT_C!1zF8E)~!r+G>ItEgMtYNGv5`3L8zD+xAjMhneQi1(Iq4 z#)Y`DWHzo0#+5tW`7_#pNkfdkfdKA3cX(;yN$z>gneU$OyTQPh!T5eGrU_sZi<@>K zx)Hx1&TU-FB1?Uyjz=tQPyt_|NJ?*3Wm9Q= zwg^LPfuKdga2+#@PG-=&gBxj0D=*kSVz}xM#1l#|BXxv*gpqH%4Tmt#MbukC_z(#< zn~q(>%!al@mWh+ra!ZsFm!X>HE<@yBZ1)!23xGARPO8{H1ozO2-6RORbwtTc8+N5E ztx*LSw{8RzQcoVR1HZq3a{Q*}5{@gzaIk|L|ZQD$zcGFe5Yyfg*5z1RO$? ztGN^N39FnS(-ia`@R|GI)`VR{Q1rK@*QL&YIlV29^<<4#z3APZ+P`ibD3a255EYcf z`f}&uz3=MRm!0#uAMzt=aO^%x*b;SXA{56aLn6W1e-$JnGpn%MtY}3zOFq_+p7J!b z2c`y(> zmfBCcNwlR8b>yK?f|6+jnmTUK5daItrK3Ss9n?&yIWNBuRMfHIJwp;RyM`|2#m(}8 Sz~k;^MNEAt{}v#GSbqTI3e3#_ delta 93 zcmZ1}HCdSNGcPX}0}!;uF34QYI+0I;(Q2Z)HA_0DCfCNMg&fR&nw*o%c}x^nG8FLu nl@);$-{P>z%}*)KNwq6d0&*FFxL9ZMZ5}PI&pgbG`e20sIky#* diff --git a/ca_core/__pycache__/property.cpython-313.pyc b/ca_core/__pycache__/property.cpython-313.pyc index 082130256df40646b1812d03c2cf857f7add1e8d..c414d69a3b81aacfe500bd77717285b8b0923976 100644 GIT binary patch literal 4062 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 U&#*i@{Jbl~o*OwWgUPZSTpVGJKB2C` z3L2B&v+Hv!xcDph`GE0O|=F#>V143PN1%*e=i dmqGC^gZd{ne#St?PbzGTdY^^37(*Gsx&i18cuW8Q diff --git a/ca_core/entity.py b/ca_core/entity.py index 89fbdf8..8c565be 100644 --- a/ca_core/entity.py +++ b/ca_core/entity.py @@ -14,14 +14,25 @@ def ensure_entity_active(cursor, entity_id): 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 insert_creator(cursor, name, public_key): """ Creators are persons with property 'creator' in the property table. """ cursor.execute( """ - INSERT INTO entity (name, type, public_key, status) - VALUES (%s, 'person', %s, 'active') + INSERT INTO entity (name, type, public_key, status, ca_reference) + VALUES (%s, 'person', %s, 'active', NULL) RETURNING id """, (name, public_key), @@ -47,8 +58,8 @@ def enroll_person(cursor, name, public_key, creator_id): cursor.execute( """ - INSERT INTO entity (name, type, public_key, creator, status) - VALUES (%s, 'person', %s, %s, 'active') + 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), @@ -59,38 +70,25 @@ def enroll_person(cursor, name, public_key, creator_id): return person_id -def create_group(cursor, name, public_key, creator_id): +def create_group(cursor, name, public_key, creator_id, ca_reference): ensure_entity_active(cursor, creator_id) + _validate_ca_reference_for_group(ca_reference) cursor.execute( """ - INSERT INTO entity (name, type, public_key, creator, status) - VALUES (%s, 'group', %s, %s, 'active') + 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), + (name, public_key, creator_id, ca_reference), ) group_id = cursor.fetchone()["id"] - log_change(cursor, f"Created group {group_id} under creator {creator_id}") - return group_id - - -def create_alias(cursor, target_entity_id): - ensure_entity_active(cursor, target_entity_id) - - cursor.execute( - """ - INSERT INTO entity (name, type, creator, status) - VALUES (%s, 'alias', %s, 'active') - RETURNING id - """, - (f"alias_for_{target_entity_id}", target_entity_id), + log_change( + cursor, + f"Created group {group_id} under creator {creator_id} with ca_reference {ca_reference}", ) - alias_id = cursor.fetchone()["id"] - - log_change(cursor, f"Created alias {alias_id} for entity {target_entity_id}") - return alias_id + return group_id def get_entity(cursor, entity_id): diff --git a/ca_core/metadata.py b/ca_core/metadata.py index ce1cb20..cf301a5 100644 --- a/ca_core/metadata.py +++ b/ca_core/metadata.py @@ -54,3 +54,24 @@ def get_private_key(cursor): row = cursor.fetchone() return row["private_key"] if row else None + +def set_defense_p(cursor, defense_p: bool): + """Set the metadata defense_p flag. + + This table is treated as a singleton row at the application level. + Current convention in this codebase is to wipe and re-insert. + """ + cursor.execute("DELETE FROM metadata") + cursor.execute( + "INSERT INTO metadata (defense_p) VALUES (%s)", + (defense_p,) + ) + log_change(cursor, f"Updated metadata defense_p to {defense_p}") + + +def get_defense_p(cursor) -> bool: + cursor.execute("SELECT defense_p FROM metadata LIMIT 1") + row = cursor.fetchone() + if not row or row["defense_p"] is None: + return False + return bool(row["defense_p"]) diff --git a/ca_core/property.py b/ca_core/property.py index 9b91627..bf52d73 100644 --- a/ca_core/property.py +++ b/ca_core/property.py @@ -2,25 +2,71 @@ from db_logging import log_change from entity import ensure_entity_active -def set_property(cursor, entity_id, property_name): +def _validate_validation_policy(validation_policy: str) -> str: + if validation_policy is None: + return "default" + if not isinstance(validation_policy, str): + raise TypeError("validation_policy must be a string") + vp = validation_policy.strip() + if not vp: + raise ValueError("validation_policy cannot be empty") + if len(vp) > 19: + raise ValueError("validation_policy must be at most 19 characters") + return vp + + +def _validate_source(source): + if source is None: + return None + if not isinstance(source, str): + raise TypeError("source must be a string or None") + s = source.strip() + if len(s) > 150: + raise ValueError("source must be at most 150 characters") + # Allow empty string -> treat as NULL for cleanliness + return s if s else None + + +def set_property(cursor, entity_id, property_name, validation_policy="default", source=None): """ - Revoked entities are immutable: cannot add properties. - Schema: property(id, property_name) + Revoked entities are immutable: cannot add/update properties. + + Schema: property(id, property_name, validation_policy, source) + - validation_policy: CHAR(19) NOT NULL DEFAULT 'default' + - source: VARCHAR(150) NULL """ ensure_entity_active(cursor, entity_id) + if not isinstance(property_name, str) or not property_name.strip(): + raise ValueError("property_name must be a non-empty string") + if len(property_name) > 100: + raise ValueError("property_name must be at most 100 characters") + + vp = _validate_validation_policy(validation_policy) + src = _validate_source(source) + cursor.execute( """ - INSERT INTO property (id, property_name) - VALUES (%s, %s) - ON CONFLICT (id, property_name) DO NOTHING + INSERT INTO property (id, property_name, validation_policy, source) + VALUES (%s, %s, %s, %s) + ON CONFLICT (id, property_name) + DO UPDATE SET + validation_policy = EXCLUDED.validation_policy, + source = EXCLUDED.source """, - (entity_id, property_name), + (entity_id, property_name, vp, src), + ) + log_change( + cursor, + f"Set property '{property_name}' for entity {entity_id} " + f"(validation_policy={vp}, source={src})", ) - log_change(cursor, f"Set property '{property_name}' for entity {entity_id}") def get_properties(cursor, entity_id): + """ + Returns a list of property_name values for the entity. + """ cursor.execute( "SELECT property_name FROM property WHERE id = %s", (entity_id,), @@ -29,6 +75,22 @@ def get_properties(cursor, entity_id): return [r["property_name"] for r in rows] +def get_property(cursor, entity_id, property_name): + """ + Returns a dict_row with keys: property_name, validation_policy, source + or None if not found. + """ + cursor.execute( + """ + SELECT property_name, validation_policy, source + FROM property + WHERE id = %s AND property_name = %s + """, + (entity_id, property_name), + ) + return cursor.fetchone() + + def delete_property(cursor, entity_id, property_name): """ Revoked entities are immutable: cannot delete properties. diff --git a/create_tables.sql b/create_tables.sql index 68ace05..e578d3d 100644 --- a/create_tables.sql +++ b/create_tables.sql @@ -1,5 +1,5 @@ -- ------------------------ --- Metadata +-- Metadata (singleton row; enforced at application level) -- ------------------------ DROP TABLE IF EXISTS metadata; @@ -7,7 +7,8 @@ CREATE TABLE metadata( name VARCHAR(50), comment VARCHAR(200), private_key VARCHAR(500), - public_key VARCHAR(500) + public_key VARCHAR(500), + defense_p BOOLEAN NOT NULL DEFAULT false ); -- ------------------------ @@ -23,8 +24,14 @@ CREATE TABLE entity( type VARCHAR(20) NOT NULL, -- person, group, device symmetrical_key VARCHAR(100), public_key VARCHAR(300) NOT NULL, + ca_reference VARCHAR(100), status VARCHAR(20) NOT NULL DEFAULT 'active', - expiration DATE + expiration DATE, + CONSTRAINT entity_ca_reference_check CHECK ( + (type = 'group' AND ca_reference IS NOT NULL) + OR + (type <> 'group' AND ca_reference IS NULL) + ) ); CREATE INDEX idx_entity_name ON entity(name); @@ -50,7 +57,9 @@ DROP TABLE IF EXISTS property; CREATE TABLE property( id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - property_name VARCHAR(100), + property_name VARCHAR(100) NOT NULL, + validation_policy CHAR(19) NOT NULL DEFAULT 'default', + source VARCHAR(150), PRIMARY KEY (id, property_name) ); @@ -66,4 +75,3 @@ CREATE TABLE log( ); CREATE INDEX idx_log_ts ON log(ts); - diff --git a/create_tables.sql.old b/create_tables.sql.old deleted file mode 100644 index 3e59690..0000000 --- a/create_tables.sql.old +++ /dev/null @@ -1,48 +0,0 @@ -drop table metadata; - -create table metadata( - name varchar(50), - comment varchar(200), - private_key varchar(500), - public_key varchar(500) -); - -insert into metadata default vALUES; - -drop table entity cascade; - -create table entity( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), - creator INT REFERENCES entity(id), - name varchar(100) NOT NULL, - group_p BOOLEAN NOT NULL, - geo_offset BIGINT, - --private_key VARCHAR(300) NOT NULL, - public_key VARCHAR(300) NOT NULL, - expiration DATE, - status VARCHAR(10) NOT NULL DEFAULT 'active' - - -); - -CREATE INDEX idx_entity_name ON entity(name); - -drop table group_member; - -create table group_member( - group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - person_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - role VARCHAR(10), - PRIMARY KEY (group_id,person_id) -); - -CREATE INDEX idx_group_member_person_group ON group_member(person_id, group_id); - -drop table property; - -create table property( - id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - property_name VARCHAR(100), - PRIMARY KEY (id, property_name) -); diff --git a/tests/__pycache__/test_entity.cpython-313.pyc b/tests/__pycache__/test_entity.cpython-313.pyc index 461cf8b92c947f0fc58d398608a04d8d99fc956d..f39ebef7e47d339de2c8db0a2d72eac4651f89f2 100644 GIT binary patch delta 1756 zcmbVMO>7%Q6yEiE?XkVP{%hRWDfUKD6K@>1N@FLOw1p^2OM#@7mk_m4Vw`wlxdc1R z+J^=t3kjrfYNZj1g^(J!kOPOJUV3ONkU)Z4+@zSUdf>nX4oIXbNSv72jj@{)gc!@; z&c1nX^!B~)d!I+Y8xWADQ;PuP-V@uUahpv%+YrB^++Ds$C57<~hvNSM2n`Ud zgJ7+vR;-8OJKz|8;858bN$eW_#XTIeBrRqRMQ)l&yPnL4zD;u~&y%N-`6O>8US9m0 z;6H1dwl-sBhFTBC&ib3SY4YXfzC9|>D0ck1YoLk}S1`40G}dL|Un+I8l<= z(J+NB0=j5=WXy9kLJv|DBnr<_7^QHMfWjk%?oSiY9cDzPkV67BK2JcSpC!|Ue|L@9 zj#JZzM(JjSP*gh9kRtb_$cFVZ@Cn#dwxz@x_q2LCd_8zw=yL%Q&yoM5Um1MN`C^Qe z#7^)+ovjq)my7RT{as^HxR6>N8^me!xX=&A2qhyk0B$9{TSB6?S)9y;h{8pua zpq6PTwm$kQahxEe#zDc0?@Gdvr#sUAY8Oa{o|*em^fttBT?~J`dSC2+DEJye~9ujf9 zFQ|O83q=e5**)@~lwzZeQ+fQd=X{$|?CqTYe@bbpjss~oepd`t+pi=ta!7pdug%wk1Dlt3K#I)3Fh1%Igm%DjBI&E*2jaEMjX-Zb(7PRoZYuXd0(+fZH!r$B zWo_mN!mKwOGUiRI2(22N!YOaT_6kkvJ(5%hzToz8rh{gR&QVY)T%xdmH+)@e z4&U}2VW;o|U#bJ}Cw31x_`%(PmCvxdf|E}^?!?{xxifT>nPc!U-O=jnfC^?jOU6DO wx+q7%O~KF|IQeENS6POqQJL5%#kB?ewO>6v#UCY{##K{I} z6k5SUuksG{An3`H2c>vZ@9|I&5n)eW`~$2M)RQy27k?Z)I4~dP{ouX%zL{SB*{Jqa zRU?S4Pt_;RXYxYZ7VskM2n!Nb*OXjz_lIykjOU@;_fl9=V1OLKD$K?+LZ|}gh^fzo zE1{ym3m}w5zE4<+!y|GSi|~e|`@7l-wQFvZX6=USxEnbczL644%a8G0c&-n^clju6 z8AC81DdGv%XA@*4j%B}~@#| z_M#W032xQ>`GI9L zWkrK{ko`|p#IN$3s4QDlMJR#1xNEmv>(_$b2sHH(sl+v}`WQ+#h+-6rhi3ZW0= z*zTV38cV^j;ek1n=E?LF#~(9313lA`&hlFKSuGPTCGKDidWnT`?xZsumpK+0Jkzq; zZq0R8teWdmXSu^}XIZogv&jK`5gN%Uy-z%`-!F;z{g@;kKZwDr3x1SSQ=_c1=FPZpRYLZpu2D zgTtATW3mrNAtUGH{d`K3A8^Dna!t15bYeFg4shE zgE@efa`HI?t@Gt81X{J9PZem@Q$7RzDo&tP0ggegej&HGoHH_WQrwI3OABuC6qF`q zr&h)%gM_L$fQp+@e>WL*X{AL1HiK~a?pi8+_Wb31tMh{;?R)4U|6xk6>V z#!8J1J_qzJ1jSs4%eX9-IXO%)hh-ll0|PK1npi-toa`lJ&kS_k{>fdb>L zm_5*SEyC(TRRTaOgIvS>yyD^!vOy=Tn zW@Ml2$5F`0G5G*TJtOC22ToT;uE}kjPMfcA)-o|(pR6Y+Ke>=ckNL82*yN==bGYE9 zEt~Ak+YU5{WAYtdOLo>^wopc(iE?}(6O;HLCMvO{GiY*7{>5h?b&Jb6BQqz(y(qu5 z;1*9oX;OA-WqdM7Xmce06-LGdK$}eDE{i90@Z1rTxh|%8NlbHv%6g5J8XJ5L=v@ek zxhxhtxlJ&KWe+0*1Jn;3lbeJrnSoYM-Y4V=w3bB}WNn$SJ_8Scq_EmiCx&iEnYh~Iwc;}v YX8`Tc<&-_ZcOf9^vT*d~dWm8t0PY8G&Hw-a diff --git a/tests/__pycache__/test_metadata.cpython-313.pyc b/tests/__pycache__/test_metadata.cpython-313.pyc index bcf6b50495fa2297287964241beedfc8f31c01cb..f93a2b37666e3ad9693472e8f16349116c60cd40 100644 GIT binary patch delta 384 zcmX@FI9HADGcPX}0}w3CU647QeIs8N6Jx~WnM{E^QVc~r!CXbW>D-z;n?Epp+CNJC;?N<51EErZE{EMk*&IR#zdhS>s*DUJdX4GecU1g>+)UE+|NQ8K?`X2lAT zD;#<^IC!peNL=KQm?LprQvZ^q{sxxq92+?pIA~-l$lftbXk0SQ22KczG$)vhRUa-2Yns6M0K43FzFx))`1zcK(h GV8a1(O>vL_ delta 135 zcmbQMc3zS1GcPX}0}%8@FUXwDwvn%ki7{yMOr}7NBA#?EP3~Kwlh{Q!^D^J!W?jkP zr^!26OE7iv6hR3_ugPl#Wf+|&pAvLr{mRGAYBX6?s8-4gXi^bBh~Nbhw>WHa^HWN5 fQtgVOC$ABT5!GNcn&EL>M)#tO?pFpN2doqT#26#- diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc index 5662eae868a0e6713540573bdbe4b4fd7a0d7893..73d4330be8acb003e518e7f532436f4285759e19 100644 GIT binary patch delta 1651 zcma)6&u<$=6!zNL&Fp&Dj(=>Db)1mWmSmkoEszA~XQ64pArMLyH$W8@*I5#`?yi|# z6H+8vaY`km2Ii7-awQHam!`_4xAG4l>co|;RfQ0oDsiD{g~S1vv76Y50%9b8o;UA% zZ=U_WHxK29{hn<>;2d~;YvxiPHr)1n?WEpCH=J*KLJr6)92Dl=GH;70?-Mt?uD&|w zH%`>;I)Xgj3+R6RcN9Rsd0#@$@a!qQF7B8PkX(CLysnR`oU}VIL)_~nR=iGOX#73h zg#M;O{YrhGvqCBdNR@pp;s8Jbs)^6Gpevw*Y9$H6I*sq~)&^Ae3LB@HF5paUucS3obiW>{*Fz5$6DPj za1*S{cV@Qy$*L%n&auP6=WrJ*fpguP9LuaL8o1jY!QB0$s% z|D&z16F+HdpsSMea8Jykx1H%=^1F@wj zMmELBTJNps&FH%Ny|E>pS>_%DL&ad{X0WrwH(ZNdjjiZ8WBt;$%PDfp>|@|yeMQi^ z30j}JOT1X(rBd5-rDUM(k(*(;#|@4^%f4Zn-SSWFAExrO96U^)7C@}VoomiZk+sPJ zh>~$Sj4MhNz;R5tCR`O(F6Q1VfMYw>LXN!;A{#$=bZ0|Nd&s{!j3#~0hj(WVD)>-# z)y@@K^;wjO1plv;2mS7!>3)O6#R<$3SioS5s%l#Jp-ZX>3(MtP7Pm=N;dx{P!qf!% zGBDr`(2++kcrZNMNOwIML_Y?SOu3j56b-h9&XZi^g{?eAVECdjmrZM<@H1ot6HxOb@{4iXf)v<*2)oG{{2`3?llSu9+#D(z!z6%i-sFSghB9yii@boE zG?|MuL3-^$gu`S$i4w-($;}dv8F?qCONMhhgM@uRMEK-$lG2QklV3^7FuF`;m$KyI zW!3tm#>cAlRb#ThR5BabGRw(3q-3QcK^pi$1TT=d#bJ}1pHiBWYFCss`Mp%4q$eYz UGvf@8>oU3*WpuwXfVp7h08UqPegFUf diff --git a/tests/test_entity.py b/tests/test_entity.py index 40b4fea..a0bb359 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -38,6 +38,7 @@ class TestEntityFunctions(unittest.TestCase): creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1") row = entity.get_entity(self.cur, creator_id) self.assertEqual(row["name"], "Creator1") + self.assertIsNone(row["ca_reference"]) log_entry = get_last_log(self.cur).lower() self.assertIn("creator entity", log_entry) @@ -47,17 +48,30 @@ class TestEntityFunctions(unittest.TestCase): creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2") person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id) + row = entity.get_entity(self.cur, person_id) + self.assertIsNone(row["ca_reference"]) + log_entry = get_last_log(self.cur).lower() self.assertIn("enrolled person", log_entry) self.assertIn(str(person_id), log_entry) - def test_create_group(self): + def test_create_group_requires_ca_reference(self): creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") - group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id) + + with self.assertRaises(ValueError): + entity.create_group(self.cur, "GroupMissingRef", "pubkey_group", creator_id, None) + + def test_create_group_sets_ca_reference(self): + creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4") + group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id, "CA-REF-1") + + row = entity.get_entity(self.cur, group_id) + self.assertEqual(row["ca_reference"], "CA-REF-1") log_entry = get_last_log(self.cur).lower() self.assertIn("created group", log_entry) self.assertIn(str(group_id), log_entry) + self.assertIn("ca-ref-1".lower(), log_entry) def test_set_and_get_symmetrical_key(self): creator_id = entity.insert_creator(self.cur, "CreatorSym", "pubkey_sym") @@ -68,4 +82,3 @@ class TestEntityFunctions(unittest.TestCase): log_entry = get_last_log(self.cur).lower() self.assertIn("symmetrical_key", log_entry) - diff --git a/tests/test_group.py b/tests/test_group.py index 1984922..3452ad7 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -38,7 +38,7 @@ class TestGroupFunctions(unittest.TestCase): def test_add_and_get_members(self): creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1") person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id) - group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id) + group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id, "CA-GROUP-1") group_member.add_group_member(self.cur, group_id, person_id, "member") @@ -55,8 +55,8 @@ class TestGroupFunctions(unittest.TestCase): def test_nested_groups(self): creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2") - parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_parent", creator_id) - child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_child", creator_id) + parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_parent", creator_id, "CA-PARENT") + child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_child", creator_id, "CA-CHILD") group_member.add_group_member(self.cur, parent_group, child_group, "subgroup") @@ -69,7 +69,7 @@ class TestGroupFunctions(unittest.TestCase): def test_revoked_group_cannot_accept_members(self): creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") - group_id = entity.create_group(self.cur, "RevokedGroup", "pubkey_group", creator_id) + group_id = entity.create_group(self.cur, "RevokedGroup", "pubkey_group", creator_id, "CA-REVOKED-GROUP") person_id = entity.enroll_person(self.cur, "Person2", "pubkey_person", creator_id) entity.set_entity_status(self.cur, group_id, "revoked", creator_id) @@ -79,11 +79,10 @@ class TestGroupFunctions(unittest.TestCase): def test_revoked_member_cannot_be_added(self): creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4") - group_id = entity.create_group(self.cur, "ActiveGroup", "pubkey_group", creator_id) + group_id = entity.create_group(self.cur, "ActiveGroup", "pubkey_group", creator_id, "CA-ACTIVE-GROUP") person_id = entity.enroll_person(self.cur, "RevokedPerson", "pubkey_person", creator_id) entity.set_entity_status(self.cur, person_id, "revoked", creator_id) with self.assertRaises(ValueError): group_member.add_group_member(self.cur, group_id, person_id, "member") - diff --git a/tests/test_metadata.py b/tests/test_metadata.py index d93cc8e..3ac95c2 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -58,3 +58,9 @@ class TestMetadataFunctions(unittest.TestCase): log_entry = get_last_log(self.cur).lower() self.assertIn("metadata keys", log_entry) + def test_set_and_get_defense_p(self): + metadata.set_defense_p(self.cur, True) + self.assertEqual(metadata.get_defense_p(self.cur), True) + + log_entry = get_last_log(self.cur).lower() + self.assertIn("defense_p", log_entry) diff --git a/tests/test_property.py b/tests/test_property.py index 87d3027..a472947 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -52,6 +52,12 @@ class TestPropertyFunctions(unittest.TestCase): props = property.get_properties(self.cur, person_id) self.assertIn("prop1", props) + details = property.get_property(self.cur, person_id, "prop1") + self.assertIsNotNone(details) + # CHAR(19) is space-padded in PostgreSQL; strip for comparison. + self.assertEqual(details["validation_policy"].strip(), "default") + self.assertIsNone(details["source"]) + log_entry = get_last_log(self.cur).lower() self.assertIn("set property", log_entry) self.assertIn("prop1", log_entry) @@ -76,6 +82,32 @@ class TestPropertyFunctions(unittest.TestCase): self.assertIn("deleted property", log_entry) self.assertIn("prop2", log_entry) + + def test_set_property_with_policy_and_source(self): + creator_id = entity.insert_creator(self.cur, "CreatorPolicy", "pubkey_policy") + person_id = entity.enroll_person( + self.cur, "PersonPolicy", "pubkey_person", creator_id + ) + + property.set_property( + self.cur, + person_id, + "prop_policy", + validation_policy="strict", + source="unit-test", + ) + + details = property.get_property(self.cur, person_id, "prop_policy") + self.assertIsNotNone(details) + self.assertEqual(details["validation_policy"].strip(), "strict") + self.assertEqual(details["source"], "unit-test") + + log_entry = get_last_log(self.cur).lower() + self.assertIn("set property", log_entry) + self.assertIn("prop_policy", log_entry) + self.assertIn("strict", log_entry) + + # ------------------------------------------------------------ # Immutability: revoked entity cannot mutate properties # ------------------------------------------------------------ @@ -93,4 +125,3 @@ class TestPropertyFunctions(unittest.TestCase): with self.assertRaises(ValueError): property.delete_property(self.cur, person_id, "prop3") -