new case for insert permission trigger generator: indirect role check (via relation)

This commit is contained in:
Michael Hoennig 2024-03-15 06:18:02 +01:00
parent 386bea0e51
commit 266cd16b52
6 changed files with 225 additions and 67 deletions

View File

@ -103,8 +103,19 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
.withRestrictedViewOrderBy(expression("validity")) .withRestrictedViewOrderBy(expression("validity"))
.withUpdatableColumns("reference", "agreement", "validity") .withUpdatableColumns("reference", "agreement", "validity")
.importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid")) .importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class,
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid")) dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid
""")
)
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class,
dependsOnColumn("bankAccountUuid"),
autoFetched()
)
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);
@ -116,14 +127,16 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
}) })
.createSubRole(AGENT, (with) -> { .createSubRole(AGENT, (with) -> {
with.outgoingSubRole("bankAccount", REFERRER); with.outgoingSubRole("bankAccount", REFERRER);
with.outgoingSubRole("debitor", AGENT); with.outgoingSubRole("debitorRel", AGENT);
}) })
.createSubRole(REFERRER, (with) -> { .createSubRole(REFERRER, (with) -> {
with.incomingSuperRole("bankAccount", ADMIN); with.incomingSuperRole("bankAccount", ADMIN);
with.incomingSuperRole("debitor", AGENT); with.incomingSuperRole("debitorRel", AGENT);
with.outgoingSubRole("debitorRel", TENANT); with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT); with.permission(SELECT);
}); })
.toRole("debitorRel", ADMIN).grantPermission("sepaMandate", INSERT);
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {

View File

@ -4,6 +4,7 @@ import java.util.Optional;
import java.util.function.BinaryOperator; import java.util.function.BinaryOperator;
import java.util.stream.Stream; 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.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
@ -91,49 +92,23 @@ public class InsertTriggerGenerator {
""", """,
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, PostgresTriggerReference.NEW.name())) with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name()))
); );
}); });
} }
private void generateInsertCheckTrigger(final StringWriter plPgSql) { private void generateInsertCheckTrigger(final StringWriter plPgSql) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
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; $$;
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
getOptionalInsertGrant().ifPresentOrElse(g -> { getOptionalInsertGrant().ifPresentOrElse(g -> {
if (!g.getSuperRoleDef().getEntityAlias().isGlobal()) { if (!g.getSuperRoleDef().getEntityAlias().isGlobal()) {
plPgSql.writeLn( if (rbacDef.isRootEntityAlias(g.getSuperRoleDef().getEntityAlias())) {
""" generateInsertPermissionTriggerAllowByDirectRole(plPgSql, g);
create trigger ${rawSubTable}_insert_permission_check_tg } else {
before insert on ${rawSubTable} generateInsertPermissionTriggerAllowByIndirectRole(plPgSql, g);
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()));
} else { } else {
switch (g.getSuperRoleDef().getRole()) { switch (g.getSuperRoleDef().getRole()) {
case ADMIN -> { case ADMIN -> {
plPgSql.writeLn( generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
"""
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()));
} }
case GUEST -> { case GUEST -> {
// no permission check trigger generated, as anybody can insert rows into this table // no permission check trigger generated, as anybody can insert rows into this table
@ -146,7 +121,8 @@ public class InsertTriggerGenerator {
} }
}, },
() -> { () -> {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- FIXME: Where is this case necessary?
create trigger ${rawSubTable}_insert_permission_check_tg create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable} before insert on ${rawSubTable}
for each row for each row
@ -159,6 +135,92 @@ public class InsertTriggerGenerator {
}); });
} }
private void generateInsertPermissionTriggerAllowByDirectRole(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
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 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 generateInsertPermissionTriggerAllowByIndirectRole(
final StringWriter plPgSql,
final RbacView.RbacGrantDefinition g) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
if ( not hasInsertPermission(
( SELECT ${varName}.uuid FROM
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("varName", g.getSuperRoleDef().getEntityAlias().aliasName()));
plPgSql.indented(3, () -> {
plPgSql.writeLn(
"(" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ") AS ${varName}",
with("varName", g.getSuperRoleDef().getEntityAlias().aliasName()),
with("ref", NEW.name()));
});
plPgSql.writeLn("""
), '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_missing_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}.
*/
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 isGlobalAdmin() )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
}
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() { private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream() return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE) .filter(g -> g.grantType() == PERM_TO_ROLE)

View File

@ -38,12 +38,26 @@ public class StringWriter {
--indentLevel; --indentLevel;
} }
void indent(int levels) {
indentLevel += levels;
}
void unindent(int levels) {
indentLevel -= levels;
}
void indented(final Runnable indented) { void indented(final Runnable indented) {
indent(); indent();
indented.run(); indented.run();
unindent(); unindent();
} }
void indented(int levels, final Runnable indented) {
indent(levels);
indented.run();
unindent(levels);
}
boolean chopTail(final String tail) { boolean chopTail(final String tail) {
if (string.toString().endsWith(tail)) { if (string.toString().endsWith(tail)) {
string.setLength(string.length() - tail.length()); string.setLength(string.length() - tail.length());

View File

@ -42,7 +42,9 @@ public class RbacGrantsDiagramService {
PERMISSIONS, PERMISSIONS,
NOT_ASSUMED, NOT_ASSUMED,
TEST_ENTITIES, TEST_ENTITIES,
NON_TEST_ENTITIES NON_TEST_ENTITIES;
public static final EnumSet<Include> ALL = EnumSet.allOf(Include.class);
} }
@Autowired @Autowired
@ -65,6 +67,10 @@ public class RbacGrantsDiagramService {
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) { private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
final var grants = rawGrantRepo.findByAscendingUuid(refUuid); final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
grants.forEach(g -> { grants.forEach(g -> {
if ( g.getDescendantIdName() == null ) {
// FIXME: what's that?
return;
}
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) { if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) {
return; return;
} }
@ -116,7 +122,7 @@ public class RbacGrantsDiagramService {
) )
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName)) .collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
.entrySet().stream() .entrySet().stream()
.map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n " .map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
+ entity.getValue().stream() + entity.getValue().stream()
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n ")) .map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted() .sorted()
@ -127,9 +133,9 @@ public class RbacGrantsDiagramService {
: ""; : "";
final var grants = graph.stream() final var grants = graph.stream()
.map(g -> quoted(g.getAscendantIdName()) .map(g -> cleanId(g.getAscendantIdName())
+ " -->" + (g.isAssumed() ? " " : "|XX| ") + " -->" + (g.isAssumed() ? " " : "|XX| ")
+ quoted(g.getDescendantIdName())) + cleanId(g.getDescendantIdName()))
.sorted() .sorted()
.collect(joining("\n")); .collect(joining("\n"));
@ -151,7 +157,7 @@ public class RbacGrantsDiagramService {
// } // }
// return "[" + table + "\n" + entity + "]"; // return "[" + table + "\n" + entity + "]";
// } // }
return "[" + entityId + "]"; return "[" + cleanId(entityId) + "]";
} }
private static String renderEntityIdName(final Node node) { private static String renderEntityIdName(final Node node) {
@ -170,7 +176,7 @@ public class RbacGrantsDiagramService {
} }
private String renderNode(final String idName, final UUID uuid) { private String renderNode(final String idName, final UUID uuid) {
return quoted(idName) + renderNodeContent(idName, uuid); return cleanId(idName) + renderNodeContent(idName, uuid);
} }
private String renderNodeContent(final String idName, final UUID uuid) { private String renderNodeContent(final String idName, final UUID uuid) {
@ -196,8 +202,9 @@ public class RbacGrantsDiagramService {
} }
@NotNull @NotNull
private static String quoted(final String idName) { private static String cleanId(final String idName) {
return idName.replace(" ", ":").replaceAll("@.*", ""); return idName.replace(" ", ":").replaceAll("@.*", "")
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "");
} }
} }

View File

@ -1,6 +1,6 @@
### rbac sepaMandate ### rbac sepaMandate
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T18:29:47.084556363. This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-15T06:12:35.337470470.
```mermaid ```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%% %%{init:{'flowchart':{'htmlLabels':false}}}%%
@ -77,6 +77,7 @@ subgraph sepaMandate["`**sepaMandate**`"]
perm:sepaMandate:DELETE{{sepaMandate:DELETE}} perm:sepaMandate:DELETE{{sepaMandate:DELETE}}
perm:sepaMandate:UPDATE{{sepaMandate:UPDATE}} perm:sepaMandate:UPDATE{{sepaMandate:UPDATE}}
perm:sepaMandate:SELECT{{sepaMandate:SELECT}} perm:sepaMandate:SELECT{{sepaMandate:SELECT}}
perm:sepaMandate:INSERT{{sepaMandate:INSERT}}
end end
end end
@ -174,5 +175,6 @@ role:sepaMandate:referrer ==> role:debitorRel:tenant
role:sepaMandate:owner ==> perm:sepaMandate:DELETE role:sepaMandate:owner ==> perm:sepaMandate:DELETE
role:sepaMandate:admin ==> perm:sepaMandate:UPDATE role:sepaMandate:admin ==> perm:sepaMandate:UPDATE
role:sepaMandate:referrer ==> perm:sepaMandate:SELECT role:sepaMandate:referrer ==> perm:sepaMandate:SELECT
role:debitorRel:admin ==> perm:sepaMandate:INSERT
``` ```

View File

@ -1,5 +1,6 @@
--liquibase formatted sql --liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T18:29:47.095199204. -- This code generated was by RbacViewPostgresGenerator at 2024-03-15T06:12:35.345630060.
-- ============================================================================ -- ============================================================================
--changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--// --changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--//
@ -34,14 +35,23 @@ declare
begin begin
call enterTriggerForObjectUuid(NEW.uuid); call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid into newBankAccount;
SELECT * FROM hs_office_relationship WHERE uuid = NEW.debitorUuid into newDebitorRel; SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeSepaMandateOwner(NEW), hsOfficeSepaMandateOwner(NEW),
permissions => array['DELETE'], permissions => array['DELETE'],
userUuids => array[currentUserUuid()], incomingSuperRoles => array[globalAdmin()],
incomingSuperRoles => array[globalAdmin()] userUuids => array[currentUserUuid()]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
@ -54,16 +64,16 @@ begin
hsOfficeSepaMandateAgent(NEW), hsOfficeSepaMandateAgent(NEW),
incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW)], incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW)],
outgoingSubRoles => array[ outgoingSubRoles => array[
hsOfficeBankAccountReferrer(newBankAccount), hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeRelationshipAgent(newDebitorRel)] hsOfficeBankAccountReferrer(newBankAccount)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeSepaMandateReferrer(NEW), hsOfficeSepaMandateReferrer(NEW),
permissions => array['SELECT'], permissions => array['SELECT'],
incomingSuperRoles => array[ incomingSuperRoles => array[
hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeBankAccountAdmin(newBankAccount), hsOfficeBankAccountAdmin(newBankAccount),
hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeSepaMandateAgent(NEW)], hsOfficeSepaMandateAgent(NEW)],
outgoingSubRoles => array[hsOfficeRelationshipTenant(newDebitorRel)] outgoingSubRoles => array[hsOfficeRelationshipTenant(newDebitorRel)]
); );
@ -88,13 +98,52 @@ create trigger insertTriggerForHsOfficeSepaMandate_tg
after insert on hs_office_sepamandate after insert on hs_office_sepamandate
for each row for each row
execute procedure insertTriggerForHsOfficeSepaMandate_tf(); execute procedure insertTriggerForHsOfficeSepaMandate_tf();
--// --//
-- ============================================================================ -- ============================================================================
--changeset hs-office-sepamandate-rbac-INSERT:1 endDelimiter:--// --changeset hs-office-sepamandate-rbac-INSERT:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
/*
Creates INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows.
*/
do language plpgsql $$
declare
row hs_office_relationship;
permissionUuid uuid;
roleUuid uuid;
begin
call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows');
FOR row IN SELECT * FROM hs_office_relationship
LOOP
roleUuid := findRoleId(hsOfficeRelationshipAdmin(row));
permissionUuid := createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate');
call grantPermissionToRole(permissionUuid, roleUuid);
END LOOP;
END;
$$;
/**
Adds hs_office_sepamandate INSERT permission to specified role of new hs_office_relationship rows.
*/
create or replace function hs_office_sepamandate_hs_office_relationship_insert_tf()
returns trigger
language plpgsql
strict as $$
begin
call grantPermissionToRole(
hsOfficeRelationshipAdmin(NEW),
createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate'));
return NEW;
end; $$;
create trigger hs_office_sepamandate_hs_office_relationship_insert_tg
after insert on hs_office_relationship
for each row
execute procedure hs_office_sepamandate_hs_office_relationship_insert_tf();
/** /**
Checks if the user or assumed roles are allowed to insert a row to hs_office_sepamandate. Checks if the user or assumed roles are allowed to insert a row to hs_office_sepamandate.
*/ */
@ -102,19 +151,29 @@ create or replace function hs_office_sepamandate_insert_permission_missing_tf()
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
raise exception '[403] insert into hs_office_sepamandate not allowed for current subjects % (%)', if ( not hasInsertPermission(
currentSubjects(), currentSubjectsUuids(); ( SELECT debitorRel.uuid FROM
(SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
) AS debitorRel
), 'INSERT', 'hs_office_sepamandate') ) then
raise exception
'[403] insert into hs_office_sepamandate not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end if;
return NEW;
end; $$; end; $$;
create trigger hs_office_sepamandate_insert_permission_check_tg create trigger hs_office_sepamandate_insert_permission_check_tg
before insert on hs_office_sepamandate before insert on hs_office_sepamandate
for each row for each row
-- As there is no explicit INSERT grant specified for this table,
-- only global admins are allowed to insert any rows.
when ( not isGlobalAdmin() )
execute procedure hs_office_sepamandate_insert_permission_missing_tf(); execute procedure hs_office_sepamandate_insert_permission_missing_tf();
--// --//
-- ============================================================================ -- ============================================================================
--changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--// --changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
@ -125,18 +184,19 @@ create trigger hs_office_sepamandate_insert_permission_check_tg
join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid
$idName$); $idName$);
--// --//
-- ============================================================================ -- ============================================================================
--changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--// --changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_office_sepamandate', call generateRbacRestrictedView('hs_office_sepamandate',
'validity', $orderBy$
validity
$orderBy$,
$updates$ $updates$
reference = new.reference, reference = new.reference,
agreement = new.agreement, agreement = new.agreement,
validity = new.validity validity = new.validity
$updates$); $updates$);
--// --//