contrib/sepgsql/expected/create.out | 46 +++++++++++ contrib/sepgsql/hooks.c | 70 +++++++++++++++++- contrib/sepgsql/relation.c | 144 +++++++++++++++++++++++++++++------ contrib/sepgsql/sql/create.sql | 18 +++++ 4 files changed, 254 insertions(+), 24 deletions(-) diff --git a/contrib/sepgsql/expected/create.out b/contrib/sepgsql/expected/create.out index dbe0591..9b5c3d1 100644 --- a/contrib/sepgsql/expected/create.out +++ b/contrib/sepgsql/expected/create.out @@ -16,8 +16,54 @@ LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_ CREATE SCHEMA regtest_schema; LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" SET search_path = regtest_schema, public; +CREATE TABLE regtest_table (x serial primary key, y text); +NOTICE: CREATE TABLE will create implicit sequence "regtest_table_x_seq" for serial column "regtest_table.x" +LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_seq_t:s0 tclass=db_sequence name="sequence regtest_table_x_seq" +LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="table regtest_table" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column tableoid" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column cmax" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column xmax" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column cmin" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column xmin" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column ctid" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column x" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column y" +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "regtest_table_pkey" for table "regtest_table" +ALTER TABLE regtest_table ADD COLUMN z int; +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table column z" +CREATE TABLE regtest_table_2 (a int) WITH OIDS; +LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="table regtest_table_2" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column tableoid" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column cmax" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column xmax" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column cmin" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column xmin" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column oid" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column ctid" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column a" +-- corresponding toast table should not have label and permission checks +ALTER TABLE regtest_table_2 ADD COLUMN b text; +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="table regtest_table_2 column b" +-- VACUUM FULL internally create a new table and swap them later. +VACUUM FULL regtest_table; +CREATE VIEW regtest_view AS SELECT * FROM regtest_table WHERE x < 100; +LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_view_t:s0 tclass=db_view name="view regtest_view" +CREATE SEQUENCE regtest_seq; +LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="schema regtest_schema" +LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:unconfined_t:s0 tcontext=unconfined_u:object_r:sepgsql_seq_t:s0 tclass=db_sequence name="sequence regtest_seq" +CREATE TYPE regtest_comptype AS (a int, b text); -- -- clean-up -- DROP DATABASE IF EXISTS regtest_sepgsql_test_database; DROP SCHEMA IF EXISTS regtest_schema CASCADE; +NOTICE: drop cascades to 5 other objects +DETAIL: drop cascades to table regtest_table +drop cascades to table regtest_table_2 +drop cascades to view regtest_view +drop cascades to sequence regtest_seq +drop cascades to type regtest_comptype diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c index fe71953..8232971 100644 --- a/contrib/sepgsql/hooks.c +++ b/contrib/sepgsql/hooks.c @@ -41,6 +41,7 @@ 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; +static ExecutorStart_hook_type next_ExecutorStart_hook = NULL; /* * Contextual information on DDL commands @@ -153,7 +154,30 @@ sepgsql_object_access(ObjectAccessType access, case RelationRelationId: if (subId == 0) - sepgsql_relation_post_create(objectId); + { + /* + * All cases we want to apply permission checks on + * creation of a new relation are invocation of the + * heap_create_with_catalog via DefineRelation or + * OpenIntoRel. + * Elsewhere, we need neither assignment of security + * label nor permission checks. + */ + switch (sepgsql_context_info.cmdtype) + { + case T_CreateStmt: + case T_ViewStmt: + case T_CreateSeqStmt: + case T_CompositeTypeStmt: + case T_CreateForeignTableStmt: + case T_SelectStmt: + sepgsql_relation_post_create(objectId); + break; + default: + /* via make_new_heap() */ + break; + } + } else sepgsql_attribute_post_create(objectId, subId); break; @@ -312,6 +336,46 @@ sepgsql_fmgr_hook(FmgrHookEventType event, } /* + * sepgsql_executor_start + * + * It saves contextual information during ExecutorStart to distinguish + * a case with/without permission checks later. + */ +static void +sepgsql_executor_start(QueryDesc *queryDesc, int eflags) +{ + sepgsql_context_info_t saved_context_info = sepgsql_context_info; + + PG_TRY(); + { + if (queryDesc->operation == CMD_SELECT) + sepgsql_context_info.cmdtype = T_SelectStmt; + else if (queryDesc->operation == CMD_INSERT) + sepgsql_context_info.cmdtype = T_InsertStmt; + else if (queryDesc->operation == CMD_DELETE) + sepgsql_context_info.cmdtype = T_DeleteStmt; + else if (queryDesc->operation == CMD_UPDATE) + sepgsql_context_info.cmdtype = T_UpdateStmt; + + /* + * XXX - If queryDesc->operation is not above four cases, an error + * shall be raised on the following executor stage soon. + */ + if (next_ExecutorStart_hook) + (*next_ExecutorStart_hook) (queryDesc, eflags); + else + standard_ExecutorStart(queryDesc, eflags); + } + PG_CATCH(); + { + sepgsql_context_info = saved_context_info; + PG_RE_THROW(); + } + PG_END_TRY(); + sepgsql_context_info = saved_context_info; +} + +/* * sepgsql_utility_command * * It tries to rough-grained control on utility commands; some of them can @@ -504,6 +568,10 @@ _PG_init(void) next_ProcessUtility_hook = ProcessUtility_hook; ProcessUtility_hook = sepgsql_utility_command; + /* ExecutorStart hook */ + next_ExecutorStart_hook = ExecutorStart_hook; + ExecutorStart_hook = sepgsql_executor_start; + /* init contextual info */ memset(&sepgsql_context_info, 0, sizeof(sepgsql_context_info)); } diff --git a/contrib/sepgsql/relation.c b/contrib/sepgsql/relation.c index 0767382..b4abc8e 100644 --- a/contrib/sepgsql/relation.c +++ b/contrib/sepgsql/relation.c @@ -36,10 +36,16 @@ void sepgsql_attribute_post_create(Oid relOid, AttrNumber attnum) { - char *scontext = sepgsql_get_client_label(); + Relation rel; + ScanKeyData skey[2]; + SysScanDesc sscan; + HeapTuple tuple; + char *scontext; char *tcontext; char *ncontext; + char audit_name[2*NAMEDATALEN + 20]; ObjectAddress object; + Form_pg_attribute attForm; /* * Only attributes within regular relation have individual security @@ -49,13 +55,44 @@ sepgsql_attribute_post_create(Oid relOid, AttrNumber attnum) return; /* - * Compute a default security label when we create a new procedure object - * under the specified namespace. + * Compute a default security label of the new column underlying the + * specified relation, and check permission to create it. */ + rel = heap_open(AttributeRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + Anum_pg_attribute_attrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relOid)); + ScanKeyInit(&skey[1], + Anum_pg_attribute_attnum, + BTEqualStrategyNumber, F_INT2EQ, + Int16GetDatum(attnum)); + + sscan = systable_beginscan(rel, AttributeRelidNumIndexId, true, + SnapshotSelf, 2, &skey[0]); + + tuple = systable_getnext(sscan); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "catalog lookup failed for column %d of relation %u", + attnum, relOid); + + attForm = (Form_pg_attribute) GETSTRUCT(tuple); + scontext = sepgsql_get_client_label(); tcontext = sepgsql_get_label(RelationRelationId, relOid, 0); ncontext = sepgsql_compute_create(scontext, tcontext, SEPG_CLASS_DB_COLUMN); + /* + * check db_column:{create} permission + */ + snprintf(audit_name, sizeof(audit_name), "table %s column %s", + get_rel_name(relOid), NameStr(attForm->attname)); + sepgsql_avc_check_perms_label(ncontext, + SEPG_CLASS_DB_COLUMN, + SEPG_DB_COLUMN__CREATE, + audit_name, + true); /* * Assign the default security label on a new procedure @@ -65,6 +102,9 @@ sepgsql_attribute_post_create(Oid relOid, AttrNumber attnum) object.objectSubId = attnum; SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ncontext); + systable_endscan(sscan); + heap_close(rel, AccessShareLock); + pfree(tcontext); pfree(ncontext); } @@ -127,10 +167,12 @@ sepgsql_relation_post_create(Oid relOid) Form_pg_class classForm; ObjectAddress object; uint16 tclass; + const char *tclass_text; char *scontext; /* subject */ char *tcontext; /* schema */ char *rcontext; /* relation */ char *ccontext; /* column */ + char audit_name[2*NAMEDATALEN + 20]; /* * Fetch catalog record of the new relation. Because pg_class entry is not @@ -152,16 +194,36 @@ sepgsql_relation_post_create(Oid 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 */ + switch (classForm->relkind) + { + case RELKIND_RELATION: + tclass = SEPG_CLASS_DB_TABLE; + tclass_text = "table"; + break; + case RELKIND_SEQUENCE: + tclass = SEPG_CLASS_DB_SEQUENCE; + tclass_text = "sequence"; + break; + case RELKIND_VIEW: + tclass = SEPG_CLASS_DB_VIEW; + tclass_text = "view"; + break; + default: + goto out; + } /* + * check db_schema:{add_name} permission of the namespace + */ + object.classId = NamespaceRelationId; + object.objectId = classForm->relnamespace; + object.objectSubId = 0; + sepgsql_avc_check_perms(&object, + SEPG_CLASS_DB_SCHEMA, + SEPG_DB_SCHEMA__ADD_NAME, + getObjectDescription(&object), + true); + /* * Compute a default security label when we create a new relation object * under the specified namespace. */ @@ -171,6 +233,16 @@ sepgsql_relation_post_create(Oid relOid) rcontext = sepgsql_compute_create(scontext, tcontext, tclass); /* + * check db_xxx:{create} permission + */ + snprintf(audit_name, sizeof(audit_name), "%s %s", + tclass_text, NameStr(classForm->relname)); + sepgsql_avc_check_perms_label(rcontext, + tclass, + SEPG_DB_DATABASE__CREATE, + audit_name, + true); + /* * Assign the default security label on the new relation */ object.classId = RelationRelationId; @@ -184,26 +256,52 @@ sepgsql_relation_post_create(Oid relOid) */ if (classForm->relkind == RELKIND_RELATION) { - AttrNumber index; + Relation arel; + ScanKeyData akey; + SysScanDesc ascan; + HeapTuple atup; + Form_pg_attribute attForm; - ccontext = sepgsql_compute_create(scontext, rcontext, - SEPG_CLASS_DB_COLUMN); - for (index = FirstLowInvalidHeapAttributeNumber + 1; - index <= classForm->relnatts; - index++) + arel = heap_open(AttributeRelationId, AccessShareLock); + + ScanKeyInit(&akey, + Anum_pg_attribute_attrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relOid)); + + ascan = systable_beginscan(arel, AttributeRelidNumIndexId, true, + SnapshotSelf, 1, &akey); + + while (HeapTupleIsValid(atup = systable_getnext(ascan))) { - if (index == InvalidAttrNumber) - continue; + attForm = (Form_pg_attribute) GETSTRUCT(atup); - if (index == ObjectIdAttributeNumber && !classForm->relhasoids) - continue; + snprintf(audit_name, sizeof(audit_name), "%s %s column %s", + tclass_text, + NameStr(classForm->relname), + NameStr(attForm->attname)); + + ccontext = sepgsql_compute_create(scontext, + rcontext, + SEPG_CLASS_DB_COLUMN); + /* + * check db_column:{create} permission + */ + sepgsql_avc_check_perms_label(ccontext, + SEPG_CLASS_DB_COLUMN, + SEPG_DB_COLUMN__CREATE, + audit_name, + true); object.classId = RelationRelationId; object.objectId = relOid; - object.objectSubId = index; + object.objectSubId = attForm->attnum; SetSecurityLabel(&object, SEPGSQL_LABEL_TAG, ccontext); + + pfree(ccontext); } - pfree(ccontext); + systable_endscan(ascan); + heap_close(arel, AccessShareLock); } pfree(rcontext); out: diff --git a/contrib/sepgsql/sql/create.sql b/contrib/sepgsql/sql/create.sql index a03c977..9a1db91 100644 --- a/contrib/sepgsql/sql/create.sql +++ b/contrib/sepgsql/sql/create.sql @@ -13,6 +13,24 @@ CREATE SCHEMA regtest_schema; SET search_path = regtest_schema, public; +CREATE TABLE regtest_table (x serial primary key, y text); + +ALTER TABLE regtest_table ADD COLUMN z int; + +CREATE TABLE regtest_table_2 (a int) WITH OIDS; + +-- corresponding toast table should not have label and permission checks +ALTER TABLE regtest_table_2 ADD COLUMN b text; + +-- VACUUM FULL internally create a new table and swap them later. +VACUUM FULL regtest_table; + +CREATE VIEW regtest_view AS SELECT * FROM regtest_table WHERE x < 100; + +CREATE SEQUENCE regtest_seq; + +CREATE TYPE regtest_comptype AS (a int, b text); + -- -- clean-up --