]> granicus.if.org Git - postgresql/commitdiff
sepgsql_setcon().
authorRobert Haas <rhaas@postgresql.org>
Thu, 15 Mar 2012 20:08:40 +0000 (16:08 -0400)
committerRobert Haas <rhaas@postgresql.org>
Thu, 15 Mar 2012 20:08:40 +0000 (16:08 -0400)
This is intended as infrastructure to allow sepgsql to cooperate with
connection pooling software, by allowing the effective security label
to be set for each new connection.

KaiGai Kohei, reviewed by Yeb Havinga.

contrib/sepgsql/expected/label.out
contrib/sepgsql/label.c
contrib/sepgsql/selinux.c
contrib/sepgsql/sepgsql-regtest.te
contrib/sepgsql/sepgsql.h
contrib/sepgsql/sepgsql.sql.in
contrib/sepgsql/sql/label.sql
doc/src/sgml/sepgsql.sgml

index bac169f37bffdc4dcc2ab82a0ef77dedd21a7723..f9587dee57a26611a79b200655e1f89061b7aa79 100644 (file)
@@ -26,7 +26,33 @@ CREATE FUNCTION f4 () RETURNS text
     AS 'SELECT sepgsql_getcon()'
     LANGUAGE sql;
 SECURITY LABEL ON FUNCTION f4()
+    IS 'system_u:object_r:sepgsql_nosuch_trusted_proc_exec_t:s0';
+CREATE FUNCTION f5 (text) RETURNS bool
+       AS 'SELECT sepgsql_setcon($1)'
+    LANGUAGE sql;
+SECURITY LABEL ON FUNCTION f5(text)
+    IS 'system_u:object_r:sepgsql_regtest_trusted_proc_exec_t:s0';
+CREATE TABLE auth_tbl(uname text, credential text, label text);
+INSERT INTO auth_tbl
+    VALUES ('foo', 'acbd18db4cc2f85cedef654fccc4a4d8', 'sepgsql_regtest_foo_t:s0'),
+           ('var', 'b2145aac704ce76dbe1ac7adac535b23', 'sepgsql_regtest_var_t:s0'),
+           ('baz', 'b2145aac704ce76dbe1ac7adac535b23', 'sepgsql_regtest_baz_t:s0');
+SECURITY LABEL ON TABLE auth_tbl
+    IS 'system_u:object_r:sepgsql_secret_table_t:s0';
+CREATE FUNCTION auth_func(text, text) RETURNS bool
+    LANGUAGE sql
+    AS 'SELECT sepgsql_setcon(regexp_replace(sepgsql_getcon(), ''_r:.*$'', ''_r:'' || label))
+        FROM auth_tbl WHERE uname = $1 AND credential = $2';
+SECURITY LABEL ON FUNCTION auth_func(text,text)
     IS 'system_u:object_r:sepgsql_regtest_trusted_proc_exec_t:s0';
+CREATE TABLE foo_tbl(a int, b text);
+INSERT INTO foo_tbl VALUES (1, 'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd');
+SECURITY LABEL ON TABLE foo_tbl
+       IS 'system_u:object_r:sepgsql_regtest_foo_table_t:s0';
+CREATE TABLE var_tbl(x int, y text);
+INSERT INTO var_tbl VALUES (2,'xxx'), (3,'yyy'), (4,'zzz'), (5,'xyz');
+SECURITY LABEL ON TABLE var_tbl
+       IS 'system_u:object_r:sepgsql_regtest_var_table_t:s0';
 --
 -- Tests for default labeling behavior
 --
@@ -99,6 +125,325 @@ SELECT sepgsql_getcon();   -- client's label must be restored
  unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0
 (1 row)
 
+--
+-- Test for Dynamic Domain Transition
+--
+-- validation of transaction aware dynamic-transition
+SELECT sepgsql_getcon();       -- confirm client privilege
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c25
+(1 row)
+
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c15');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c15
+(1 row)
+
+SELECT sepgsql_setcon(NULL);   -- failed to reset
+ERROR:  SELinux: security policy violation
+SELECT sepgsql_getcon();
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c15
+(1 row)
+
+BEGIN;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c12');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c12
+(1 row)
+
+SAVEPOINT svpt_1;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c9');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c9
+(1 row)
+
+SAVEPOINT svpt_2;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c6');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c6
+(1 row)
+
+SAVEPOINT svpt_3;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c3');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c3
+(1 row)
+
+ROLLBACK TO SAVEPOINT svpt_2;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c9'
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c9
+(1 row)
+
+ROLLBACK TO SAVEPOINT svpt_1;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c12'
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c12
+(1 row)
+
+ABORT;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c15'
+                  sepgsql_getcon                  
+--------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c15
+(1 row)
+
+BEGIN;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c8');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c8
+(1 row)
+
+SAVEPOINT svpt_1;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c4');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c4
+(1 row)
+
+ROLLBACK TO SAVEPOINT svpt_1;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c8'
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c8
+(1 row)
+
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c6');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+COMMIT;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c6'
+                 sepgsql_getcon                  
+-------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0:c0.c6
+(1 row)
+
+-- sepgsql_regtest_user_t is not available dynamic-transition,
+-- unless sepgsql_setcon() is called inside of trusted-procedure
+SELECT sepgsql_getcon();       -- confirm client privilege
+                       sepgsql_getcon                       
+------------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c15
+(1 row)
+
+-- sepgsql_regtest_user_t has no permission to switch current label
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0');  -- failed
+ERROR:  SELinux: security policy violation
+SELECT sepgsql_getcon();
+                       sepgsql_getcon                       
+------------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c15
+(1 row)
+
+-- trusted procedure allows to switch, but unavailable to override MCS rules
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7');        -- OK
+ f5 
+----
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                      sepgsql_getcon                       
+-----------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7
+(1 row)
+
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c31');       -- Failed
+ERROR:  SELinux: security policy violation
+CONTEXT:  SQL function "f5" statement 1
+SELECT sepgsql_getcon();
+                      sepgsql_getcon                       
+-----------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7
+(1 row)
+
+SELECT f5(NULL);       -- Failed
+ERROR:  SELinux: security policy violation
+CONTEXT:  SQL function "f5" statement 1
+SELECT sepgsql_getcon();
+                      sepgsql_getcon                       
+-----------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7
+(1 row)
+
+BEGIN;
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c3');        -- OK
+ f5 
+----
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                      sepgsql_getcon                       
+-----------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c3
+(1 row)
+
+ABORT;
+SELECT sepgsql_getcon();
+                      sepgsql_getcon                       
+-----------------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7
+(1 row)
+
+--
+-- Test for simulation of typical connection pooling server
+--
+SELECT sepgsql_getcon();       -- confirm client privilege
+                   sepgsql_getcon                    
+-----------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_pool_t:s0
+(1 row)
+
+-- we shouldn't allow to switch client label without trusted procedure
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:sepgsql_regtest_foo_t:s0');
+ERROR:  SELinux: security policy violation
+SELECT * FROM auth_tbl;        -- failed, no permission to reference
+ERROR:  SELinux: security policy violation
+-- switch to "foo"
+SELECT auth_func('foo', 'acbd18db4cc2f85cedef654fccc4a4d8');
+ auth_func 
+-----------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                   sepgsql_getcon                   
+----------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_foo_t:s0
+(1 row)
+
+SELECT * FROM foo_tbl; -- OK
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+(4 rows)
+
+SELECT * FROM var_tbl; -- failed
+ERROR:  SELinux: security policy violation
+SELECT * FROM auth_tbl;        -- failed
+ERROR:  SELinux: security policy violation
+SELECT sepgsql_setcon(NULL);   -- end of session
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                   sepgsql_getcon                    
+-----------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_pool_t:s0
+(1 row)
+
+-- the pooler cannot touch these tables directry
+SELECT * FROM foo_tbl; -- failed
+ERROR:  SELinux: security policy violation
+SELECT * FROM var_tbl; -- failed
+ERROR:  SELinux: security policy violation
+-- switch to "var"
+SELECT auth_func('var', 'b2145aac704ce76dbe1ac7adac535b23');
+ auth_func 
+-----------
+ t
+(1 row)
+
+SELECT sepgsql_getcon();
+                   sepgsql_getcon                   
+----------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_var_t:s0
+(1 row)
+
+SELECT * FROM foo_tbl;  -- failed
+ERROR:  SELinux: security policy violation
+SELECT * FROM var_tbl;  -- OK
+ x |  y  
+---+-----
+ 2 | xxx
+ 3 | yyy
+ 4 | zzz
+ 5 | xyz
+(4 rows)
+
+SELECT * FROM auth_tbl;        -- failed
+ERROR:  SELinux: security policy violation
+SELECT sepgsql_setcon(NULL);    -- end of session
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+-- misc checks
+SELECT auth_func('var', 'invalid credential'); -- not works
+ auth_func 
+-----------
+(1 row)
+
+SELECT sepgsql_getcon();
+                   sepgsql_getcon                    
+-----------------------------------------------------
+ unconfined_u:unconfined_r:sepgsql_regtest_pool_t:s0
+(1 row)
+
 --
 -- Clean up
 --
@@ -115,3 +460,4 @@ DROP FUNCTION IF EXISTS f1() CASCADE;
 DROP FUNCTION IF EXISTS f2() CASCADE;
 DROP FUNCTION IF EXISTS f3() CASCADE;
 DROP FUNCTION IF EXISTS f4() CASCADE;
+DROP FUNCTION IF EXISTS f5(text) CASCADE;
index 340bec6864caaa81777e404bbea1457a16357425..deadd88b041c2affe233558a8517ffc0f6897df5 100644 (file)
@@ -12,6 +12,7 @@
 
 #include "access/heapam.h"
 #include "access/genam.h"
+#include "access/xact.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
@@ -27,7 +28,9 @@
 #include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
+#include "utils/guc.h"
 #include "utils/lsyscache.h"
+#include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/tqual.h"
 
@@ -43,16 +46,182 @@ static needs_fmgr_hook_type next_needs_fmgr_hook = NULL;
 static fmgr_hook_type next_fmgr_hook = NULL;
 
 /*
- * client_label
+ * client_label_*
  *
- * security label of the client process
+ * security label of the database client.  Initially the client security label
+ * is equal to client_label_peer, and can be changed by one or more calls to
+ * sepgsql_setcon(), and also be temporarily overridden during execution of a
+ * trusted-procedure.
+ *
+ * sepgsql_setcon() is a transaction-aware operation; a (sub-)transaction
+ * rollback should also rollback the current client security label.  Therefore
+ * we use the list client_label_pending of pending_label to keep track of which
+ * labels were set during the (sub-)transactions.
  */
-static char *client_label = NULL;
+static char *client_label_peer         = NULL; /* set by getpeercon(3) */
+static List *client_label_pending      = NIL;  /* pending list being set by
+                                                                                        * sepgsql_setcon() */
+static char *client_label_committed    = NULL; /* set by sepgsql_setcon(),
+                                                                                        * and already committed */
+static char *client_label_func         = NULL; /* set by trusted procedure */
+
+typedef struct {
+       SubTransactionId        subid;
+       char                       *label;
+} pending_label;
 
+/*
+ * sepgsql_get_client_label
+ *
+ * Returns the current security label of the client.  All code should use this
+ * routine to get the current label, instead of refering to the client_label_*
+ * variables above.
+ */
 char *
 sepgsql_get_client_label(void)
 {
-       return client_label;
+       /* trusted procedure client label override */
+       if (client_label_func)
+               return client_label_func;
+
+       /* uncommitted sepgsql_setcon() value */
+       if (client_label_pending)
+       {
+               pending_label  *plabel = llast(client_label_pending);
+
+               if (plabel->label)
+                       return plabel->label;
+       }
+       else if (client_label_committed)
+               return client_label_committed;  /* set by sepgsql_setcon() committed */
+
+       /* default label */
+       Assert(client_label_peer != NULL);
+       return client_label_peer;
+}
+
+/*
+ * sepgsql_set_client_label
+ *
+ * This routine tries to switch the current security label of the client, and
+ * checks related permissions.  The supplied new label shall be added to the
+ * client_label_pending list, then saved at transaction-commit time to ensure
+ * transaction-awareness.
+ */
+static void
+sepgsql_set_client_label(const char *new_label)
+{
+       const char         *tcontext;
+       MemoryContext   oldcxt;
+       pending_label  *plabel;
+
+       /* Reset to the initial client label, if NULL */
+       if (!new_label)
+               tcontext = client_label_peer;
+       else
+       {
+               if (security_check_context_raw((security_context_t) new_label) < 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_NAME),
+                                        errmsg("SELinux: invalid security label: \"%s\"",
+                                                       new_label)));
+               tcontext = new_label;
+       }
+
+       /* Check process:{setcurrent} permission. */
+       sepgsql_avc_check_perms_label(sepgsql_get_client_label(),
+                                                                 SEPG_CLASS_PROCESS,
+                                                                 SEPG_PROCESS__SETCURRENT,
+                                                                 NULL,
+                                                                 true);
+       /* Check process:{dyntransition} permission. */
+       sepgsql_avc_check_perms_label(tcontext,
+                                                                 SEPG_CLASS_PROCESS,
+                                                                 SEPG_PROCESS__DYNTRANSITION,
+                                                                 NULL,
+                                                                 true);
+       /*
+        * Append the supplied new_label on the pending list until
+        * the current transaction is committed.
+        */
+       oldcxt = MemoryContextSwitchTo(CurTransactionContext);
+
+       plabel = palloc0(sizeof(pending_label));
+       plabel->subid = GetCurrentSubTransactionId();
+       if (new_label)
+               plabel->label = pstrdup(new_label);
+       client_label_pending = lappend(client_label_pending, plabel);
+
+       MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * sepgsql_xact_callback
+ *
+ * A callback routine of transaction commit/abort/prepare.  Commmit or abort
+ * changes in the client_label_pending list.
+ */
+static void
+sepgsql_xact_callback(XactEvent event, void *arg)
+{
+       if (event == XACT_EVENT_COMMIT)
+       {
+               if (client_label_pending != NIL)
+               {
+                       pending_label  *plabel = llast(client_label_pending);
+                       char               *new_label;
+
+                       if (plabel->label)
+                               new_label = MemoryContextStrdup(TopMemoryContext,
+                                                                                               plabel->label);
+                       else
+                               new_label = NULL;
+
+                       if (client_label_committed)
+                               pfree(client_label_committed);
+
+                       client_label_committed = new_label;
+                       /*
+                        * XXX - Note that items of client_label_pending are allocated
+                        * on CurTransactionContext, thus, all acquired memory region
+                        * shall be released implicitly.
+                        */
+                       client_label_pending = NIL;
+               }
+       }
+       else if (event == XACT_EVENT_ABORT)
+               client_label_pending = NIL;
+}
+
+/*
+ * sepgsql_subxact_callback
+ *
+ * A callback routine of sub-transaction start/abort/commit.  Releases all
+ * security labels that are set within the sub-transaction that is aborted.
+ */
+static void
+sepgsql_subxact_callback(SubXactEvent event, SubTransactionId mySubid,
+                                                SubTransactionId parentSubid, void *arg)
+{
+       ListCell   *cell;
+       ListCell   *prev;
+       ListCell   *next;
+
+       if (event == SUBXACT_EVENT_ABORT_SUB)
+       {
+               prev = NULL;
+               for (cell = list_head(client_label_pending); cell; cell = next)
+               {
+                       pending_label  *plabel = lfirst(cell);
+                       next = lnext(cell);
+
+                       if (plabel->subid == mySubid)
+                               client_label_pending
+                                       = list_delete_cell(client_label_pending, cell, prev);
+                       else
+                               prev = cell;
+               }
+       }
 }
 
 /*
@@ -78,7 +247,7 @@ sepgsql_client_auth(Port *port, int status)
        /*
         * Getting security label of the peer process using API of libselinux.
         */
-       if (getpeercon_raw(port->sock, &client_label) < 0)
+       if (getpeercon_raw(port->sock, &client_label_peer) < 0)
                ereport(FATAL,
                                (errcode(ERRCODE_INTERNAL_ERROR),
                                 errmsg("SELinux: unable to get peer label: %m")));
@@ -185,8 +354,8 @@ sepgsql_fmgr_hook(FmgrHookEventType event,
                        Assert(!stack->old_label);
                        if (stack->new_label)
                        {
-                               stack->old_label = client_label;
-                               client_label = stack->new_label;
+                               stack->old_label = client_label_func;
+                               client_label_func = stack->new_label;
                        }
                        if (next_fmgr_hook)
                                (*next_fmgr_hook) (event, flinfo, &stack->next_private);
@@ -201,7 +370,7 @@ sepgsql_fmgr_hook(FmgrHookEventType event,
 
                        if (stack->new_label)
                        {
-                               client_label = stack->old_label;
+                               client_label_func = stack->old_label;
                                stack->old_label = NULL;
                        }
                        break;
@@ -215,8 +384,8 @@ sepgsql_fmgr_hook(FmgrHookEventType event,
 /*
  * sepgsql_init_client_label
  *
- * This routine initialize security label of the client, and set up related
- * hooks to be invoked later.
+ * Initializes the client security label and sets up related hooks for client
+ * label management.
  */
 void
 sepgsql_init_client_label(void)
@@ -231,7 +400,7 @@ sepgsql_init_client_label(void)
         * In this case, the process is always hooked on post-authentication, and
         * we can initialize the sepgsql_mode and client_label correctly.
         */
-       if (getcon_raw(&client_label) < 0)
+       if (getcon_raw(&client_label_peer) < 0)
                ereport(ERROR,
                                (errcode(ERRCODE_INTERNAL_ERROR),
                                 errmsg("SELinux: failed to get server security label: %m")));
@@ -246,6 +415,10 @@ sepgsql_init_client_label(void)
 
        next_fmgr_hook = fmgr_hook;
        fmgr_hook = sepgsql_fmgr_hook;
+
+       /* Transaction/Sub-transaction callbacks */
+       RegisterXactCallback(sepgsql_xact_callback, NULL);
+       RegisterSubXactCallback(sepgsql_subxact_callback, NULL);
 }
 
 /*
@@ -360,6 +533,27 @@ sepgsql_getcon(PG_FUNCTION_ARGS)
        PG_RETURN_TEXT_P(cstring_to_text(client_label));
 }
 
+/*
+ * BOOL sepgsql_setcon(TEXT)
+ *
+ * It switches the security label of the client.
+ */
+PG_FUNCTION_INFO_V1(sepgsql_setcon);
+Datum
+sepgsql_setcon(PG_FUNCTION_ARGS)
+{
+       const char *new_label;
+
+       if (PG_ARGISNULL(0))
+               new_label = NULL;
+       else
+               new_label = TextDatumGetCString(PG_GETARG_DATUM(0));
+
+       sepgsql_set_client_label(new_label);
+
+       PG_RETURN_BOOL(true);
+}
+
 /*
  * TEXT sepgsql_mcstrans_in(TEXT)
  *
index 8819b8cb99a90eb31531b05f252ce5c94460173f..0652e294b7f500ce898126bb98838811a0efb29f 100644 (file)
@@ -45,6 +45,12 @@ static struct
                        {
                                "transition", SEPG_PROCESS__TRANSITION
                        },
+                       {
+                               "dyntransition", SEPG_PROCESS__DYNTRANSITION
+                       },
+                       {
+                               "setcurrent", SEPG_PROCESS__SETCURRENT
+                       },
                        {
                                NULL, 0UL
                        }
index a8fe2476a4fb37e7076f0aaa1ed3d7466e1bb472..d8729450747473f949af38a3e380ff0cec703592 100644 (file)
@@ -1,4 +1,4 @@
-policy_module(sepgsql-regtest, 1.03)
+policy_module(sepgsql-regtest, 1.04)
 
 gen_require(`
        all_userspace_class_perms
@@ -17,6 +17,8 @@ gen_tunable(sepgsql_regression_test_mode, false)
 #
 type sepgsql_regtest_trusted_proc_exec_t;
 postgresql_procedure_object(sepgsql_regtest_trusted_proc_exec_t)
+type sepgsql_nosuch_trusted_proc_exec_t;
+postgresql_procedure_object(sepgsql_nosuch_trusted_proc_exec_t)
 
 #
 # Test domains for database administrators
@@ -35,6 +37,12 @@ optional_policy(`
        unconfined_rw_pipes(sepgsql_regtest_dba_t)
 ')
 
+# Type transition rules
+allow sepgsql_regtest_dba_t self : process { setcurrent };
+allow sepgsql_regtest_dba_t sepgsql_regtest_user_t : process { dyntransition };
+allow sepgsql_regtest_dba_t sepgsql_regtest_foo_t : process { dyntransition };
+allow sepgsql_regtest_dba_t sepgsql_regtest_var_t : process { dyntransition };
+
 #
 # Dummy domain for unpriv users
 #
@@ -51,6 +59,72 @@ optional_policy(`
        unconfined_stream_connect(sepgsql_regtest_user_t)
        unconfined_rw_pipes(sepgsql_regtest_user_t)
 ')
+# Type transition rules
+allow sepgsql_regtest_user_t sepgsql_regtest_dba_t : process { transition };
+type_transition sepgsql_regtest_user_t sepgsql_regtest_trusted_proc_exec_t:process sepgsql_regtest_dba_t;
+type_transition sepgsql_regtest_user_t sepgsql_nosuch_trusted_proc_exec_t:process sepgsql_regtest_nosuch_t;
+
+#
+# Dummy domain for (virtual) connection pooler software
+#
+# XXX - this test scenario assumes sepgsql_regtest_pool_t domain performs
+# as a typical connection pool server; that switches the client label of
+# this session prior to any user queries. The sepgsql_regtest_(foo|var)_t
+# is allowed to access its own table types, but not allowed to reference
+# other's one.
+#
+role sepgsql_regtest_pool_r;
+userdom_base_user_template(sepgsql_regtest_pool)
+userdom_manage_home_role(sepgsql_regtest_pool_r, sepgsql_regtest_pool_t)
+userdom_exec_user_home_content_files(sepgsql_regtest_pool_t)
+userdom_write_user_tmp_sockets(sepgsql_regtest_pool_t)
+
+type sepgsql_regtest_foo_t;
+type sepgsql_regtest_var_t;
+type sepgsql_regtest_foo_table_t;
+type sepgsql_regtest_var_table_t;
+
+allow sepgsql_regtest_foo_t sepgsql_regtest_foo_table_t:db_table { getattr select update insert delete lock };
+allow sepgsql_regtest_foo_t sepgsql_regtest_foo_table_t:db_column { getattr select update insert };
+allow sepgsql_regtest_foo_t sepgsql_regtest_foo_table_t:db_tuple { select update insert delete };
+
+allow sepgsql_regtest_var_t sepgsql_regtest_var_table_t:db_table { getattr select update insert delete lock };
+allow sepgsql_regtest_var_t sepgsql_regtest_var_table_t:db_column { getattr select update insert };
+allow sepgsql_regtest_var_t sepgsql_regtest_var_table_t:db_tuple { select update insert delete };
+
+optional_policy(`
+       gen_require(`
+               role unconfined_r;
+       ')
+       postgresql_role(unconfined_r, sepgsql_regtest_foo_t)
+       postgresql_role(unconfined_r, sepgsql_regtest_var_t)
+       postgresql_table_object(sepgsql_regtest_foo_table_t)
+       postgresql_table_object(sepgsql_regtest_var_table_t)
+')
+optional_policy(`
+       postgresql_stream_connect(sepgsql_regtest_pool_t)
+       postgresql_role(sepgsql_regtest_pool_r, sepgsql_regtest_pool_t)
+')
+optional_policy(`
+       unconfined_stream_connect(sepgsql_regtest_pool_t)
+       unconfined_rw_pipes(sepgsql_regtest_pool_t)
+')
+# type transitions
+allow sepgsql_regtest_pool_t self:process { setcurrent };
+allow sepgsql_regtest_pool_t sepgsql_regtest_dba_t:process { transition };
+type_transition sepgsql_regtest_pool_t sepgsql_regtest_trusted_proc_exec_t:process sepgsql_regtest_dba_t;
+
+allow { sepgsql_regtest_foo_t sepgsql_regtest_var_t } self:process { setcurrent };
+allow { sepgsql_regtest_foo_t sepgsql_regtest_var_t } sepgsql_regtest_pool_t:process { dyntransition };
+
+#
+# Dummy domain for non-exist users
+#
+role sepgsql_regtest_nosuch_r;
+userdom_base_user_template(sepgsql_regtest_nosuch)
+optional_policy(`
+    postgresql_role(sepgsql_regtest_nosuch_r, sepgsql_regtest_nosuch_t)
+')
 
 #
 # Rules to launch psql in the dummy domains
@@ -62,26 +136,43 @@ optional_policy(`
                type sepgsql_trusted_proc_t;
        ')
        tunable_policy(`sepgsql_regression_test_mode',`
-               allow unconfined_t sepgsql_regtest_dba_t : process { transition };
-               allow unconfined_t sepgsql_regtest_user_t : process { transition };
+               allow unconfined_t self : process { setcurrent dyntransition };
+               allow unconfined_t sepgsql_regtest_dba_t : process { transition dyntransition };
+               allow unconfined_t sepgsql_regtest_user_t : process { transition dyntransition };
+               allow unconfined_t sepgsql_regtest_pool_t : process { transition dyntransition };
        ')
        role unconfined_r types sepgsql_regtest_dba_t;
        role unconfined_r types sepgsql_regtest_user_t;
+       role unconfined_r types sepgsql_regtest_nosuch_t;
        role unconfined_r types sepgsql_trusted_proc_t;
+
+       role unconfined_r types sepgsql_regtest_pool_t;
+       role unconfined_r types sepgsql_regtest_foo_t;
+       role unconfined_r types sepgsql_regtest_var_t;
 ')
 
 #
-# Rule to check 
+# Rule to execute original trusted procedures
+#
+# XXX - sepgsql_client_type contains any valid client types, so we allow
+# them to execute the original trusted procedure at once.
 #
 optional_policy(`
-       # These rules intends sepgsql_regtest_user_t domain to translate
-       # sepgsql_regtest_dba_t on execution of procedures labeled as
-       # sepgsql_regtest_trusted_proc_exec_t, but does not allow transition
-       # permission from sepgsql_regtest_user_t to sepgsql_regtest_dba_t.
-       #
        gen_require(`
                attribute sepgsql_client_type;
        ')
-       allow sepgsql_client_type sepgsql_regtest_trusted_proc_exec_t:db_procedure { getattr execute install };
-       type_transition sepgsql_regtest_user_t sepgsql_regtest_trusted_proc_exec_t:process sepgsql_regtest_dba_t;
+       allow sepgsql_client_type { sepgsql_regtest_trusted_proc_exec_t sepgsql_nosuch_trusted_proc_exec_t }:db_procedure { getattr execute };
+
+       # These rules intends sepgsql_regtest_user_t domain to translate
+       # sepgsql_regtest_dba_t on execution of procedures labeled as
+       # sepgsql_regtest_trusted_proc_exec_t.
+       #
+#      allow sepgsql_client_type sepgsql_regtest_trusted_proc_exec_t:db_procedure { getattr execute };
+
+       # These rules intends sepgsql_regtest_user_t domain to translate
+       # sepgsql_regtest_nosuch_t on execution of procedures labeled as
+       # sepgsql_nosuch_trusted_proc_exec_t, without permissions to
+       # translate to sepgsql_nosuch_trusted_proc_exec_t.
+       #
+#      allow sepgsql_client_type sepgsql_nosuch_trusted_proc_exec_t:db_procedure { getattr execute install };
 ')
index 0100a09d49b1bef242d64fc4f82d0bd01b6df908..708d4ee6563c82dd90d8da8b0e67b3f82baa49f0 100644 (file)
@@ -57,6 +57,8 @@
  * Internally used code of access vectors
  */
 #define SEPG_PROCESS__TRANSITION                       (1<<0)
+#define SEPG_PROCESS__DYNTRANSITION                    (1<<1)
+#define SEPG_PROCESS__SETCURRENT                       (1<<2)
 
 #define SEPG_FILE__READ                                                (1<<0)
 #define SEPG_FILE__WRITE                                       (1<<1)
@@ -274,6 +276,7 @@ extern void sepgsql_object_relabel(const ObjectAddress *object,
                                           const char *seclabel);
 
 extern Datum sepgsql_getcon(PG_FUNCTION_ARGS);
+extern Datum sepgsql_setcon(PG_FUNCTION_ARGS);
 extern Datum sepgsql_mcstrans_in(PG_FUNCTION_ARGS);
 extern Datum sepgsql_mcstrans_out(PG_FUNCTION_ARGS);
 extern Datum sepgsql_restorecon(PG_FUNCTION_ARGS);
index 45ffe31e6bde1b154b9bfb7c4f5758a0cc9bc528..917d12dbbedc851b00d8a54506e412358b13dec1 100644 (file)
@@ -30,6 +30,7 @@
 --
 LOAD 'MODULE_PATHNAME';
 CREATE OR REPLACE FUNCTION pg_catalog.sepgsql_getcon() RETURNS text AS 'MODULE_PATHNAME', 'sepgsql_getcon' LANGUAGE C;
+CREATE OR REPLACE FUNCTION pg_catalog.sepgsql_setcon(text) RETURNS bool AS 'MODULE_PATHNAME', 'sepgsql_setcon' LANGUAGE C;
 CREATE OR REPLACE FUNCTION pg_catalog.sepgsql_mcstrans_in(text) RETURNS text AS 'MODULE_PATHNAME', 'sepgsql_mcstrans_in' LANGUAGE C STRICT;
 CREATE OR REPLACE FUNCTION pg_catalog.sepgsql_mcstrans_out(text) RETURNS text AS 'MODULE_PATHNAME', 'sepgsql_mcstrans_out' LANGUAGE C STRICT;
 CREATE OR REPLACE FUNCTION pg_catalog.sepgsql_restorecon(text) RETURNS bool AS 'MODULE_PATHNAME', 'sepgsql_restorecon' LANGUAGE C;
index 2b1841281c1f973811f768ba9243eeef37c225d2..e63b5f691dcbe4853a2640a6707f7320f96c349d 100644 (file)
@@ -31,8 +31,39 @@ CREATE FUNCTION f4 () RETURNS text
     AS 'SELECT sepgsql_getcon()'
     LANGUAGE sql;
 SECURITY LABEL ON FUNCTION f4()
+    IS 'system_u:object_r:sepgsql_nosuch_trusted_proc_exec_t:s0';
+
+CREATE FUNCTION f5 (text) RETURNS bool
+       AS 'SELECT sepgsql_setcon($1)'
+    LANGUAGE sql;
+SECURITY LABEL ON FUNCTION f5(text)
     IS 'system_u:object_r:sepgsql_regtest_trusted_proc_exec_t:s0';
 
+CREATE TABLE auth_tbl(uname text, credential text, label text);
+INSERT INTO auth_tbl
+    VALUES ('foo', 'acbd18db4cc2f85cedef654fccc4a4d8', 'sepgsql_regtest_foo_t:s0'),
+           ('var', 'b2145aac704ce76dbe1ac7adac535b23', 'sepgsql_regtest_var_t:s0'),
+           ('baz', 'b2145aac704ce76dbe1ac7adac535b23', 'sepgsql_regtest_baz_t:s0');
+SECURITY LABEL ON TABLE auth_tbl
+    IS 'system_u:object_r:sepgsql_secret_table_t:s0';
+
+CREATE FUNCTION auth_func(text, text) RETURNS bool
+    LANGUAGE sql
+    AS 'SELECT sepgsql_setcon(regexp_replace(sepgsql_getcon(), ''_r:.*$'', ''_r:'' || label))
+        FROM auth_tbl WHERE uname = $1 AND credential = $2';
+SECURITY LABEL ON FUNCTION auth_func(text,text)
+    IS 'system_u:object_r:sepgsql_regtest_trusted_proc_exec_t:s0';
+
+CREATE TABLE foo_tbl(a int, b text);
+INSERT INTO foo_tbl VALUES (1, 'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd');
+SECURITY LABEL ON TABLE foo_tbl
+       IS 'system_u:object_r:sepgsql_regtest_foo_table_t:s0';
+
+CREATE TABLE var_tbl(x int, y text);
+INSERT INTO var_tbl VALUES (2,'xxx'), (3,'yyy'), (4,'zzz'), (5,'xyz');
+SECURITY LABEL ON TABLE var_tbl
+       IS 'system_u:object_r:sepgsql_regtest_var_table_t:s0';
+
 --
 -- Tests for default labeling behavior
 --
@@ -68,6 +99,129 @@ SELECT f3();                        -- trusted procedure that raises an error
 SELECT f4();                   -- failed on domain transition
 SELECT sepgsql_getcon();       -- client's label must be restored
 
+--
+-- Test for Dynamic Domain Transition
+--
+
+-- validation of transaction aware dynamic-transition
+-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:unconfined_t:s0:c0.c25
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c15');
+SELECT sepgsql_getcon();
+
+SELECT sepgsql_setcon(NULL);   -- failed to reset
+SELECT sepgsql_getcon();
+
+BEGIN;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c12');
+SELECT sepgsql_getcon();
+
+SAVEPOINT svpt_1;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c9');
+SELECT sepgsql_getcon();
+
+SAVEPOINT svpt_2;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c6');
+SELECT sepgsql_getcon();
+
+SAVEPOINT svpt_3;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c3');
+SELECT sepgsql_getcon();
+
+ROLLBACK TO SAVEPOINT svpt_2;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c9'
+
+ROLLBACK TO SAVEPOINT svpt_1;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c12'
+
+ABORT;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c15'
+
+BEGIN;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c8');
+SELECT sepgsql_getcon();
+
+SAVEPOINT svpt_1;
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c4');
+SELECT sepgsql_getcon();
+
+ROLLBACK TO SAVEPOINT svpt_1;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c8'
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0:c0.c6');
+
+COMMIT;
+SELECT sepgsql_getcon();               -- should be 's0:c0.c6'
+
+-- sepgsql_regtest_user_t is not available dynamic-transition,
+-- unless sepgsql_setcon() is called inside of trusted-procedure
+-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c15
+
+-- sepgsql_regtest_user_t has no permission to switch current label
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0');  -- failed
+SELECT sepgsql_getcon();
+
+-- trusted procedure allows to switch, but unavailable to override MCS rules
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c7');        -- OK
+SELECT sepgsql_getcon();
+
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c31');       -- Failed
+SELECT sepgsql_getcon();
+
+SELECT f5(NULL);       -- Failed
+SELECT sepgsql_getcon();
+
+BEGIN;
+SELECT f5('unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0:c0.c3');        -- OK
+SELECT sepgsql_getcon();
+
+ABORT;
+SELECT sepgsql_getcon();
+
+--
+-- Test for simulation of typical connection pooling server
+--
+-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_pool_t:s0
+
+-- we shouldn't allow to switch client label without trusted procedure
+SELECT sepgsql_setcon('unconfined_u:unconfined_r:sepgsql_regtest_foo_t:s0');
+
+SELECT * FROM auth_tbl;        -- failed, no permission to reference
+
+-- switch to "foo"
+SELECT auth_func('foo', 'acbd18db4cc2f85cedef654fccc4a4d8');
+
+SELECT sepgsql_getcon();
+
+SELECT * FROM foo_tbl; -- OK
+
+SELECT * FROM var_tbl; -- failed
+
+SELECT * FROM auth_tbl;        -- failed
+
+SELECT sepgsql_setcon(NULL);   -- end of session
+SELECT sepgsql_getcon();
+
+-- the pooler cannot touch these tables directry
+SELECT * FROM foo_tbl; -- failed
+
+SELECT * FROM var_tbl; -- failed
+
+-- switch to "var"
+SELECT auth_func('var', 'b2145aac704ce76dbe1ac7adac535b23');
+
+SELECT sepgsql_getcon();
+
+SELECT * FROM foo_tbl;  -- failed
+
+SELECT * FROM var_tbl;  -- OK
+
+SELECT * FROM auth_tbl;        -- failed
+
+SELECT sepgsql_setcon(NULL);    -- end of session
+
+-- misc checks
+SELECT auth_func('var', 'invalid credential'); -- not works
+SELECT sepgsql_getcon();
+
 --
 -- Clean up
 --
@@ -79,3 +233,4 @@ DROP FUNCTION IF EXISTS f1() CASCADE;
 DROP FUNCTION IF EXISTS f2() CASCADE;
 DROP FUNCTION IF EXISTS f3() CASCADE;
 DROP FUNCTION IF EXISTS f4() CASCADE;
+DROP FUNCTION IF EXISTS f5(text) CASCADE;
index dbddf86bb1c9a6bb2564620dee4765bea4ad9770..56c465b4e9f246295f4cd3acf8e73ba2beb9defc 100644 (file)
@@ -187,7 +187,7 @@ $ cd .../contrib/sepgsql
 $ make -f /usr/share/selinux/devel/Makefile
 $ sudo semodule -u sepgsql-regtest.pp
 $ sudo semodule -l | grep sepgsql
-sepgsql-regtest 1.03
+sepgsql-regtest 1.04
 </screen>
 
   <para>
@@ -525,6 +525,68 @@ postgres=# SELECT cid, cname, show_credit(cid) FROM customer;
    </para>
   </sect3>
 
+  <sect3>
+   <title>Dynamic domain transitions</title>
+   <para>
+    It is possible to use SELinux's dynamic domain transition feature
+    to switch the security label of the client process, the client domain,
+    to a new context, if that is allowed by the security policy.
+    The client domain needs the 'setcurrent' permission and also
+    'dyntransaction' from the old to the new domain.
+   </para>
+   <para>
+    Dynamic domain transitions should be considered carefully, because it
+    means we allows users to switch their label (also peforms a set of
+    privileges in SELinux model) in arbitrary way, unlike regular
+    mandatory way such as trusted procedures.
+    Thus, The dyntransition permission is only considered safe when used
+    to switch to a domain with a smaller set of privileges than the
+    original one, for example:
+   </para>
+<screen>
+regression=# select sepgsql_getcon();
+                    sepgsql_getcon
+-------------------------------------------------------
+ unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
+(1 row)
+
+regression=# SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0-s0:c1.c4');
+ sepgsql_setcon 
+----------------
+ t
+(1 row)
+
+regression=# SELECT sepgsql_setcon('unconfined_u:unconfined_r:unconfined_t:s0-s0:c1.c1023');
+ERROR:  SELinux: security policy violation
+</screen>
+   <para>
+    In this example above we were allowed to switch from the larger MCS
+    range c1.c1023 to the smaller range c1.c4, but switching back was
+    denied.
+   </para>
+   <para>
+    A combination of dynamic domain transition and trusted procedure
+    enables an interesting use case that fits typical process life-
+    cycle of connection pooling software.
+    Even if your connection pooling software is not allowed to run most
+    of SQL commands, it shall be available to switch the security label
+    of the client using <literal>sepgsql_setcon()</literal> function
+    to be invoked inside of the trusted procedure; that should take some
+    credential to authorize the request to switch the client label.
+    After that, this session performs with privileges of the user being
+    switched, but it shall be unavailable to reference database objects
+    labeled as other user's one.
+    Then, it can revert the security label alsp using
+    <literal>sepgsql_setcon()</literal> with <literal>NULL</literal>
+    argument, unless the security policy prevent it.
+    The points of this use case are the trusted procedure is only way
+    for the connection pooling software to switch security label of
+    the clinet, and the trusted procedure does not work without
+    appropriate credentials. In addition, it is also a point that the
+    table to store credentials is only visible from trusted procedure.
+   </para>
+  </sect3>
+
   <sect3>
    <title>Miscellaneous</title>
    <para>
@@ -533,6 +595,56 @@ postgres=# SELECT cid, cname, show_credit(cid) FROM customer;
    </para>
 
   </sect3>
+</sect2>
+
+ <sect2 id="sepgsql-functions">
+  <title>Sepgsql Functions</title>
+  <para>
+   <xref linkend="sepgsql-functions-table"> shows the available functions.
+  </para>
+
+  <table id="sepgsql-functions-table">
+   <title>Sepgsql Functions</title>
+   <tgroup cols="2">
+    <tbody>
+     <row>
+      <entry><literal>sepgsql_getcon() returns text</literal></entry>
+      <entry>
+       Returns the client domain, the current security label of the client.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>sepgsql_setcon(text) returns bool</literal></entry>
+      <entry>
+       Switches the client domain of the current session to the new domain,
+       if allowed by the security policy.
+       It also accepts <literal>NULL</literal> input, and it shall be
+       considered as a transition to the original one.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>sepgsql_mcstrans_in(text) returns text</literal></entry>
+      <entry>Translates the given qualifies MLS/MCS range into raw format if
+      the mcstrans daemon is running.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>sepgsql_mcstrans_out(text) returns text</literal></entry>
+      <entry>Translates the given raw MCS/MCS range into qualified format if
+      the mcstrans daemon is running.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>sepgsql_restorecon(text) returns bool</literal></entry>
+      <entry>
+       Sets up initial security labels for all objectes within the
+       current database. The argument may be NULL, or the name of a specfile
+       to be used as alternative of the system default.
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
  </sect2>
 
  <sect2 id="sepgsql-limitations">