@7365admin1/core 3.48.1-staging.201 → 3.48.1-staging.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/hrmlabs-attendance-location-name.md +35 -0
- package/.changeset/invite-org-scope.md +38 -0
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +28 -5
- package/test/e2e/hrmlabs-location-name.e2e.test.mjs +492 -0
- package/test/e2e/invite-org-scoping.e2e.test.mjs +538 -0
- package/test/e2e/org-invite-seats.e2e.test.mjs +1 -1
- package/test/e2e/subscription-seat-update.e2e.test.mjs +1 -1
- package/test/invite-org-scope.test.mjs +111 -0
- package/test/member-staff-gate.test.mjs +12 -6
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Decide the HRM Labs location server-side, so an entitled caller cannot read
|
|
6
|
+
another site's roster.
|
|
7
|
+
|
|
8
|
+
`requireSiteReach(req, siteId, ATTENDANCE_READ)` guards all four manpower-attendance
|
|
9
|
+
reads, but `siteId` is not what selects the roster — it only picks the local
|
|
10
|
+
`manpower-settings` row. The roster is selected by `locationName`, the field HRM Labs
|
|
11
|
+
is asked for, and that came straight from the request body's `siteName` with nothing
|
|
12
|
+
checking it against `siteId`.
|
|
13
|
+
|
|
14
|
+
So the two could disagree. A caller entitled to site A could send site A's `siteId`,
|
|
15
|
+
pass the guard, and pair it with **another** site's `siteName` — and read that site's
|
|
16
|
+
employee roster back: names, staff numbers, job titles, NRICs
|
|
17
|
+
(`identificationNumber`) and the check-in/check-out photographs. The exposure had been
|
|
18
|
+
narrowed from anyone on the internet to any signed-in account; this closes it.
|
|
19
|
+
|
|
20
|
+
The location now comes from `config.siteName`, the settings row the caller was already
|
|
21
|
+
entitled to, via one `locationOf()` helper called in each of the four functions. That
|
|
22
|
+
is the same source the screen reads from: the security app fills `siteId` and
|
|
23
|
+
`siteName` from one HRM Labs site-list entry (`site.id` / `site.text`),
|
|
24
|
+
`manpowerMonitoringSchema` makes `siteName` required on both write paths, and
|
|
25
|
+
`TManpowerMonitoringUpdate` cannot change it — so for every legitimate row the pair
|
|
26
|
+
already agreed and the server simply stops asking.
|
|
27
|
+
|
|
28
|
+
A row with no stored name is refused rather than falling back to the body; a fallback
|
|
29
|
+
would re-open the hole for exactly the rows an attacker would want to name for
|
|
30
|
+
themselves.
|
|
31
|
+
|
|
32
|
+
Nothing legitimate changes. The Joi schemas still accept `siteName`, so no client needs
|
|
33
|
+
a coordinated release — it is accepted and ignored. The three internal callers
|
|
34
|
+
(`manpower.event.ts` and the two `cron.util.ts` sweeps) get more reliable, not less:
|
|
35
|
+
they were passing `manpower-remarks.siteName`, which that model marks optional.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope invitation creation to an organisation the caller can reach, and stop a
|
|
6
|
+
non-staff caller minting a Seven365 staff invitation on the v1 endpoints.
|
|
7
|
+
|
|
8
|
+
Six endpoints took `metadata.org` — and the `app` that becomes the accepted
|
|
9
|
+
membership's `type` — straight from the request body, behind `requireAuth` and
|
|
10
|
+
nothing else:
|
|
11
|
+
|
|
12
|
+
- `POST /api/auth/invite` (`auth.route.ts:29`)
|
|
13
|
+
- `POST /api/auth/invite/user` (`auth.route.ts:30`)
|
|
14
|
+
- `POST /api/auth/invite/member` (`auth.route.ts:31`)
|
|
15
|
+
- `POST /api/auth/invite/service-provider/with-org` (`auth.route.ts:33`)
|
|
16
|
+
- `POST /api/auth/v2/invite` (`auth-v2.route.ts:22`)
|
|
17
|
+
- `POST /api/auth/v2/invite/organization` (`auth-v2.route.ts:23`)
|
|
18
|
+
|
|
19
|
+
Two things followed from that.
|
|
20
|
+
|
|
21
|
+
**The organisation.** The roles/members fix lets somebody holding an invitation
|
|
22
|
+
into an organisation reach it, because the client onboarding wizard needs that.
|
|
23
|
+
An attacker could therefore issue themselves the invitation the gate asks for —
|
|
24
|
+
one authenticated write naming any victim organisation — and walk straight back
|
|
25
|
+
into the chain that fix closed.
|
|
26
|
+
|
|
27
|
+
**`app`.** "admin" is what makes a membership Seven365 staff, and
|
|
28
|
+
`member.service.ts acceptInvite` writes `type: invite.metadata.app` verbatim.
|
|
29
|
+
`createMemberDirect` already refuses a staff membership from a non-staff caller;
|
|
30
|
+
the invitation road to the same place was open on the v1 mounts. The v2 twin
|
|
31
|
+
asked; the v1 mounts, which are the ones the web apps call, did not.
|
|
32
|
+
|
|
33
|
+
All six now call `requireInviteReach` (`utils/console-authz.util.ts`): Seven365
|
|
34
|
+
staff, a live member of the organisation, or somebody holding an invitation into
|
|
35
|
+
it — plus `requirePlatformStaff` when the invitation names the staff app. An
|
|
36
|
+
invitation that names no organisation is unchanged; it grants no organisation,
|
|
37
|
+
exactly as an org-less `GET /api/roles` does. The two service-provider invites
|
|
38
|
+
that already resolved their caller from the session are unchanged.
|
package/dist/index.js
CHANGED
|
@@ -18337,6 +18337,14 @@ async function requireOrgReach(req, orgId) {
|
|
|
18337
18337
|
return id;
|
|
18338
18338
|
throw new import_node_server_utils41.UnauthorizedError("Not authorized.");
|
|
18339
18339
|
}
|
|
18340
|
+
async function requireInviteReach(req, org, app) {
|
|
18341
|
+
const apps = Array.isArray(app) ? app : [app];
|
|
18342
|
+
if (apps.some((a) => a === PLATFORM_STAFF_MEMBER_TYPE)) {
|
|
18343
|
+
await requirePlatformStaff(req);
|
|
18344
|
+
}
|
|
18345
|
+
if (org)
|
|
18346
|
+
await requireOrgReach(req, org);
|
|
18347
|
+
}
|
|
18340
18348
|
|
|
18341
18349
|
// src/utils/user-response.util.ts
|
|
18342
18350
|
function withoutCredentials(user) {
|
|
@@ -26092,6 +26100,7 @@ function useVerificationController() {
|
|
|
26092
26100
|
const siteId = req.body.siteId ?? "";
|
|
26093
26101
|
const siteName = req.body.siteName ?? "";
|
|
26094
26102
|
try {
|
|
26103
|
+
await requireInviteReach(req, org, app);
|
|
26095
26104
|
await _createUserInvite({
|
|
26096
26105
|
email,
|
|
26097
26106
|
metadata: {
|
|
@@ -26145,6 +26154,7 @@ function useVerificationController() {
|
|
|
26145
26154
|
siteId,
|
|
26146
26155
|
siteName
|
|
26147
26156
|
} = value;
|
|
26157
|
+
await requireInviteReach(req, org, app);
|
|
26148
26158
|
const verificationIds = await _createSimpleUserInvite({
|
|
26149
26159
|
email,
|
|
26150
26160
|
metadata: {
|
|
@@ -26208,6 +26218,7 @@ function useVerificationController() {
|
|
|
26208
26218
|
const siteId = req.body.siteId ?? "";
|
|
26209
26219
|
const siteName = req.body.siteName ?? "";
|
|
26210
26220
|
try {
|
|
26221
|
+
await requireInviteReach(req, org, app);
|
|
26211
26222
|
await _createSimpleMemberInvite({
|
|
26212
26223
|
email,
|
|
26213
26224
|
metadata: {
|
|
@@ -26332,6 +26343,7 @@ function useVerificationController() {
|
|
|
26332
26343
|
const siteId = req.body.siteId ?? "";
|
|
26333
26344
|
const siteName = req.body.siteName ?? "";
|
|
26334
26345
|
try {
|
|
26346
|
+
await requireInviteReach(req, orgId, app);
|
|
26335
26347
|
await _createSimpleServiceProviderInvite({
|
|
26336
26348
|
email,
|
|
26337
26349
|
app,
|
|
@@ -78077,6 +78089,15 @@ var {
|
|
|
78077
78089
|
totalCountPerStatus: totalCountPerStatus2,
|
|
78078
78090
|
chartCountData: chartCountData2
|
|
78079
78091
|
} = hrmlabs_attendance_util_default;
|
|
78092
|
+
function locationOf(config2) {
|
|
78093
|
+
const name = typeof config2?.siteName === "string" ? config2.siteName.trim() : "";
|
|
78094
|
+
if (!name) {
|
|
78095
|
+
throw new Error(
|
|
78096
|
+
"Manpower monitoring settings for this site have no HRM Labs location name."
|
|
78097
|
+
);
|
|
78098
|
+
}
|
|
78099
|
+
return name;
|
|
78100
|
+
}
|
|
78080
78101
|
function useHrmLabsAttendanceSrvc() {
|
|
78081
78102
|
const { getManpowerSettingsBySiteId: _getManpowerSettingsBySiteId } = useManpowerMonitoringRepo();
|
|
78082
78103
|
const { getManpowerDesignationsBySiteId: _getManpowerDesignationsBySiteId } = useManpowerDesignationRepo();
|
|
@@ -78084,7 +78105,6 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78084
78105
|
const {
|
|
78085
78106
|
startDate,
|
|
78086
78107
|
endDate,
|
|
78087
|
-
siteName,
|
|
78088
78108
|
siteId,
|
|
78089
78109
|
shiftName,
|
|
78090
78110
|
search,
|
|
@@ -78114,6 +78134,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78114
78134
|
message: "Manpower monitoring settings not found for the provided site ID."
|
|
78115
78135
|
};
|
|
78116
78136
|
}
|
|
78137
|
+
const siteName = locationOf(config2);
|
|
78117
78138
|
const shiftType = config2.shiftType === "2-shifts" ? "2-shifts" : "3-shifts";
|
|
78118
78139
|
const shiftData = config2.shifts[shiftType].find(
|
|
78119
78140
|
(s) => s.name === shiftName
|
|
@@ -78183,7 +78204,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78183
78204
|
}
|
|
78184
78205
|
}
|
|
78185
78206
|
async function getAttendanceDataCount(payload) {
|
|
78186
|
-
const { startDate, endDate,
|
|
78207
|
+
const { startDate, endDate, siteId, search, serviceProviderId } = payload;
|
|
78187
78208
|
const username = process.env.HRMLABS_USERNAME;
|
|
78188
78209
|
const domain = process.env.HRMLABS_DOMAIN;
|
|
78189
78210
|
const password = process.env.HRMLABS_PASSWORD;
|
|
@@ -78206,6 +78227,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78206
78227
|
message: "Manpower monitoring settings not found for the provided site ID."
|
|
78207
78228
|
};
|
|
78208
78229
|
}
|
|
78230
|
+
const siteName = locationOf(config2);
|
|
78209
78231
|
const shiftType = config2.shiftType;
|
|
78210
78232
|
const shiftData = config2.shifts[shiftType];
|
|
78211
78233
|
if (!shiftData) {
|
|
@@ -78296,7 +78318,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78296
78318
|
}
|
|
78297
78319
|
}
|
|
78298
78320
|
async function getAllAttendance(payload) {
|
|
78299
|
-
const { startDate, endDate,
|
|
78321
|
+
const { startDate, endDate, siteId, search, serviceProviderId } = payload;
|
|
78300
78322
|
let { shiftName } = payload;
|
|
78301
78323
|
const username = process.env.HRMLABS_USERNAME;
|
|
78302
78324
|
const domain = process.env.HRMLABS_DOMAIN;
|
|
@@ -78319,6 +78341,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78319
78341
|
message: "Manpower monitoring settings not found for the provided site ID."
|
|
78320
78342
|
};
|
|
78321
78343
|
}
|
|
78344
|
+
const siteName = locationOf(config2);
|
|
78322
78345
|
const designationConfig = await _getManpowerDesignationsBySiteId(
|
|
78323
78346
|
siteId,
|
|
78324
78347
|
serviceProviderId
|
|
@@ -78470,7 +78493,6 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78470
78493
|
const {
|
|
78471
78494
|
startDate,
|
|
78472
78495
|
endDate,
|
|
78473
|
-
siteName,
|
|
78474
78496
|
siteId,
|
|
78475
78497
|
search,
|
|
78476
78498
|
serviceProviderId,
|
|
@@ -78498,6 +78520,7 @@ function useHrmLabsAttendanceSrvc() {
|
|
|
78498
78520
|
message: "Manpower monitoring settings not found for the provided site ID."
|
|
78499
78521
|
};
|
|
78500
78522
|
}
|
|
78523
|
+
const siteName = locationOf(config2);
|
|
78501
78524
|
const designationConfig = await _getManpowerDesignationsBySiteId(
|
|
78502
78525
|
siteId,
|
|
78503
78526
|
serviceProviderId
|
|
@@ -81041,9 +81064,7 @@ function useVerificationControllerV2() {
|
|
|
81041
81064
|
}
|
|
81042
81065
|
const { email, app, role, name, org, siteId, siteName } = value;
|
|
81043
81066
|
try {
|
|
81044
|
-
|
|
81045
|
-
await requirePlatformStaff(req);
|
|
81046
|
-
}
|
|
81067
|
+
await requireInviteReach(req, org, app);
|
|
81047
81068
|
await _createUserInvite({
|
|
81048
81069
|
email,
|
|
81049
81070
|
metadata: {
|
|
@@ -81115,6 +81136,7 @@ function useVerificationControllerV2() {
|
|
|
81115
81136
|
}
|
|
81116
81137
|
try {
|
|
81117
81138
|
const { email, app, role, name, org, siteId, siteName } = value;
|
|
81139
|
+
await requireInviteReach(req, org, app);
|
|
81118
81140
|
await _createOrganizationInvite({
|
|
81119
81141
|
email,
|
|
81120
81142
|
metadata: {
|