From d285b440ea852b7a00f62c8df7b0b1d973cfbcce Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 25 Apr 2024 09:36:40 +0200 Subject: [PATCH] generated multiple insert permission grants fo hs_hosting_asset --- .../hosting/asset/HsHostingAssetEntity.java | 2 +- .../rbac/rbacdef/InsertTriggerGenerator.java | 298 ++++++++---------- .../7013-hs-hosting-asset-rbac.sql | 48 +-- 3 files changed, 168 insertions(+), 180 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index c258259f..f915646e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -174,6 +174,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-generated"); + rbac().generateWithBaseFileName("7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index 66ef1481..79dee390 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -7,6 +7,7 @@ import java.util.stream.Stream; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize; @@ -22,194 +23,111 @@ public class InsertTriggerGenerator { } void generateTo(final StringWriter plPgSql) { - generateLiquibaseChangesetHeader(plPgSql); - generateGrantInsertRoleToExistingObjects(plPgSql); - generateInsertPermissionGrantTrigger(plPgSql); - generateInsertCheckTrigger(plPgSql); + generateInsertGrants(plPgSql); + generateInsertPermissionChecks(plPgSql); plPgSql.writeLn("--//"); } - private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) { - plPgSql.writeLn(""" - -- ============================================================================ - --changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--// - -- ---------------------------------------------------------------------------- - """, - with("liquibaseTagPrefix", liquibaseTagPrefix)); + private void generateInsertGrants(final StringWriter plPgSql) { + if (isInsertPermissionGrantedToGlobalGuest()) { + // any user is allowed to insert new rows => no insert check needed + return; + } + + if (isInsertPermissionIsNotGrantedAtAll()) { + generateInsertPermissionTriggerAlwaysDisallow(plPgSql); + } else { + generateInsertPermissionGrants(plPgSql); + } } - private void generateGrantInsertRoleToExistingObjects(final StringWriter plPgSql) { - getOptionalInsertSuperRole().ifPresent( superRoleDef -> { + private void generateInsertPermissionGrants(final StringWriter plPgSql) { + plPgSql.writeLn(""" + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + """, + with("liquibaseTagPrefix", liquibaseTagPrefix)); + + getInsertGrants().forEach( g -> { + plPgSql.writeLn(""" + -- granting INSERT permission to ${rawSubTable} ---------------------------- + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + plPgSql.writeLn(""" /* - Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows. + Grants INSERT INTO ${rawSubTable} permissions to specified role of pre-existing ${rawSuperTable} rows. */ do language plpgsql $$ declare - row ${rawSuperTableName}; + preExistingRow ${rawSuperTable}; begin - call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); - - FOR row IN SELECT * FROM ${rawSuperTableName}${typeCondition} + call defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows'); + + FOR preExistingRow IN SELECT * FROM ${rawSuperTable} + ${whenCondition} LOOP call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', '${rawSubTableName}'), - ${rawSuperRoleDescriptor}); + createPermission(preExistingRow.uuid, 'INSERT', '${rawSubTable}'), + hsBookingItemAGENT(preExistingRow)); END LOOP; - END; + end; $$; """, - with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), - with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), - with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")), - with("typeCondition", superRoleDef.getEntityAlias().isCaseDependent() - ? "\n\t\t\tWHERE type = '${case}'".replace("${case}", superRoleDef.getEntityAlias().usingCase().value) - : "") - ); - }); - } + with("whenCondition", g.getSuperRoleDef().getEntityAlias().isCaseDependent() + // TODO.impl: .type needs to be dynamically generated + ? "WHERE preExistingRow.type = '${value}'" + .replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value) + : "-- unconditional for all rows in that table"), + with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()), + with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName())); - private void generateInsertPermissionGrantTrigger(final StringWriter plPgSql) { - getOptionalInsertSuperRole().ifPresent( superRoleDef -> { - plPgSql.writeLn(""" + plPgSql.writeLn(""" /** - Adds ${rawSubTableName} INSERT permission to specified role of new ${rawSuperTableName} rows. + Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows. */ - create or replace function ${rawSubTableName}_${rawSuperTableName}_insert_tf() + create or replace function new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf() returns trigger language plpgsql strict as $$ begin - ${typeConditionIf}call grantPermissionToRole( - createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'), - ${rawSuperRoleDescriptor});${typeConditionEndIf} + ${ifConditionThen} + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', '${rawSubTable}'), + ${superRoleRef}); + ${ifConditionEnd} return NEW; end; $$; - - -- z_... is to put it at the end of after insert triggers, to make sure the roles exist - create trigger z_${rawSubTableName}_${rawSuperTableName}_insert_tg - after insert on ${rawSuperTableName} + + -- z_... is to put it at the end of after insert triggers, to make sure the roles exist + create trigger z_new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tg + after insert on ${rawSuperTable} for each row - execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf(); + execute procedure new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf(); """, - with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), - with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), - with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())), - with("typeConditionIf", - superRoleDef.getEntityAlias().isCaseDependent() - ? "if NEW.type = '${case}' then\n\t\t".replace("${case}", superRoleDef.getEntityAlias().usingCase().value) - : ""), - with("typeConditionEndIf", superRoleDef.getEntityAlias().isCaseDependent() - ? "\n\tend if;" - : "") - ); + with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent() + // TODO.impl: .type needs to be dynamically generated + ? "if NEW.type = '" + g.getSuperRoleDef().getEntityAlias().usingCase().value + "' then" + : "-- unconditional for all rows in that table"), + with("ifConditionEnd", g.getSuperRoleDef().getEntityAlias().isCaseDependent() + ? "end if;" + : "-- end."), + with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), NEW.name())), + with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()), + with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName())); + }); } - private void generateInsertCheckTrigger(final StringWriter plPgSql) { - getOptionalInsertGrant().ifPresentOrElse(g -> { - if (g.getSuperRoleDef().getEntityAlias().isGlobal()) { - switch (g.getSuperRoleDef().getRole()) { - case ADMIN -> { - generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql); - } - case GUEST -> { - // no permission check trigger generated, as anybody can insert rows into this table - } - default -> { - throw new IllegalArgumentException( - "invalid global role for INSERT permission: " + g.getSuperRoleDef().getRole()); - } - } - } else { - if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) { - generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(plPgSql, g); - } else { - generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(plPgSql, g); - } - } - }, - () -> { - System.err.println("WARNING: no explicit INSERT grant for " + rbacDef.getRootEntityAlias().simpleName() + " => implicitly grant INSERT to global:ADMIN"); - generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql); - }); - } - - private void generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) { + private void generateInsertPermissionTriggerAlwaysDisallow(final StringWriter plPgSql) { plPgSql.writeLn(""" - /** - Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, - where the check is performed by a direct role. - - A direct role is a role depending on a foreign key directly available in the NEW row. - */ - create or replace function ${rawSubTable}_insert_permission_missing_tf() - returns trigger - language plpgsql as $$ - begin - raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); - end; $$; + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + """, + with("liquibaseTagPrefix", liquibaseTagPrefix)); - create trigger ${rawSubTable}_insert_permission_check_tg - before insert on ${rawSubTable} - for each row - when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) - execute procedure ${rawSubTable}_insert_permission_missing_tf(); - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), - with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName())); - } - - private void generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey( - final StringWriter plPgSql, - final RbacView.RbacGrantDefinition g) { - plPgSql.writeLn(""" - /** - Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, - where the check is performed by an indirect role. - - An indirect role is a role which depends on an object uuid which is not a direct foreign key - of the source entity, but needs to be fetched via joined tables. - */ - create or replace function ${rawSubTable}_insert_permission_check_tf() - returns trigger - language plpgsql as $$ - - declare - superRoleObjectUuid uuid; - - begin - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); - plPgSql.chopEmptyLines(); - plPgSql.indented(2, () -> { - plPgSql.writeLn( - "superRoleObjectUuid := (" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ");\n" + - "assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null';", - with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"), - with("ref", NEW.name())); - }); - plPgSql.writeLn(); - plPgSql.writeLn(""" - if ( not hasInsertPermission(superRoleObjectUuid, 'INSERT', '${rawSubTable}') ) then - raise exception - '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); - end if; - return NEW; - end; $$; - - create trigger ${rawSubTable}_insert_permission_check_tg - before insert on ${rawSubTable} - for each row - execute procedure ${rawSubTable}_insert_permission_check_tf(); - - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); - } - - private void generateInsertPermissionTriggerAllowOnlyGlobalAdmin(final StringWriter plPgSql) { plPgSql.writeLn(""" /** Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, @@ -219,17 +137,67 @@ public class InsertTriggerGenerator { returns trigger language plpgsql as $$ begin - raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); + raise exception '[403] insert into ${rawSubTable} not allowed regardless of current subject, no insert permissions grated at all'; end; $$; create trigger ${rawSubTable}_insert_permission_check_tg before insert on ${rawSubTable} for each row - when ( not isGlobalAdmin() ) execute procedure ${rawSubTable}_insert_permission_missing_tf(); """, with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + + plPgSql.writeLn("--//"); + } + + + private void generateInsertPermissionChecks(final StringWriter plPgSql) { + plPgSql.writeLn(""" + -- ============================================================================ + --changeset ${rawSubTable}-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, + where the check is performed by a direct role. + + A direct role is a role depending on a foreign key directly available in the NEW row. + */ + create or replace function ${rawSubTable}_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ + begin + raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end; $$; + + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + when ( not ( + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + + plPgSql.indented(2, () -> { + getInsertGrants().forEach(g -> { + final RbacView.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias(); + final var caseCondition = superRoleEntityAlias.isCaseDependent() + ? "NEW.type = '" + superRoleEntityAlias.usingCase().value + "' and " + : ""; + plPgSql.writeLn("${caseCondition}hasInsertPermission(NEW.${refColumn}, 'INSERT', '${rawSubTable}') or", + with("caseCondition", caseCondition), + with("refColumn", superRoleEntityAlias.dependsOnColumName()), + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + }); + plPgSql.chopTail(" or\n"); + }); + + plPgSql.writeLn(""" + ) ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + --// + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); } private Stream getInsertGrants() { @@ -238,6 +206,15 @@ public class InsertTriggerGenerator { .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT); } + private boolean isInsertPermissionIsNotGrantedAtAll() { + return getInsertGrants().findAny().isEmpty(); + } + + private boolean isInsertPermissionGrantedToGlobalGuest() { + return getInsertGrants().anyMatch(g -> + g.getSuperRoleDef().getEntityAlias().isGlobal() && g.getSuperRoleDef().getRole() == GUEST); + } + private Optional getOptionalInsertGrant() { return getInsertGrants() .reduce(singleton()); @@ -252,7 +229,8 @@ public class InsertTriggerGenerator { private static BinaryOperator singleton() { return (x, y) -> { if ( !x.equals(y) ) { - throw new IllegalStateException("only a single INSERT permission grant allowed"); + return x; + // throw new IllegalStateException("only a single INSERT permission grant allowed"); } return x; }; diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index f492e778..4fc326c3 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql @@ -93,25 +93,23 @@ execute procedure insertTriggerForHsHostingAsset_tf(); --changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- --- --------------------------------------------------------------------------------------- --- to hs_booking_item --- --------------------------------------------------------------------------------------- +-- granting INSERT permission to hs_hosting_asset ---------------------------- /* Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_booking_item rows. */ do language plpgsql $$ declare - hsBookingItem hs_booking_item; + preExistingRow hs_booking_item; begin call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising hs_booking_item rows'); - FOR hsBookingItem IN SELECT * FROM hs_booking_item + FOR preExistingRow IN SELECT * FROM hs_booking_item -- unconditional for all rows in that table LOOP call grantPermissionToRole( - createPermission(hsBookingItem.uuid, 'INSERT', 'hs_hosting_asset'), - hsBookingItemAGENT(hsBookingItem)); + createPermission(preExistingRow.uuid, 'INSERT', 'hs_hosting_asset'), + hsBookingItemAGENT(preExistingRow)); END LOOP; end; $$; @@ -124,7 +122,7 @@ create or replace function new_hs_hosting_asset_grants_insert_to_hs_booking_item language plpgsql strict as $$ begin - -- unconditional for all rows in that table: + -- unconditional for all rows in that table call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), hsBookingItemAGENT(NEW)); @@ -138,14 +136,26 @@ create trigger z_new_hs_hosting_asset_grants_insert_to_hs_booking_item_tg for each row execute procedure new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf(); --- --------------------------------------------------------------------------------------- --- to hs_hosting_asset --- --------------------------------------------------------------------------------------- +-- granting INSERT permission to hs_hosting_asset ---------------------------- /* - Grants INSERT INTO hs_hosting_asset permissions to specified role of new hs_hosting_asset rows. + Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows. */ --- such rows cannot yet exist => code block skipped +do language plpgsql $$ + declare + preExistingRow hs_hosting_asset; + begin + call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising hs_hosting_asset rows'); + + FOR preExistingRow IN SELECT * FROM hs_hosting_asset + WHERE preExistingRow.type = 'MANAGED_SERVER' + LOOP + call grantPermissionToRole( + createPermission(preExistingRow.uuid, 'INSERT', 'hs_hosting_asset'), + hsBookingItemAGENT(preExistingRow)); + END LOOP; + end; +$$; /** Grants hs_hosting_asset INSERT permission to specified role of new hs_hosting_asset rows. @@ -155,7 +165,7 @@ create or replace function new_hs_hosting_asset_grants_insert_to_hs_hosting_asse language plpgsql strict as $$ begin - if NEW.type in ('MANAGED_SERVER') then + if NEW.type = 'MANAGED_SERVER' then call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), hsHostingAssetADMIN(NEW)); @@ -168,11 +178,10 @@ create trigger z_new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tg after insert on hs_hosting_asset for each row execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf(); ---// -- ============================================================================ ---changeset hs-hosting-asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// +--changeset hs_hosting_asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /** @@ -193,11 +202,12 @@ create trigger hs_hosting_asset_insert_permission_check_tg before insert on hs_hosting_asset for each row when ( not ( - hasInsertPermission(NEW.bookingItemUuid, 'INSERT', 'hs_hosting_asset') or - NEW.type = 'MANAGED_WEBSPACE' and hasInsertPermission(NEW.parentAssetUuid, 'INSERT', 'hs_hosting_asset') - ) ) + + hasInsertPermission(NEW.bookingItemUuid, 'INSERT', 'hs_hosting_asset') or + NEW.type = 'MANAGED_SERVER' and hasInsertPermission(NEW.parentAssetUuid, 'INSERT', 'hs_hosting_asset') ) ) execute procedure hs_hosting_asset_insert_permission_missing_tf(); --// +--// -- ============================================================================ --changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--//