allow-multiple-insert-permission-grants #49

Merged
hsh-michaelhoennig merged 15 commits from allow-multiple-insert-permission-grants into master 2024-04-29 11:43:49 +02:00
3 changed files with 168 additions and 180 deletions
Showing only changes of commit d285b440ea - Show all commits

View File

@ -174,6 +174,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
} }
public static void main(String[] args) throws IOException { 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");
} }
} }

View File

@ -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.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.RbacView.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@ -22,194 +23,111 @@ public class InsertTriggerGenerator {
} }
void generateTo(final StringWriter plPgSql) { void generateTo(final StringWriter plPgSql) {
generateLiquibaseChangesetHeader(plPgSql); generateInsertGrants(plPgSql);
generateGrantInsertRoleToExistingObjects(plPgSql); generateInsertPermissionChecks(plPgSql);
generateInsertPermissionGrantTrigger(plPgSql);
generateInsertCheckTrigger(plPgSql);
plPgSql.writeLn("--//"); plPgSql.writeLn("--//");
} }
private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) { private void generateInsertGrants(final StringWriter plPgSql) {
plPgSql.writeLn(""" if (isInsertPermissionGrantedToGlobalGuest()) {
-- ============================================================================ // any user is allowed to insert new rows => no insert check needed
--changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--// return;
-- ---------------------------------------------------------------------------- }
""",
with("liquibaseTagPrefix", liquibaseTagPrefix)); if (isInsertPermissionIsNotGrantedAtAll()) {
generateInsertPermissionTriggerAlwaysDisallow(plPgSql);
} else {
generateInsertPermissionGrants(plPgSql);
}
} }
private void generateGrantInsertRoleToExistingObjects(final StringWriter plPgSql) { private void generateInsertPermissionGrants(final StringWriter plPgSql) {
getOptionalInsertSuperRole().ifPresent( superRoleDef -> { 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(""" 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 $$ do language plpgsql $$
declare declare
row ${rawSuperTableName}; preExistingRow ${rawSuperTable};
begin begin
call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); call defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows');
FOR row IN SELECT * FROM ${rawSuperTableName}${typeCondition} FOR preExistingRow IN SELECT * FROM ${rawSuperTable}
${whenCondition}
LOOP LOOP
call grantPermissionToRole( call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', '${rawSubTableName}'), createPermission(preExistingRow.uuid, 'INSERT', '${rawSubTable}'),
${rawSuperRoleDescriptor}); hsBookingItemAGENT(preExistingRow));
END LOOP; END LOOP;
END; end;
$$; $$;
""", """,
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("whenCondition", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), // TODO.impl: .type needs to be dynamically generated
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")), ? "WHERE preExistingRow.type = '${value}'"
with("typeCondition", superRoleDef.getEntityAlias().isCaseDependent() .replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value)
? "\n\t\t\tWHERE type = '${case}'".replace("${case}", superRoleDef.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) { plPgSql.writeLn("""
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
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 returns trigger
language plpgsql language plpgsql
strict as $$ strict as $$
begin begin
${typeConditionIf}call grantPermissionToRole( ${ifConditionThen}
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'), call grantPermissionToRole(
${rawSuperRoleDescriptor});${typeConditionEndIf} createPermission(NEW.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef});
${ifConditionEnd}
return NEW; return NEW;
end; $$; end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist -- 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 create trigger z_new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tg
after insert on ${rawSuperTableName} after insert on ${rawSuperTable}
for each row for each row
execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf(); execute procedure new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf();
""", """,
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), // TODO.impl: .type needs to be dynamically generated
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())), ? "if NEW.type = '" + g.getSuperRoleDef().getEntityAlias().usingCase().value + "' then"
with("typeConditionIf", : "-- unconditional for all rows in that table"),
superRoleDef.getEntityAlias().isCaseDependent() with("ifConditionEnd", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
? "if NEW.type = '${case}' then\n\t\t".replace("${case}", superRoleDef.getEntityAlias().usingCase().value) ? "end if;"
: ""), : "-- end."),
with("typeConditionEndIf", superRoleDef.getEntityAlias().isCaseDependent() with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), NEW.name())),
? "\n\tend if;" with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
: "") with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName()));
);
}); });
} }
private void generateInsertCheckTrigger(final StringWriter plPgSql) { private void generateInsertPermissionTriggerAlwaysDisallow(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) {
plPgSql.writeLn(""" plPgSql.writeLn("""
/** -- ============================================================================
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, --changeset ${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT:1 endDelimiter:--//
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. with("liquibaseTagPrefix", liquibaseTagPrefix));
*/
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 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(""" plPgSql.writeLn("""
/** /**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}, Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
@ -219,17 +137,67 @@ public class InsertTriggerGenerator {
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', raise exception '[403] insert into ${rawSubTable} not allowed regardless of current subject, no insert permissions grated at all';
currentSubjects(), currentSubjectsUuids();
end; $$; end; $$;
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
when ( not isGlobalAdmin() )
execute procedure ${rawSubTable}_insert_permission_missing_tf(); execute procedure ${rawSubTable}_insert_permission_missing_tf();
""", """,
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); 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<RbacView.RbacGrantDefinition> getInsertGrants() { private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
@ -238,6 +206,15 @@ public class InsertTriggerGenerator {
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT); .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<RbacView.RbacGrantDefinition> getOptionalInsertGrant() { private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants() return getInsertGrants()
.reduce(singleton()); .reduce(singleton());
@ -252,7 +229,8 @@ public class InsertTriggerGenerator {
private static <T> BinaryOperator<T> singleton() { private static <T> BinaryOperator<T> singleton() {
return (x, y) -> { return (x, y) -> {
if ( !x.equals(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; return x;
}; };

View File

@ -93,25 +93,23 @@ execute procedure insertTriggerForHsHostingAsset_tf();
--changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// --changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- --------------------------------------------------------------------------------------- -- granting INSERT permission to hs_hosting_asset ----------------------------
-- to hs_booking_item
-- ---------------------------------------------------------------------------------------
/* /*
Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_booking_item rows. Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_booking_item rows.
*/ */
do language plpgsql $$ do language plpgsql $$
declare declare
hsBookingItem hs_booking_item; preExistingRow hs_booking_item;
begin begin
call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising hs_booking_item rows'); 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 -- unconditional for all rows in that table
LOOP LOOP
call grantPermissionToRole( call grantPermissionToRole(
createPermission(hsBookingItem.uuid, 'INSERT', 'hs_hosting_asset'), createPermission(preExistingRow.uuid, 'INSERT', 'hs_hosting_asset'),
hsBookingItemAGENT(hsBookingItem)); hsBookingItemAGENT(preExistingRow));
END LOOP; END LOOP;
end; end;
$$; $$;
@ -124,7 +122,7 @@ create or replace function new_hs_hosting_asset_grants_insert_to_hs_booking_item
language plpgsql language plpgsql
strict as $$ strict as $$
begin begin
-- unconditional for all rows in that table: -- unconditional for all rows in that table
call grantPermissionToRole( call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'),
hsBookingItemAGENT(NEW)); hsBookingItemAGENT(NEW));
@ -138,14 +136,26 @@ create trigger z_new_hs_hosting_asset_grants_insert_to_hs_booking_item_tg
for each row for each row
execute procedure new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf(); execute procedure new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf();
-- --------------------------------------------------------------------------------------- -- granting INSERT permission to hs_hosting_asset ----------------------------
-- 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. 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 language plpgsql
strict as $$ strict as $$
begin begin
if NEW.type in ('MANAGED_SERVER') then if NEW.type = 'MANAGED_SERVER' then
call grantPermissionToRole( call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'),
hsHostingAssetADMIN(NEW)); 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 after insert on hs_hosting_asset
for each row for each row
execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf(); 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 before insert on hs_hosting_asset
for each row for each row
when ( not ( 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(); execute procedure hs_hosting_asset_insert_permission_missing_tf();
--// --//
--//
-- ============================================================================ -- ============================================================================
--changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--// --changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--//