Skip to content

Commit 948aeb1

Browse files
NicoleNG18claude
andauthored
fix(intent): generate enforced default roles per entity (#6331)
EdmIntentGenerator hardcoded generateDefaultRoles="false" and never wrote roleRead/roleWrite, so intent-generated apps had no access control: every generated REST controller was open and default-roles.roles was empty. Emit generateDefaultRoles="true" plus the standard <project>.<perspective>.<Entity>ReadOnly / FullAccess role names for every real (controller-backed) entity, mirroring the EDM editor's "Generate Default Roles" checkbox. The perspective segment is the entity's resolved perspective (resolvePerspective): its own name for a top-level entity, its parent's for a composition child, and its own name for a setting too (never the "Settings" shell grouping) - consistent with how regular entities name their roles. The downstream template-application-rest-java template already consumes this metadata to emit checkPermissions() and default-roles.roles; no template change is needed. PROJECTION entities stay unsecured (read-only). The .edm XML carries the attributes too so a generated .edm opens with the box checked. Covered by EdmIntentGeneratorTest (regular + setting entity role names, PROJECTION-stays-false) and IntentEngineIT (model/edm role assertions incl. the setting entity not using the Settings perspective). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d1be29c commit 948aeb1

3 files changed

Lines changed: 55 additions & 4 deletions

File tree

components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ private static EdmDocument buildDocument(IntentGenerationContext context, Intent
153153
List<Map<String, Object>> entityList = new ArrayList<>();
154154
List<Map<String, Object>> perspectiveList = new ArrayList<>();
155155
String tablePrefix = IntentNaming.upperSnake(intentName);
156+
String projectName = context != null && notBlank(context.getProjectName()) ? context.getProjectName() : intentName;
156157
// Document (header-items) layout: a master that owns a composition child whose name ends in
157158
// "Item" (SalesInvoice -> SalesInvoiceItem) renders as a document - header form, inline items
158159
// table, totals footer - rather than the default master-detail. Maps master -> its items entity.
@@ -175,8 +176,9 @@ private static EdmDocument buildDocument(IntentGenerationContext context, Intent
175176
// A setting lives under the global Settings perspective (provided by the shell); it does not
176177
// own a generated perspective.
177178
String perspective = perspectiveFor(name, compositionParents, settingEntities);
179+
String rolePerspective = resolvePerspective(name, compositionParents);
178180
Map<String, Object> entityMap = entityDefaults(name, entity.getDescription(), entity.getIcon(), dependent, setting, perspective,
179-
tablePrefix, perspectiveOrder);
181+
tablePrefix, perspectiveOrder, projectName, rolePerspective);
180182
// A navigation-group id makes the generated perspective nest under that group in the shared
181183
// application shell (the standalone shell is unaffected). Defaults to empty (top-level).
182184
if (notBlank(entity.getGroup())) {
@@ -740,7 +742,7 @@ private static Map<String, Object> perspectiveEntry(String name, int order, Stri
740742
}
741743

742744
private static Map<String, Object> entityDefaults(String name, String description, String icon, boolean dependent, boolean setting,
743-
String perspective, String tablePrefix, int order) {
745+
String perspective, String tablePrefix, int order, String projectName, String rolePerspective) {
744746
String dataName = tablePrefix + "_" + IntentNaming.upperSnake(name);
745747
Map<String, Object> entity = new LinkedHashMap<>();
746748
entity.put("name", name);
@@ -771,7 +773,9 @@ private static Map<String, Object> entityDefaults(String name, String descriptio
771773
entity.put("perspectiveNavId", "");
772774
entity.put("perspectiveRole", "");
773775
entity.put("generateReport", "false");
774-
entity.put("generateDefaultRoles", "false");
776+
entity.put("generateDefaultRoles", "true");
777+
entity.put("roleRead", projectName + "." + rolePerspective + "." + name + "ReadOnly");
778+
entity.put("roleWrite", projectName + "." + rolePerspective + "." + name + "FullAccess");
775779
return entity;
776780
}
777781

@@ -1972,7 +1976,7 @@ private static void appendEntityValue(StringBuilder sb, Map<String, Object> enti
19721976
appendAttribute(sb, "entityType", entityType);
19731977
}
19741978
for (String key : new String[] {"dataName", "dataCount", "dataQuery", "title", "caption", "description", "tooltip", "menuKey",
1975-
"menuLabel", "layoutType", "perspectiveName", "importsCode"}) {
1979+
"menuLabel", "layoutType", "perspectiveName", "importsCode", "generateDefaultRoles", "roleRead", "roleWrite"}) {
19761980
if (entity.get(key) != null) {
19771981
appendAttribute(sb, key, entity.get(key));
19781982
}

components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ void customerEmitsCrossModelProjectionsAndForeignKeys() {
4747
assertEquals("Country", country.get("projectionReferencedEntity"));
4848
// A projection must stay out of this app's navigation - no perspective.
4949
assertEquals("", country.get("perspectiveName"));
50+
assertEquals("false", country.get("generateDefaultRoles"));
51+
assertNull(country.get("roleRead"));
5052

5153
Map<String, Object> customer = entityByName(entities, "Customer");
54+
assertEquals("true", customer.get("generateDefaultRoles"));
5255
Map<String, Object> countryFk = propertyByName(customer, "Country");
5356
assertEquals("INTEGER", countryFk.get("dataType"));
5457
assertEquals("DROPDOWN", countryFk.get("widgetType"));
@@ -253,6 +256,38 @@ void immutableWhenEmitsStatusGuardAttributes() {
253256
assertEquals("2,3", entry.get("immutableStatusValues"));
254257
}
255258

259+
@Test
260+
void securedByDefaultEmitsGenerateDefaultRolesAndRoleNames() {
261+
String yaml = """
262+
name: library
263+
entities:
264+
- name: Genre
265+
kind: setting
266+
fields:
267+
- { name: id, type: integer, primaryKey: true, generated: true }
268+
- { name: name, type: string }
269+
- name: Book
270+
fields:
271+
- { name: id, type: integer, primaryKey: true, generated: true }
272+
- { name: title, type: string, required: true }
273+
relations:
274+
- { name: Genre, kind: manyToOne, to: Genre }
275+
""";
276+
Map<String, Object> model = EdmIntentGenerator.buildModelJsonForTest(IntentParser.parse(yaml), "library");
277+
List<Map<String, Object>> entities = entities(model);
278+
279+
Map<String, Object> book = entityByName(entities, "Book");
280+
assertEquals("true", book.get("generateDefaultRoles"));
281+
assertEquals("library.Book.BookReadOnly", book.get("roleRead"));
282+
assertEquals("library.Book.BookFullAccess", book.get("roleWrite"));
283+
284+
Map<String, Object> genre = entityByName(entities, "Genre");
285+
assertEquals("true", genre.get("generateDefaultRoles"));
286+
assertEquals("Settings", genre.get("perspectiveName"));
287+
assertEquals("library.Genre.GenreReadOnly", genre.get("roleRead"));
288+
assertEquals("library.Genre.GenreFullAccess", genre.get("roleWrite"));
289+
}
290+
256291
@Test
257292
void hierarchyEmitsTreeAndLeafOnlyAttributes() {
258293
String yaml = """

tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,16 @@ private void assertEdmAndModel() {
18251825
assertTrue(edmXml.contains("isReadOnlyProperty=\"true\""),
18261826
"system fields (ProcessId, audit columns) should be flagged read-only so forms render them in the read-only details block");
18271827

1828+
assertTrue(edmXml.contains("generateDefaultRoles=\"true\""),
1829+
"entities should carry generateDefaultRoles=\"true\" so the REST template enforces access control");
1830+
assertTrue(edmXml.contains("roleRead=\"") && edmXml.contains("OrderReadOnly\""),
1831+
"a secured entity must carry a roleRead (<project>.<perspective>.<Entity>ReadOnly)");
1832+
assertTrue(edmXml.contains("roleWrite=\"") && edmXml.contains("OrderFullAccess\""),
1833+
"a secured entity must carry a roleWrite (<project>.<perspective>.<Entity>FullAccess)");
1834+
assertTrue(edmXml.contains(".Country.CountryReadOnly\""),
1835+
"a setting entity's role must use its own domain perspective (Country), like the codbex convention");
1836+
assertFalse(edmXml.contains(".Settings.CountryReadOnly\""), "a setting entity's role must NOT use the Settings shell perspective");
1837+
18281838
// The EDM editor renders the canvas ONLY from mxGraphModel - without it the editor opens
18291839
// empty. Assert the diagram block, an entity vertex, and a relation edge are present.
18301840
assertTrue(edmXml.contains("<mxGraphModel>"), "EDM must carry an mxGraphModel diagram or the editor renders an empty canvas");
@@ -1836,6 +1846,8 @@ private void assertEdmAndModel() {
18361846
assertTrue(resource("orders.model").exists(), "orders.model should be generated");
18371847
String modelBody = contentOf("orders.model");
18381848
assertTrue(modelBody.contains("\"entities\""), "model JSON should have an entities array");
1849+
assertTrue(modelBody.contains("\"generateDefaultRoles\": \"true\"") && modelBody.contains("OrderFullAccess"),
1850+
"the .model JSON (which drives generation) must carry generateDefaultRoles + the role names");
18391851
assertTrue(modelBody.contains("\"perspectives\""), "model JSON should carry the perspectives array like editor-written files");
18401852
assertTrue(modelBody.contains("\"navigations\""), "model JSON should carry the navigations array like editor-written files");
18411853
// Process glue (triggers, resolvers) is NOT in the EDM model - it lives in the .glue file.

0 commit comments

Comments
 (0)