Update sepgsql to add mandatory access control for TRUNCATE

Use SELinux "db_table: { truncate }" to check if permission is granted to
TRUNCATE. Update example SELinux policy to grant needed permission for
TRUNCATE. Add new regression test to demonstrate a positive and negative
cases. Test will only be run if the loaded SELinux policy has the
"db_table: { truncate }" permission. Makes use of recent commit which added
object TRUNCATE hook. Patch by Yuli Khodorkovskiy with minor
editorialization by me. Not back-patched because the object TRUNCATE hook
was not.

Author: Yuli Khodorkovskiy
Reviewed-by: Joe Conway
Discussion: https://postgr.es/m/CAFL5wJcomybj1Xdw7qWmPJRpGuFukKgNrDb6uVBaCMgYS9dkaA%40mail.gmail.com

---
diff --git a/contrib/sepgsql/expected/truncate.out b/contrib/sepgsql/expected/truncate.out
index ...e2cabd7 .
*** a/contrib/sepgsql/expected/truncate.out
--- b/contrib/sepgsql/expected/truncate.out
***************
*** 0 ****
--- 1,46 ----
+ --
+ -- Regression Test for TRUNCATE
+ --
+ --
+ -- Setup
+ --
+ CREATE TABLE julio_claudians (name text, birth_date date);
+ SECURITY LABEL ON TABLE julio_claudians IS 'system_u:object_r:sepgsql_regtest_foo_table_t:s0';
+ INSERT INTO julio_claudians VALUES ('Augustus', 'September 23, 63 BC'), ('Tiberius', 'November 16, 42 BC'), ('Caligula', 'August 31, 0012'), ('Claudius', 'August 1, 0010'), ('Nero', 'December 15, 0037');
+ CREATE TABLE flavians (name text, birth_date date);
+ SECURITY LABEL ON TABLE flavians IS 'system_u:object_r:sepgsql_table_t:s0';
+ INSERT INTO flavians VALUES ('Vespasian', 'November 17, 0009'), ('Titus', 'December 30, 0039'), ('Domitian', 'October 24, 0051');
+ SELECT * from julio_claudians;
+    name   |  birth_date   
+ ----------+---------------
+  Augustus | 09-23-0063 BC
+  Tiberius | 11-16-0042 BC
+  Caligula | 08-31-0012
+  Claudius | 08-01-0010
+  Nero     | 12-15-0037
+ (5 rows)
+ 
+ SELECT * from flavians;
+    name    | birth_date 
+ -----------+------------
+  Vespasian | 11-17-0009
+  Titus     | 12-30-0039
+  Domitian  | 10-24-0051
+ (3 rows)
+ 
+ TRUNCATE TABLE julio_claudians;			-- ok
+ TRUNCATE TABLE flavians;			-- failed
+ ERROR:  SELinux: security policy violation
+ SELECT * from julio_claudians;
+  name | birth_date 
+ ------+------------
+ (0 rows)
+ 
+ SELECT * from flavians;
+    name    | birth_date 
+ -----------+------------
+  Vespasian | 11-17-0009
+  Titus     | 12-30-0039
+  Domitian  | 10-24-0051
+ (3 rows)
+ 
diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c
index 49f32ac..cdf1452 100644
*** a/contrib/sepgsql/hooks.c
--- b/contrib/sepgsql/hooks.c
*************** sepgsql_object_access(ObjectAccessType a
*** 188,193 ****
--- 188,207 ----
  			}
  			break;
  
+ 		case OAT_TRUNCATE:
+ 			{
+ 				switch (classId)
+ 				{
+ 					case RelationRelationId:
+ 						sepgsql_relation_truncate(objectId);
+ 						break;
+ 					default:
+ 						/* Ignore unsupported object classes */
+ 						break;
+ 				}
+ 			}
+ 			break;
+ 
  		case OAT_POST_ALTER:
  			{
  				ObjectAccessPostAlter *pa_arg = arg;
diff --git a/contrib/sepgsql/relation.c b/contrib/sepgsql/relation.c
index 714cffe..fa34221 100644
*** a/contrib/sepgsql/relation.c
--- b/contrib/sepgsql/relation.c
*************** sepgsql_relation_drop(Oid relOid)
*** 517,522 ****
--- 517,562 ----
  }
  
  /*
+  * sepgsql_relation_truncate
+  *
+  * Check privileges to TRUNCATE the supplied relation.
+  */
+ void
+ sepgsql_relation_truncate(Oid relOid)
+ {
+ 	ObjectAddress object;
+ 	char	   *audit_name;
+ 	uint16_t	tclass = 0;
+ 	char		relkind = get_rel_relkind(relOid);
+ 
+ 	switch (relkind)
+ 	{
+ 		case RELKIND_RELATION:
+ 		case RELKIND_PARTITIONED_TABLE:
+ 			tclass = SEPG_CLASS_DB_TABLE;
+ 			break;
+ 		default:
+ 			/* ignore other relkinds */
+ 			return;
+ 	}
+ 
+ 	/*
+ 	 * check db_table:{truncate} permission
+ 	 */
+ 	object.classId = RelationRelationId;
+ 	object.objectId = relOid;
+ 	object.objectSubId = 0;
+ 	audit_name = getObjectIdentity(&object);
+ 
+ 	sepgsql_avc_check_perms(&object,
+ 							tclass,
+ 							SEPG_DB_TABLE__TRUNCATE,
+ 							audit_name,
+ 							true);
+ 	pfree(audit_name);
+ }
+ 
+ /*
   * sepgsql_relation_relabel
   *
   * It checks privileges to relabel the supplied relation by the `seclabel'.
diff --git a/contrib/sepgsql/selinux.c b/contrib/sepgsql/selinux.c
index b7c489c..5e6189a 100644
*** a/contrib/sepgsql/selinux.c
--- b/contrib/sepgsql/selinux.c
*************** static struct
*** 360,365 ****
--- 360,368 ----
  				"lock", SEPG_DB_TABLE__LOCK
  			},
  			{
+ 				"truncate", SEPG_DB_TABLE__TRUNCATE
+ 			},
+ 			{
  				NULL, 0UL
  			},
  		}
diff --git a/contrib/sepgsql/sepgsql-regtest.te b/contrib/sepgsql/sepgsql-regtest.te
index 5d9af1a..569c4da 100644
*** a/contrib/sepgsql/sepgsql-regtest.te
--- b/contrib/sepgsql/sepgsql-regtest.te
*************** allow sepgsql_regtest_var_t sepgsql_regt
*** 152,157 ****
--- 152,165 ----
  
  optional_policy(`
  	gen_require(`
+ 		class db_table { truncate };
+ 	')
+ 
+ 	allow sepgsql_regtest_superuser_t sepgsql_regtest_foo_table_t:db_table { truncate };
+ ')
+ 
+ optional_policy(`
+ 	gen_require(`
  		role unconfined_r;
  	')
  	postgresql_role(unconfined_r, sepgsql_regtest_foo_t)
diff --git a/contrib/sepgsql/sepgsql.h b/contrib/sepgsql/sepgsql.h
index 4787934..31828e9 100644
*** a/contrib/sepgsql/sepgsql.h
--- b/contrib/sepgsql/sepgsql.h
***************
*** 145,150 ****
--- 145,151 ----
  #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__TRUNCATE				(1<<11)
  
  #define SEPG_DB_SEQUENCE__CREATE			(SEPG_DB_DATABASE__CREATE)
  #define SEPG_DB_SEQUENCE__DROP				(SEPG_DB_DATABASE__DROP)
*************** extern void sepgsql_attribute_relabel(Oi
*** 312,317 ****
--- 313,319 ----
  extern void sepgsql_attribute_setattr(Oid relOid, AttrNumber attnum);
  extern void sepgsql_relation_post_create(Oid relOid);
  extern void sepgsql_relation_drop(Oid relOid);
+ extern void sepgsql_relation_truncate(Oid relOid);
  extern void sepgsql_relation_relabel(Oid relOid, const char *seclabel);
  extern void sepgsql_relation_setattr(Oid relOid);
  
diff --git a/contrib/sepgsql/sql/truncate.sql b/contrib/sepgsql/sql/truncate.sql
index ...3748a1b .
*** a/contrib/sepgsql/sql/truncate.sql
--- b/contrib/sepgsql/sql/truncate.sql
***************
*** 0 ****
--- 1,24 ----
+ --
+ -- Regression Test for TRUNCATE
+ --
+ 
+ --
+ -- Setup
+ --
+ CREATE TABLE julio_claudians (name text, birth_date date);
+ SECURITY LABEL ON TABLE julio_claudians IS 'system_u:object_r:sepgsql_regtest_foo_table_t:s0';
+ INSERT INTO julio_claudians VALUES ('Augustus', 'September 23, 63 BC'), ('Tiberius', 'November 16, 42 BC'), ('Caligula', 'August 31, 0012'), ('Claudius', 'August 1, 0010'), ('Nero', 'December 15, 0037');
+ 
+ CREATE TABLE flavians (name text, birth_date date);
+ SECURITY LABEL ON TABLE flavians IS 'system_u:object_r:sepgsql_table_t:s0';
+ 
+ INSERT INTO flavians VALUES ('Vespasian', 'November 17, 0009'), ('Titus', 'December 30, 0039'), ('Domitian', 'October 24, 0051');
+ 
+ SELECT * from julio_claudians;
+ SELECT * from flavians;
+ 
+ TRUNCATE TABLE julio_claudians;			-- ok
+ TRUNCATE TABLE flavians;			-- failed
+ 
+ SELECT * from julio_claudians;
+ SELECT * from flavians;
diff --git a/contrib/sepgsql/test_sepgsql b/contrib/sepgsql/test_sepgsql
index 7530363..3a29556 100755
*** a/contrib/sepgsql/test_sepgsql
--- b/contrib/sepgsql/test_sepgsql
*************** echo "found ${NUM}"
*** 287,292 ****
  echo
  echo "============== running sepgsql regression tests       =============="
  
! make REGRESS="label dml ddl alter misc" REGRESS_OPTS="--launcher ./launcher" installcheck
  
  # exit with the exit code provided by "make"
--- 287,306 ----
  echo
  echo "============== running sepgsql regression tests       =============="
  
! tests="label dml ddl alter misc"
! 
! # Check if the truncate permission exists in the loaded policy, and if so,
! # run the truncate test
! #
! # Testing the TRUNCATE regression test can be done by manually adding
! # the permission with CIL if necessary:
! #     sudo semodule -cE base
! #     sudo sed -i -E 's/(class db_table.*?) \)/\1 truncate\)/' base.cil
! #     sudo semodule -i base.cil
! 
! if [ -f /sys/fs/selinux/class/db_table/perms/truncate ]; then
! 	tests+=" truncate"
! fi
  
+ make REGRESS="$tests" REGRESS_OPTS="--launcher ./launcher" installcheck
  # exit with the exit code provided by "make"
