InsertTriggerGenerator.java
package net.hostsharing.hsadminng.rbac.generator;
import java.util.Optional;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
public class InsertTriggerGenerator {
private final RbacSpec rbacDef;
private final String liquibaseTagPrefix;
public InsertTriggerGenerator(final RbacSpec rbacDef, final String liqibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liqibaseTagPrefix;
}
void generateTo(final StringWriter plPgSql) {
if (isInsertPermissionGrantedToGlobalGuest()) {
// any user is allowed to insert new rows => no insert check needed
return;
}
generateInsertGrants(plPgSql);
generateInsertPermissionChecks(plPgSql);
}
private void generateInsertGrants(final StringWriter plPgSql) {
if (isInsertPermissionIsNotGrantedAtAll()) {
generateInsertPermissionTriggerAlwaysDisallow(plPgSql);
} else {
generateInsertPermissionGrants(plPgSql);
}
}
private void generateInsertPermissionGrants(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-GRANTING-INSERT-PERMISSION endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
getInsertGrants().forEach( g -> {
plPgSql.writeLn("""
-- granting INSERT permission to ${rawSubTable} ----------------------------
""",
with("rawSubTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()));
if (isGrantToADifferentTable(g)) {
plPgSql.writeLn(
"""
/*
Grants INSERT INTO ${rawSubTable} permissions to specified role of pre-existing ${rawSuperTable} rows.
*/
do language plpgsql $$
declare
row ${rawSuperTable};
begin
call base.defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows');
FOR row IN SELECT * FROM ${rawSuperTable}
${whenCondition}
LOOP
call rbac.grantPermissionToRole(
rbac.createPermission(row.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef});
END LOOP;
end;
$$;
""",
with("whenCondition", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
// TODO.impl: 'type' needs to be dynamically generated
? "WHERE type = '${value}'"
.replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value)
: "-- unconditional for all rows in that table"),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()),
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row")));
} else {
plPgSql.writeLn("""
-- Granting INSERT INTO hs_hosting.asset permissions to specified role of pre-existing hs_hosting.asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet.
""",
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()));
}
plPgSql.writeLn("""
/**
Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows.
*/
create or replace function ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf()
returns trigger
language plpgsql
strict as $$
begin
${ifConditionThen}
call rbac.grantPermissionToRole(
rbac.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 ${rawSubTableName}_z_grants_after_insert_tg
after insert on ${rawSuperTableWithSchema}
for each row
execute procedure ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf();
""",
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("rawSuperTableWithSchema", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSuperTableShortName", g.getSuperRoleDef().getEntityAlias().getRawTableShortName()),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTableSchemaPrefix", g.getPermDef().getEntityAlias().getRawTableSchemaPrefix()),
with("rawSubTableName", g.getPermDef().getEntityAlias().getRawTableName()),
with("rawSubTableShortName", g.getPermDef().getEntityAlias().getRawTableShortName()));
});
}
private void generateInsertPermissionTriggerAlwaysDisallow(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
where only global-admin has that permission.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTableWithSchema} values(%) not allowed regardless of current subject, no insert permissions granted at all', NEW;
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
execute procedure ${rawSubTableWithSchema}_insert_permission_missing_tf();
""",
with("rawSubTableWithSchema", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
plPgSql.writeLn("--//");
}
private void generateInsertPermissionChecks(final StringWriter plPgSql) {
generateInsertPermissionsCheckHeader(plPgSql);
plPgSql.indented(1, () -> {
getInsertGrants().forEach(g -> {
generateInsertPermissionChecksForSingleGrant(plPgSql, g);
});
plPgSql.chopTail(" or\n");
});
generateInsertPermissionsChecksFooter(plPgSql);
}
private void generateInsertPermissionsCheckHeader(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-CHECKING-INSERT-PERMISSION runOnChange:true endDelimiter:--//
--validCheckSum: ANY
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()));
plPgSql.chopEmptyLines();
}
private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacSpec.RbacGrantDefinition g) {
final RbacSpec.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias();
final var caseCondition = g.isConditional()
? ("NEW.type in (" + toStringList(g.getForCases()) + ") and ")
: "";
if (g.getSuperRoleDef().isGlobal(GUEST)) {
plPgSql.writeLn(
"""
-- check INSERT permission for rbac.global anyone
if ${caseCondition}true then
return NEW;
end if;
""",
with("caseCondition", caseCondition));
} else if (g.getSuperRoleDef().isGlobal(ADMIN)) {
plPgSql.writeLn(
"""
-- check INSERT permission if rbac.global ADMIN
if ${caseCondition}rbac.hasGlobalAdminRole() then
return NEW;
end if;
""",
with("caseCondition", caseCondition));
} else if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) {
plPgSql.writeLn(
"""
-- check INSERT permission via direct foreign key: NEW.${refColumn}
if ${caseCondition}rbac.hasInsertPermission(NEW.${refColumn}, '${rawSubTable}') then
return NEW;
end if;
""",
with("caseCondition", caseCondition),
with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()));
} else {
plPgSql.writeLn(
"""
-- check INSERT permission via indirect foreign key: NEW.${refColumn}
superObjectUuid := (${fetchSql});
assert superObjectUuid is not null, 'object uuid fetched depending on ${rawSubTable}.${refColumn} must not be null, also check fetchSql in RBAC DSL';
if ${caseCondition}rbac.hasInsertPermission(superObjectUuid, '${rawSubTable}') then
return NEW;
end if;
""",
with("caseCondition", caseCondition),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("fetchSql", g.getSuperRoleDef().getEntityAlias().fetchSql().sql),
with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
with("ref", NEW.name()));
}
}
private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) {
plPgSql.writeLn();
plPgSql.writeLn("""
raise exception '[403] insert into ${rawSubTableWithSchema} values(%) not allowed for current subjects % (%)',
NEW, base.currentSubjects(), rbac.currentSubjectOrAssumedRolesUuids();
end; $$;
create or replace trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTableWithSchema}
for each row
execute procedure ${rawSubTableWithSchema}_insert_permission_check_tf();
--//
""",
with("rawSubTableWithSchema", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
}
private String toStringList(final Set<RbacSpec.CaseDef> cases) {
return cases.stream().map(c -> "'" + c.value + "'").collect(joining(", "));
}
private boolean isGrantToADifferentTable(final RbacSpec.RbacGrantDefinition g) {
return !rbacDef.getRootEntityAlias().getRawTableNameWithSchema().equals(g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema());
}
private Stream<RbacSpec.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.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<RbacSpec.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants()
.reduce(singleton());
}
private Optional<RbacSpec.RbacRoleDefinition> getOptionalInsertSuperRole() {
return getInsertGrants()
.map(RbacSpec.RbacGrantDefinition::getSuperRoleDef)
.reduce(singleton());
}
private static <T> BinaryOperator<T> singleton() {
return (x, y) -> {
if ( !x.equals(y) ) {
return x;
// throw new IllegalStateException("only a single INSERT permission grant allowed");
}
return x;
};
}
private static String toVar(final RbacSpec.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().name());
}
private String toRoleDescriptor(final RbacSpec.RbacRoleDefinition roleDef, final String ref) {
final var functionName = roleDef.descriptorFunctionName();
if (roleDef.getEntityAlias().isGlobal()) {
return functionName + "()";
}
return functionName + "(" + ref + ")";
}
}