]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/006.schema.permissions.sql
LP2042879 Shelving Location Groups Admin accessibility
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 006.schema.permissions.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18
19 DROP SCHEMA IF EXISTS permission CASCADE;
20
21 BEGIN;
22 CREATE SCHEMA permission;
23
24 CREATE TABLE permission.perm_list (
25         id              SERIAL  PRIMARY KEY,
26         code            TEXT    NOT NULL UNIQUE,
27         description     TEXT
28 );
29 CREATE INDEX perm_list_code_idx ON permission.perm_list (code);
30 CREATE TRIGGER maintain_perm_i18n_tgr
31     AFTER UPDATE ON permission.perm_list
32     FOR EACH ROW EXECUTE PROCEDURE oils_i18n_id_tracking('ppl');
33
34 CREATE TABLE permission.grp_tree (
35         id                      SERIAL  PRIMARY KEY,
36         name                    TEXT    NOT NULL UNIQUE,
37         parent                  INT     REFERENCES permission.grp_tree (id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
38         usergroup               BOOL    NOT NULL DEFAULT TRUE,
39         perm_interval           INTERVAL DEFAULT '3 years'::interval NOT NULL,
40         description             TEXT,
41         application_perm        TEXT,
42         hold_priority       INT   NOT NULL DEFAULT 0
43 );
44 CREATE INDEX grp_tree_parent_idx ON permission.grp_tree (parent);
45
46 CREATE TABLE permission.grp_penalty_threshold (
47     id          SERIAL          PRIMARY KEY,
48     grp         INT             NOT NULL REFERENCES permission.grp_tree (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
49     org_unit    INT             NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
50     penalty     INT             NOT NULL REFERENCES config.standing_penalty (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
51     threshold   NUMERIC(8,2)    NOT NULL,
52     CONSTRAINT penalty_grp_once UNIQUE (grp,penalty,org_unit)
53 );
54
55 CREATE TABLE permission.grp_perm_map (
56         id              SERIAL  PRIMARY KEY,
57         grp             INT     NOT NULL REFERENCES permission.grp_tree (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
58         perm            INT     NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
59         depth           INT     NOT NULL,
60         grantable       BOOL    NOT NULL DEFAULT FALSE,
61                 CONSTRAINT perm_grp_once UNIQUE (grp,perm)
62 );
63
64 CREATE TABLE permission.usr_perm_map (
65         id              SERIAL  PRIMARY KEY,
66         usr             INT     NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
67         perm            INT     NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
68         depth           INT     NOT NULL,
69         grantable       BOOL    NOT NULL DEFAULT FALSE,
70                 CONSTRAINT perm_usr_once UNIQUE (usr,perm)
71 );
72
73 CREATE TABLE permission.usr_object_perm_map (
74         id              SERIAL  PRIMARY KEY,
75         usr             INT     NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
76         perm            INT     NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
77     object_type TEXT NOT NULL,
78     object_id   TEXT NOT NULL,
79         grantable       BOOL    NOT NULL DEFAULT FALSE,
80                 CONSTRAINT perm_usr_obj_once UNIQUE (usr,perm,object_type,object_id)
81 );
82
83 CREATE INDEX uopm_usr_idx ON permission.usr_object_perm_map (usr);
84
85 CREATE TABLE permission.usr_grp_map (
86         id      SERIAL  PRIMARY KEY,
87         usr     INT     NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
88         grp     INT     NOT NULL REFERENCES permission.grp_tree (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
89                 CONSTRAINT usr_grp_once UNIQUE (usr,grp)
90 );
91
92 CREATE OR REPLACE FUNCTION permission.grp_ancestors( INT ) RETURNS SETOF permission.grp_tree AS $$
93     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
94             SELECT $1, 0
95         UNION
96             SELECT ou.parent, ouad.distance+1
97             FROM permission.grp_tree ou JOIN grp_ancestors_distance ouad ON (ou.id = ouad.id)
98             WHERE ou.parent IS NOT NULL
99     )
100     SELECT ou.* FROM permission.grp_tree ou JOIN grp_ancestors_distance ouad USING (id) ORDER BY ouad.distance DESC;
101 $$ LANGUAGE SQL ROWS 1;
102
103 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
104     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
105             SELECT $1, 0
106         UNION
107             SELECT pgt.parent, gad.distance+1
108             FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON (pgt.id = gad.id)
109             WHERE pgt.parent IS NOT NULL
110     )
111     SELECT * FROM grp_ancestors_distance;
112 $$ LANGUAGE SQL STABLE ROWS 1;
113
114 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
115     WITH RECURSIVE grp_descendants_distance(id, distance) AS (
116             SELECT $1, 0
117         UNION
118             SELECT pgt.id, gdd.distance+1
119             FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON (pgt.parent = gdd.id)
120     )
121     SELECT * FROM grp_descendants_distance;
122 $$ LANGUAGE SQL STABLE ROWS 1;
123
124 CREATE OR REPLACE FUNCTION permission.grp_descendants( INT ) RETURNS SETOF permission.grp_tree AS $$
125     WITH RECURSIVE descendant_depth AS (
126         SELECT  gr.id,
127                 gr.parent
128           FROM  permission.grp_tree gr
129           WHERE gr.id = $1
130             UNION ALL
131         SELECT  gr.id,
132                 gr.parent
133           FROM  permission.grp_tree gr
134                 JOIN descendant_depth dd ON (dd.id = gr.parent)
135     ) SELECT gr.* FROM permission.grp_tree gr JOIN descendant_depth USING (id);
136 $$ LANGUAGE SQL STABLE ROWS 1;
137
138 CREATE OR REPLACE FUNCTION permission.grp_tree_full_path ( INT ) RETURNS SETOF permission.grp_tree AS $$
139         SELECT  *
140           FROM  permission.grp_ancestors($1)
141                         UNION
142         SELECT  *
143           FROM  permission.grp_descendants($1);
144 $$ LANGUAGE SQL STABLE ROWS 1;
145
146 CREATE OR REPLACE FUNCTION permission.grp_tree_combined_ancestors ( INT, INT ) RETURNS SETOF permission.grp_tree AS $$
147         SELECT  *
148           FROM  permission.grp_ancestors($1)
149                         UNION
150         SELECT  *
151           FROM  permission.grp_ancestors($2);
152 $$ LANGUAGE SQL STABLE ROWS 1;
153
154 CREATE OR REPLACE FUNCTION permission.grp_tree_common_ancestors ( INT, INT ) RETURNS SETOF permission.grp_tree AS $$
155         SELECT  *
156           FROM  permission.grp_ancestors($1)
157                         INTERSECT
158         SELECT  *
159           FROM  permission.grp_ancestors($2);
160 $$ LANGUAGE SQL STABLE ROWS 1;
161
162 CREATE OR REPLACE FUNCTION permission.usr_perms ( INT ) RETURNS SETOF permission.usr_perm_map AS $$
163         SELECT  DISTINCT ON (usr,perm) *
164           FROM  (
165                         (SELECT * FROM permission.usr_perm_map WHERE usr = $1)
166                                         UNION ALL
167                         (SELECT -p.id, $1 AS usr, p.perm, p.depth, p.grantable
168                           FROM  permission.grp_perm_map p
169                           WHERE p.grp IN (
170                                 SELECT  (permission.grp_ancestors(
171                                                 (SELECT profile FROM actor.usr WHERE id = $1)
172                                         )).id
173                                 )
174                         )
175                                         UNION ALL
176                         (SELECT -p.id, $1 AS usr, p.perm, p.depth, p.grantable
177                           FROM  permission.grp_perm_map p 
178                           WHERE p.grp IN (SELECT (permission.grp_ancestors(m.grp)).id FROM permission.usr_grp_map m WHERE usr = $1))
179                 ) AS x
180           ORDER BY 2, 3, 4 ASC, 5 DESC ;
181 $$ LANGUAGE SQL STABLE ROWS 10;
182
183 CREATE TABLE permission.usr_work_ou_map (
184         id      SERIAL  PRIMARY KEY,
185         usr     INT     NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
186         work_ou INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
187                 CONSTRAINT usr_work_ou_once UNIQUE (usr,work_ou)
188 );
189
190 CREATE OR REPLACE FUNCTION permission.usr_can_grant_perm ( iuser INT, tperm TEXT, target_ou INT ) RETURNS BOOL AS $$
191 DECLARE
192         r_usr   actor.usr%ROWTYPE;
193         r_perm  permission.usr_perm_map%ROWTYPE;
194 BEGIN
195
196         SELECT * INTO r_usr FROM actor.usr WHERE id = iuser;
197
198         IF r_usr.active = FALSE THEN
199                 RETURN FALSE;
200         END IF;
201
202         IF r_usr.super_user = TRUE THEN
203                 RETURN TRUE;
204         END IF;
205
206         FOR r_perm IN   SELECT  *
207                           FROM  permission.usr_perms(iuser) p
208                                 JOIN permission.perm_list l
209                                         ON (l.id = p.perm)
210                           WHERE (l.code = tperm AND p.grantable IS TRUE)
211                 LOOP
212
213                 PERFORM *
214                   FROM  actor.org_unit_descendants(target_ou,r_perm.depth)
215                   WHERE id = r_usr.home_ou;
216
217                 IF FOUND THEN
218                         RETURN TRUE;
219                 ELSE
220                         RETURN FALSE;
221                 END IF;
222         END LOOP;
223
224         RETURN FALSE;
225 END;
226 $$ LANGUAGE PLPGSQL;
227
228 CREATE OR REPLACE FUNCTION permission.usr_has_home_perm ( iuser INT, tperm TEXT, target_ou INT ) RETURNS BOOL AS $$
229 DECLARE
230         r_usr   actor.usr%ROWTYPE;
231         r_perm  permission.usr_perm_map%ROWTYPE;
232 BEGIN
233
234         SELECT * INTO r_usr FROM actor.usr WHERE id = iuser;
235
236         IF r_usr.active = FALSE THEN
237                 RETURN FALSE;
238         END IF;
239
240         IF r_usr.super_user = TRUE THEN
241                 RETURN TRUE;
242         END IF;
243
244         FOR r_perm IN   SELECT  *
245                           FROM  permission.usr_perms(iuser) p
246                                 JOIN permission.perm_list l
247                                         ON (l.id = p.perm)
248                           WHERE l.code = tperm
249                                 OR p.perm = -1 LOOP
250
251                 PERFORM *
252                   FROM  actor.org_unit_descendants(target_ou,r_perm.depth)
253                   WHERE id = r_usr.home_ou;
254
255                 IF FOUND THEN
256                         RETURN TRUE;
257                 ELSE
258                         RETURN FALSE;
259                 END IF;
260         END LOOP;
261
262         RETURN FALSE;
263 END;
264 $$ LANGUAGE PLPGSQL;
265
266 CREATE OR REPLACE FUNCTION permission.usr_has_work_perm ( iuser INT, tperm TEXT, target_ou INT ) RETURNS BOOL AS $$
267 DECLARE
268         r_woum  permission.usr_work_ou_map%ROWTYPE;
269         r_usr   actor.usr%ROWTYPE;
270         r_perm  permission.usr_perm_map%ROWTYPE;
271 BEGIN
272
273         SELECT * INTO r_usr FROM actor.usr WHERE id = iuser;
274
275         IF r_usr.active = FALSE THEN
276                 RETURN FALSE;
277         END IF;
278
279         IF r_usr.super_user = TRUE THEN
280                 RETURN TRUE;
281         END IF;
282
283         FOR r_perm IN   SELECT  *
284                           FROM  permission.usr_perms(iuser) p
285                                 JOIN permission.perm_list l
286                                         ON (l.id = p.perm)
287                           WHERE l.code = tperm
288                                 OR p.perm = -1
289                 LOOP
290
291                 FOR r_woum IN   SELECT  *
292                                   FROM  permission.usr_work_ou_map
293                                   WHERE usr = iuser
294                         LOOP
295
296                         PERFORM *
297                           FROM  actor.org_unit_descendants(target_ou,r_perm.depth)
298                           WHERE id = r_woum.work_ou;
299
300                         IF FOUND THEN
301                                 RETURN TRUE;
302                         END IF;
303
304                 END LOOP;
305
306         END LOOP;
307
308         RETURN FALSE;
309 END;
310 $$ LANGUAGE PLPGSQL;
311
312 CREATE OR REPLACE FUNCTION permission.usr_has_object_perm ( iuser INT, tperm TEXT, obj_type TEXT, obj_id TEXT, target_ou INT ) RETURNS BOOL AS $$
313 DECLARE
314         r_usr   actor.usr%ROWTYPE;
315         res     BOOL;
316 BEGIN
317
318         SELECT * INTO r_usr FROM actor.usr WHERE id = iuser;
319
320         IF r_usr.active = FALSE THEN
321                 RETURN FALSE;
322         END IF;
323
324         IF r_usr.super_user = TRUE THEN
325                 RETURN TRUE;
326         END IF;
327
328         SELECT TRUE INTO res FROM permission.usr_object_perm_map WHERE usr = r_usr.id AND object_type = obj_type AND object_id = obj_id;
329
330         IF FOUND THEN
331                 RETURN TRUE;
332         END IF;
333
334         IF target_ou > -1 THEN
335                 RETURN permission.usr_has_perm( iuser, tperm, target_ou);
336         END IF;
337
338         RETURN FALSE;
339
340 END;
341 $$ LANGUAGE PLPGSQL;
342
343 CREATE OR REPLACE FUNCTION permission.usr_has_object_perm ( INT, TEXT, TEXT, TEXT ) RETURNS BOOL AS $$
344     SELECT permission.usr_has_object_perm( $1, $2, $3, $4, -1 );
345 $$ LANGUAGE SQL;
346
347 CREATE OR REPLACE FUNCTION permission.usr_has_perm ( INT, TEXT, INT ) RETURNS BOOL AS $$
348         SELECT  CASE
349                         WHEN permission.usr_has_home_perm( $1, $2, $3 ) THEN TRUE
350                         WHEN permission.usr_has_work_perm( $1, $2, $3 ) THEN TRUE
351                         ELSE FALSE
352                 END;
353 $$ LANGUAGE SQL;
354
355
356 CREATE OR REPLACE FUNCTION permission.usr_has_perm_at_nd(
357         user_id    IN INTEGER,
358         perm_code  IN TEXT
359 )
360 RETURNS SETOF INTEGER AS $$
361 --
362 -- Return a set of all the org units for which a given user has a given
363 -- permission, granted directly (not through inheritance from a parent
364 -- org unit).
365 --
366 -- The permissions apply to a minimum depth of the org unit hierarchy,
367 -- for the org unit(s) to which the user is assigned.  (They also apply
368 -- to the subordinates of those org units, but we don't report the
369 -- subordinates here.)
370 --
371 -- For purposes of this function, the permission.usr_work_ou_map table
372 -- defines which users belong to which org units.  I.e. we ignore the
373 -- home_ou column of actor.usr.
374 --
375 -- The result set may contain duplicates, which should be eliminated
376 -- by a DISTINCT clause.
377 --
378 DECLARE
379         b_super       BOOLEAN;
380         n_perm        INTEGER;
381         n_min_depth   INTEGER; 
382         n_work_ou     INTEGER;
383         n_curr_ou     INTEGER;
384         n_depth       INTEGER;
385         n_curr_depth  INTEGER;
386 BEGIN
387         --
388         -- Check for superuser
389         --
390         SELECT INTO b_super
391                 super_user
392         FROM
393                 actor.usr
394         WHERE
395                 id = user_id;
396         --
397         IF NOT FOUND THEN
398                 return;                         -- No user?  No permissions.
399         ELSIF b_super THEN
400                 --
401                 -- Super user has all permissions everywhere
402                 --
403                 FOR n_work_ou IN
404                         SELECT
405                                 id
406                         FROM
407                                 actor.org_unit
408                         WHERE
409                                 parent_ou IS NULL
410                 LOOP
411                         RETURN NEXT n_work_ou; 
412                 END LOOP;
413                 RETURN;
414         END IF;
415         --
416         -- Translate the permission name
417         -- to a numeric permission id
418         --
419         SELECT INTO n_perm
420                 id
421         FROM
422                 permission.perm_list
423         WHERE
424                 code = perm_code;
425         --
426         IF NOT FOUND THEN
427                 RETURN;               -- No such permission
428         END IF;
429         --
430         -- Find the highest-level org unit (i.e. the minimum depth)
431         -- to which the permission is applied for this user
432         --
433         -- This query is modified from the one in permission.usr_perms().
434         --
435         SELECT INTO n_min_depth
436                 min( depth )
437         FROM    (
438                 SELECT depth 
439                   FROM permission.usr_perm_map upm
440                  WHERE upm.usr = user_id 
441                    AND (upm.perm = n_perm OR upm.perm = -1)
442                                 UNION
443                 SELECT  gpm.depth
444                   FROM  permission.grp_perm_map gpm
445                   WHERE (gpm.perm = n_perm OR gpm.perm = -1)
446                 AND gpm.grp IN (
447                            SELECT       (permission.grp_ancestors(
448                                         (SELECT profile FROM actor.usr WHERE id = user_id)
449                                 )).id
450                         )
451                                 UNION
452                 SELECT  p.depth
453                   FROM  permission.grp_perm_map p 
454                   WHERE (p.perm = n_perm OR p.perm = -1)
455                     AND p.grp IN (
456                                 SELECT (permission.grp_ancestors(m.grp)).id 
457                                 FROM   permission.usr_grp_map m
458                                 WHERE  m.usr = user_id
459                         )
460         ) AS x;
461         --
462         IF NOT FOUND THEN
463                 RETURN;                -- No such permission for this user
464         END IF;
465         --
466         -- Identify the org units to which the user is assigned.  Note that
467         -- we pay no attention to the home_ou column in actor.usr.
468         --
469         FOR n_work_ou IN
470                 SELECT
471                         work_ou
472                 FROM
473                         permission.usr_work_ou_map
474                 WHERE
475                         usr = user_id
476         LOOP            -- For each org unit to which the user is assigned
477                 --
478                 -- Determine the level of the org unit by a lookup in actor.org_unit_type.
479                 -- We take it on faith that this depth agrees with the actual hierarchy
480                 -- defined in actor.org_unit.
481                 --
482                 SELECT INTO n_depth
483                     type.depth
484                 FROM
485                     actor.org_unit_type type
486                         INNER JOIN actor.org_unit ou
487                             ON ( ou.ou_type = type.id )
488                 WHERE
489                     ou.id = n_work_ou;
490                 --
491                 IF NOT FOUND THEN
492                         CONTINUE;        -- Maybe raise exception?
493                 END IF;
494                 --
495                 -- Compare the depth of the work org unit to the
496                 -- minimum depth, and branch accordingly
497                 --
498                 IF n_depth = n_min_depth THEN
499                         --
500                         -- The org unit is at the right depth, so return it.
501                         --
502                         RETURN NEXT n_work_ou;
503                 ELSIF n_depth > n_min_depth THEN
504                         --
505                         -- Traverse the org unit tree toward the root,
506                         -- until you reach the minimum depth determined above
507                         --
508                         n_curr_depth := n_depth;
509                         n_curr_ou := n_work_ou;
510                         WHILE n_curr_depth > n_min_depth LOOP
511                                 SELECT INTO n_curr_ou
512                                         parent_ou
513                                 FROM
514                                         actor.org_unit
515                                 WHERE
516                                         id = n_curr_ou;
517                                 --
518                                 IF FOUND THEN
519                                         n_curr_depth := n_curr_depth - 1;
520                                 ELSE
521                                         --
522                                         -- This can happen only if the hierarchy defined in
523                                         -- actor.org_unit is corrupted, or out of sync with
524                                         -- the depths defined in actor.org_unit_type.
525                                         -- Maybe we should raise an exception here, instead
526                                         -- of silently ignoring the problem.
527                                         --
528                                         n_curr_ou = NULL;
529                                         EXIT;
530                                 END IF;
531                         END LOOP;
532                         --
533                         IF n_curr_ou IS NOT NULL THEN
534                                 RETURN NEXT n_curr_ou;
535                         END IF;
536                 ELSE
537                         --
538                         -- The permission applies only at a depth greater than the work org unit.
539                         -- Use connectby() to find all dependent org units at the specified depth.
540                         --
541                         FOR n_curr_ou IN
542                                 SELECT id
543                                 FROM actor.org_unit_descendants_distance(n_work_ou)
544                                 WHERE
545                                         distance = n_min_depth - n_depth
546                         LOOP
547                                 RETURN NEXT n_curr_ou;
548                         END LOOP;
549                 END IF;
550                 --
551         END LOOP;
552         --
553         RETURN;
554         --
555 END;
556 $$ LANGUAGE 'plpgsql' ROWS 1;
557
558
559 CREATE OR REPLACE FUNCTION permission.usr_has_perm_at_all_nd(
560         user_id    IN INTEGER,
561         perm_code  IN TEXT
562 )
563 RETURNS SETOF INTEGER AS $$
564 --
565 -- Return a set of all the org units for which a given user has a given
566 -- permission, granted either directly or through inheritance from a parent
567 -- org unit.
568 --
569 -- The permissions apply to a minimum depth of the org unit hierarchy, and
570 -- to the subordinates of those org units, for the org unit(s) to which the
571 -- user is assigned.
572 --
573 -- For purposes of this function, the permission.usr_work_ou_map table
574 -- assigns users to org units.  I.e. we ignore the home_ou column of actor.usr.
575 --
576 -- The result set may contain duplicates, which should be eliminated
577 -- by a DISTINCT clause.
578 --
579 DECLARE
580         n_head_ou     INTEGER;
581         n_child_ou    INTEGER;
582 BEGIN
583         FOR n_head_ou IN
584                 SELECT DISTINCT * FROM permission.usr_has_perm_at_nd( user_id, perm_code )
585         LOOP
586                 --
587                 -- The permission applies only at a depth greater than the work org unit.
588                 --
589                 FOR n_child_ou IN
590             SELECT id
591             FROM actor.org_unit_descendants(n_head_ou)
592                 LOOP
593                         RETURN NEXT n_child_ou;
594                 END LOOP;
595         END LOOP;
596         --
597         RETURN;
598         --
599 END;
600 $$ LANGUAGE 'plpgsql' ROWS 1;
601
602
603 CREATE OR REPLACE FUNCTION permission.usr_has_perm_at(
604         user_id    IN INTEGER,
605         perm_code  IN TEXT
606 )
607 RETURNS SETOF INTEGER AS $$
608 SELECT DISTINCT * FROM permission.usr_has_perm_at_nd( $1, $2 );
609 $$ LANGUAGE 'sql' ROWS 1;
610
611
612 CREATE OR REPLACE FUNCTION permission.usr_has_perm_at_all(
613         user_id    IN INTEGER,
614         perm_code  IN TEXT
615 )
616 RETURNS SETOF INTEGER AS $$
617 SELECT DISTINCT * FROM permission.usr_has_perm_at_all_nd( $1, $2 );
618 $$ LANGUAGE 'sql' ROWS 1;
619
620 CREATE TABLE permission.grp_tree_display_entry (
621     id      SERIAL PRIMARY KEY,
622     position INTEGER NOT NULL,
623     org     INTEGER NOT NULL REFERENCES actor.org_unit (id)
624             DEFERRABLE INITIALLY DEFERRED,
625     grp     INTEGER NOT NULL REFERENCES permission.grp_tree (id)
626             DEFERRABLE INITIALLY DEFERRED,
627     CONSTRAINT pgtde_once_per_org UNIQUE (org, grp)
628 );
629
630 ALTER TABLE permission.grp_tree_display_entry
631     ADD COLUMN parent integer REFERENCES permission.grp_tree_display_entry (id)
632             DEFERRABLE INITIALLY DEFERRED;
633
634 COMMIT;
635