diff --git a/dba/base-data.sql b/dba/base-data.sql index a5be7127..c69de848 100644 --- a/dba/base-data.sql +++ b/dba/base-data.sql @@ -17,18 +17,21 @@ INSERT INTO role_member (user_no, role_no) VALUES(1, 1); -- Set the insert sequence to the next number, with a minimum of 1000 SELECT setval('usr_user_no_seq', (SELECT 1000 UNION SELECT user_no FROM usr ORDER BY 1 DESC LIMIT 1) ); -INSERT INTO relationship_type ( rt_id, rt_name, confers ) - VALUES( 1, 'Administers', 'A' ); +INSERT INTO relationship_type ( rt_id, rt_name, confers, bit_confers ) + VALUES( 1, 'Administers', 'A', privilege_to_bits('DAV::all') ); INSERT INTO relationship_type ( rt_id, rt_name, confers ) - VALUES( 2, 'Can read/write to', 'RW' ); + VALUES( 2, 'Can read/write to', 'RW', privilege_to_bits( ARRAY['DAV::read','DAV::write']) ); INSERT INTO relationship_type ( rt_id, rt_name, confers ) - VALUES( 3, 'Can read from', 'R' ); + VALUES( 3, 'Can read from', 'R', privilege_to_bits( 'DAV::read') ); INSERT INTO relationship_type ( rt_id, rt_name, confers ) - VALUES( 4, 'Can see free/busy time of', 'F' ); + VALUES( 4, 'Can see free/busy time of', 'F', privilege_to_bits( 'caldav:read-free-busy') ); +INSERT INTO principal_type (principal_type_id, principal_type_desc) VALUES( 1, 'Person' ); +INSERT INTO principal_type (principal_type_id, principal_type_desc) VALUES( 2, 'Resource' ); +INSERT INTO principal_type (principal_type_id, principal_type_desc) VALUES( 3, 'Group' ); -- Set the insert sequence to the next number, with a minimum of 1000 SELECT setval('relationship_type_rt_id_seq', (SELECT 10 UNION SELECT rt_id FROM relationship_type ORDER BY 1 DESC LIMIT 1) ); diff --git a/dba/caldav_functions.sql b/dba/caldav_functions.sql index 6a8b1656..35231ea7 100644 --- a/dba/caldav_functions.sql +++ b/dba/caldav_functions.sql @@ -475,13 +475,477 @@ CREATE TRIGGER caldav_data_modified AFTER INSERT OR UPDATE OR DELETE ON caldav_d FOR EACH ROW EXECUTE PROCEDURE caldav_data_modified(); -CREATE or REPLACE FUNCTION proxy_list( INT ) RETURNS SETOF grants AS $$ - SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants - WHERE by_principal = $1 AND NOT is_group AND (privileges & 7::BIT(24))::INT::BOOLEAN - UNION SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants JOIN group_member ON (to_principal=group_id) - WHERE by_principal = $1 and is_group AND (privileges & 7::BIT(24))::INT::BOOLEAN - UNION SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants - WHERE to_principal = $1 AND NOT is_group AND (privileges & 7::BIT(24))::INT::BOOLEAN - UNION SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants JOIN group_member ON (to_principal=group_id) - WHERE member_id = $1 and is_group AND (privileges & 7::BIT(24))::INT::BOOLEAN +-- New in 1.2.6 + +CREATE or REPLACE FUNCTION legacy_privilege_to_bits( TEXT ) RETURNS BIT(24) AS $$ +DECLARE + in_priv ALIAS FOR $1; + out_bits BIT(24); +BEGIN + out_bits := 0::BIT(24); + IF in_priv ~* 'A' THEN + out_bits = ~ out_bits; + RETURN out_bits; + END IF; + + -- The CALDAV:read-free-busy privilege MUST be aggregated in the DAV:read privilege. + -- 1 DAV:read + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF in_priv ~* 'R' THEN + out_bits := out_bits | 4609::BIT(24); + END IF; + + -- DAV:write => DAV:write MUST contain DAV:bind, DAV:unbind, DAV:write-properties and DAV:write-content + -- 2 DAV:write-properties + -- 4 DAV:write-content + -- 64 DAV:bind + -- 128 DAV:unbind + IF in_priv ~* 'W' THEN + out_bits := out_bits | 198::BIT(24); + END IF; + + -- 64 DAV:bind + IF in_priv ~* 'B' THEN + out_bits := out_bits | 64::BIT(24); + END IF; + + -- 128 DAV:unbind + IF in_priv ~* 'U' THEN + out_bits := out_bits | 128::BIT(24); + END IF; + + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF in_priv ~* 'F' THEN + out_bits := out_bits | 4608::BIT(24); + END IF; + + RETURN out_bits; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + +-- This legacy conversion function will eventually be removed, once all logic +-- has been converted to use bitmaps, or to use the bits_to_priv() output. +-- +-- NOTE: Round-trip through this and then back through legacy_privilege_to_bits +-- function is lossy! Through legacy_privilege_to_bits() and back through +-- this one is not. +-- +CREATE or REPLACE FUNCTION bits_to_legacy_privilege( BIT(24) ) RETURNS TEXT AS $$ +DECLARE + in_bits ALIAS FOR $1; + out_priv TEXT; +BEGIN + out_priv := ''; + IF in_bits = (~ 0::BIT(24)) THEN + out_priv = 'A'; + RETURN out_priv; + END IF; + + -- The CALDAV:read-free-busy privilege MUST be aggregated in the DAV:read privilege. + -- 1 DAV:read + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF (in_bits & 4609::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 1::BIT(24)) != 0::BIT(24) THEN + out_priv := 'R'; + ELSE + out_priv := 'F'; + END IF; + END IF; + + -- DAV:write => DAV:write MUST contain DAV:bind, DAV:unbind, DAV:write-properties and DAV:write-content + -- 2 DAV:write-properties + -- 4 DAV:write-content + -- 64 DAV:bind + -- 128 DAV:unbind + IF (in_bits & 198::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 6::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'W'; + ELSE + IF (in_bits & 64::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'B'; + END IF; + IF (in_bits & 128::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'U'; + END IF; + END IF; + END IF; + + RETURN out_priv; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + +CREATE or REPLACE FUNCTION get_permissions( INT, INT ) RETURNS TEXT AS $$ +DECLARE + in_from ALIAS FOR $1; + in_to ALIAS FOR $2; + out_confers TEXT; + bit_confers BIT(24); + group_role_no INT; + tmp_txt TEXT; + dbg TEXT DEFAULT ''; + r RECORD; + counter INT; +BEGIN + -- Self can always have full access + IF in_from = in_to THEN + RETURN 'A'; + END IF; + + -- dbg := 'S-'; + SELECT bits_to_legacy_privilege(r1.confers) INTO out_confers FROM relationship r1 + WHERE r1.from_user = in_from AND r1.to_user = in_to AND NOT usr_is_role(r1.to_user,'Group'); + IF FOUND THEN + RETURN dbg || out_confers; + END IF; + -- RAISE NOTICE 'No simple relationships between % and %', in_from, in_to; + + SELECT bit_or(r1.confers & r2.confers) INTO bit_confers + FROM relationship r1 + JOIN relationship r2 ON r1.to_user=r2.from_user + WHERE r1.from_user=in_from AND r2.to_user=in_to + AND r2.from_user IN (SELECT user_no FROM roles LEFT JOIN role_member USING(role_no) WHERE role_name='Group'); + IF bit_confers != 0::BIT(24) THEN + RETURN dbg || bits_to_legacy_privilege(bit_confers); + END IF; + + RETURN ''; + -- RAISE NOTICE 'No complex relationships between % and %', in_from, in_to; + + SELECT bits_to_legacy_privilege(r1.confers) INTO out_confers FROM relationship r1 LEFT OUTER JOIN relationship r2 ON(r1.to_user = r2.to_user) + WHERE r1.from_user = in_from AND r2.from_user = in_to AND r1.from_user != r2.from_user + AND NOT EXISTS( SELECT 1 FROM relationship r3 WHERE r3.from_user = r1.to_user ) ; + + IF FOUND THEN + -- dbg := 'H-'; + -- RAISE NOTICE 'Permissions to shared group % ', out_confers; + RETURN dbg || out_confers; + END IF; + + -- RAISE NOTICE 'No common group relationships between % and %', in_from, in_to; + + RETURN ''; +END; +$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT; + + +CREATE or REPLACE FUNCTION get_group_role_no() RETURNS INT AS $$ + SELECT role_no FROM roles WHERE role_name = 'Group' +$$ LANGUAGE 'SQL' IMMUTABLE; + +CREATE or REPLACE FUNCTION has_legacy_privilege( INT, TEXT, INT ) RETURNS BOOLEAN AS $$ +DECLARE + in_from ALIAS FOR $1; + in_legacy_privilege ALIAS FOR $2; + in_to ALIAS FOR $3; + in_confers BIT(24); + group_role_no INT; +BEGIN + -- Self can always have full access + IF in_from = in_to THEN + RETURN TRUE; + END IF; + + SELECT get_group_role_no() INTO group_role_no; + SELECT legacy_privilege_to_bits(in_legacy_privilege) INTO in_confers; + + IF EXISTS(SELECT 1 FROM relationship WHERE from_user = in_from AND to_user = in_to + AND (in_confers & confers) = in_confers + AND NOT EXISTS(SELECT 1 FROM role_member WHERE to_user = user_no AND role_no = group_role_no) ) THEN + -- A direct relationship from A to B that grants sufficient + -- RAISE NOTICE 'Permissions directly granted'; + RETURN TRUE; + END IF; + + IF EXISTS( SELECT 1 FROM relationship r1 JOIN relationship r2 ON r1.to_user=r2.from_user + WHERE (in_confers & r1.confers & r2.confers) = in_confers + AND r1.from_user=in_from AND r2.to_user=in_to + AND r2.from_user IN (SELECT user_no FROM role_member WHERE role_no=group_role_no) ) THEN + -- An indirect relationship from A to B via group G that grants sufficient + -- RAISE NOTICE 'Permissions mediated via group'; + RETURN TRUE; + END IF; + + IF EXISTS( SELECT 1 FROM relationship r1 JOIN relationship r2 ON r1.to_user=r2.to_user + WHERE (in_confers & r1.confers & r2.confers) = in_confers + AND r1.from_user=in_from AND r2.from_user=in_to + AND r2.to_user IN (SELECT user_no FROM role_member WHERE role_no=group_role_no) + AND NOT EXISTS(SELECT 1 FROM relationship WHERE from_user=r2.to_user) ) THEN + -- An indirect reflexive relationship from both A & B to group G which grants sufficient + -- RAISE NOTICE 'Permissions to shared group'; + RETURN TRUE; + END IF; + + -- RAISE NOTICE 'No common group relationships between % and %', in_from, in_to; + + RETURN FALSE; +END; +$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT; + + +-- Given a verbose DAV: or CalDAV: privilege name return the bitmask +CREATE or REPLACE FUNCTION privilege_to_bits( TEXT ) RETURNS BIT(24) AS $$ +DECLARE + raw_priv ALIAS FOR $1; + in_priv TEXT; +BEGIN + in_priv := trim(lower(regexp_replace(raw_priv, '^.*:', ''))); + IF in_priv = 'all' THEN + RETURN ~ 0::BIT(24); + END IF; + + RETURN (CASE + WHEN in_priv = 'read' THEN 4609 -- 1 + 512 + 4096 + WHEN in_priv = 'write' THEN 198 -- 2 + 4 + 64 + 128 + WHEN in_priv = 'write-properties' THEN 2 + WHEN in_priv = 'write-content' THEN 4 + WHEN in_priv = 'unlock' THEN 8 + WHEN in_priv = 'read-acl' THEN 16 + WHEN in_priv = 'read-current-user-privilege-set' THEN 32 + WHEN in_priv = 'bind' THEN 64 + WHEN in_priv = 'unbind' THEN 128 + WHEN in_priv = 'write-acl' THEN 256 + WHEN in_priv = 'read-free-busy' THEN 4608 -- 512 + 4096 + WHEN in_priv = 'schedule-deliver' THEN 7168 -- 1024 + 2048 + 4096 + WHEN in_priv = 'schedule-deliver-invite' THEN 1024 + WHEN in_priv = 'schedule-deliver-reply' THEN 2048 + WHEN in_priv = 'schedule-query-freebusy' THEN 4096 + WHEN in_priv = 'schedule-send' THEN 57344 -- 8192 + 16384 + 32768 + WHEN in_priv = 'schedule-send-invite' THEN 8192 + WHEN in_priv = 'schedule-send-reply' THEN 16384 + WHEN in_priv = 'schedule-send-freebusy' THEN 32768 + ELSE 0 END)::BIT(24); +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + +-- Given an array of verbose DAV: or CalDAV: privilege names return the bitmask +CREATE or REPLACE FUNCTION privilege_to_bits( TEXT[] ) RETURNS BIT(24) AS $$ +DECLARE + raw_privs ALIAS FOR $1; + in_priv TEXT; + out_bits BIT(24); + i INT; + all BIT(24); + start INT; + finish INT; +BEGIN + out_bits := 0::BIT(24); + all := ~ out_bits; + SELECT array_lower(raw_privs,1) INTO start; + SELECT array_upper(raw_privs,1) INTO finish; + FOR i IN start .. finish LOOP + SELECT out_bits | privilege_to_bits(raw_privs[i]) INTO out_bits; + IF out_bits = all THEN + RETURN all; + END IF; + END LOOP; + RETURN out_bits; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + +-- This legacy conversion function will eventually be removed, once all logic +-- has been converted to use bitmaps, or to use the bits_to_priv() output. +-- +-- NOTE: Round-trip through this and then back through privilege_to_bits +-- function is lossy! Through privilege_to_bits() and back through +-- this one is not. +-- +CREATE or REPLACE FUNCTION bits_to_privilege( BIT(24) ) RETURNS TEXT[] AS $$ +DECLARE + in_bits ALIAS FOR $1; + out_priv TEXT[]; +BEGIN + out_priv := ARRAY[]::text[]; + IF in_bits = (~ 0::BIT(24)) THEN + out_priv := out_priv || ARRAY['DAV:all']; + END IF; + + IF (in_bits & 513::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 1::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read']; + END IF; + IF (in_bits & 512::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:read-free-busy']; + END IF; + END IF; + + IF (in_bits & 198::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 198::BIT(24)) = 198::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write']; + ELSE + IF (in_bits & 2::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-properties']; + END IF; + IF (in_bits & 4::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-content']; + END IF; + IF (in_bits & 64::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:bind']; + END IF; + IF (in_bits & 128::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:unbind']; + END IF; + END IF; + END IF; + + IF (in_bits & 8::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:unlock']; + END IF; + + IF (in_bits & 16::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read-acl']; + END IF; + + IF (in_bits & 32::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read-current-user-privilege-set']; + END IF; + + IF (in_bits & 256::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-acl']; + END IF; + + IF (in_bits & 7168::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 7168::BIT(24)) = 7168::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver']; + ELSE + IF (in_bits & 1024::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver-invite']; + END IF; + IF (in_bits & 2048::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver-reply']; + END IF; + IF (in_bits & 4096::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-query-freebusy']; + END IF; + END IF; + END IF; + + IF (in_bits & 57344::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 57344::BIT(24)) = 57344::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send']; + ELSE + IF (in_bits & 8192::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-invite']; + END IF; + IF (in_bits & 16384::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-reply']; + END IF; + IF (in_bits & 32768::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-freebusy']; + END IF; + END IF; + END IF; + + RETURN out_priv; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + +CREATE or REPLACE FUNCTION get_permissions_new( INT, INT ) RETURNS BIT(24) AS $$ +DECLARE + in_accessor ALIAS FOR $1; + in_grantor ALIAS FOR $2; + out_conferred BIT(24); +BEGIN + out_conferred := 0::BIT(24); + -- Self can always have full access + IF in_grantor = in_accessor THEN + RETURN ~ out_conferred; + END IF; + + SELECT bit_or(subquery.privileges) INTO out_conferred FROM + (SELECT privileges FROM grants WHERE by_principal = in_grantor AND to_principal = in_accessor AND NOT is_group + UNION + SELECT privileges FROM grants JOIN group_member ON (to_principal=group_id AND member_id=in_accessor) + WHERE by_principal = in_grantor AND is_group + ) AS subquery ; + IF out_conferred IS NULL THEN + SELECT default_privileges INTO out_conferred FROM principal WHERE principal_id = in_grantor; + END IF; + + RETURN out_conferred; +END; +$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT; + +-- A list of the principals who can proxy to this principal +CREATE or REPLACE FUNCTION i_proxy_to( INT ) RETURNS SETOF grants AS $$ +SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants WHERE by_principal = $1 AND NOT is_group + UNION +SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants + JOIN group_member ON (to_principal=group_id) where by_principal = $1 and is_group; $$ LANGUAGE 'SQL' STRICT; + +-- A list of the principals who this principal can proxy +CREATE or REPLACE FUNCTION proxied_by( INT ) RETURNS SETOF grants AS $$ +SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants WHERE to_principal = $1 AND NOT is_group + UNION +SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants + JOIN group_member ON (to_principal=group_id) where member_id = $1 and is_group; +$$ LANGUAGE 'SQL' STRICT; + +CREATE or REPLACE FUNCTION proxy_list( INT ) RETURNS SETOF grants AS $$ +SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants WHERE by_principal = $1 AND NOT is_group + UNION +SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants + JOIN group_member ON (to_principal=group_id) where by_principal = $1 and is_group + UNION +SELECT by_principal, dav_name, to_principal, privileges, is_group FROM grants WHERE to_principal = $1 AND NOT is_group + UNION +SELECT by_principal, dav_name, member_id, privileges, is_group FROM grants + JOIN group_member ON (to_principal=group_id) where member_id = $1 and is_group; +$$ LANGUAGE 'SQL' STRICT; + + +-- New in 1.2.7 + +CREATE or REPLACE FUNCTION write_sync_change( INT8, INT, TEXT ) RETURNS BOOLEAN AS $$ +DECLARE + in_collection_id ALIAS FOR $1; + in_status ALIAS FOR $2; + in_dav_name ALIAS FOR $3; + tmp_int INT8; +BEGIN + SELECT 1 INTO tmp_int FROM sync_tokens + WHERE collection_id = in_collection_id + LIMIT 1; + IF NOT FOUND THEN + RETURN FALSE; + END IF; + SELECT dav_id INTO tmp_int FROM calendar_item WHERE dav_name = in_dav_name; + INSERT INTO sync_changes ( collection_id, sync_status, dav_id, dav_name) + VALUES( in_collection_id, in_status, tmp_int, in_dav_name); + RETURN TRUE; +END +$$ LANGUAGE 'PlPgSQL' VOLATILE STRICT; + + +CREATE or REPLACE FUNCTION new_sync_token( INT8, INT8 ) RETURNS INT8 AS $$ +DECLARE + in_old_sync_token ALIAS FOR $1; + in_collection_id ALIAS FOR $2; + tmp_int INT8; +BEGIN + IF in_old_sync_token > 0 THEN + SELECT 1 INTO tmp_int FROM sync_changes + WHERE collection_id = in_collection_id + AND sync_time > (SELECT modification_time FROM sync_tokens WHERE sync_token = in_old_sync_token) + LIMIT 1; + IF NOT FOUND THEN + RETURN in_old_sync_token; + END IF; + END IF; + SELECT nextval('sync_tokens_sync_token_seq') INTO tmp_int; + INSERT INTO sync_tokens(collection_id, sync_token) VALUES( in_collection_id, tmp_int ); + RETURN tmp_int; +END +$$ LANGUAGE 'PlPgSQL' STRICT; +