Skip to content

Commit 9b8931e

Browse files
Merge pull request #8066 from espoon-voltti/staff-no-decision-edit
Poista muiden päätösten muokkausoikeus henkilökunnalta
2 parents 4dcac14 + b4aa835 commit 9b8931e

File tree

4 files changed

+112
-18
lines changed

4 files changed

+112
-18
lines changed

frontend/src/employee-frontend/components/child-information/ChildDocuments.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DateRange from 'lib-common/date-range'
1313
import { boolean, openEndedLocalDateRange } from 'lib-common/form/fields'
1414
import { object, oneOf, required, validated } from 'lib-common/form/form'
1515
import { useForm, useFormFields } from 'lib-common/form/hooks'
16+
import type { Action } from 'lib-common/generated/action'
1617
import type {
1718
ChildDocumentSummary,
1819
ChildDocumentSummaryWithPermittedActions,
@@ -157,13 +158,11 @@ const DecisionValidityModal = React.memo(function DecisionValidityModal({
157158
onClose,
158159
document,
159160
childId,
160-
permittedActions: _permittedActions,
161161
onSuccess
162162
}: {
163163
onClose: () => void
164164
document: ChildDocumentSummary
165165
childId: ChildId
166-
permittedActions: string[]
167166
onSuccess: () => void
168167
}) {
169168
const { i18n, lang } = useTranslation()
@@ -220,7 +219,7 @@ const DecisionValidityCell = React.memo(function DecisionValidityCell({
220219
}: {
221220
document: ChildDocumentSummary
222221
childId: ChildId
223-
permittedActions: string[]
222+
permittedActions: Action.ChildDocument[]
224223
}) {
225224
const { i18n } = useTranslation()
226225
const [modalOpen, setModalOpen] = useState(false)
@@ -248,7 +247,6 @@ const DecisionValidityCell = React.memo(function DecisionValidityCell({
248247
onClose={() => setModalOpen(false)}
249248
document={document}
250249
childId={childId}
251-
permittedActions={permittedActions}
252250
onSuccess={() => setModalOpen(false)}
253251
/>
254252
)}

service/src/integrationTest/kotlin/fi/espoo/evaka/document/childdocument/ChildDocumentControllerIntegrationTest.kt

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,23 @@ import fi.espoo.evaka.shared.ChildDocumentDecisionId
2727
import fi.espoo.evaka.shared.ChildDocumentId
2828
import fi.espoo.evaka.shared.DocumentTemplateId
2929
import fi.espoo.evaka.shared.EmployeeId
30+
import fi.espoo.evaka.shared.PlacementId
3031
import fi.espoo.evaka.shared.async.AsyncJob
3132
import fi.espoo.evaka.shared.async.AsyncJobRunner
3233
import fi.espoo.evaka.shared.auth.AuthenticatedUser
3334
import fi.espoo.evaka.shared.auth.UserRole
3435
import fi.espoo.evaka.shared.auth.insertDaycareAclRow
3536
import fi.espoo.evaka.shared.dev.DevDaycare
37+
import fi.espoo.evaka.shared.dev.DevDaycareGroup
38+
import fi.espoo.evaka.shared.dev.DevDaycareGroupPlacement
3639
import fi.espoo.evaka.shared.dev.DevDocumentTemplate
3740
import fi.espoo.evaka.shared.dev.DevEmployee
3841
import fi.espoo.evaka.shared.dev.DevPerson
3942
import fi.espoo.evaka.shared.dev.DevPersonType
4043
import fi.espoo.evaka.shared.dev.DevPlacement
4144
import fi.espoo.evaka.shared.dev.DevSfiMessageEvent
4245
import fi.espoo.evaka.shared.dev.insert
46+
import fi.espoo.evaka.shared.dev.insertEmployeeToDaycareGroupAcl
4347
import fi.espoo.evaka.shared.domain.BadRequest
4448
import fi.espoo.evaka.shared.domain.Conflict
4549
import fi.espoo.evaka.shared.domain.DateRange
@@ -77,6 +81,7 @@ class ChildDocumentControllerIntegrationTest : FullApplicationTest(resetDbBefore
7781
lateinit var areaId: AreaId
7882
val employeeUser = DevEmployee(roles = setOf(UserRole.ADMIN))
7983
lateinit var unitSupervisorUser: AuthenticatedUser.Employee
84+
lateinit var placementId: PlacementId
8085

8186
final val clock = MockEvakaClock(2022, 1, 1, 15, 0)
8287

@@ -209,14 +214,15 @@ class ChildDocumentControllerIntegrationTest : FullApplicationTest(resetDbBefore
209214
tx.insert(testChild_1, DevPersonType.CHILD)
210215
tx.insert(testAdult_1, DevPersonType.ADULT)
211216
tx.insertGuardian(testAdult_1.id, testChild_1.id)
212-
tx.insert(
213-
DevPlacement(
214-
childId = testChild_1.id,
215-
unitId = testDaycare.id,
216-
startDate = clock.today(),
217-
endDate = clock.today().plusDays(5),
217+
placementId =
218+
tx.insert(
219+
DevPlacement(
220+
childId = testChild_1.id,
221+
unitId = testDaycare.id,
222+
startDate = clock.today(),
223+
endDate = clock.today().plusDays(5),
224+
)
218225
)
219-
)
220226
tx.insert(devTemplatePed)
221227
tx.insert(devTemplatePedagogicalReport)
222228
tx.insert(devTemplateHojks)
@@ -1252,6 +1258,84 @@ class ChildDocumentControllerIntegrationTest : FullApplicationTest(resetDbBefore
12521258
)
12531259
}
12541260

1261+
@Test
1262+
fun `employee with STAFF permission can edit ordinary document but not decision document`() {
1263+
val groupId =
1264+
db.transaction { tx ->
1265+
val id = tx.insert(DevDaycareGroup(daycareId = testDaycare.id))
1266+
tx.insert(
1267+
DevDaycareGroupPlacement(
1268+
daycarePlacementId = placementId,
1269+
daycareGroupId = id,
1270+
startDate = clock.today(),
1271+
endDate = clock.today().plusDays(10),
1272+
)
1273+
)
1274+
id
1275+
}
1276+
1277+
val staffEmployee = DevEmployee()
1278+
val staffUser =
1279+
db.transaction { tx ->
1280+
val staffId = tx.insert(staffEmployee)
1281+
tx.insertDaycareAclRow(testDaycare.id, staffId, UserRole.STAFF)
1282+
tx.insertEmployeeToDaycareGroupAcl(groupId, staffId)
1283+
AuthenticatedUser.Employee(staffId, setOf(UserRole.STAFF))
1284+
}
1285+
1286+
// Create an ordinary child document (PEDAGOGICAL_ASSESSMENT)
1287+
val ordinaryDocumentId =
1288+
controller.createDocument(
1289+
dbInstance(),
1290+
employeeUser.user,
1291+
clock,
1292+
ChildDocumentCreateRequest(testChild_1.id, templateIdPed),
1293+
)
1294+
1295+
// Create a decision document (OTHER_DECISION)
1296+
val decisionDocumentId =
1297+
controller.createDocument(
1298+
dbInstance(),
1299+
employeeUser.user,
1300+
clock,
1301+
ChildDocumentCreateRequest(testChild_1.id, templateIdAssistanceDecision),
1302+
)
1303+
1304+
// Staff employee should be able to update the ordinary document
1305+
val laterClock = MockEvakaClock(clock.now().plusMinutes(6))
1306+
val ordinaryContent =
1307+
DocumentContent(answers = listOf(AnsweredQuestion.TextAnswer("q1", "staff edit")))
1308+
updateDocumentContent(
1309+
ordinaryDocumentId,
1310+
ordinaryContent,
1311+
now = laterClock,
1312+
user = staffUser,
1313+
)
1314+
1315+
// Verify the update succeeded
1316+
assertEquals(
1317+
ordinaryContent,
1318+
controller
1319+
.getDocument(dbInstance(), staffUser, laterClock, ordinaryDocumentId)
1320+
.data
1321+
.content,
1322+
)
1323+
1324+
// Staff employee should NOT be able to update the decision document
1325+
val decisionContent =
1326+
DocumentContent(
1327+
answers = listOf(AnsweredQuestion.TextAnswer("q1", "staff edit decision"))
1328+
)
1329+
assertThrows<Forbidden> {
1330+
updateDocumentContent(
1331+
decisionDocumentId,
1332+
decisionContent,
1333+
now = laterClock,
1334+
user = staffUser,
1335+
)
1336+
}
1337+
}
1338+
12551339
private fun getDocument(id: ChildDocumentId) =
12561340
controller.getDocument(dbInstance(), employeeUser.user, clock, id).data
12571341

service/src/main/kotlin/fi/espoo/evaka/shared/security/Action.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,32 +2384,39 @@ sealed interface Action {
23842384
HasGlobalRole(ADMIN),
23852385
HasUnitRole(UNIT_SUPERVISOR, SPECIAL_EDUCATION_TEACHER)
23862386
.inPlacementUnitOfChildOfChildDocument(editable = true),
2387-
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(editable = true),
2387+
HasGroupRole(STAFF)
2388+
.inPlacementGroupOfChildOfChildDocument(editable = true, denyForDecisions = true),
23882389
),
23892390
PUBLISH(
23902391
HasGlobalRole(ADMIN),
23912392
HasUnitRole(UNIT_SUPERVISOR, SPECIAL_EDUCATION_TEACHER)
23922393
.inPlacementUnitOfChildOfChildDocument(publishable = true),
2393-
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(publishable = true),
2394+
HasGroupRole(STAFF)
2395+
.inPlacementGroupOfChildOfChildDocument(publishable = true, denyForDecisions = true),
23942396
),
23952397
NEXT_STATUS(
23962398
HasGlobalRole(ADMIN),
23972399
HasUnitRole(UNIT_SUPERVISOR, SPECIAL_EDUCATION_TEACHER)
23982400
.inPlacementUnitOfChildOfChildDocument(),
2399-
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(),
2401+
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(denyForDecisions = true),
24002402
),
24012403
PREV_STATUS(
24022404
HasGlobalRole(ADMIN),
24032405
HasUnitRole(UNIT_SUPERVISOR, SPECIAL_EDUCATION_TEACHER)
24042406
.inPlacementUnitOfChildOfChildDocument(canGoToPrevStatus = true),
2405-
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(canGoToPrevStatus = true),
2407+
HasGroupRole(STAFF)
2408+
.inPlacementGroupOfChildOfChildDocument(
2409+
canGoToPrevStatus = true,
2410+
denyForDecisions = true,
2411+
),
24062412
IsEmployee.andIsDecisionMakerForChildDocumentDecision(),
24072413
),
24082414
DELETE(
24092415
HasGlobalRole(ADMIN),
24102416
HasUnitRole(UNIT_SUPERVISOR, SPECIAL_EDUCATION_TEACHER)
24112417
.inPlacementUnitOfChildOfChildDocument(deletable = true),
2412-
HasGroupRole(STAFF).inPlacementGroupOfChildOfChildDocument(deletable = true),
2418+
HasGroupRole(STAFF)
2419+
.inPlacementGroupOfChildOfChildDocument(deletable = true, denyForDecisions = true),
24132420
),
24142421
PROPOSE_DECISION(
24152422
HasGlobalRole(ADMIN),

service/src/main/kotlin/fi/espoo/evaka/shared/security/actionrule/HasGroupRole.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,11 @@ WHERE employee_id = ${bind(user.id)}
174174
deletable: Boolean = false,
175175
publishable: Boolean = false,
176176
canGoToPrevStatus: Boolean = false,
177-
) =
178-
rule<ChildDocumentId> { user, now ->
177+
denyForDecisions: Boolean = false,
178+
): DatabaseActionRule.Scoped<ChildDocumentId, HasGroupRole> {
179+
val denyDecisionSql =
180+
if (denyForDecisions) " AND child_document.type <> 'OTHER_DECISION' " else ""
181+
return rule { user, now ->
179182
sql(
180183
"""
181184
SELECT child_document.id AS id, role, enabled_pilot_features AS unit_features, provider_type AS unit_provider_type
@@ -187,10 +190,12 @@ ${if (editable) "AND status = ANY(${bind(DocumentStatus.entries.filter { it.empl
187190
${if (deletable) "AND status = 'DRAFT' AND published_at IS NULL" else ""}
188191
${if (publishable) "AND status <> 'COMPLETED'" else ""}
189192
${if (canGoToPrevStatus) "AND child_document.type = 'CITIZEN_BASIC' AND child_document.content -> 'answers' = '[]'::jsonb AND child_document.status <> 'COMPLETED'" else ""}
193+
$denyDecisionSql
190194
"""
191195
.trimIndent()
192196
)
193197
}
198+
}
194199

195200
fun inPlacementGroupOfDuplicateChildOfHojksChildDocument() =
196201
rule<ChildDocumentId> { user, now ->

0 commit comments

Comments
 (0)