Skip to content

Commit bbfe0e2

Browse files
delchevclaude
andauthored
feat(intent): read-only mode for personal surfaces (personalReadOnly) (#6369)
* feat(intent): read-only mode for personal surfaces (personalReadOnly) A personal relation may declare personalReadOnly: true alongside personal: true - the generated <Entity>MyController then serves only the scoped reads and its create/update/delete return 405, and the my pages drop New/Edit/ Delete. For owner-owned reference records the person may SEE but never author (a leave-balance account, a payslip) - closes the self-grant hole where a writable personal surface let the owner create their own entitlement. The regular (power) controller is unaffected. Chain: RelationIntent.personalReadOnly -> EdmIntentGenerator emits relationshipPersonalReadOnly -> parameterUtils sets e.personalReadOnly (+ composition-child inheritance) -> EntityMyController.java.template guards the writes -> my-list-view/my-form-view drop the buttons. Guide + IT (IntentEmissionCoverageIT: Balance entity, emission asserts + runtime GET 200 / POST 405) extended per the engine-intent keyword contract. Addresses #6367. * fix: personalReadOnly write refusal uses 403 FORBIDDEN (SDK honors it; 405 mapped to 400) + IT posts PascalCase body Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5a525bc commit bbfe0e2

13 files changed

Lines changed: 96 additions & 46 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,11 @@ private static void putPersonal(Map<String, Object> p, RelationIntent relation,
12141214
return;
12151215
}
12161216
p.put("relationshipPersonal", "true");
1217+
if (relation.isPersonalReadOnly()) {
1218+
// The personal surface is see-only for the owner: the my controller's write methods
1219+
// 405 and the my pages drop New/Edit/Delete (parameterUtils -> the rest/UI templates).
1220+
p.put("relationshipPersonalReadOnly", "true");
1221+
}
12171222
p.put("relationshipIdentityProperty", targetIdentityProperty);
12181223
// The identity entity's display/label field - the personal controller's /me returns it so the
12191224
// personal pages can show "New/Edit <Doc> for <owner>". Falls back to the identity match field.

components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/model/RelationIntent.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ public class RelationIntent {
109109
*/
110110
private boolean personal;
111111

112+
/**
113+
* When set together with {@link #personal}, the personal (my) surface is READ-ONLY for the owner:
114+
* the generated {@code <Entity>MyController} exposes only the scoped reads (getAll / get / count)
115+
* and its create/update/delete return 405, and the personal pages render without New / Edit /
116+
* Delete. Use for records the owner may SEE but never author (a leave-balance account, a payslip);
117+
* the regular (power) controller is unaffected. Ignored unless {@link #personal} is also true.
118+
*/
119+
private boolean personalReadOnly;
120+
112121
/**
113122
* Marks this to-one relation as the OWNER of the record for the PARTNER surface: the generated
114123
* partner REST controller scopes reads to the logged-in external partner's mapped identity record
@@ -262,6 +271,14 @@ public void setPersonal(boolean personal) {
262271
this.personal = personal;
263272
}
264273

274+
public boolean isPersonalReadOnly() {
275+
return personalReadOnly;
276+
}
277+
278+
public void setPersonalReadOnly(boolean personalReadOnly) {
279+
this.personalReadOnly = personalReadOnly;
280+
}
281+
265282
public boolean isPartner() {
266283
return partner;
267284
}

components/engine/engine-intent/src/main/resources/intent-assistant-guide.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ scope through their parent). The entity then gets an ADDITIONAL generated `<Enti
290290
scoped to the logged-in user: reads filtered to the mapped identity record, the owner FK forced
291291
server-side on writes, foreign records 404. A field marked `sensitive: true` (not the PK, the
292292
identity field, or the owner FK) is stripped from personal responses and ignored on personal
293-
writes - use it for billing rates and amounts the person must not see. The regular controller is
293+
writes - use it for billing rates and amounts the person must not see. Add `personalReadOnly: true`
294+
alongside `personal: true` to make the personal surface **see-only**: the generated `MyController`
295+
serves the scoped reads but its create/update/delete return **405**, and the my pages drop
296+
New/Edit/Delete - for records the owner may view but never author (a leave-balance account, a
297+
payslip); the regular (power) controller still writes them normally. The regular controller is
294298
unaffected. Sensitivity propagates to derived fields automatically: a rollup target (`op: sum` /
295299
`latest`) whose `of:` child field is sensitive, and an `aggregate: true` master field fed by a
296300
same-named sensitive item field, are treated as sensitive whenever their entity has a personal

components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,28 @@ public class ${name}MyController {
9090
return scrub(requireMine(id));
9191
}
9292

93+
#if($personalReadOnly)
94+
// See-only personal surface (intent personalReadOnly): the owner may read their own records but
95+
// never author them - the write endpoints exist only to refuse with 405 (the power controller
96+
// remains the sole write path). Fixes the self-grant risk on owner-owned reference records.
97+
@Post
98+
@Documentation("Not allowed - this ${name} is read-only on the personal surface")
99+
public ${name}Entity create(@Body ${name}Entity entity) {
100+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "This ${name} is read-only on your personal surface");
101+
}
102+
103+
@Put("/{id}")
104+
@Documentation("Not allowed - this ${name} is read-only on the personal surface")
105+
public ${name}Entity update(@PathParam("id") #foreach($property in $properties)#if($property.dataPrimaryKey)${property.dataTypeJavaClass}#end#end id, @Body ${name}Entity entity) {
106+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "This ${name} is read-only on your personal surface");
107+
}
108+
109+
@Delete("/{id}")
110+
@Documentation("Not allowed - this ${name} is read-only on the personal surface")
111+
public void deleteById(@PathParam("id") #foreach($property in $properties)#if($property.dataPrimaryKey)${property.dataTypeJavaClass}#end#end id) {
112+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "This ${name} is read-only on your personal surface");
113+
}
114+
#else
93115
@Post
94116
@Documentation("Create a ${name} of mine")
95117
public ${name}Entity create(@Body ${name}Entity entity) {
@@ -150,6 +172,7 @@ public class ${name}MyController {
150172
throw e;
151173
}
152174
}
175+
#end
153176

154177
/**
155178
* The current user's identity record id: the ${personalIdentityProperty} of the identity entity

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-form-view.html.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@
153153
#end
154154

155155
<div class="hbox gap-2" style="max-width: 900px">
156+
#if($personalReadOnly)
157+
<div class="grow"></div>
158+
<button x-h-button data-variant="transparent" @click="goBack()"
159+
x-text="T('$projectName:${tprefix}.defaults.back', 'Back')"></button>
160+
#else
156161
<button x-h-button data-variant="negative" x-show="mode === 'edit'" @click="deleteOpen = true"
157162
x-text="T('$projectName:${tprefix}.defaults.delete', 'Delete')"></button>
158163
<div class="grow"></div>
@@ -162,6 +167,7 @@
162167
<span x-show="saving" x-h-spinner></span>
163168
<span x-text="T('$projectName:${tprefix}.defaults.save', 'Save')"></span>
164169
</button>
170+
#end
165171
</div>
166172
</div>
167173

components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-list-view.html.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
<div x-h-toolbar data-variant="transparent">
88
<span x-h-toolbar-title class="shrink-0" x-text="'My ' + T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}')"></span>
99
<div x-h-toolbar-spacer></div>
10+
#if(!$personalReadOnly)
1011
<button x-h-button data-variant="primary" @click="newEntity()">
1112
<i role="img" x-h-lucide data-lucide="plus"></i>
1213
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
1314
</button>
15+
#end
1416
<!-- Export the own rows as CSV / print them (Save as PDF via the browser dialog). -->
1517
<button x-h-button data-variant="outline" data-size="sm" :disabled="items.length === 0" @click="exportCsv()">
1618
<i role="img" x-h-lucide data-lucide="download"></i><span x-text="T('$projectName:${tprefix}.defaults.export', 'Export')"></span>

components/ui/service-generate/src/main/resources/META-INF/dirigible/service-generate/template/parameterUtils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ export function process(model, parameters) {
220220
e.personalIdentityLabel = p.relationshipIdentityLabel || p.relationshipIdentityProperty;
221221
e.personalIdentityEntityClass = `gen.${javaGen}.data.${javaPerspective}.${p.relationshipEntityName}Entity`;
222222
e.personalIdentityRepositoryClass = `gen.${javaGen}.data.${javaPerspective}.${p.relationshipEntityName}Repository`;
223+
// See-only personal surface (intent personalReadOnly): the my controller's
224+
// writes 405 and the my pages drop New/Edit/Delete.
225+
e.personalReadOnly = !!p.relationshipPersonalReadOnly;
223226
}
224227
// partner (intent `partner: true`): the external-partner mirror of the personal
225228
// owner - resolves the current external user through the TARGET's repository.
@@ -265,6 +268,7 @@ export function process(model, parameters) {
265268
personalProperty: parent.personalProperty,
266269
personalFkJavaClass: parent.personalFkJavaClass
267270
};
271+
e.personalReadOnly = !!parent.personalReadOnly; // children inherit the see-only mode
268272
e.personalIdentityProperty = parent.personalIdentityProperty;
269273
e.personalIdentityLabel = parent.personalIdentityLabel;
270274
e.personalIdentityEntityClass = parent.personalIdentityEntityClass;

modules/parsers/typescript/src/main/java/org/eclipse/dirigible/parsers/typescript/TypeScriptParser.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
/*
2-
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3-
*
4-
* All rights reserved. This program and the accompanying materials are made available under the
5-
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6-
* http://www.eclipse.org/legal/epl-v20.html
7-
*
8-
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9-
*/
101
// Generated from org/eclipse/dirigible/parsers/typescript/TypeScriptParser.g4 by ANTLR 4.13.2
112
package org.eclipse.dirigible.parsers.typescript;
123

modules/parsers/typescript/src/main/java/org/eclipse/dirigible/parsers/typescript/TypeScriptParserBaseListener.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
/*
2-
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3-
*
4-
* All rights reserved. This program and the accompanying materials are made available under the
5-
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6-
* http://www.eclipse.org/legal/epl-v20.html
7-
*
8-
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9-
*/
101
// Generated from org/eclipse/dirigible/parsers/typescript/TypeScriptParser.g4 by ANTLR 4.13.2
112
package org.eclipse.dirigible.parsers.typescript;
123

modules/parsers/typescript/src/main/java/org/eclipse/dirigible/parsers/typescript/TypeScriptParserBaseVisitor.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
/*
2-
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3-
*
4-
* All rights reserved. This program and the accompanying materials are made available under the
5-
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6-
* http://www.eclipse.org/legal/epl-v20.html
7-
*
8-
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9-
*/
101
// Generated from org/eclipse/dirigible/parsers/typescript/TypeScriptParser.g4 by ANTLR 4.13.2
112
package org.eclipse.dirigible.parsers.typescript;
123

0 commit comments

Comments
 (0)