From 968bc6fac91d6aaca594488ab85c179b686cbbdd Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Sun, 23 Jan 2011 20:44:48 -0500 Subject: [PATCH] sepgsql, an SE-Linux integration for PostgreSQL This is still pretty rough - among other things, the documentation needs work, and the messages need a visit from the style police - but this gets the basic framework in place. KaiGai Kohei --- configure | 122 +++++++ configure.in | 13 + contrib/Makefile | 4 + contrib/README | 4 + contrib/sepgsql/.gitignore | 1 + contrib/sepgsql/Makefile | 25 ++ contrib/sepgsql/dml.c | 353 +++++++++++++++++++ contrib/sepgsql/expected/dml.out | 182 ++++++++++ contrib/sepgsql/expected/label.out | 109 ++++++ contrib/sepgsql/expected/misc.out | 5 + contrib/sepgsql/hooks.c | 446 +++++++++++++++++++++++ contrib/sepgsql/label.c | 477 +++++++++++++++++++++++++ contrib/sepgsql/launcher | 52 +++ contrib/sepgsql/proc.c | 158 +++++++++ contrib/sepgsql/relation.c | 267 ++++++++++++++ contrib/sepgsql/schema.c | 98 ++++++ contrib/sepgsql/selinux.c | 631 +++++++++++++++++++++++++++++++++ contrib/sepgsql/sepgsql-regtest.te | 59 ++++ contrib/sepgsql/sepgsql.h | 288 +++++++++++++++ contrib/sepgsql/sepgsql.sql.in | 36 ++ contrib/sepgsql/sql/dml.sql | 118 +++++++ contrib/sepgsql/sql/label.sql | 73 ++++ contrib/sepgsql/sql/misc.sql | 5 + doc/src/sgml/contrib.sgml | 1 + doc/src/sgml/filelist.sgml | 1 + doc/src/sgml/sepgsql.sgml | 704 +++++++++++++++++++++++++++++++++++++ src/Makefile.global.in | 1 + src/test/regress/pg_regress.c | 6 + src/test/regress/pg_regress.h | 1 + src/test/regress/pg_regress_main.c | 7 +- 30 files changed, 4246 insertions(+), 1 deletion(-) create mode 100644 contrib/sepgsql/.gitignore create mode 100644 contrib/sepgsql/Makefile create mode 100644 contrib/sepgsql/dml.c create mode 100644 contrib/sepgsql/expected/dml.out create mode 100644 contrib/sepgsql/expected/label.out create mode 100644 contrib/sepgsql/expected/misc.out create mode 100644 contrib/sepgsql/hooks.c create mode 100644 contrib/sepgsql/label.c create mode 100644 contrib/sepgsql/launcher create mode 100644 contrib/sepgsql/proc.c create mode 100644 contrib/sepgsql/relation.c create mode 100644 contrib/sepgsql/schema.c create mode 100644 contrib/sepgsql/selinux.c create mode 100644 contrib/sepgsql/sepgsql-regtest.te create mode 100644 contrib/sepgsql/sepgsql.h create mode 100644 contrib/sepgsql/sepgsql.sql.in create mode 100644 contrib/sepgsql/sql/dml.sql create mode 100644 contrib/sepgsql/sql/label.sql create mode 100644 contrib/sepgsql/sql/misc.sql create mode 100644 doc/src/sgml/sepgsql.sgml diff --git a/configure b/configure index 104ae3072c..c34816caa5 100755 --- a/configure +++ b/configure @@ -715,6 +715,7 @@ with_libxslt with_libxml XML2_CONFIG with_ossp_uuid +with_selinux with_openssl with_bonjour with_ldap @@ -837,6 +838,7 @@ with_pam with_ldap with_bonjour with_openssl +with_selinux with_readline with_libedit_preferred with_ossp_uuid @@ -848,6 +850,7 @@ with_gnu_ld enable_largefile enable_float4_byval enable_float8_byval +enable_float8_byval ' ac_precious_vars='build_alias host_alias @@ -858,6 +861,7 @@ LDFLAGS LIBS CPPFLAGS CPP +CPPFLAGS LDFLAGS_EX LDFLAGS_SL DOCBOOKSTYLE' @@ -1533,6 +1537,7 @@ Optional Packages: --with-ldap build with LDAP support --with-bonjour build with Bonjour support --with-openssl build with OpenSSL support + --with-selinux build with SELinux support --without-readline do not use GNU Readline nor BSD Libedit for editing --with-libedit-preferred prefer BSD Libedit over GNU Readline @@ -5364,6 +5369,40 @@ fi $as_echo "$with_openssl" >&6; } +# +# SELinux +# +{ $as_echo "$as_me:$LINENO: checking whether to build with SELinux support" >&5 +$as_echo_n "checking whether to build with SELinux support... " >&6; } + + + +# Check whether --with-selinux was given. +if test "${with_selinux+set}" = set; then + withval=$with_selinux; + case $withval in + yes) + : + ;; + no) + : + ;; + *) + { { $as_echo "$as_me:$LINENO: error: no argument expected for --with-selinux option" >&5 +$as_echo "$as_me: error: no argument expected for --with-selinux option" >&2;} + { (exit 1); exit 1; }; } + ;; + esac + +else + with_selinux=no + +fi + + + +{ $as_echo "$as_me:$LINENO: result: $with_selinux" >&5 +$as_echo "$with_selinux" >&6; } # # Readline @@ -9291,6 +9330,89 @@ fi fi +# for contrib/sepgsql +if test "$with_selinux" = yes; then + +{ $as_echo "$as_me:$LINENO: checking for getpeercon_raw in -lselinux" >&5 +$as_echo_n "checking for getpeercon_raw in -lselinux... " >&6; } +if test "${ac_cv_lib_selinux_getpeercon_raw+set}" = set; then + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lselinux $LIBS" +cat >conftest.$ac_ext <<_ACEOF +/* confdefs.h. */ +_ACEOF +cat confdefs.h >>conftest.$ac_ext +cat >>conftest.$ac_ext <<_ACEOF +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char getpeercon_raw (); +int +main () +{ +return getpeercon_raw (); + ; + return 0; +} +_ACEOF +rm -f conftest.$ac_objext conftest$ac_exeext +if { (ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:$LINENO: $ac_try_echo\"" +$as_echo "$ac_try_echo") >&5 + (eval "$ac_link") 2>conftest.er1 + ac_status=$? + grep -v '^ *+' conftest.er1 >conftest.err + rm -f conftest.er1 + cat conftest.err >&5 + $as_echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest$ac_exeext && { + test "$cross_compiling" = yes || + $as_test_x conftest$ac_exeext + }; then + ac_cv_lib_selinux_getpeercon_raw=yes +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_cv_lib_selinux_getpeercon_raw=no +fi + +rm -rf conftest.dSYM +rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:$LINENO: result: $ac_cv_lib_selinux_getpeercon_raw" >&5 +$as_echo "$ac_cv_lib_selinux_getpeercon_raw" >&6; } +if test "x$ac_cv_lib_selinux_getpeercon_raw" = x""yes; then + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBSELINUX 1 +_ACEOF + + LIBS="-lselinux $LIBS" + +else + { { $as_echo "$as_me:$LINENO: error: library 'libselinux' is required for SELinux support" >&5 +$as_echo "$as_me: error: library 'libselinux' is required for SELinux support" >&2;} + { (exit 1); exit 1; }; } +fi + +fi + # for contrib/uuid-ossp if test "$with_ossp_uuid" = yes ; then { $as_echo "$as_me:$LINENO: checking for uuid_export in -lossp-uuid" >&5 diff --git a/configure.in b/configure.in index 109eb0c0b0..581a2198b9 100644 --- a/configure.in +++ b/configure.in @@ -676,6 +676,13 @@ PGAC_ARG_BOOL(with, openssl, no, [build with OpenSSL support], AC_MSG_RESULT([$with_openssl]) AC_SUBST(with_openssl) +# +# SELinux +# +AC_MSG_CHECKING([whether to build with SELinux support]) +PGAC_ARG_BOOL(with, selinux, no, [build with SELinux support]) +AC_SUBST(with_selinux) +AC_MSG_RESULT([$with_selinux]) # # Readline @@ -948,6 +955,12 @@ if test "$with_libxslt" = yes ; then AC_CHECK_LIB(xslt, xsltCleanupGlobals, [], [AC_MSG_ERROR([library 'xslt' is required for XSLT support])]) fi +# for contrib/sepgsql +if test "$with_selinux" = yes; then + AC_CHECK_LIB(selinux, getpeercon_raw, [], + [AC_MSG_ERROR([library 'libselinux' is required for SELinux support])]) +fi + # for contrib/uuid-ossp if test "$with_ossp_uuid" = yes ; then AC_CHECK_LIB(ossp-uuid, uuid_export, diff --git a/contrib/Makefile b/contrib/Makefile index 76fa2a6576..2b314501f7 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -61,6 +61,10 @@ ifeq ($(with_libxml),yes) SUBDIRS += xml2 endif +ifeq ($(with_selinux),yes) +SUBDIRS += sepgsql +endif + # Missing: # start-scripts \ (does not have a makefile) diff --git a/contrib/README b/contrib/README index fdc5dc11e6..3c4e324271 100644 --- a/contrib/README +++ b/contrib/README @@ -163,6 +163,10 @@ seg - Confidence-interval datatype (GiST indexing example) by Gene Selkov, Jr. +sepgsql - + External security provider using SELinux + by KaiGai Kohei + spi - Various trigger functions, examples for using SPI. diff --git a/contrib/sepgsql/.gitignore b/contrib/sepgsql/.gitignore new file mode 100644 index 0000000000..1e4a297b09 --- /dev/null +++ b/contrib/sepgsql/.gitignore @@ -0,0 +1 @@ +/sepgsql.sql diff --git a/contrib/sepgsql/Makefile b/contrib/sepgsql/Makefile new file mode 100644 index 0000000000..37a6dce89d --- /dev/null +++ b/contrib/sepgsql/Makefile @@ -0,0 +1,25 @@ +# contrib/sepgsql/Makefile + +MODULE_big = sepgsql +OBJS = hooks.o selinux.o label.o dml.o \ + schema.o relation.o proc.o +DATA_built = sepgsql.sql sepgsql-regtest.pp +REGRESS = label dml misc +EXTRA_CLEAN = -r tmp *.pp sepgsql-regtest.if sepgsql-regtest.fc + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/sepgsql +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +SHLIB_LINK += $(filter -lselinux, $(LIBS)) +REGRESS_OPTS += --launcher $(top_builddir)/contrib/sepgsql/launcher + +sepgsql-regtest.pp: sepgsql-regtest.te + $(MAKE) -f $(DESTDIR)/usr/share/selinux/devel/Makefile $@ diff --git a/contrib/sepgsql/dml.c b/contrib/sepgsql/dml.c new file mode 100644 index 0000000000..cfa436d37d --- /dev/null +++ b/contrib/sepgsql/dml.c @@ -0,0 +1,353 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/dml.c + * + * Routines to handle DML permission checks + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/sysattr.h" +#include "access/tupdesc.h" +#include "catalog/catalog.h" +#include "catalog/heap.h" +#include "catalog/pg_attribute.h" +#include "catalog/pg_class.h" +#include "catalog/pg_inherits_fn.h" +#include "commands/seclabel.h" +#include "commands/tablecmds.h" +#include "executor/executor.h" +#include "nodes/bitmapset.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +#include "sepgsql.h" + +/* + * fixup_whole_row_references + * + * When user reference a whole of row, it is equivalent to reference to + * all the user columns (not system columns). So, we need to fix up the + * given bitmapset, if it contains a whole of the row reference. + */ +static Bitmapset * +fixup_whole_row_references(Oid relOid, Bitmapset *columns) +{ + Bitmapset *result; + HeapTuple tuple; + AttrNumber natts; + AttrNumber attno; + int index; + + /* if no whole of row references, do not anything */ + index = InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber; + if (!bms_is_member(index, columns)) + return columns; + + /* obtain number of attributes */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relOid); + natts = ((Form_pg_class) GETSTRUCT(tuple))->relnatts; + ReleaseSysCache(tuple); + + /* fix up the given columns */ + result = bms_copy(columns); + result = bms_del_member(result, index); + + for (attno=1; attno <= natts; attno++) + { + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relOid), + Int16GetDatum(attno)); + if (!HeapTupleIsValid(tuple)) + continue; + + if (((Form_pg_attribute) GETSTRUCT(tuple))->attisdropped) + continue; + + index = attno - FirstLowInvalidHeapAttributeNumber; + + result = bms_add_member(result, index); + + ReleaseSysCache(tuple); + } + return result; +} + +/* + * fixup_inherited_columns + * + * When user is querying on a table with children, it implicitly accesses + * child tables also. So, we also need to check security label of child + * tables and columns, but here is no guarantee attribute numbers are + * same between the parent ans children. + * It returns a bitmapset which contains attribute number of the child + * table based on the given bitmapset of the parent. + */ +static Bitmapset * +fixup_inherited_columns(Oid parentId, Oid childId, Bitmapset *columns) +{ + AttrNumber attno; + Bitmapset *tmpset; + Bitmapset *result = NULL; + char *attname; + int index; + + /* + * obviously, no need to do anything here + */ + if (parentId == childId) + return columns; + + tmpset = bms_copy(columns); + while ((index = bms_first_member(tmpset)) > 0) + { + attno = index + FirstLowInvalidHeapAttributeNumber; + /* + * whole-row-reference shall be fixed-up later + */ + if (attno == InvalidAttrNumber) + { + result = bms_add_member(result, index); + continue; + } + + attname = get_attname(parentId, attno); + if (!attname) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attno, parentId); + attno = get_attnum(childId, attname); + if (attno == InvalidAttrNumber) + elog(ERROR, "cache lookup failed for attribute %s of relation %u", + attname, childId); + + index = attno - FirstLowInvalidHeapAttributeNumber; + result = bms_add_member(result, index); + + pfree(attname); + } + bms_free(tmpset); + + return result; +} + +/* + * check_relation_privileges + * + * It actually checks required permissions on a certain relation + * and its columns. + */ +static bool +check_relation_privileges(Oid relOid, + Bitmapset *selected, + Bitmapset *modified, + uint32 required, + bool abort) +{ + char relkind = get_rel_relkind(relOid); + char *scontext = sepgsql_get_client_label(); + char *tcontext; + Bitmapset *columns; + int index; + bool result = true; + + /* + * Hardwired Policies: + * SE-PostgreSQL enforces + * - clients cannot modify system catalogs using DMLs + * - clients cannot reference/modify toast relations using DMLs + */ + if (sepgsql_getenforce() > 0) + { + Oid relnamespace = get_rel_namespace(relOid); + + if (IsSystemNamespace(relnamespace) && + (required & (SEPG_DB_TABLE__UPDATE | + SEPG_DB_TABLE__INSERT | + SEPG_DB_TABLE__DELETE)) != 0) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("selinux: hardwired security policy violation"))); + + if (relkind == RELKIND_TOASTVALUE) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("selinux: hardwired security policy violation"))); + } + + /* + * Check permissions on the relation + */ + tcontext = sepgsql_get_label(RelationRelationId, relOid, 0); + switch (relkind) + { + case RELKIND_RELATION: + result = sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_TABLE, + required, + get_rel_name(relOid), + abort); + if (!result) + return false; + break; + + case RELKIND_SEQUENCE: + Assert((required & ~SEPG_DB_TABLE__SELECT) == 0); + + if (required & SEPG_DB_TABLE__SELECT) + result = sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_SEQUENCE, + SEPG_DB_SEQUENCE__GET_VALUE, + get_rel_name(relOid), + abort); + return result; + + case RELKIND_VIEW: + result = sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_VIEW, + SEPG_DB_VIEW__EXPAND, + get_rel_name(relOid), + abort); + return result; + + default: + /* nothing to be checked */ + return true; + } + + /* + * Check permissions on the columns + */ + selected = fixup_whole_row_references(relOid, selected); + modified = fixup_whole_row_references(relOid, modified); + columns = bms_union(selected, modified); + + while ((index = bms_first_member(columns)) >= 0) + { + AttrNumber attnum; + uint32 column_perms = 0; + char audit_name[NAMEDATALEN * 2 + 10]; + + if (bms_is_member(index, selected)) + column_perms |= SEPG_DB_COLUMN__SELECT; + if (bms_is_member(index, modified)) + { + if (required & SEPG_DB_TABLE__UPDATE) + column_perms |= SEPG_DB_COLUMN__UPDATE; + if (required & SEPG_DB_TABLE__INSERT) + column_perms |= SEPG_DB_COLUMN__INSERT; + } + if (column_perms == 0) + continue; + + /* obtain column's permission */ + attnum = index + FirstLowInvalidHeapAttributeNumber; + tcontext = sepgsql_get_label(RelationRelationId, relOid, attnum); + snprintf(audit_name, sizeof(audit_name), "%s.%s", + get_rel_name(relOid), get_attname(relOid, attnum)); + + result = sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_COLUMN, + column_perms, + audit_name, + abort); + if (!result) + return result; + } + return true; +} + +/* + * sepgsql_dml_privileges + * + * Entrypoint of the DML permission checks + */ +bool +sepgsql_dml_privileges(List *rangeTabls, bool abort) +{ + ListCell *lr; + + foreach (lr, rangeTabls) + { + RangeTblEntry *rte = lfirst(lr); + uint32 required = 0; + List *tableIds; + ListCell *li; + + /* + * Only regular relations shall be checked + */ + if (rte->rtekind != RTE_RELATION) + continue; + + /* + * Find out required permissions + */ + if (rte->requiredPerms & ACL_SELECT) + required |= SEPG_DB_TABLE__SELECT; + if (rte->requiredPerms & ACL_INSERT) + required |= SEPG_DB_TABLE__INSERT; + if (rte->requiredPerms & ACL_UPDATE) + { + if (!bms_is_empty(rte->modifiedCols)) + required |= SEPG_DB_TABLE__UPDATE; + else + required |= SEPG_DB_TABLE__LOCK; + } + if (rte->requiredPerms & ACL_DELETE) + required |= SEPG_DB_TABLE__DELETE; + + /* + * Skip, if nothing to be checked + */ + if (required == 0) + continue; + + /* + * If this RangeTblEntry is also supposed to reference inherited + * tables, we need to check security label of the child tables. + * So, we expand rte->relid into list of OIDs of inheritance + * hierarchy, then checker routine will be invoked for each + * relations. + */ + if (!rte->inh) + tableIds = list_make1_oid(rte->relid); + else + tableIds = find_all_inheritors(rte->relid, NoLock, NULL); + + foreach (li, tableIds) + { + Oid tableOid = lfirst_oid(li); + Bitmapset *selectedCols; + Bitmapset *modifiedCols; + + /* + * child table has different attribute numbers, so we need + * to fix up them. + */ + selectedCols = fixup_inherited_columns(rte->relid, tableOid, + rte->selectedCols); + modifiedCols = fixup_inherited_columns(rte->relid, tableOid, + rte->modifiedCols); + + /* + * check permissions on individual tables + */ + if (!check_relation_privileges(tableOid, + selectedCols, + modifiedCols, + required, abort)) + return false; + } + list_free(tableIds); + } + return true; +} diff --git a/contrib/sepgsql/expected/dml.out b/contrib/sepgsql/expected/dml.out new file mode 100644 index 0000000000..5625ebcd9e --- /dev/null +++ b/contrib/sepgsql/expected/dml.out @@ -0,0 +1,182 @@ +-- +-- Regression Test for DML Permissions +-- +-- +-- Setup +-- +CREATE TABLE t1 (a int, b text); +SECURITY LABEL ON TABLE t1 IS 'system_u:object_r:sepgsql_table_t:s0'; +INSERT INTO t1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'); +CREATE TABLE t2 (x int, y text); +SECURITY LABEL ON TABLE t2 IS 'system_u:object_r:sepgsql_ro_table_t:s0'; +INSERT INTO t2 VALUES (1, 'xxx'), (2, 'yyy'), (3, 'zzz'); +CREATE TABLE t3 (s int, t text); +SECURITY LABEL ON TABLE t3 IS 'system_u:object_r:sepgsql_fixed_table_t:s0'; +INSERT INTO t3 VALUES (1, 'sss'), (2, 'ttt'), (3, 'uuu'); +CREATE TABLE t4 (m int, n text); +SECURITY LABEL ON TABLE t4 IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +INSERT INTO t4 VALUES (1, 'mmm'), (2, 'nnn'), (3, 'ooo'); +CREATE TABLE t5 (e text, f text, g text); +SECURITY LABEL ON TABLE t5 IS 'system_u:object_r:sepgsql_table_t:s0'; +SECURITY LABEL ON COLUMN t5.e IS 'system_u:object_r:sepgsql_table_t:s0'; +SECURITY LABEL ON COLUMN t5.f IS 'system_u:object_r:sepgsql_ro_table_t:s0'; +SECURITY LABEL ON COLUMN t5.g IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +CREATE TABLE customer (cid int primary key, cname text, ccredit text); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "customer_pkey" for table "customer" +SECURITY LABEL ON COLUMN customer.ccredit IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +INSERT INTO customer VALUES (1, 'Taro', '1111-2222-3333-4444'), + (2, 'Hanako', '5555-6666-7777-8888'); +CREATE FUNCTION customer_credit(int) RETURNS text + AS 'SELECT regexp_replace(ccredit, ''-[0-9]+$'', ''-????'') FROM customer WHERE cid = $1' + LANGUAGE sql; +SECURITY LABEL ON FUNCTION customer_credit(int) + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; +SELECT objtype, objname, label FROM pg_seclabels + WHERE provider = 'selinux' + AND objtype in ('table', 'column') + AND objname in ('t1', 't2', 't3', 't4', 't5', 't5.e', 't5.f', 't5.g'); + objtype | objname | label +---------+---------+--------------------------------------------- + table | t1 | system_u:object_r:sepgsql_table_t:s0 + table | t2 | system_u:object_r:sepgsql_ro_table_t:s0 + table | t3 | system_u:object_r:sepgsql_fixed_table_t:s0 + table | t4 | system_u:object_r:sepgsql_secret_table_t:s0 + table | t5 | system_u:object_r:sepgsql_table_t:s0 + column | t5.g | system_u:object_r:sepgsql_secret_table_t:s0 + column | t5.f | system_u:object_r:sepgsql_ro_table_t:s0 + column | t5.e | system_u:object_r:sepgsql_table_t:s0 +(8 rows) + +-- Hardwired Rules +UPDATE pg_attribute SET attisdropped = true + WHERE attrelid = 't5'::regclass AND attname = 'f'; -- failed +ERROR: selinux: hardwired security policy violation +-- +-- Simple DML statements +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +(1 row) + +SELECT * FROM t1; -- ok + a | b +---+----- + 1 | aaa + 2 | bbb + 3 | ccc +(3 rows) + +SELECT * FROM t2; -- ok + x | y +---+----- + 1 | xxx + 2 | yyy + 3 | zzz +(3 rows) + +SELECT * FROM t3; -- ok + s | t +---+----- + 1 | sss + 2 | ttt + 3 | uuu +(3 rows) + +SELECT * FROM t4; -- failed +ERROR: SELinux: security policy violation +SELECT * FROM t5; -- failed +ERROR: SELinux: security policy violation +SELECT e,f FROM t5; -- ok + e | f +---+--- +(0 rows) + +SELECT * FROM customer; -- failed +ERROR: SELinux: security policy violation +SELECT cid, cname, customer_credit(cid) FROM customer; -- ok + cid | cname | customer_credit +-----+--------+--------------------- + 1 | Taro | 1111-2222-3333-???? + 2 | Hanako | 5555-6666-7777-???? +(2 rows) + +SELECT count(*) FROM t5; -- ok + count +------- + 0 +(1 row) + +SELECT count(*) FROM t5 WHERE g IS NULL; -- failed +ERROR: SELinux: security policy violation +INSERT INTO t1 VALUES (4, 'abc'); -- ok +INSERT INTO t2 VALUES (4, 'xyz'); -- failed +ERROR: SELinux: security policy violation +INSERT INTO t3 VALUES (4, 'stu'); -- ok +INSERT INTO t4 VALUES (4, 'mno'); -- failed +ERROR: SELinux: security policy violation +INSERT INTO t5 VALUES (1,2,3); -- failed +ERROR: SELinux: security policy violation +INSERT INTO t5 (e,f) VALUES ('abc', 'def'); -- failed +ERROR: SELinux: security policy violation +INSERT INTO t5 (e) VALUES ('abc'); -- ok +UPDATE t1 SET b = b || '_upd'; -- ok +UPDATE t2 SET y = y || '_upd'; -- failed +ERROR: SELinux: security policy violation +UPDATE t3 SET t = t || '_upd'; -- failed +ERROR: SELinux: security policy violation +UPDATE t4 SET n = n || '_upd'; -- failed +ERROR: SELinux: security policy violation +UPDATE t5 SET e = 'xyz'; -- ok +UPDATE t5 SET e = f || '_upd'; -- ok +UPDATE t5 SET e = g || '_upd'; -- failed +ERROR: SELinux: security policy violation +DELETE FROM t1; -- ok +DELETE FROM t2; -- failed +ERROR: SELinux: security policy violation +DELETE FROM t3; -- failed +ERROR: SELinux: security policy violation +DELETE FROM t4; -- failed +ERROR: SELinux: security policy violation +DELETE FROM t5; -- ok +DELETE FROM t5 WHERE f IS NULL; -- ok +DELETE FROM t5 WHERE g IS NULL; -- failed +ERROR: SELinux: security policy violation +-- +-- COPY TO/FROM statements +-- +COPY t1 TO '/dev/null'; -- ok +COPY t2 TO '/dev/null'; -- ok +COPY t3 TO '/dev/null'; -- ok +COPY t4 TO '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t5 TO '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t5(e,f) TO '/dev/null'; -- ok +COPY t1 FROM '/dev/null'; -- ok +COPY t2 FROM '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t3 FROM '/dev/null'; -- ok +COPY t4 FROM '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t5 FROM '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t5 (e,f) FROM '/dev/null'; -- failed +ERROR: SELinux: security policy violation +COPY t5 (e) FROM '/dev/null'; -- ok +-- +-- Clean up +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +------------------------------------------------------ + unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c255 +(1 row) + +DROP TABLE IF EXISTS t1 CASCADE; +DROP TABLE IF EXISTS t2 CASCADE; +DROP TABLE IF EXISTS t3 CASCADE; +DROP TABLE IF EXISTS t4 CASCADE; +DROP TABLE IF EXISTS t5 CASCADE; +DROP TABLE IF EXISTS customer CASCADE; diff --git a/contrib/sepgsql/expected/label.out b/contrib/sepgsql/expected/label.out new file mode 100644 index 0000000000..0f0615cb30 --- /dev/null +++ b/contrib/sepgsql/expected/label.out @@ -0,0 +1,109 @@ +-- +-- Regression Tests for Label Management +-- +-- +-- Setup +-- +CREATE TABLE t1 (a int, b text); +INSERT INTO t1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'); +SELECT * INTO t2 FROM t1 WHERE a % 2 = 0; +CREATE FUNCTION f1 () RETURNS text + AS 'SELECT sepgsql_getcon()' + LANGUAGE sql; +CREATE FUNCTION f2 () RETURNS text + AS 'SELECT sepgsql_getcon()' + LANGUAGE sql; +SECURITY LABEL ON FUNCTION f2() + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; +CREATE FUNCTION f3 () RETURNS text + AS 'BEGIN + RAISE EXCEPTION ''an exception from f3()''; + RETURN NULL; + END;' LANGUAGE plpgsql; +SECURITY LABEL ON FUNCTION f3() + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; +-- +-- Tests for default labeling behavior +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +(1 row) + +CREATE TABLE t3 (s int, t text); +INSERT INTO t3 VALUES (1, 'sss'), (2, 'ttt'), (3, 'uuu'); +SELECT objtype, objname, label FROM pg_seclabels + WHERE provider = 'selinux' + AND objtype in ('table', 'column') + AND objname in ('t1', 't2', 't3'); + objtype | objname | label +---------+---------+----------------------------------------------- + table | t1 | unconfined_u:object_r:sepgsql_table_t:s0 + table | t2 | unconfined_u:object_r:sepgsql_table_t:s0 + table | t3 | unconfined_u:object_r:user_sepgsql_table_t:s0 +(3 rows) + +-- +-- Tests for SECURITY LABEL +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +---------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_dba_t:s0 +(1 row) + +SECURITY LABEL ON TABLE t1 + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- ok +SECURITY LABEL ON TABLE t2 + IS 'invalid seuciryt context'; -- be failed +ERROR: invalid security label: "invalid seuciryt context" +SECURITY LABEL ON COLUMN t2 + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- be failed +ERROR: improper relation name (too many dotted names): +SECURITY LABEL ON COLUMN t2.b + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- ok +-- +-- Tests for Trusted Procedures +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +(1 row) + +SELECT f1(); -- normal procedure + f1 +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +(1 row) + +SELECT f2(); -- trusted procedure + f2 +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_trusted_proc_t:s0 +(1 row) + +SELECT f3(); -- trusted procedure that raises an error +ERROR: an exception from f3() +SELECT sepgsql_getcon(); -- client's label must be restored + sepgsql_getcon +----------------------------------------------------- + unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +(1 row) + +-- +-- Clean up +-- +SELECT sepgsql_getcon(); -- confirm client privilege + sepgsql_getcon +------------------------------------------------------ + unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c255 +(1 row) + +DROP TABLE IF EXISTS t1 CASCADE; +DROP TABLE IF EXISTS t2 CASCADE; +DROP TABLE IF EXISTS t3 CASCADE; +DROP FUNCTION IF EXISTS f1() CASCADE; +DROP FUNCTION IF EXISTS f2() CASCADE; +DROP FUNCTION IF EXISTS f3() CASCADE; diff --git a/contrib/sepgsql/expected/misc.out b/contrib/sepgsql/expected/misc.out new file mode 100644 index 0000000000..5242333bf4 --- /dev/null +++ b/contrib/sepgsql/expected/misc.out @@ -0,0 +1,5 @@ +-- +-- Regression Test for Misc Permission Checks +-- +LOAD '$libdir/sepgsql'; -- failed +ERROR: SELinux: LOAD is not allowed anyway. diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c new file mode 100644 index 0000000000..6b55e484cf --- /dev/null +++ b/contrib/sepgsql/hooks.c @@ -0,0 +1,446 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/hooks.c + * + * Entrypoints of the hooks in PostgreSQL, and dispatches the callbacks. + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/objectaccess.h" +#include "catalog/pg_class.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" +#include "commands/seclabel.h" +#include "executor/executor.h" +#include "fmgr.h" +#include "libpq/auth.h" +#include "miscadmin.h" +#include "tcop/utility.h" +#include "utils/guc.h" + +#include "sepgsql.h" + +PG_MODULE_MAGIC; + +/* + * Declarations + */ +void _PG_init(void); + +/* + * Saved hook entries (if stacked) + */ +static object_access_hook_type next_object_access_hook = NULL; +static ClientAuthentication_hook_type next_client_auth_hook = NULL; +static ExecutorCheckPerms_hook_type next_exec_check_perms_hook = NULL; +static needs_fmgr_hook_type next_needs_fmgr_hook = NULL; +static fmgr_hook_type next_fmgr_hook = NULL; +static ProcessUtility_hook_type next_ProcessUtility_hook = NULL; + +/* + * GUC: sepgsql.permissive = (on|off) + */ +static bool sepgsql_permissive; + +bool +sepgsql_get_permissive(void) +{ + return sepgsql_permissive; +} + +/* + * GUC: sepgsql.debug_audit = (on|off) + */ +static bool sepgsql_debug_audit; + +bool +sepgsql_get_debug_audit(void) +{ + return sepgsql_debug_audit; +} + +/* + * sepgsql_client_auth + * + * Entrypoint of the client authentication hook. + * It switches the client label according to getpeercon(), and the current + * performing mode according to the GUC setting. + */ +static void +sepgsql_client_auth(Port *port, int status) +{ + char *context; + + if (next_client_auth_hook) + (*next_client_auth_hook)(port, status); + + /* + * In the case when authentication failed, the supplied socket + * shall be closed soon, so we don't need to do anything here. + */ + if (status != STATUS_OK) + return; + + /* + * Getting security label of the peer process using API of libselinux. + */ + if (getpeercon_raw(port->sock, &context) < 0) + ereport(FATAL, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("selinux: failed to get the peer label"))); + + sepgsql_set_client_label(context); + + /* + * Switch the current performing mode from INTERNAL to either + * DEFAULT or PERMISSIVE. + */ + if (sepgsql_permissive) + sepgsql_set_mode(SEPGSQL_MODE_PERMISSIVE); + else + sepgsql_set_mode(SEPGSQL_MODE_DEFAULT); +} + +/* + * sepgsql_object_access + * + * Entrypoint of the object_access_hook. This routine performs as + * a dispatcher of invocation based on access type and object classes. + */ +static void +sepgsql_object_access(ObjectAccessType access, + Oid classId, + Oid objectId, + int subId) +{ + if (next_object_access_hook) + (*next_object_access_hook)(access, classId, objectId, subId); + + switch (access) + { + case OAT_POST_CREATE: + switch (classId) + { + case NamespaceRelationId: + sepgsql_schema_post_create(objectId); + break; + + case RelationRelationId: + if (subId == 0) + sepgsql_relation_post_create(objectId); + else + sepgsql_attribute_post_create(objectId, subId); + break; + + case ProcedureRelationId: + sepgsql_proc_post_create(objectId); + break; + + default: + /* Ignore unsupported object classes */ + break; + } + break; + + default: + elog(ERROR, "unexpected object access type: %d", (int)access); + break; + } +} + +/* + * sepgsql_exec_check_perms + * + * Entrypoint of DML permissions + */ +static bool +sepgsql_exec_check_perms(List *rangeTabls, bool abort) +{ + /* + * If security provider is stacking and one of them replied 'false' + * at least, we don't need to check any more. + */ + if (next_exec_check_perms_hook && + !(*next_exec_check_perms_hook)(rangeTabls, abort)) + return false; + + if (!sepgsql_dml_privileges(rangeTabls, abort)) + return false; + + return true; +} + +/* + * sepgsql_needs_fmgr_hook + * + * It informs the core whether the supplied function is trusted procedure, + * or not. If true, sepgsql_fmgr_hook shall be invoked at start, end, and + * abort time of function invocation. + */ +static bool +sepgsql_needs_fmgr_hook(Oid functionId) +{ + char *old_label; + char *new_label; + char *function_label; + + if (next_needs_fmgr_hook && + (*next_needs_fmgr_hook)(functionId)) + return true; + + /* + * SELinux needs the function to be called via security_definer + * wrapper, if this invocation will take a domain-transition. + * We call these functions as trusted-procedure, if the security + * policy has a rule that switches security label of the client + * on execution. + */ + old_label = sepgsql_get_client_label(); + new_label = sepgsql_proc_get_domtrans(functionId); + if (strcmp(old_label, new_label) != 0) + { + pfree(new_label); + return true; + } + pfree(new_label); + + /* + * Even if not a trusted-procedure, this function should not be inlined + * unless the client has db_procedure:{execute} permission. + * Please note that it shall be actually failed later because of same + * reason with ACL_EXECUTE. + */ + function_label = sepgsql_get_label(ProcedureRelationId, functionId, 0); + if (sepgsql_check_perms(sepgsql_get_client_label(), + function_label, + SEPG_CLASS_DB_PROCEDURE, + SEPG_DB_PROCEDURE__EXECUTE, + NULL, false) != true) + { + pfree(function_label); + return true; + } + pfree(function_label); + return false; +} + +/* + * sepgsql_fmgr_hook + * + * It switches security label of the client on execution of trusted + * procedures. + */ +static void +sepgsql_fmgr_hook(FmgrHookEventType event, + FmgrInfo *flinfo, Datum *private) +{ + struct { + char *old_label; + char *new_label; + Datum next_private; + } *stack; + + switch (event) + { + case FHET_START: + stack = (void *)DatumGetPointer(*private); + if (!stack) + { + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(flinfo->fn_mcxt); + stack = palloc(sizeof(*stack)); + stack->old_label = NULL; + stack->new_label = sepgsql_proc_get_domtrans(flinfo->fn_oid); + stack->next_private = 0; + + MemoryContextSwitchTo(oldcxt); + + *private = PointerGetDatum(stack); + } + Assert(!stack->old_label); + stack->old_label = sepgsql_set_client_label(stack->new_label); + + if (next_fmgr_hook) + (*next_fmgr_hook)(event, flinfo, &stack->next_private); + break; + + case FHET_END: + case FHET_ABORT: + stack = (void *)DatumGetPointer(*private); + + if (next_fmgr_hook) + (*next_fmgr_hook)(event, flinfo, &stack->next_private); + + sepgsql_set_client_label(stack->old_label); + stack->old_label = NULL; + break; + + default: + elog(ERROR, "unexpected event type: %d", (int)event); + break; + } +} + +/* + * sepgsql_utility_command + * + * It tries to rough-grained control on utility commands; some of them can + * break whole of the things if nefarious user would use. + */ +static void +sepgsql_utility_command(Node *parsetree, + const char *queryString, + ParamListInfo params, + bool isTopLevel, + DestReceiver *dest, + char *completionTag) +{ + if (next_ProcessUtility_hook) + (*next_ProcessUtility_hook)(parsetree, queryString, params, + isTopLevel, dest, completionTag); + + /* + * Check command tag to avoid nefarious operations + */ + switch (nodeTag(parsetree)) + { + case T_LoadStmt: + /* + * We reject LOAD command across the board on enforcing mode, + * because a binary module can arbitrarily override hooks. + */ + if (sepgsql_getenforce()) + { + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("SELinux: LOAD is not allowed anyway."))); + } + break; + default: + /* + * Right now we don't check any other utility commands, + * because it needs more detailed information to make + * access control decision here, but we don't want to + * have two parse and analyze routines individually. + */ + break; + } + + /* + * Original implementation + */ + standard_ProcessUtility(parsetree, queryString, params, + isTopLevel, dest, completionTag); +} + +/* + * Module load/unload callback + */ +void +_PG_init(void) +{ + char *context; + + /* + * We allow to load the SE-PostgreSQL module on single-user-mode or + * shared_preload_libraries settings only. + */ + if (IsUnderPostmaster) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Not allowed to load SE-PostgreSQL now"))); + + /* + * Check availability of SELinux on the platform. + * If disabled, we cannot activate any SE-PostgreSQL features, + * and we have to skip rest of initialization. + */ + if (is_selinux_enabled() < 1) + { + sepgsql_set_mode(SEPGSQL_MODE_DISABLED); + return; + } + + /* + * sepgsql.permissive = (on|off) + * + * This variable controls performing mode of SE-PostgreSQL + * on user's session. + */ + DefineCustomBoolVariable("sepgsql.permissive", + "Turn on/off permissive mode in SE-PostgreSQL", + NULL, + &sepgsql_permissive, + false, + PGC_SIGHUP, + GUC_NOT_IN_SAMPLE, + NULL, + NULL); + + /* + * sepgsql.debug_audit = (on|off) + * + * This variable allows users to turn on/off audit logs on access + * control decisions, independent from auditallow/auditdeny setting + * in the security policy. + * We intend to use this option for debugging purpose. + */ + DefineCustomBoolVariable("sepgsql.debug_audit", + "Turn on/off debug audit messages", + NULL, + &sepgsql_debug_audit, + false, + PGC_USERSET, + GUC_NOT_IN_SAMPLE, + NULL, + NULL); + + /* + * Set up dummy client label. + * + * XXX - note that PostgreSQL launches background worker process + * like autovacuum without authentication steps. So, we initialize + * sepgsql_mode with SEPGSQL_MODE_INTERNAL, and client_label with + * the security context of server process. + * Later, it also launches background of user session. 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(&context) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("selinux: unable to get security label of server"))); + sepgsql_set_client_label(context); + + /* Security label provider hook */ + register_label_provider(SEPGSQL_LABEL_TAG, + sepgsql_object_relabel); + + /* Client authentication hook */ + next_client_auth_hook = ClientAuthentication_hook; + ClientAuthentication_hook = sepgsql_client_auth; + + /* Object access hook */ + next_object_access_hook = object_access_hook; + object_access_hook = sepgsql_object_access; + + /* DML permission check */ + next_exec_check_perms_hook = ExecutorCheckPerms_hook; + ExecutorCheckPerms_hook = sepgsql_exec_check_perms; + + /* Trusted procedure hooks */ + next_needs_fmgr_hook = needs_fmgr_hook; + needs_fmgr_hook = sepgsql_needs_fmgr_hook; + + next_fmgr_hook = fmgr_hook; + fmgr_hook = sepgsql_fmgr_hook; + + /* ProcessUtility hook */ + next_ProcessUtility_hook = ProcessUtility_hook; + ProcessUtility_hook = sepgsql_utility_command; +} diff --git a/contrib/sepgsql/label.c b/contrib/sepgsql/label.c new file mode 100644 index 0000000000..bc28adfea5 --- /dev/null +++ b/contrib/sepgsql/label.c @@ -0,0 +1,477 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/label.c + * + * Routines to support SELinux labels (security context) + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/genam.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/pg_attribute.h" +#include "catalog/pg_class.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" +#include "commands/dbcommands.h" +#include "commands/seclabel.h" +#include "libpq/libpq-be.h" +#include "miscadmin.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/tqual.h" + +#include "sepgsql.h" + +#include + +/* + * client_label + * + * security label of the client process + */ +static char *client_label = NULL; + +char * +sepgsql_get_client_label(void) +{ + return client_label; +} + +char * +sepgsql_set_client_label(char *new_label) +{ + char *old_label = client_label; + + client_label = new_label; + + return old_label; +} + +/* + * sepgsql_get_label + * + * It returns a security context of the specified database object. + * If unlabeled or incorrectly labeled, the system "unlabeled" label + * shall be returned. + */ +char * +sepgsql_get_label(Oid classId, Oid objectId, int32 subId) +{ + ObjectAddress object; + char *label; + + object.classId = classId; + object.objectId = objectId; + object.objectSubId = subId; + + label = GetSecurityLabel(&object, SEPGSQL_LABEL_TAG); + if (!label || security_check_context_raw((security_context_t)label)) + { + security_context_t unlabeled; + + if (security_get_initial_context_raw("unlabeled", &unlabeled) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("selinux: unable to get initial security label"))); + PG_TRY(); + { + label = pstrdup(unlabeled); + } + PG_CATCH(); + { + freecon(unlabeled); + PG_RE_THROW(); + } + PG_END_TRY(); + + freecon(unlabeled); + } + return label; +} + +/* + * sepgsql_object_relabel + * + * An entrypoint of SECURITY LABEL statement + */ +void +sepgsql_object_relabel(const ObjectAddress *object, const char *seclabel) +{ + /* + * validate format of the supplied security label, + * if it is security context of selinux. + */ + if (seclabel && + security_check_context_raw((security_context_t) seclabel) < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("invalid security label: \"%s\"", seclabel))); + /* + * Do actual permission checks for each object classes + */ + switch (object->classId) + { + case NamespaceRelationId: + sepgsql_schema_relabel(object->objectId, seclabel); + break; + case RelationRelationId: + if (object->objectSubId == 0) + sepgsql_relation_relabel(object->objectId, + seclabel); + else + sepgsql_attribute_relabel(object->objectId, + object->objectSubId, + seclabel); + break; + case ProcedureRelationId: + sepgsql_proc_relabel(object->objectId, seclabel); + break; + + default: + elog(ERROR, "unsupported object type: %u", object->classId); + break; + } +} + +/* + * TEXT sepgsql_getcon(VOID) + * + * It returns the security label of the client. + */ +PG_FUNCTION_INFO_V1(sepgsql_getcon); +Datum +sepgsql_getcon(PG_FUNCTION_ARGS) +{ + char *client_label; + + if (!sepgsql_is_enabled()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SELinux: now disabled"))); + + client_label = sepgsql_get_client_label(); + + PG_RETURN_POINTER(cstring_to_text(client_label)); +} + +/* + * TEXT sepgsql_mcstrans_in(TEXT) + * + * It translate the given qualified MLS/MCS range into raw format + * when mcstrans daemon is working. + */ +PG_FUNCTION_INFO_V1(sepgsql_mcstrans_in); +Datum +sepgsql_mcstrans_in(PG_FUNCTION_ARGS) +{ + text *label = PG_GETARG_TEXT_P(0); + char *raw_label; + char *result; + + if (!sepgsql_is_enabled()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SELinux: now disabled"))); + + if (selinux_trans_to_raw_context(text_to_cstring(label), + &raw_label) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("SELinux: internal error on mcstrans"))); + + PG_TRY(); + { + result = pstrdup(raw_label); + } + PG_CATCH(); + { + freecon(raw_label); + PG_RE_THROW(); + } + PG_END_TRY(); + freecon(raw_label); + + PG_RETURN_POINTER(cstring_to_text(result)); +} + +/* + * TEXT sepgsql_mcstrans_out(TEXT) + * + * It translate the given raw MLS/MCS range into qualified format + * when mcstrans daemon is working. + */ +PG_FUNCTION_INFO_V1(sepgsql_mcstrans_out); +Datum +sepgsql_mcstrans_out(PG_FUNCTION_ARGS) +{ + text *label = PG_GETARG_TEXT_P(0); + char *qual_label; + char *result; + + if (!sepgsql_is_enabled()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SELinux: now disabled"))); + + if (selinux_raw_to_trans_context(text_to_cstring(label), + &qual_label) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("SELinux: internal error on mcstrans"))); + + PG_TRY(); + { + result = pstrdup(qual_label); + } + PG_CATCH(); + { + freecon(qual_label); + PG_RE_THROW(); + } + PG_END_TRY(); + freecon(qual_label); + + PG_RETURN_POINTER(cstring_to_text(result)); +} + +/* + * exec_object_restorecon + * + * This routine is a helper called by sepgsql_restorecon; it set up + * initial security labels of database objects within the supplied + * catalog OID. + */ +static void +exec_object_restorecon(struct selabel_handle *sehnd, Oid catalogId) +{ + Relation rel; + SysScanDesc sscan; + HeapTuple tuple; + char *database_name = get_database_name(MyDatabaseId); + char *namespace_name; + Oid namespace_id; + char *relation_name; + + /* + * Open the target catalog. We don't want to allow writable + * accesses by other session during initial labeling. + */ + rel = heap_open(catalogId, AccessShareLock); + + sscan = systable_beginscan(rel, InvalidOid, false, + SnapshotNow, 0, NULL); + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + Form_pg_namespace nspForm; + Form_pg_class relForm; + Form_pg_attribute attForm; + Form_pg_proc proForm; + char objname[NAMEDATALEN * 4 + 10]; + int objtype = 1234; + ObjectAddress object; + security_context_t context; + + /* + * The way to determine object name depends on object classes. + * So, any branches set up `objtype', `objname' and `object' here. + */ + switch (catalogId) + { + case NamespaceRelationId: + nspForm = (Form_pg_namespace) GETSTRUCT(tuple); + + objtype = SELABEL_DB_SCHEMA; + snprintf(objname, sizeof(objname), "%s.%s", + database_name, NameStr(nspForm->nspname)); + + object.classId = NamespaceRelationId; + object.objectId = HeapTupleGetOid(tuple); + object.objectSubId = 0; + break; + + case RelationRelationId: + relForm = (Form_pg_class) GETSTRUCT(tuple); + + if (relForm->relkind == RELKIND_RELATION) + objtype = SELABEL_DB_TABLE; + else if (relForm->relkind == RELKIND_SEQUENCE) + objtype = SELABEL_DB_SEQUENCE; + else if (relForm->relkind == RELKIND_VIEW) + objtype = SELABEL_DB_VIEW; + else + continue; /* no need to assign security label */ + + namespace_name = get_namespace_name(relForm->relnamespace); + snprintf(objname, sizeof(objname), "%s.%s.%s", + database_name, namespace_name, + NameStr(relForm->relname)); + pfree(namespace_name); + + object.classId = RelationRelationId; + object.objectId = HeapTupleGetOid(tuple); + object.objectSubId = 0; + break; + + case AttributeRelationId: + attForm = (Form_pg_attribute) GETSTRUCT(tuple); + + if (get_rel_relkind(attForm->attrelid) != RELKIND_RELATION) + continue; /* no need to assign security label */ + + objtype = SELABEL_DB_COLUMN; + + namespace_id = get_rel_namespace(attForm->attrelid); + namespace_name = get_namespace_name(namespace_id); + relation_name = get_rel_name(attForm->attrelid); + snprintf(objname, sizeof(objname), "%s.%s.%s.%s", + database_name, namespace_name, + relation_name, NameStr(attForm->attname)); + pfree(relation_name); + pfree(namespace_name); + + object.classId = RelationRelationId; + object.objectId = attForm->attrelid; + object.objectSubId = attForm->attnum; + break; + + case ProcedureRelationId: + proForm = (Form_pg_proc) GETSTRUCT(tuple); + + objtype = SELABEL_DB_PROCEDURE; + + namespace_name = get_namespace_name(proForm->pronamespace); + snprintf(objname, sizeof(objname), "%s.%s.%s", + database_name, namespace_name, + NameStr(proForm->proname)); + pfree(namespace_name); + + object.classId = ProcedureRelationId; + object.objectId = HeapTupleGetOid(tuple); + object.objectSubId = 0; + break; + + default: + elog(ERROR, "Bug? %u is not supported to set initial labels", + catalogId); + break; + } + + if (selabel_lookup_raw(sehnd, &context, objname, objtype) == 0) + { + PG_TRY(); + { + /* + * Check SELinux permission to relabel the fetched object, + * then do the actual relabeling. + */ + sepgsql_object_relabel(&object, context); + + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, context); + } + PG_CATCH(); + { + freecon(context); + PG_RE_THROW(); + } + PG_END_TRY(); + freecon(context); + } + else if (errno == ENOENT) + ereport(WARNING, + (errmsg("no valid initial label on %s (type=%d), skipped", + objname, objtype))); + else + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("libselinux: internal error"))); + } + systable_endscan(sscan); + + heap_close(rel, NoLock); +} + +/* + * BOOL sepgsql_restorecon(TEXT specfile) + * + * This function tries to assign initial security labels on all the object + * within the current database, according to the system setting. + * It is typically invoked by sepgsql-install script just after initdb, to + * assign initial security labels. + * + * If @specfile is not NULL, it uses explicitly specified specfile, instead + * of the system default. + */ +PG_FUNCTION_INFO_V1(sepgsql_restorecon); +Datum +sepgsql_restorecon(PG_FUNCTION_ARGS) +{ + struct selabel_handle *sehnd; + struct selinux_opt seopts; + + /* + * SELinux has to be enabled on the running platform. + */ + if (!sepgsql_is_enabled()) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SELinux: now disabled"))); + /* + * Check DAC permission. Only superuser can set up initial + * security labels, like root-user in filesystems + */ + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to restore initial contexts"))); + + /* + * Open selabel_lookup(3) stuff. It provides a set of mapping + * between an initial security label and object class/name due + * to the system setting. + */ + if (PG_ARGISNULL(0)) + { + seopts.type = SELABEL_OPT_UNUSED; + seopts.value = NULL; + } + else + { + seopts.type = SELABEL_OPT_PATH; + seopts.value = TextDatumGetCString(PG_GETARG_DATUM(0)); + } + sehnd = selabel_open(SELABEL_CTX_DB, &seopts, 1); + if (!sehnd) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("SELinux internal error"))); + PG_TRY(); + { + /* + * Right now, we have no support labeling on the shared + * database objects, such as database, role, or tablespace. + */ + exec_object_restorecon(sehnd, NamespaceRelationId); + exec_object_restorecon(sehnd, RelationRelationId); + exec_object_restorecon(sehnd, AttributeRelationId); + exec_object_restorecon(sehnd, ProcedureRelationId); + } + PG_CATCH(); + { + selabel_close(sehnd); + PG_RE_THROW(); + } + PG_END_TRY(); + + selabel_close(sehnd); + + PG_RETURN_BOOL(true); +} diff --git a/contrib/sepgsql/launcher b/contrib/sepgsql/launcher new file mode 100644 index 0000000000..9e5ecdc400 --- /dev/null +++ b/contrib/sepgsql/launcher @@ -0,0 +1,52 @@ +#!/bin/sh +# +# A wrapper script to launch psql command in regression test +# +# Copyright (c) 2010-2011, PostgreSQL Global Development Group +# +# ------------------------------------------------------------------------- + +if [ $# -lt 1 ]; then + echo "usage: `basename $0` [options...]" + exit 1 +fi + +RUNCON=`which runcon` +if [ ! -e "$RUNCON" ]; then + echo "runcon command is not found" + exit 1 +fi + +# +# Read SQL from stdin +# +TEMP=`mktemp` +CONTEXT="" + +while IFS='\\n' read LINE +do + if echo "$LINE" | grep -q "^-- @SECURITY-CONTEXT="; then + if [ -s "$TEMP" ]; then + if [ -n "$CONTEXT" ]; then + "$RUNCON" "$CONTEXT" $* < "$TEMP" + else + $* < $TEMP + fi + truncate -s0 $TEMP + fi + CONTEXT=`echo "$LINE" | sed 's/^-- @SECURITY-CONTEXT=//g'` + LINE="SELECT sepgsql_getcon(); -- confirm client privilege" + fi + echo "$LINE" >> $TEMP +done + +if [ -s "$TEMP" ]; then + if [ -n "$CONTEXT" ]; then + "$RUNCON" "$CONTEXT" $* < "$TEMP" + else + $* < $TEMP + fi +fi + +# cleanup temp file +rm -f $TEMP diff --git a/contrib/sepgsql/proc.c b/contrib/sepgsql/proc.c new file mode 100644 index 0000000000..f1a7b9b750 --- /dev/null +++ b/contrib/sepgsql/proc.c @@ -0,0 +1,158 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/proc.c + * + * Routines corresponding to procedure objects + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/sysattr.h" +#include "catalog/indexing.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" +#include "commands/seclabel.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/tqual.h" + +#include "sepgsql.h" + +/* + * sepgsql_proc_post_create + * + * This routine assigns a default security label on a newly defined + * procedure. + */ +void +sepgsql_proc_post_create(Oid functionId) +{ + Relation rel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + Oid namespaceId; + ObjectAddress object; + char *scontext; + char *tcontext; + char *ncontext; + + /* + * Fetch namespace of the new procedure. Because pg_proc entry is not + * visible right now, we need to scan the catalog using SnapshotSelf. + */ + rel = heap_open(ProcedureRelationId, AccessShareLock); + + ScanKeyInit(&skey, + ObjectIdAttributeNumber, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(functionId)); + + sscan = systable_beginscan(rel, ProcedureOidIndexId, true, + SnapshotSelf, 1, &skey); + + tuple = systable_getnext(sscan); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "catalog lookup failed for proc %u", functionId); + + namespaceId = ((Form_pg_proc) GETSTRUCT(tuple))->pronamespace; + + systable_endscan(sscan); + heap_close(rel, AccessShareLock); + + /* + * Compute a default security label when we create a new procedure + * object under the specified namespace. + */ + scontext = sepgsql_get_client_label(); + tcontext = sepgsql_get_label(NamespaceRelationId, namespaceId, 0); + ncontext = sepgsql_compute_create(scontext, tcontext, + SEPG_CLASS_DB_PROCEDURE); + + /* + * Assign the default security label on a new procedure + */ + object.classId = ProcedureRelationId; + object.objectId = functionId; + object.objectSubId = 0; + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ncontext); + + pfree(tcontext); + pfree(ncontext); +} + +/* + * sepgsql_proc_relabel + * + * It checks privileges to relabel the supplied function + * by the `seclabel'. + */ +void +sepgsql_proc_relabel(Oid functionId, const char *seclabel) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *audit_name; + + audit_name = get_func_name(functionId); + + /* + * check db_procedure:{setattr relabelfrom} permission + */ + tcontext = sepgsql_get_label(ProcedureRelationId, functionId, 0); + sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_PROCEDURE, + SEPG_DB_PROCEDURE__SETATTR | + SEPG_DB_PROCEDURE__RELABELFROM, + audit_name, + true); + pfree(tcontext); + + /* + * check db_procedure:{relabelto} permission + */ + sepgsql_check_perms(scontext, + seclabel, + SEPG_CLASS_DB_PROCEDURE, + SEPG_DB_PROCEDURE__RELABELTO, + audit_name, + true); + pfree(audit_name); +} + +/* + * sepgsql_proc_get_domtrans + * + * It computes security label of the client that shall be applied when + * the current client invokes the supplied function. + * This computed label is either same or different from the current one. + * If security policy informed the function is a trusted-procedure, + * we need to switch security label of the client during execution of + * the function. + * + * Also note that the translated label shall be allocated using palloc(). + * So, need to switch memory context, if you want to hold the string in + * someone except for CurrentMemoryContext. + */ +char * +sepgsql_proc_get_domtrans(Oid functionId) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *ncontext; + + tcontext = sepgsql_get_label(ProcedureRelationId, functionId, 0); + + ncontext = sepgsql_compute_create(scontext, + tcontext, + SEPG_CLASS_PROCESS); + pfree(tcontext); + + return ncontext; +} diff --git a/contrib/sepgsql/relation.c b/contrib/sepgsql/relation.c new file mode 100644 index 0000000000..ceaa6b0235 --- /dev/null +++ b/contrib/sepgsql/relation.c @@ -0,0 +1,267 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/label.c + * + * Routines corresponding to relation/attribute objects + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/sysattr.h" +#include "catalog/indexing.h" +#include "catalog/pg_attribute.h" +#include "catalog/pg_class.h" +#include "catalog/pg_namespace.h" +#include "commands/seclabel.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/tqual.h" + +#include "sepgsql.h" + +/* + * sepgsql_attribute_post_create + * + * This routine assigns a default security label on a newly defined + * column, using ALTER TABLE ... ADD COLUMN. + * Note that this routine is not invoked in the case of CREATE TABLE, + * although it also defines columns in addition to table. + */ +void +sepgsql_attribute_post_create(Oid relOid, AttrNumber attnum) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *ncontext; + ObjectAddress object; + + /* + * Only attributes within regular relation have individual + * security labels. + */ + if (get_rel_relkind(relOid) != RELKIND_RELATION) + return; + + /* + * Compute a default security label when we create a new procedure + * object under the specified namespace. + */ + scontext = sepgsql_get_client_label(); + tcontext = sepgsql_get_label(RelationRelationId, relOid, 0); + ncontext = sepgsql_compute_create(scontext, tcontext, + SEPG_CLASS_DB_COLUMN); + /* + * Assign the default security label on a new procedure + */ + object.classId = RelationRelationId; + object.objectId = relOid; + object.objectSubId = attnum; + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ncontext); + + pfree(tcontext); + pfree(ncontext); +} + +/* + * sepgsql_attribute_relabel + * + * It checks privileges to relabel the supplied column + * by the `seclabel'. + */ +void +sepgsql_attribute_relabel(Oid relOid, AttrNumber attnum, + const char *seclabel) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char audit_name[NAMEDATALEN * 2 + 10]; + + if (get_rel_relkind(relOid) != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot set security label on non-regular columns"))); + + snprintf(audit_name, sizeof(audit_name), "%s.%s", + get_rel_name(relOid), get_attname(relOid, attnum)); + + /* + * check db_column:{setattr relabelfrom} permission + */ + tcontext = sepgsql_get_label(RelationRelationId, relOid, attnum); + sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_COLUMN, + SEPG_DB_COLUMN__SETATTR | + SEPG_DB_COLUMN__RELABELFROM, + audit_name, + true); + pfree(tcontext); + + /* + * check db_column:{relabelto} permission + */ + sepgsql_check_perms(scontext, + seclabel, + SEPG_CLASS_DB_COLUMN, + SEPG_DB_PROCEDURE__RELABELTO, + audit_name, + true); +} + +/* + * sepgsql_relation_post_create + * + * The post creation hook of relation/attribute + */ +void +sepgsql_relation_post_create(Oid relOid) +{ + Relation rel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + Form_pg_class classForm; + ObjectAddress object; + uint16 tclass; + char *scontext; /* subject */ + char *tcontext; /* schema */ + char *rcontext; /* relation */ + char *ccontext; /* column */ + + /* + * Fetch catalog record of the new relation. Because pg_class entry is + * not visible right now, we need to scan the catalog using SnapshotSelf. + */ + rel = heap_open(RelationRelationId, AccessShareLock); + + ScanKeyInit(&skey, + ObjectIdAttributeNumber, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relOid)); + + sscan = systable_beginscan(rel, ClassOidIndexId, true, + SnapshotSelf, 1, &skey); + + tuple = systable_getnext(sscan); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "catalog lookup failed for relation %u", relOid); + + classForm = (Form_pg_class) GETSTRUCT(tuple); + + if (classForm->relkind == RELKIND_RELATION) + tclass = SEPG_CLASS_DB_TABLE; + else if (classForm->relkind == RELKIND_SEQUENCE) + tclass = SEPG_CLASS_DB_SEQUENCE; + else if (classForm->relkind == RELKIND_VIEW) + tclass = SEPG_CLASS_DB_VIEW; + else + goto out; /* No need to assign individual labels */ + + /* + * Compute a default security label when we create a new relation + * object under the specified namespace. + */ + scontext = sepgsql_get_client_label(); + tcontext = sepgsql_get_label(NamespaceRelationId, + classForm->relnamespace, 0); + rcontext = sepgsql_compute_create(scontext, tcontext, tclass); + + /* + * Assign the default security label on the new relation + */ + object.classId = RelationRelationId; + object.objectId = relOid; + object.objectSubId = 0; + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, rcontext); + + /* + * We also assigns a default security label on columns of the new + * regular tables. + */ + if (classForm->relkind == RELKIND_RELATION) + { + AttrNumber index; + + ccontext = sepgsql_compute_create(scontext, rcontext, + SEPG_CLASS_DB_COLUMN); + for (index = FirstLowInvalidHeapAttributeNumber + 1; + index <= classForm->relnatts; + index++) + { + if (index == InvalidAttrNumber) + continue; + + if (index == ObjectIdAttributeNumber && !classForm->relhasoids) + continue; + + object.classId = RelationRelationId; + object.objectId = relOid; + object.objectSubId = index; + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ccontext); + } + pfree(ccontext); + } + pfree(rcontext); +out: + systable_endscan(sscan); + heap_close(rel, AccessShareLock); +} + +/* + * sepgsql_relation_relabel + * + * It checks privileges to relabel the supplied relation by the `seclabel'. + */ +void +sepgsql_relation_relabel(Oid relOid, const char *seclabel) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *audit_name; + char relkind; + uint16_t tclass = 0; + + relkind = get_rel_relkind(relOid); + if (relkind == RELKIND_RELATION) + tclass = SEPG_CLASS_DB_TABLE; + else if (relkind == RELKIND_SEQUENCE) + tclass = SEPG_CLASS_DB_SEQUENCE; + else if (relkind == RELKIND_VIEW) + tclass = SEPG_CLASS_DB_VIEW; + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot set security labels on relations except " + "for tables, sequences or views"))); + + audit_name = get_rel_name(relOid); + + /* + * check db_xxx:{setattr relabelfrom} permission + */ + tcontext = sepgsql_get_label(RelationRelationId, relOid, 0); + + sepgsql_check_perms(scontext, + tcontext, + tclass, + SEPG_DB_TABLE__SETATTR | + SEPG_DB_TABLE__RELABELFROM, + audit_name, + true); + pfree(tcontext); + + /* + * check db_xxx:{relabelto} permission + */ + sepgsql_check_perms(scontext, + seclabel, + tclass, + SEPG_DB_TABLE__RELABELTO, + audit_name, + true); +} diff --git a/contrib/sepgsql/schema.c b/contrib/sepgsql/schema.c new file mode 100644 index 0000000000..df33a02735 --- /dev/null +++ b/contrib/sepgsql/schema.c @@ -0,0 +1,98 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/schema.c + * + * Routines corresponding to schema objects + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_namespace.h" +#include "commands/seclabel.h" +#include "utils/lsyscache.h" + +#include "sepgsql.h" + +/* + * sepgsql_schema_post_create + * + * This routine assigns a default security label on a newly defined + * schema. + */ +void +sepgsql_schema_post_create(Oid namespaceId) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *ncontext; + ObjectAddress object; + + /* + * FIXME: Right now, we assume pg_database object has a fixed + * security label, because pg_seclabel does not support to store + * label of shared database objects. + */ + tcontext = "system_u:object_r:sepgsql_db_t:s0"; + + /* + * Compute a default security label when we create a new schema + * object under the working database. + */ + ncontext = sepgsql_compute_create(scontext, tcontext, + SEPG_CLASS_DB_SCHEMA); + + /* + * Assign the default security label on a new procedure + */ + object.classId = NamespaceRelationId; + object.objectId = namespaceId; + object.objectSubId = 0; + SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ncontext); + + pfree(ncontext); +} + +/* + * sepgsql_schema_relabel + * + * It checks privileges to relabel the supplied schema + * by the `seclabel'. + */ +void +sepgsql_schema_relabel(Oid namespaceId, const char *seclabel) +{ + char *scontext = sepgsql_get_client_label(); + char *tcontext; + char *audit_name; + + audit_name = get_namespace_name(namespaceId); + + /* + * check db_schema:{setattr relabelfrom} permission + */ + tcontext = sepgsql_get_label(NamespaceRelationId, namespaceId, 0); + + sepgsql_check_perms(scontext, + tcontext, + SEPG_CLASS_DB_SCHEMA, + SEPG_DB_SCHEMA__SETATTR | + SEPG_DB_SCHEMA__RELABELFROM, + audit_name, + true); + + /* + * check db_schema:{relabelto} permission + */ + sepgsql_check_perms(scontext, + seclabel, + SEPG_CLASS_DB_SCHEMA, + SEPG_DB_SCHEMA__RELABELTO, + audit_name, + true); + + pfree(tcontext); + pfree(audit_name); +} diff --git a/contrib/sepgsql/selinux.c b/contrib/sepgsql/selinux.c new file mode 100644 index 0000000000..a67bd56711 --- /dev/null +++ b/contrib/sepgsql/selinux.c @@ -0,0 +1,631 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/selinux.c + * + * Interactions between userspace and selinux in kernelspace, + * using libselinux api. + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "lib/stringinfo.h" + +#include "sepgsql.h" + +/* + * selinux_catalog + * + * This mapping table enables to translate the name of object classes and + * access vectors to/from their own codes. + * When we ask SELinux whether the required privileges are allowed or not, + * we use security_compute_av(3). It needs us to represent object classes + * and access vectors using 'external' codes defined in the security policy. + * It is determinded in the runtime, not build time. So, it needs an internal + * service to translate object class/access vectors which we want to check + * into the code which kernel want to be given. + */ +static struct +{ + const char *class_name; + uint16 class_code; + struct + { + const char *av_name; + uint32 av_code; + } av[32]; +} selinux_catalog[] = { + { + "process", SEPG_CLASS_PROCESS, + { + { "transition", SEPG_PROCESS__TRANSITION }, + { NULL, 0UL } + } + }, + { + "file", SEPG_CLASS_FILE, + { + { "read", SEPG_FILE__READ }, + { "write", SEPG_FILE__WRITE }, + { "create", SEPG_FILE__CREATE }, + { "getattr", SEPG_FILE__GETATTR }, + { "unlink", SEPG_FILE__UNLINK }, + { "rename", SEPG_FILE__RENAME }, + { "append", SEPG_FILE__APPEND }, + { NULL, 0UL } + } + }, + { + "dir", SEPG_CLASS_DIR, + { + { "read", SEPG_DIR__READ }, + { "write", SEPG_DIR__WRITE }, + { "create", SEPG_DIR__CREATE }, + { "getattr", SEPG_DIR__GETATTR }, + { "unlink", SEPG_DIR__UNLINK }, + { "rename", SEPG_DIR__RENAME }, + { "search", SEPG_DIR__SEARCH }, + { "add_name", SEPG_DIR__ADD_NAME }, + { "remove_name", SEPG_DIR__REMOVE_NAME }, + { "rmdir", SEPG_DIR__RMDIR }, + { "reparent", SEPG_DIR__REPARENT }, + { NULL, 0UL } + } + }, + { + "lnk_file", SEPG_CLASS_LNK_FILE, + { + { "read", SEPG_LNK_FILE__READ }, + { "write", SEPG_LNK_FILE__WRITE }, + { "create", SEPG_LNK_FILE__CREATE }, + { "getattr", SEPG_LNK_FILE__GETATTR }, + { "unlink", SEPG_LNK_FILE__UNLINK }, + { "rename", SEPG_LNK_FILE__RENAME }, + { NULL, 0UL } + } + }, + { + "chr_file", SEPG_CLASS_CHR_FILE, + { + { "read", SEPG_CHR_FILE__READ }, + { "write", SEPG_CHR_FILE__WRITE }, + { "create", SEPG_CHR_FILE__CREATE }, + { "getattr", SEPG_CHR_FILE__GETATTR }, + { "unlink", SEPG_CHR_FILE__UNLINK }, + { "rename", SEPG_CHR_FILE__RENAME }, + { NULL, 0UL } + } + }, + { + "blk_file", SEPG_CLASS_BLK_FILE, + { + { "read", SEPG_BLK_FILE__READ }, + { "write", SEPG_BLK_FILE__WRITE }, + { "create", SEPG_BLK_FILE__CREATE }, + { "getattr", SEPG_BLK_FILE__GETATTR }, + { "unlink", SEPG_BLK_FILE__UNLINK }, + { "rename", SEPG_BLK_FILE__RENAME }, + { NULL, 0UL } + } + }, + { + "sock_file", SEPG_CLASS_SOCK_FILE, + { + { "read", SEPG_SOCK_FILE__READ }, + { "write", SEPG_SOCK_FILE__WRITE }, + { "create", SEPG_SOCK_FILE__CREATE }, + { "getattr", SEPG_SOCK_FILE__GETATTR }, + { "unlink", SEPG_SOCK_FILE__UNLINK }, + { "rename", SEPG_SOCK_FILE__RENAME }, + { NULL, 0UL } + } + }, + { + "fifo_file", SEPG_CLASS_FIFO_FILE, + { + { "read", SEPG_FIFO_FILE__READ }, + { "write", SEPG_FIFO_FILE__WRITE }, + { "create", SEPG_FIFO_FILE__CREATE }, + { "getattr", SEPG_FIFO_FILE__GETATTR }, + { "unlink", SEPG_FIFO_FILE__UNLINK }, + { "rename", SEPG_FIFO_FILE__RENAME }, + { NULL, 0UL } + } + }, + { + "db_database", SEPG_CLASS_DB_DATABASE, + { + { "create", SEPG_DB_DATABASE__CREATE }, + { "drop", SEPG_DB_DATABASE__DROP }, + { "getattr", SEPG_DB_DATABASE__GETATTR }, + { "setattr", SEPG_DB_DATABASE__SETATTR }, + { "relabelfrom", SEPG_DB_DATABASE__RELABELFROM }, + { "relabelto", SEPG_DB_DATABASE__RELABELTO }, + { "access", SEPG_DB_DATABASE__ACCESS }, + { "load_module", SEPG_DB_DATABASE__LOAD_MODULE }, + { NULL, 0UL }, + } + }, + { + "db_schema", SEPG_CLASS_DB_SCHEMA, + { + { "create", SEPG_DB_SCHEMA__CREATE }, + { "drop", SEPG_DB_SCHEMA__DROP }, + { "getattr", SEPG_DB_SCHEMA__GETATTR }, + { "setattr", SEPG_DB_SCHEMA__SETATTR }, + { "relabelfrom", SEPG_DB_SCHEMA__RELABELFROM }, + { "relabelto", SEPG_DB_SCHEMA__RELABELTO }, + { "search", SEPG_DB_SCHEMA__SEARCH }, + { "add_name", SEPG_DB_SCHEMA__ADD_NAME }, + { "remove_name", SEPG_DB_SCHEMA__REMOVE_NAME }, + { NULL, 0UL }, + } + }, + { + "db_table", SEPG_CLASS_DB_TABLE, + { + { "create", SEPG_DB_TABLE__CREATE }, + { "drop", SEPG_DB_TABLE__DROP }, + { "getattr", SEPG_DB_TABLE__GETATTR }, + { "setattr", SEPG_DB_TABLE__SETATTR }, + { "relabelfrom", SEPG_DB_TABLE__RELABELFROM }, + { "relabelto", SEPG_DB_TABLE__RELABELTO }, + { "select", SEPG_DB_TABLE__SELECT }, + { "update", SEPG_DB_TABLE__UPDATE }, + { "insert", SEPG_DB_TABLE__INSERT }, + { "delete", SEPG_DB_TABLE__DELETE }, + { "lock", SEPG_DB_TABLE__LOCK }, + { NULL, 0UL }, + } + }, + { + "db_sequence", SEPG_CLASS_DB_SEQUENCE, + { + { "create", SEPG_DB_SEQUENCE__CREATE }, + { "drop", SEPG_DB_SEQUENCE__DROP }, + { "getattr", SEPG_DB_SEQUENCE__GETATTR }, + { "setattr", SEPG_DB_SEQUENCE__SETATTR }, + { "relabelfrom", SEPG_DB_SEQUENCE__RELABELFROM }, + { "relabelto", SEPG_DB_SEQUENCE__RELABELTO }, + { "get_value", SEPG_DB_SEQUENCE__GET_VALUE }, + { "next_value", SEPG_DB_SEQUENCE__NEXT_VALUE }, + { "set_value", SEPG_DB_SEQUENCE__SET_VALUE }, + { NULL, 0UL }, + } + }, + { + "db_procedure", SEPG_CLASS_DB_PROCEDURE, + { + { "create", SEPG_DB_PROCEDURE__CREATE }, + { "drop", SEPG_DB_PROCEDURE__DROP }, + { "getattr", SEPG_DB_PROCEDURE__GETATTR }, + { "setattr", SEPG_DB_PROCEDURE__SETATTR }, + { "relabelfrom", SEPG_DB_PROCEDURE__RELABELFROM }, + { "relabelto", SEPG_DB_PROCEDURE__RELABELTO }, + { "execute", SEPG_DB_PROCEDURE__EXECUTE }, + { "entrypoint", SEPG_DB_PROCEDURE__ENTRYPOINT }, + { "install", SEPG_DB_PROCEDURE__INSTALL }, + { NULL, 0UL }, + } + }, + { + "db_column", SEPG_CLASS_DB_COLUMN, + { + { "create", SEPG_DB_COLUMN__CREATE }, + { "drop", SEPG_DB_COLUMN__DROP }, + { "getattr", SEPG_DB_COLUMN__GETATTR }, + { "setattr", SEPG_DB_COLUMN__SETATTR }, + { "relabelfrom", SEPG_DB_COLUMN__RELABELFROM }, + { "relabelto", SEPG_DB_COLUMN__RELABELTO }, + { "select", SEPG_DB_COLUMN__SELECT }, + { "update", SEPG_DB_COLUMN__UPDATE }, + { "insert", SEPG_DB_COLUMN__INSERT }, + { NULL, 0UL }, + } + }, + { + "db_tuple", SEPG_CLASS_DB_TUPLE, + { + { "relabelfrom", SEPG_DB_TUPLE__RELABELFROM }, + { "relabelto", SEPG_DB_TUPLE__RELABELTO }, + { "select", SEPG_DB_TUPLE__SELECT }, + { "update", SEPG_DB_TUPLE__UPDATE }, + { "insert", SEPG_DB_TUPLE__INSERT }, + { "delete", SEPG_DB_TUPLE__DELETE }, + { NULL, 0UL }, + } + }, + { + "db_blob", SEPG_CLASS_DB_BLOB, + { + { "create", SEPG_DB_BLOB__CREATE }, + { "drop", SEPG_DB_BLOB__DROP }, + { "getattr", SEPG_DB_BLOB__GETATTR }, + { "setattr", SEPG_DB_BLOB__SETATTR }, + { "relabelfrom", SEPG_DB_BLOB__RELABELFROM }, + { "relabelto", SEPG_DB_BLOB__RELABELTO }, + { "read", SEPG_DB_BLOB__READ }, + { "write", SEPG_DB_BLOB__WRITE }, + { "import", SEPG_DB_BLOB__IMPORT }, + { "export", SEPG_DB_BLOB__EXPORT }, + { NULL, 0UL }, + } + }, + { + "db_language", SEPG_CLASS_DB_LANGUAGE, + { + { "create", SEPG_DB_LANGUAGE__CREATE }, + { "drop", SEPG_DB_LANGUAGE__DROP }, + { "getattr", SEPG_DB_LANGUAGE__GETATTR }, + { "setattr", SEPG_DB_LANGUAGE__SETATTR }, + { "relabelfrom", SEPG_DB_LANGUAGE__RELABELFROM }, + { "relabelto", SEPG_DB_LANGUAGE__RELABELTO }, + { "implement", SEPG_DB_LANGUAGE__IMPLEMENT }, + { "execute", SEPG_DB_LANGUAGE__EXECUTE }, + { NULL, 0UL }, + } + }, + { + "db_view", SEPG_CLASS_DB_VIEW, + { + { "create", SEPG_DB_VIEW__CREATE }, + { "drop", SEPG_DB_VIEW__DROP }, + { "getattr", SEPG_DB_VIEW__GETATTR }, + { "setattr", SEPG_DB_VIEW__SETATTR }, + { "relabelfrom", SEPG_DB_VIEW__RELABELFROM }, + { "relabelto", SEPG_DB_VIEW__RELABELTO }, + { "expand", SEPG_DB_VIEW__EXPAND }, + { NULL, 0UL }, + } + }, +}; + +/* + * sepgsql_mode + * + * SEPGSQL_MODE_DISABLED: Disabled on runtime + * SEPGSQL_MODE_DEFAULT: Same as system settings + * SEPGSQL_MODE_PERMISSIVE: Always permissive mode + * SEPGSQL_MODE_INTERNAL: Same as permissive, except for no audit logs + */ +static int sepgsql_mode = SEPGSQL_MODE_INTERNAL; + +/* + * sepgsql_is_enabled + */ +bool +sepgsql_is_enabled(void) +{ + return (sepgsql_mode != SEPGSQL_MODE_DISABLED ? true : false); +} + +/* + * sepgsql_get_mode + */ +int +sepgsql_get_mode(void) +{ + return sepgsql_mode; +} + +/* + * sepgsql_set_mode + */ +int +sepgsql_set_mode(int new_mode) +{ + int old_mode = sepgsql_mode; + + sepgsql_mode = new_mode; + + return old_mode; +} + +/* + * sepgsql_getenforce + * + * It returns whether the current working mode tries to enforce access + * control decision, or not. It shall be enforced when sepgsql_mode is + * SEPGSQL_MODE_DEFAULT and system is running in enforcing mode. + */ +bool +sepgsql_getenforce(void) +{ + if (sepgsql_mode == SEPGSQL_MODE_DEFAULT && + security_getenforce() > 0) + return true; + + return false; +} + +/* + * sepgsql_audit_log + * + * It generates a security audit record. In the default, it writes out + * audit records into standard PG's logfile. It also allows to set up + * external audit log receiver, such as auditd in Linux, using the + * sepgsql_audit_hook. + * + * SELinux can control what should be audited and should not using + * "auditdeny" and "auditallow" rules in the security policy. In the + * default, all the access violations are audited, and all the access + * allowed are not audited. But we can set up the security policy, so + * we can have exceptions. So, it is necessary to follow the suggestion + * come from the security policy. (av_decision.auditallow and auditdeny) + * + * Security audit is an important feature, because it enables us to check + * what was happen if we have a security incident. In fact, ISO/IEC15408 + * defines several security functionalities for audit features. + */ +void +sepgsql_audit_log(bool denied, + const char *scontext, + const char *tcontext, + uint16 tclass, + uint32 audited, + const char *audit_name) +{ + StringInfoData buf; + const char *class_name; + const char *av_name; + int i; + + /* lookup name of the object class */ + Assert(tclass < SEPG_CLASS_MAX); + class_name = selinux_catalog[tclass].class_name; + + /* lookup name of the permissions */ + initStringInfo(&buf); + appendStringInfo(&buf, "%s {", + (denied ? "denied" : "allowed")); + for (i=0; selinux_catalog[tclass].av[i].av_name; i++) + { + if (audited & (1UL << i)) + { + av_name = selinux_catalog[tclass].av[i].av_name; + appendStringInfo(&buf, " %s", av_name); + } + } + appendStringInfo(&buf, " }"); + + /* + * Call external audit module, if loaded + */ + appendStringInfo(&buf, " scontext=%s tcontext=%s tclass=%s", + scontext, tcontext, class_name); + if (audit_name) + appendStringInfo(&buf, " name=%s", audit_name); + + ereport(LOG, (errmsg("SELinux: %s", buf.data))); +} + +/* + * sepgsql_compute_avd + * + * It actually asks SELinux what permissions are allowed on a pair of + * the security contexts and object class. It also returns what permissions + * should be audited on access violation or allowed. + * In most cases, subject's security context (scontext) is a client, and + * target security context (tcontext) is a database object. + * + * The access control decision shall be set on the given av_decision. + * The av_decision.allowed has a bitmask of SEPG___ + * to suggest a set of allowed actions in this object class. + */ +void +sepgsql_compute_avd(const char *scontext, + const char *tcontext, + uint16 tclass, + struct av_decision *avd) +{ + const char *tclass_name; + security_class_t tclass_ex; + struct av_decision avd_ex; + int i, deny_unknown = security_deny_unknown(); + + /* Get external code of the object class*/ + Assert(tclass < SEPG_CLASS_MAX); + Assert(tclass == selinux_catalog[tclass].class_code); + + tclass_name = selinux_catalog[tclass].class_name; + tclass_ex = string_to_security_class(tclass_name); + + if (tclass_ex == 0) + { + /* + * If the current security policy does not support permissions + * corresponding to database objects, we fill up them with dummy + * data. + * If security_deny_unknown() returns positive value, undefined + * permissions should be denied. Otherwise, allowed + */ + avd->allowed = (security_deny_unknown() > 0 ? 0 : ~0); + avd->auditallow = 0U; + avd->auditdeny = ~0U; + avd->flags = 0; + + return; + } + + /* + * Ask SELinux what is allowed set of permissions on a pair of the + * security contexts and the given object class. + */ + if (security_compute_av_flags_raw((security_context_t)scontext, + (security_context_t)tcontext, + tclass_ex, 0, &avd_ex) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("SELinux could not compute av_decision: " + "scontext=%s tcontext=%s tclass=%s", + scontext, tcontext, tclass_name))); + + /* + * SELinux returns its access control decision as a set of permissions + * represented in external code which depends on run-time environment. + * So, we need to translate it to the internal representation before + * returning results for the caller. + */ + memset(avd, 0, sizeof(struct av_decision)); + + for (i=0; selinux_catalog[tclass].av[i].av_name; i++) + { + access_vector_t av_code_ex; + const char *av_name = selinux_catalog[tclass].av[i].av_name; + uint32 av_code = selinux_catalog[tclass].av[i].av_code; + + av_code_ex = string_to_av_perm(tclass_ex, av_name); + if (av_code_ex == 0) + { + /* fill up undefined permissions */ + if (!deny_unknown) + avd->allowed |= av_code; + avd->auditdeny |= av_code; + + continue; + } + + if (avd_ex.allowed & av_code_ex) + avd->allowed |= av_code; + if (avd_ex.auditallow & av_code_ex) + avd->auditallow |= av_code; + if (avd_ex.auditdeny & av_code_ex) + avd->auditdeny |= av_code; + } + + return; +} + +/* + * sepgsql_compute_create + * + * It returns a default security context to be assigned on a new database + * object. SELinux compute it based on a combination of client, upper object + * which owns the new object and object class. + * + * For example, when a client (staff_u:staff_r:staff_t:s0) tries to create + * a new table within a schema (system_u:object_r:sepgsql_schema_t:s0), + * SELinux looks-up its security policy. If it has a special rule on the + * combination of these security contexts and object class (db_table), + * it returns the security context suggested by the special rule. + * Otherwise, it returns the security context of schema, as is. + * + * We expect the caller already applies sanity/validation checks on the + * given security context. + * + * scontext: security context of the subject (mostly, peer process). + * tcontext: security context of the the upper database object. + * tclass: class code (SEPG_CLASS_*) of the new object in creation + */ +char * +sepgsql_compute_create(const char *scontext, + const char *tcontext, + uint16 tclass) +{ + security_context_t ncontext; + security_class_t tclass_ex; + const char *tclass_name; + char *result; + + /* Get external code of the object class*/ + Assert(tclass < SEPG_CLASS_MAX); + + tclass_name = selinux_catalog[tclass].class_name; + tclass_ex = string_to_security_class(tclass_name); + + /* + * Ask SELinux what is the default context for the given object class + * on a pair of security contexts + */ + if (security_compute_create_raw((security_context_t)scontext, + (security_context_t)tcontext, + tclass_ex, &ncontext) < 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("SELinux could not compute a new context: " + "scontext=%s tcontext=%s tclass=%s", + scontext, tcontext, tclass_name))); + + /* + * libselinux returns malloc()'ed string, so we need to copy it + * on the palloc()'ed region. + */ + PG_TRY(); + { + result = pstrdup(ncontext); + } + PG_CATCH(); + { + freecon(ncontext); + PG_RE_THROW(); + } + PG_END_TRY(); + freecon(ncontext); + + return result; +} + +/* + * sepgsql_check_perms + * + * It makes access control decision without userspace caching mechanism. + * If SELinux denied the required accesses on the pair of security labels, + * it raises an error or returns false. + * + * scontext: security label of the subject (mostly, peer process) + * tcontext: security label of the object being referenced + * tclass: class code (SEPG_CLASS_*) of the object being referenced + * required: a mask of required permissions (SEPG___) + * audit_name: a human readable object name for audit logs, or NULL. + * abort: true, if caller wants to raise an error on access violation + */ +bool +sepgsql_check_perms(const char *scontext, + const char *tcontext, + uint16 tclass, + uint32 required, + const char *audit_name, + bool abort) +{ + struct av_decision avd; + uint32 denied; + uint32 audited; + bool result = true; + + sepgsql_compute_avd(scontext, tcontext, tclass, &avd); + + denied = required & ~avd.allowed; + + if (sepgsql_get_debug_audit()) + audited = (denied ? denied : required); + else + audited = (denied ? (denied & avd.auditdeny) + : (required & avd.auditallow)); + + if (denied && + sepgsql_getenforce() > 0 && + (avd.flags & SELINUX_AVD_FLAGS_PERMISSIVE) == 0) + result = false; + + /* + * It records a security audit for the request, if needed. + * But, when SE-PgSQL performs 'internal' mode, it needs to keep silent. + */ + if (audited && sepgsql_mode != SEPGSQL_MODE_INTERNAL) + { + sepgsql_audit_log(denied, + scontext, + tcontext, + tclass, + audited, + audit_name); + } + + if (!result && abort) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("SELinux: security policy violation"))); + return result; +} diff --git a/contrib/sepgsql/sepgsql-regtest.te b/contrib/sepgsql/sepgsql-regtest.te new file mode 100644 index 0000000000..66666d0c38 --- /dev/null +++ b/contrib/sepgsql/sepgsql-regtest.te @@ -0,0 +1,59 @@ +policy_module(sepgsql-regtest, 1.01) + +## +##

+## Allow to launch regression test of SE-PostgreSQL +## Don't switch to TRUE in normal cases +##

+##
+gen_tunable(sepgsql_regression_test_mode, false) + +# +# Test domains for database administrators +# +role sepgsql_regtest_dba_r; +userdom_base_user_template(sepgsql_regtest_dba) +userdom_manage_home_role(sepgsql_regtest_dba_r, sepgsql_regtest_dba_t) +userdom_write_user_tmp_sockets(sepgsql_regtest_user_t) +optional_policy(` + postgresql_admin(sepgsql_regtest_dba_t, sepgsql_regtest_dba_r) + postgresql_stream_connect(sepgsql_regtest_dba_t) +') +optional_policy(` + unconfined_stream_connect(sepgsql_regtest_dba_t) + unconfined_rw_pipes(sepgsql_regtest_dba_t) +') + +# +# Dummy domain for unpriv users +# +role sepgsql_regtest_user_r; +userdom_base_user_template(sepgsql_regtest_user) +userdom_manage_home_role(sepgsql_regtest_user_r, sepgsql_regtest_user_t) +userdom_write_user_tmp_sockets(sepgsql_regtest_user_t) +optional_policy(` + postgresql_role(sepgsql_regtest_user_r, sepgsql_regtest_user_t) + postgresql_stream_connect(sepgsql_regtest_user_t) +') +optional_policy(` + unconfined_stream_connect(sepgsql_regtest_user_t) + unconfined_rw_pipes(sepgsql_regtest_user_t) +') + +# +# Rules to launch psql in the dummy domains +# +optional_policy(` + gen_require(` + role unconfined_r; + type unconfined_t; + 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 }; + ') + role unconfined_r types sepgsql_regtest_dba_t; + role unconfined_r types sepgsql_regtest_user_t; + role unconfined_r types sepgsql_trusted_proc_t; +') diff --git a/contrib/sepgsql/sepgsql.h b/contrib/sepgsql/sepgsql.h new file mode 100644 index 0000000000..1a27818fbe --- /dev/null +++ b/contrib/sepgsql/sepgsql.h @@ -0,0 +1,288 @@ +/* ------------------------------------------------------------------------- + * + * contrib/sepgsql/sepgsql.h + * + * Definitions corresponding to SE-PostgreSQL + * + * Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * ------------------------------------------------------------------------- + */ +#ifndef SEPGSQL_H +#define SEPGSQL_H + +#include "catalog/objectaddress.h" +#include + +/* + * SE-PostgreSQL Label Tag + */ +#define SEPGSQL_LABEL_TAG "selinux" + +/* + * SE-PostgreSQL performing mode + */ +#define SEPGSQL_MODE_DEFAULT 1 +#define SEPGSQL_MODE_PERMISSIVE 2 +#define SEPGSQL_MODE_INTERNAL 3 +#define SEPGSQL_MODE_DISABLED 4 + +/* + * Internally used code of object classes + */ +#define SEPG_CLASS_PROCESS 0 +#define SEPG_CLASS_FILE 1 +#define SEPG_CLASS_DIR 2 +#define SEPG_CLASS_LNK_FILE 3 +#define SEPG_CLASS_CHR_FILE 4 +#define SEPG_CLASS_BLK_FILE 5 +#define SEPG_CLASS_SOCK_FILE 6 +#define SEPG_CLASS_FIFO_FILE 7 +#define SEPG_CLASS_DB_DATABASE 8 +#define SEPG_CLASS_DB_SCHEMA 9 +#define SEPG_CLASS_DB_TABLE 10 +#define SEPG_CLASS_DB_SEQUENCE 11 +#define SEPG_CLASS_DB_PROCEDURE 12 +#define SEPG_CLASS_DB_COLUMN 13 +#define SEPG_CLASS_DB_TUPLE 14 +#define SEPG_CLASS_DB_BLOB 15 +#define SEPG_CLASS_DB_LANGUAGE 16 +#define SEPG_CLASS_DB_VIEW 17 +#define SEPG_CLASS_MAX 18 + +/* + * Internally used code of access vectors + */ +#define SEPG_PROCESS__TRANSITION (1<<0) + +#define SEPG_FILE__READ (1<<0) +#define SEPG_FILE__WRITE (1<<1) +#define SEPG_FILE__CREATE (1<<2) +#define SEPG_FILE__GETATTR (1<<3) +#define SEPG_FILE__UNLINK (1<<4) +#define SEPG_FILE__RENAME (1<<5) +#define SEPG_FILE__APPEND (1<<6) + +#define SEPG_DIR__READ (SEPG_FILE__READ) +#define SEPG_DIR__WRITE (SEPG_FILE__WRITE) +#define SEPG_DIR__CREATE (SEPG_FILE__CREATE) +#define SEPG_DIR__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_DIR__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_DIR__RENAME (SEPG_FILE__RENAME) +#define SEPG_DIR__SEARCH (1<<6) +#define SEPG_DIR__ADD_NAME (1<<7) +#define SEPG_DIR__REMOVE_NAME (1<<8) +#define SEPG_DIR__RMDIR (1<<9) +#define SEPG_DIR__REPARENT (1<<10) + +#define SEPG_LNK_FILE__READ (SEPG_FILE__READ) +#define SEPG_LNK_FILE__WRITE (SEPG_FILE__WRITE) +#define SEPG_LNK_FILE__CREATE (SEPG_FILE__CREATE) +#define SEPG_LNK_FILE__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_LNK_FILE__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_LNK_FILE__RENAME (SEPG_FILE__RENAME) + +#define SEPG_CHR_FILE__READ (SEPG_FILE__READ) +#define SEPG_CHR_FILE__WRITE (SEPG_FILE__WRITE) +#define SEPG_CHR_FILE__CREATE (SEPG_FILE__CREATE) +#define SEPG_CHR_FILE__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_CHR_FILE__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_CHR_FILE__RENAME (SEPG_FILE__RENAME) + +#define SEPG_BLK_FILE__READ (SEPG_FILE__READ) +#define SEPG_BLK_FILE__WRITE (SEPG_FILE__WRITE) +#define SEPG_BLK_FILE__CREATE (SEPG_FILE__CREATE) +#define SEPG_BLK_FILE__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_BLK_FILE__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_BLK_FILE__RENAME (SEPG_FILE__RENAME) + +#define SEPG_SOCK_FILE__READ (SEPG_FILE__READ) +#define SEPG_SOCK_FILE__WRITE (SEPG_FILE__WRITE) +#define SEPG_SOCK_FILE__CREATE (SEPG_FILE__CREATE) +#define SEPG_SOCK_FILE__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_SOCK_FILE__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_SOCK_FILE__RENAME (SEPG_FILE__RENAME) + +#define SEPG_FIFO_FILE__READ (SEPG_FILE__READ) +#define SEPG_FIFO_FILE__WRITE (SEPG_FILE__WRITE) +#define SEPG_FIFO_FILE__CREATE (SEPG_FILE__CREATE) +#define SEPG_FIFO_FILE__GETATTR (SEPG_FILE__GETATTR) +#define SEPG_FIFO_FILE__UNLINK (SEPG_FILE__UNLINK) +#define SEPG_FIFO_FILE__RENAME (SEPG_FILE__RENAME) + +#define SEPG_DB_DATABASE__CREATE (1<<0) +#define SEPG_DB_DATABASE__DROP (1<<1) +#define SEPG_DB_DATABASE__GETATTR (1<<2) +#define SEPG_DB_DATABASE__SETATTR (1<<3) +#define SEPG_DB_DATABASE__RELABELFROM (1<<4) +#define SEPG_DB_DATABASE__RELABELTO (1<<5) +#define SEPG_DB_DATABASE__ACCESS (1<<6) +#define SEPG_DB_DATABASE__LOAD_MODULE (1<<7) + +#define SEPG_DB_SCHEMA__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_SCHEMA__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_SCHEMA__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_SCHEMA__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_SCHEMA__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_SCHEMA__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_SCHEMA__SEARCH (1<<6) +#define SEPG_DB_SCHEMA__ADD_NAME (1<<7) +#define SEPG_DB_SCHEMA__REMOVE_NAME (1<<8) + +#define SEPG_DB_TABLE__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_TABLE__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_TABLE__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_TABLE__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_TABLE__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_TABLE__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_TABLE__SELECT (1<<6) +#define SEPG_DB_TABLE__UPDATE (1<<7) +#define SEPG_DB_TABLE__INSERT (1<<8) +#define SEPG_DB_TABLE__DELETE (1<<9) +#define SEPG_DB_TABLE__LOCK (1<<10) +#define SEPG_DB_TABLE__INDEXON (1<<11) + +#define SEPG_DB_SEQUENCE__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_SEQUENCE__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_SEQUENCE__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_SEQUENCE__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_SEQUENCE__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_SEQUENCE__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_SEQUENCE__GET_VALUE (1<<6) +#define SEPG_DB_SEQUENCE__NEXT_VALUE (1<<7) +#define SEPG_DB_SEQUENCE__SET_VALUE (1<<8) + +#define SEPG_DB_PROCEDURE__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_PROCEDURE__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_PROCEDURE__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_PROCEDURE__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_PROCEDURE__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_PROCEDURE__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_PROCEDURE__EXECUTE (1<<6) +#define SEPG_DB_PROCEDURE__ENTRYPOINT (1<<7) +#define SEPG_DB_PROCEDURE__INSTALL (1<<8) + +#define SEPG_DB_COLUMN__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_COLUMN__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_COLUMN__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_COLUMN__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_COLUMN__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_COLUMN__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_COLUMN__SELECT (1<<6) +#define SEPG_DB_COLUMN__UPDATE (1<<7) +#define SEPG_DB_COLUMN__INSERT (1<<8) + +#define SEPG_DB_TUPLE__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_TUPLE__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_TUPLE__SELECT (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_TUPLE__UPDATE (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_TUPLE__INSERT (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_TUPLE__DELETE (SEPG_DB_DATABASE__DROP) + +#define SEPG_DB_BLOB__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_BLOB__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_BLOB__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_BLOB__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_BLOB__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_BLOB__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_BLOB__READ (1<<6) +#define SEPG_DB_BLOB__WRITE (1<<7) +#define SEPG_DB_BLOB__IMPORT (1<<8) +#define SEPG_DB_BLOB__EXPORT (1<<9) + +#define SEPG_DB_LANGUAGE__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_LANGUAGE__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_LANGUAGE__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_LANGUAGE__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_LANGUAGE__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_LANGUAGE__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_LANGUAGE__IMPLEMENT (1<<6) +#define SEPG_DB_LANGUAGE__EXECUTE (1<<7) + +#define SEPG_DB_VIEW__CREATE (SEPG_DB_DATABASE__CREATE) +#define SEPG_DB_VIEW__DROP (SEPG_DB_DATABASE__DROP) +#define SEPG_DB_VIEW__GETATTR (SEPG_DB_DATABASE__GETATTR) +#define SEPG_DB_VIEW__SETATTR (SEPG_DB_DATABASE__SETATTR) +#define SEPG_DB_VIEW__RELABELFROM (SEPG_DB_DATABASE__RELABELFROM) +#define SEPG_DB_VIEW__RELABELTO (SEPG_DB_DATABASE__RELABELTO) +#define SEPG_DB_VIEW__EXPAND (1<<6) + +/* + * hooks.c + */ +extern bool sepgsql_get_permissive(void); +extern bool sepgsql_get_debug_audit(void); + +/* + * selinux.c + */ +extern bool sepgsql_is_enabled(void); +extern int sepgsql_get_mode(void); +extern int sepgsql_set_mode(int new_mode); +extern bool sepgsql_getenforce(void); + +extern void sepgsql_audit_log(bool denied, + const char *scontext, + const char *tcontext, + uint16 tclass, + uint32 audited, + const char *audit_name); + +extern void sepgsql_compute_avd(const char *scontext, + const char *tcontext, + uint16 tclass, + struct av_decision *avd); + +extern char *sepgsql_compute_create(const char *scontext, + const char *tcontext, + uint16 tclass); + +extern bool sepgsql_check_perms(const char *scontext, + const char *tcontext, + uint16 tclass, + uint32 required, + const char *audit_name, + bool abort); +/* + * label.c + */ +extern char *sepgsql_get_client_label(void); +extern char *sepgsql_set_client_label(char *new_label); +extern char *sepgsql_get_label(Oid relOid, Oid objOid, int32 subId); + +extern void sepgsql_object_relabel(const ObjectAddress *object, + const char *seclabel); + +extern Datum sepgsql_getcon(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); + +/* + * dml.c + */ +extern bool sepgsql_dml_privileges(List *rangeTabls, bool abort); + +/* + * schema.c + */ +extern void sepgsql_schema_post_create(Oid namespaceId); +extern void sepgsql_schema_relabel(Oid namespaceId, const char *seclabel); + +/* + * relation.c + */ +extern void sepgsql_attribute_post_create(Oid relOid, AttrNumber attnum); +extern void sepgsql_attribute_relabel(Oid relOid, AttrNumber attnum, + const char *seclabel); +extern void sepgsql_relation_post_create(Oid relOid); +extern void sepgsql_relation_relabel(Oid relOid, const char *seclabel); + +/* + * proc.c + */ +extern void sepgsql_proc_post_create(Oid functionId); +extern void sepgsql_proc_relabel(Oid functionId, const char *seclabel); +extern char *sepgsql_proc_get_domtrans(Oid functionId); + +#endif /* SEPGSQL_H */ diff --git a/contrib/sepgsql/sepgsql.sql.in b/contrib/sepgsql/sepgsql.sql.in new file mode 100644 index 0000000000..45ffe31e6b --- /dev/null +++ b/contrib/sepgsql/sepgsql.sql.in @@ -0,0 +1,36 @@ +-- +-- contrib/sepgsql/sepgsql.sql +-- +-- [Step to install] +-- +-- 1. Run initdb +-- to set up a new database cluster. +-- +-- 2. Edit $PGDATA/postgresql.conf +-- to add 'MODULE_PATHNAME' to shared_preload_libraries. +-- +-- Example) +-- shared_preload_libraries = 'MODULE_PATHNAME' +-- +-- 3. Run this script for each databases +-- This script installs corresponding functions, and assigns initial +-- security labels on target database objects. +-- It can be run both single-user mode and multi-user mode, according +-- to your preference. +-- +-- Example) +-- $ for DBNAME in template0 template1 postgres; \ +-- do \ +-- postgres --single -F -c exit_on_error=true -D $PGDATA $DBNAME \ +-- < /path/to/script/sepgsql.sql > /dev/null \ +-- done +-- +-- 4. Start postmaster, +-- if you initialized the database in single-user mode. +-- +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_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; +SELECT sepgsql_restorecon(NULL); diff --git a/contrib/sepgsql/sql/dml.sql b/contrib/sepgsql/sql/dml.sql new file mode 100644 index 0000000000..6fa1eb802e --- /dev/null +++ b/contrib/sepgsql/sql/dml.sql @@ -0,0 +1,118 @@ +-- +-- Regression Test for DML Permissions +-- + +-- +-- Setup +-- +CREATE TABLE t1 (a int, b text); +SECURITY LABEL ON TABLE t1 IS 'system_u:object_r:sepgsql_table_t:s0'; +INSERT INTO t1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'); + +CREATE TABLE t2 (x int, y text); +SECURITY LABEL ON TABLE t2 IS 'system_u:object_r:sepgsql_ro_table_t:s0'; +INSERT INTO t2 VALUES (1, 'xxx'), (2, 'yyy'), (3, 'zzz'); + +CREATE TABLE t3 (s int, t text); +SECURITY LABEL ON TABLE t3 IS 'system_u:object_r:sepgsql_fixed_table_t:s0'; +INSERT INTO t3 VALUES (1, 'sss'), (2, 'ttt'), (3, 'uuu'); + +CREATE TABLE t4 (m int, n text); +SECURITY LABEL ON TABLE t4 IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +INSERT INTO t4 VALUES (1, 'mmm'), (2, 'nnn'), (3, 'ooo'); + +CREATE TABLE t5 (e text, f text, g text); +SECURITY LABEL ON TABLE t5 IS 'system_u:object_r:sepgsql_table_t:s0'; +SECURITY LABEL ON COLUMN t5.e IS 'system_u:object_r:sepgsql_table_t:s0'; +SECURITY LABEL ON COLUMN t5.f IS 'system_u:object_r:sepgsql_ro_table_t:s0'; +SECURITY LABEL ON COLUMN t5.g IS 'system_u:object_r:sepgsql_secret_table_t:s0'; + +CREATE TABLE customer (cid int primary key, cname text, ccredit text); +SECURITY LABEL ON COLUMN customer.ccredit IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +INSERT INTO customer VALUES (1, 'Taro', '1111-2222-3333-4444'), + (2, 'Hanako', '5555-6666-7777-8888'); +CREATE FUNCTION customer_credit(int) RETURNS text + AS 'SELECT regexp_replace(ccredit, ''-[0-9]+$'', ''-????'') FROM customer WHERE cid = $1' + LANGUAGE sql; +SECURITY LABEL ON FUNCTION customer_credit(int) + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; + +SELECT objtype, objname, label FROM pg_seclabels + WHERE provider = 'selinux' + AND objtype in ('table', 'column') + AND objname in ('t1', 't2', 't3', 't4', 't5', 't5.e', 't5.f', 't5.g'); + +-- Hardwired Rules +UPDATE pg_attribute SET attisdropped = true + WHERE attrelid = 't5'::regclass AND attname = 'f'; -- failed + +-- +-- Simple DML statements +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 + +SELECT * FROM t1; -- ok +SELECT * FROM t2; -- ok +SELECT * FROM t3; -- ok +SELECT * FROM t4; -- failed +SELECT * FROM t5; -- failed +SELECT e,f FROM t5; -- ok + +SELECT * FROM customer; -- failed +SELECT cid, cname, customer_credit(cid) FROM customer; -- ok + +SELECT count(*) FROM t5; -- ok +SELECT count(*) FROM t5 WHERE g IS NULL; -- failed + +INSERT INTO t1 VALUES (4, 'abc'); -- ok +INSERT INTO t2 VALUES (4, 'xyz'); -- failed +INSERT INTO t3 VALUES (4, 'stu'); -- ok +INSERT INTO t4 VALUES (4, 'mno'); -- failed +INSERT INTO t5 VALUES (1,2,3); -- failed +INSERT INTO t5 (e,f) VALUES ('abc', 'def'); -- failed +INSERT INTO t5 (e) VALUES ('abc'); -- ok + +UPDATE t1 SET b = b || '_upd'; -- ok +UPDATE t2 SET y = y || '_upd'; -- failed +UPDATE t3 SET t = t || '_upd'; -- failed +UPDATE t4 SET n = n || '_upd'; -- failed +UPDATE t5 SET e = 'xyz'; -- ok +UPDATE t5 SET e = f || '_upd'; -- ok +UPDATE t5 SET e = g || '_upd'; -- failed + +DELETE FROM t1; -- ok +DELETE FROM t2; -- failed +DELETE FROM t3; -- failed +DELETE FROM t4; -- failed +DELETE FROM t5; -- ok +DELETE FROM t5 WHERE f IS NULL; -- ok +DELETE FROM t5 WHERE g IS NULL; -- failed + +-- +-- COPY TO/FROM statements +-- +COPY t1 TO '/dev/null'; -- ok +COPY t2 TO '/dev/null'; -- ok +COPY t3 TO '/dev/null'; -- ok +COPY t4 TO '/dev/null'; -- failed +COPY t5 TO '/dev/null'; -- failed +COPY t5(e,f) TO '/dev/null'; -- ok + +COPY t1 FROM '/dev/null'; -- ok +COPY t2 FROM '/dev/null'; -- failed +COPY t3 FROM '/dev/null'; -- ok +COPY t4 FROM '/dev/null'; -- failed +COPY t5 FROM '/dev/null'; -- failed +COPY t5 (e,f) FROM '/dev/null'; -- failed +COPY t5 (e) FROM '/dev/null'; -- ok + +-- +-- Clean up +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c255 +DROP TABLE IF EXISTS t1 CASCADE; +DROP TABLE IF EXISTS t2 CASCADE; +DROP TABLE IF EXISTS t3 CASCADE; +DROP TABLE IF EXISTS t4 CASCADE; +DROP TABLE IF EXISTS t5 CASCADE; +DROP TABLE IF EXISTS customer CASCADE; diff --git a/contrib/sepgsql/sql/label.sql b/contrib/sepgsql/sql/label.sql new file mode 100644 index 0000000000..3162494878 --- /dev/null +++ b/contrib/sepgsql/sql/label.sql @@ -0,0 +1,73 @@ +-- +-- Regression Tests for Label Management +-- + +-- +-- Setup +-- +CREATE TABLE t1 (a int, b text); +INSERT INTO t1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'); +SELECT * INTO t2 FROM t1 WHERE a % 2 = 0; + +CREATE FUNCTION f1 () RETURNS text + AS 'SELECT sepgsql_getcon()' + LANGUAGE sql; + +CREATE FUNCTION f2 () RETURNS text + AS 'SELECT sepgsql_getcon()' + LANGUAGE sql; +SECURITY LABEL ON FUNCTION f2() + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; + +CREATE FUNCTION f3 () RETURNS text + AS 'BEGIN + RAISE EXCEPTION ''an exception from f3()''; + RETURN NULL; + END;' LANGUAGE plpgsql; +SECURITY LABEL ON FUNCTION f3() + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; + +-- +-- Tests for default labeling behavior +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +CREATE TABLE t3 (s int, t text); +INSERT INTO t3 VALUES (1, 'sss'), (2, 'ttt'), (3, 'uuu'); + +SELECT objtype, objname, label FROM pg_seclabels + WHERE provider = 'selinux' + AND objtype in ('table', 'column') + AND objname in ('t1', 't2', 't3'); + +-- +-- Tests for SECURITY LABEL +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_dba_t:s0 +SECURITY LABEL ON TABLE t1 + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- ok +SECURITY LABEL ON TABLE t2 + IS 'invalid seuciryt context'; -- be failed +SECURITY LABEL ON COLUMN t2 + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- be failed +SECURITY LABEL ON COLUMN t2.b + IS 'system_u:object_r:sepgsql_ro_table_t:s0'; -- ok + +-- +-- Tests for Trusted Procedures +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:sepgsql_regtest_user_t:s0 +SELECT f1(); -- normal procedure +SELECT f2(); -- trusted procedure +SELECT f3(); -- trusted procedure that raises an error +SELECT sepgsql_getcon(); -- client's label must be restored + +-- +-- Clean up +-- +-- @SECURITY-CONTEXT=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c255 +DROP TABLE IF EXISTS t1 CASCADE; +DROP TABLE IF EXISTS t2 CASCADE; +DROP TABLE IF EXISTS t3 CASCADE; +DROP FUNCTION IF EXISTS f1() CASCADE; +DROP FUNCTION IF EXISTS f2() CASCADE; +DROP FUNCTION IF EXISTS f3() CASCADE; diff --git a/contrib/sepgsql/sql/misc.sql b/contrib/sepgsql/sql/misc.sql new file mode 100644 index 0000000000..a46d8a6b5c --- /dev/null +++ b/contrib/sepgsql/sql/misc.sql @@ -0,0 +1,5 @@ +-- +-- Regression Test for Misc Permission Checks +-- + +LOAD '$libdir/sepgsql'; -- failed diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index f5bd493543..75d08d5f69 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -116,6 +116,7 @@ psql -d dbname -f SHAREDIR/contrib/module.sql &pgtrgm; &pgupgrade; &seg; + &sepgsql; &contrib-spi; &sslinfo; &tablefunc; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 9d4cfc927b..99437ac378 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -129,6 +129,7 @@ + diff --git a/doc/src/sgml/sepgsql.sgml b/doc/src/sgml/sepgsql.sgml new file mode 100644 index 0000000000..91b8614b81 --- /dev/null +++ b/doc/src/sgml/sepgsql.sgml @@ -0,0 +1,704 @@ + + + + sepgsql + + + sepgsql + + + + The sepgsql is a module which performs as an external + security provider; to support label based mandatory access control + (MAC) base on SELinux policy. + + + This extension won't build at all unless the installation was configured + with --with-selinux. + + + + Overview + + + PostgreSQL provides various kind of hooks. Some of these + hooks can be utilized to make access control decision on the supplied + users' accesses on database objects. + We call plug-in modules making access control decision based on its own + security model as an external security provider. + + + This module acquires control on these strategic points, then it asks + SELinux to check whether the supplied access shall be + allowed, or not. Then, it returns its access control decision. + If violated, this module prevents this access with rising an error for + example. + + + A series of making decision is done independently from the default + database privilege mechanism. Users must be allowed with both of access + control models, whenever they try to access something. + + + We can see SELinux as a function which takes two arguments + then returns a bool value; allowed or denied. The first argument in this + analogy is label of subject which tries to reference a certain obejct. + The other one is label of the object being referenced in this operation. + + + Label is a formatted string, + like system_u:object_r:sepgsql_table_t:s0. + It is not a property depending on characteristics of a certain kind of + object, so we can apply common credentials on either database objects + or others. + + + PostgreSQL 9.1 or later supports + statement that allows to assign + a security label on specified database objects, if user wants to change + label from the creation default. + Also SELinux provides an interface to obtain security + label of the peer process that connected to. + + + These facilities enable to integrate SELinux model within + access controls to database objects. Because it makes access control + decision according to a common centralized security policy (a set of rules), + its decision will be always consistent independent from the way to store + information assets. + + + + Installation + + The sepgsql module requires the following packages to install. + Please check it at first. + + + + Linux kernel + + + v2.6.28 or later with built with SELinux enabled + + + + + libselinux + + + v2.0.80 or later + + + This library provides a set of APIs to communicate with + SELinux in kernel. + + + + + selinux-policy + + + v3.9.13 or later + + + The default security policy provides a set of access control rules. + Some of distribution may backports necessary rules, even if base + policy was older than above version. + + + + + + SE-PostgreSQL needs SELinux being + available on the platform. You can check the current setting using + sestatus. + +$ sestatus +SELinux status: enabled +SELinuxfs mount: /selinux +Current mode: enforcing +Mode from config file: enforcing +Policy version: 24 +Policy from config file: targeted + + If disabled or not-installed, you need to set up SELinux + prior to all the installation step of SE-PostgreSQL. + + + On the compile time, add --with-selinux option to + the configure script to check existence of + the libselinux, and to set a flag whether + we build this contrib module, or not. + +$ ./configure --enable-debug --enable-cassert --with-selinux +$ make +$ make install + + + + Next to the initdb, add '$libdir/sepgsql' + to in + the postgresql.conf. + + It enables to load sepgsql on the starting up of + postmaster process. + + + Then, load the sepgsql.sql script for each databases. + It installs functions corresponding to security label management, and + tries to assign initial labels on the target objects. + + + The following instruction assumes your installation is under the + /usr/local/pgsql directory, and the database cluster is in + /usr/local/pgsql/data. Substitute your paths appropriately. + + +$ initdb -D $PGDATA +$ vi $PGDATA/postgresql.conf +$ for DBNAME in template0 template1 postgres; do + postgres --single -F -O -c exit_on_error=true -D $PGDATA $DBNAME \ + < /usr/local/pgsql/share/contrib/sepgsql.sql > /dev/null + done + + + If all the installation process was done with no errors, start postmaster + process. SE-PostgreSQL shall prevent violated accesses + according to the security policy of SELinux. + + + + + Regression Tests + + The regression test of this module requires a few more configurations + on the platform system, in addition to the above installation process. + See the following steps. + + + First, install the policy package for regression test. + The sepgsql-regtest.pp is a special purpose policy package + that provides a set of rules to be allowed during the regression test + cases. It shall be installed at /usr/local/pgsql/share/contrib + directory in the default setup. + + + You need to install this policy package using semodule + command which enables to link supplied policy packages and load them + into the kernel space. If you could install the pakage correctly, + semodule -l prints sepgsql-regtest as a part + of policy packages currently available. + + +$ su +# semodule -u /usr/local/pgsql/share/contrib/sepgsql-regtest.pp +# semodule -l + : +sepgsql-regtest 1.03 + : + + + Second, turn on the sepgsql_regression_test_mode. + We don't enable all the rules in the sepgsql-regtest.pp + in the default, for your system's safety. + The sepgsql_regression_test_mode parameter is associated + with rules to launch regression test. + It can be turned on using setsebool command. + + +$ su +# setsebool sepgsql_regression_test_mode on +# getsebool sepgsql_regression_test_mode +sepgsql_regression_test_mode --> on + + + Last, kick the regression test from the unconfined_t domain. + + + This test policy is designed to kick each test cases from the + unconfined_t domain that is a default choice in most of + the known SELinux installation base. + So, you don't need to set up anything special, as long as you didn't + change default configuration of SELinux before. + + + The id command tells us the current working domain. + Confirm your shell is now performing with unconfined_t + domain as follows. + + +$ id -Z +unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 + + + If not an expected one, you should revert this configuration. + The section will give you + some useful hints. + + + Then, you will see the all-green result of regression test, + if we have no problem here. + + +$ make -C contrib/sepgsql/ installcheck + : +../../src/test/regress/pg_regress --inputdir=. --psqldir=/usr/local/pgsql/bin \ + --dbname=contrib_regression --launcher ../../contrib/sepgsql/launcher \ + label dml +(using postmaster on Unix socket, default port) +============== dropping database "contrib_regression" ============== +DROP DATABASE +============== creating database "contrib_regression" ============== +CREATE DATABASE +ALTER DATABASE +============== running regression test queries ============== +test label ... ok +test dml ... ok +test misc ... ok + +===================== + All 3 tests passed. +===================== + + + If pg_regress failed to launch psql command, + here is a hint to fix up the matter. + + When we try to launch psql command with restrictive + privileges, the psql must eb labeled as bin_t. + If not, try to run restorecon to fix up security label of + the commands as expected. + + +$ restorecon -R /usr/local/pgsql/ + + + + + GUC Parameters + + + + sepgsql.permissive (boolean) + + sepgsql.permissive configuration parameter + + + + This parameter enables to perform SE-PostgreSQL + in permissive mode independent from the system setting. + The default is off (according to the system setting). + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + We have two performing mode except for disabled; The one is enforcing + mode that checks the security policy on references and actually prevents + violated accesses. The other is permissive mode that only checks + the security policy, but does not prevents anything except for log + generation. + This log shall be utilized for debugging of the security policy itself. + + + When this parameter is on, SE-PostgreSQL performs + in permissive mode, even if the platform system is working on enforcing + mode. + We recommend users to keep the default setting, except for the case + when we develop security policy by ourself. + + + + + sepgsql.debug_audit (boolean) + + sepgsql.debug_audit configuration parameter + + + + This parameter enables to print audit messages independent from + the policy setting. + The default is off (according to the security policy setting). + + + The security policy of SELinux also has rules to + control what accesses shall be logged, or not. + In the default, any access violations are logged, but any allowed + accesses are not logged. + + + When this parameter is on, all the possible logs shall be printed + independently from the policy settings. + We recommend to keep the variable turned off in normal cases to + avoid noisy messages. + + + + + + + + Features + + controlled object classes + + The security model of SELinux describes all the access + control rules as a relationship between a subject entity (typically, + it is a client of database) and an object entity. + And, these entities are identified by a security label. + + + We call a set of these rules as security policy. + All the access control decision shall be made according to the security + policy, when we ask SELinux whether the required action shall be allowed + or not. + Thus, we have no way to control accesses on any sort of objects without + security labels. + (SELinux assumes unlabeled_t is assigned, + if no valid security label is assigned on the target object.) + + + This version of SE-PostgreSQL supports to assign + a security label on these database object classes: schema, table, column, + sequence, view and procedure. + Other database object classes are not supported to assign security label + on, right now. + + + A security label shall be automatically assigned to the supported + database objects on their creation time. + This label is called as a default security label; being decided according + to the security policy, or a pair of security label of the client and + upper object for more correctly. + + + A new database object basically inherits security label of the upper + object. A new column inherits security label of its parent table for + instance. + If and when the security policy has special rules called as + type-transition on a pair of the client and upper object, we can assign + an individual label as a default. The upper object depends on sort of + object classes as follows. + + + + schema + + + Its upper object is the current database. + + + + + table + + + Its upper object is the schema object which owns the new table. + + + + + column + + + Its upper object is the table object which owns the new column. + + + + + sequence + + + Its upper object is the schema object which owns the new sequence. + + + + + view + + + Its upper object is the schema object which owns the new view. + + + + + procedure + + + Its upper object is the schema object which owns the new procedure. + + + + + + + DML Permissions + + This section introduces what permissions shall be checked on DML; + SELECT, INSERT, UPDATE and + DELETE. + + + DML statements are used to reference or modify contents within + the specified database objects; such as tables or columns. + We basically checks access rights of the client on all the appeared + objects in the given statement, and kind of privileges depend on + class of object and sort of accesses. + + + For tables, db_table:select, db_table:insert, + db_table:update or db_table:delete shall be + checked for all the appeared target tables depending on the sort of + statement; + In addition, db_table:select shall be also checked for + all the tables that containin the columns to be referenced in + WHERE or RETURNING clause, as a data source + of UPDATE, and so on. + + + +UPDATE t1 SET x = 2, y = md5sum(y) WHERE z = 100; + + In this case, we must have db_table:select, not only + db_table:update, because t1.a is referenced + within WHERE clause. + Also note that column-level permission shall be checked individually. + + + The client must be allowed to reference all the appeared tables and + columns, even if they are originated from views then expanded, unlike + the default database privileges, because we intend to apply consistent + access control rules independent from the route to reference contents + of the tables. + + + For columns, db_column:select shall be also checked on + not only the columns being read using SELECT, but being + referenced in other DML statement. + + + Of course, it also checks db_column:update or + db_column:insert on the column being modified by + UPDATE or INSERT. + Note that we have no definition of column-level delete permission, + like as the default database privilege doing. + + + +UPDATE t1 SET x = 2, y = md5sum(y) WHERE z = 100; + + In this case, it checks db_column:update on + the t1.x being updated, db_column:{select update} + on the t1.y being updated and referenced, + and db_column:select on the t1.z being only + referenced in the WHERE clause. + Also note that db_table:{select update} shall be checked + in the table-level granularity. + + + For sequences, db_sequence:get_value when we reference + a sequence object using SELECT, however, note that we + cannot check permissions on execution of corresponding functions + such as lastval() right now, although they performs same + job, because here is no object access hook to acquire controls. + + + For views, db_view:expand shall be checked, then any other + corresponding permissions shall be also checked on the objects being + expanded from the view, individually. + Note that both of permissions have to be allowed. + + + For procedures, db_procedure:{execute} is defined, but not + checked in this version. + + + Here is a few more corner cases. + The default database privilege system allows database superusers to + modify system catalogs using DML commands, and reference or modify + toast tables, however, both of the cases shall be denied when + SE-PostgreSQL is enabled. + + + + DDL Permissions + + On command, setattr and + relabelfrom shall be checked on the object being relabeled + with an old security label, then relabelto on the supplied + new security label. + + + In a case when multiple label providers are installed and user tries + to set a security label, but is not managed by SELinux, + only setattr should be checked here. + However, it is not unavailable because of limitation of the hook. + + + As we will describe in section, + SE-PostgreSQL does not control any other DDL operations. + + + + Trusted Procedure + + It is a similar idea to security definer functions or set-uid commands + on operating systems. SELinux provides a feature to + switch privilege of the client (that is a security label of the client + for more correctness) during execution of certain functions; being + called as trusted procedures. + + + A trusted function is a function with a special security label being + set up as a trusted procedure. + So, we need to assign the special security label on the function that + we hope it to perform as a trusted procedure, by administrative users. + The default security policy also provides this special security label. + See the following example. + + +postgres=# CREATE TABLE customer ( + cid int primary key, + cname text, + credit text + ); +CREATE TABLE +postgres=# SECURITY LABEL ON COLUMN customer.credit + IS 'system_u:object_r:sepgsql_secret_table_t:s0'; +SECURITY LABEL +postgres=# CREATE FUNCTION show_credit(int) RETURNS text + AS 'SELECT regexp_replace(credit, ''-[0-9]+$'', ''-xxxx'', ''g'') + FROM customer WHERE cid = $1' + LANGUAGE sql; +CREATE FUNCTION +postgres=# SECURITY LABEL ON FUNCTION show_credit(int) + IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0'; +SECURITY LABEL + + + Above operations shall be done by administrative users. + + +postgres=# SELECT * FROM customer; +ERROR: SELinux: security policy violation +postgres=# SELECT cid, cname, show_credit(cid) FROM customer; + cid | cname | show_credit +-----+--------+--------------------- + 1 | taro | 1111-2222-3333-xxxx + 2 | hanako | 5555-6666-7777-xxxx +(2 rows) + + + In this case, a regular user cannot reference customer.credit + directly, but a trusted procedure show_credit enables us + to print credit number of customers with a bit modification. + + + + Miscellaneous + + In this version, we reject command across + the board, because the binary module can override security hooks to + make access control decision. It means a risk to invalidate all the + control by security providers. + + + + + Limitations + + This section introduces limitations of SE-PostgreSQL + in this version. + + + + Userspace access vector cache + + + SE-PostgreSQL tells SELinux its access + control decision. It takes system call invocation being heavy, however, + we can reduce number of the invocations using caching mechanism; called + as access vector cache in SELinux. + Because of code size, SE-PostgreSQL does not support + this mechanism yet. + + + + + DDL Permissions + + + Now PostgreSQL does not provide a set of hooks on + the DDL routines. + It means plugin modules cannot acquire control here, + so SE-PostgreSQL does not check DDL Permissions + right now. + + + + + Row-level access control + + + Now SE-PostgreSQL does not support row-level access + control, because a few needed facilities are not supported yet. + The one is security labels on users' tables. The other is behavior of + optimizer. Also see for more details. + We know similar issue on VIEW. + + + + + Covert channels + + + SE-PostgreSQL never tries to hide existence of + a certain object, even if user is not allowed to reference. + For example, we can infer an existence of invisible object using + primary-key confliction, foreign-key violation, and so on, even if + we cannot reference contents of these objects. + + + + + + + External Resources + + + SE-PostgreSQL Introduction + + + This wikipage provides a brief-overview, security design, architecture, + administration and upcoming feature for more details. + + + + + Fedora SELinux User Guide + + + This document provides wide spectrum of knowledge to administrate + SELinux on your systems. + It primary focuses on Fedora, but not limited to Fedora. + + + + + Fedora SELinux FAQ + + + This document provides FAQs about SELinux. + It primary focuses on Fedora, but not limited to Fedora. + + + + + + + Author + + KaiGai Kohei (kaigai@ak.jp.nec.com) + + + diff --git a/src/Makefile.global.in b/src/Makefile.global.in index ebeee0c3b2..d6b7b4769d 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -158,6 +158,7 @@ with_python = @with_python@ with_tcl = @with_tcl@ with_openssl = @with_openssl@ with_ossp_uuid = @with_ossp_uuid@ +with_selinux = @with_selinux@ with_libxml = @with_libxml@ with_libxslt = @with_libxslt@ with_system_tzdata = @with_system_tzdata@ diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 79655cd653..a3211622b9 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -84,6 +84,7 @@ bool debug = false; char *inputdir = "."; char *outputdir = "."; char *psqldir = PGBINDIR; +char *launcher = NULL; static _stringlist *loadlanguage = NULL; static int max_connections = 0; static char *encoding = NULL; @@ -1871,6 +1872,7 @@ help(void) printf(_(" --dlpath=DIR look for dynamic libraries in DIR\n")); printf(_(" --temp-install=DIR create a temporary installation in DIR\n")); printf(_(" --use-existing use an existing installation\n")); + printf(_(" --launcher=CMD use CMD as launcher of psql\n")); printf(_("\n")); printf(_("Options for \"temp-install\" mode:\n")); printf(_(" --no-locale use C locale\n")); @@ -1922,6 +1924,7 @@ regression_main(int argc, char *argv[], init_function ifunc, test_function tfunc {"create-role", required_argument, NULL, 18}, {"temp-config", required_argument, NULL, 19}, {"use-existing", no_argument, NULL, 20}, + {"launcher", required_argument, NULL, 21}, {NULL, 0, NULL, 0} }; @@ -2015,6 +2018,9 @@ regression_main(int argc, char *argv[], init_function ifunc, test_function tfunc case 20: use_existing = true; break; + case 21: + launcher = strdup(optarg); + break; default: /* getopt_long already emitted a complaint */ fprintf(stderr, _("\nTry \"%s -h\" for more information.\n"), diff --git a/src/test/regress/pg_regress.h b/src/test/regress/pg_regress.h index 26069f642e..606c7a1a70 100644 --- a/src/test/regress/pg_regress.h +++ b/src/test/regress/pg_regress.h @@ -41,6 +41,7 @@ extern _stringlist *dblist; extern bool debug; extern char *inputdir; extern char *outputdir; +extern char *launcher; /* * This should not be global but every module should be able to read command diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c index 710e55841a..3e43dd72b8 100644 --- a/src/test/regress/pg_regress_main.c +++ b/src/test/regress/pg_regress_main.c @@ -33,6 +33,7 @@ psql_start_test(const char *testname, char outfile[MAXPGPATH]; char expectfile[MAXPGPATH]; char psql_cmd[MAXPGPATH * 3]; + size_t offset = 0; /* * Look for files in the output dir first, consistent with a vpath search. @@ -58,7 +59,11 @@ psql_start_test(const char *testname, add_stringlist_item(resultfiles, outfile); add_stringlist_item(expectfiles, expectfile); - snprintf(psql_cmd, sizeof(psql_cmd), + if (launcher) + offset += snprintf(psql_cmd + offset, sizeof(psql_cmd) - offset, + "%s ", launcher); + + snprintf(psql_cmd + offset, sizeof(psql_cmd) - offset, SYSTEMQUOTE "\"%s%spsql\" -X -a -q -d \"%s\" < \"%s\" > \"%s\" 2>&1" SYSTEMQUOTE, psqldir ? psqldir : "", psqldir ? "/" : "", -- 2.11.0