@cedarjs/cli 8.0.0-canary.3538 → 8.0.0-canary.3540
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/dist/commands/generate/realtime/realtimeHandler.js +13 -5
- package/dist/commands/generate/sdl/sdlHandler.js +10 -2
- package/dist/commands/setup/realtime/realtimeHandler.js +43 -18
- package/dist/commands/setup/tenancy/authCodemod.js +181 -16
- package/dist/commands/setup/tenancy/authDirectives.js +101 -0
- package/dist/commands/setup/tenancy/permissionsPackage.js +240 -0
- package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -1
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +8 -6
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +24 -28
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +6 -6
- package/dist/commands/setup/tenancy/templates/permissions.ts.template +20 -0
- package/dist/commands/setup/tenancy/templates/permissionsPackage.index.ts.template +28 -0
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.test.ts.template +56 -0
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.ts.template +25 -0
- package/dist/commands/setup/tenancy/templates/requirePermission.test.ts.template +50 -0
- package/dist/commands/setup/tenancy/templates/requirePermission.ts.template +32 -0
- package/dist/commands/setup/tenancy/templates/requireUserAuth.test.ts.template +26 -0
- package/dist/commands/setup/tenancy/templates/requireUserAuth.ts.template +26 -0
- package/dist/commands/setup/tenancy/tenancyHandler.js +127 -13
- package/dist/commands/setup/tenancy/webHasRole.js +45 -0
- package/dist/commands/setup/uploads/uploadsHandler.js +4 -0
- package/dist/lib/authDirective.js +37 -0
- package/package.json +13 -13
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +0 -35
- package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +0 -25
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
|
|
2
|
+
|
|
3
|
+
import requirePermission from './requirePermission.js'
|
|
4
|
+
|
|
5
|
+
const currentOrg = (role: string) => ({
|
|
6
|
+
id: 'org-1',
|
|
7
|
+
slug: 'org-1',
|
|
8
|
+
role,
|
|
9
|
+
membershipId: 'membership-1',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('requirePermission directive', () => {
|
|
13
|
+
it('declares the directive sdl as schema, with the correct name', () => {
|
|
14
|
+
expect(requirePermission.schema).toBeTruthy()
|
|
15
|
+
expect(getDirectiveName(requirePermission.schema)).toBe('requirePermission')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('throws when there is no current user', () => {
|
|
19
|
+
const mockExecution = mockRedwoodDirective(requirePermission, {
|
|
20
|
+
context: {},
|
|
21
|
+
directiveArgs: { permission: 'organization.read' },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(mockExecution).toThrowError()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("throws when the role in the current organization doesn't have it", () => {
|
|
28
|
+
const mockExecution = mockRedwoodDirective(requirePermission, {
|
|
29
|
+
context: {
|
|
30
|
+
currentUser: { id: 'user-1' },
|
|
31
|
+
currentOrg: currentOrg('member'),
|
|
32
|
+
},
|
|
33
|
+
directiveArgs: { permission: 'members.manage' },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(mockExecution).toThrowError()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('passes when the role in the current organization has it', () => {
|
|
40
|
+
const mockExecution = mockRedwoodDirective(requirePermission, {
|
|
41
|
+
context: {
|
|
42
|
+
currentUser: { id: 'user-1' },
|
|
43
|
+
currentOrg: currentOrg('owner'),
|
|
44
|
+
},
|
|
45
|
+
directiveArgs: { permission: 'members.manage' },
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(mockExecution).not.toThrowError()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Permission } from '__PERMISSIONS_PACKAGE__'
|
|
2
|
+
import { gql } from 'graphql-tag'
|
|
3
|
+
|
|
4
|
+
import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
|
|
5
|
+
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
6
|
+
|
|
7
|
+
import { requirePermission as applicationRequirePermission } from 'src/lib/permissions'
|
|
8
|
+
|
|
9
|
+
export const schema = gql`
|
|
10
|
+
"""
|
|
11
|
+
Use to check that the signed-in user may do something: that their role in
|
|
12
|
+
the current organization has the permission (see packages/permissions).
|
|
13
|
+
"""
|
|
14
|
+
directive @requirePermission(permission: String!) on FIELD_DEFINITION
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
// Optional to fit `ValidatorDirectiveFunc`; the SDL makes it required
|
|
18
|
+
type RequirePermissionValidate = ValidatorDirectiveFunc<{
|
|
19
|
+
permission?: string
|
|
20
|
+
}>
|
|
21
|
+
|
|
22
|
+
const validate: RequirePermissionValidate = ({ directiveArgs }) => {
|
|
23
|
+
// A name the permissions map doesn't know throws, so a typo in the SDL is
|
|
24
|
+
// an error rather than a field nobody can use
|
|
25
|
+
applicationRequirePermission({
|
|
26
|
+
permission: directiveArgs.permission as Permission,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const requirePermission = createValidatorDirective(schema, validate)
|
|
31
|
+
|
|
32
|
+
export default requirePermission
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
|
|
2
|
+
|
|
3
|
+
import requireUserAuth from './requireUserAuth.js'
|
|
4
|
+
|
|
5
|
+
describe('requireUserAuth directive', () => {
|
|
6
|
+
it('declares the directive sdl as schema, with the correct name', () => {
|
|
7
|
+
expect(requireUserAuth.schema).toBeTruthy()
|
|
8
|
+
expect(getDirectiveName(requireUserAuth.schema)).toBe('requireUserAuth')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('throws when there is no current user', () => {
|
|
12
|
+
const mockExecution = mockRedwoodDirective(requireUserAuth, {
|
|
13
|
+
context: {},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(mockExecution).toThrowError("You don't have permission to do that.")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('does not throw for a signed-in user, with no organization', () => {
|
|
20
|
+
const mockExecution = mockRedwoodDirective(requireUserAuth, {
|
|
21
|
+
context: { currentUser: { id: 'user-1' } },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(mockExecution).not.toThrowError()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { gql } from 'graphql-tag'
|
|
2
|
+
|
|
3
|
+
import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
|
|
4
|
+
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
5
|
+
|
|
6
|
+
import { requireUserAuth as applicationRequireUserAuth } from 'src/lib/auth'
|
|
7
|
+
|
|
8
|
+
export const schema = gql`
|
|
9
|
+
"""
|
|
10
|
+
Use to check whether or not a user is authenticated and is associated
|
|
11
|
+
with an optional set of roles, in any organization or none. For fields
|
|
12
|
+
about the user rather than an organization.
|
|
13
|
+
"""
|
|
14
|
+
directive @requireUserAuth(roles: [String]) on FIELD_DEFINITION
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
type RequireUserAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
|
18
|
+
|
|
19
|
+
const validate: RequireUserAuthValidate = ({ directiveArgs }) => {
|
|
20
|
+
const { roles } = directiveArgs
|
|
21
|
+
applicationRequireUserAuth({ roles })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const requireUserAuth = createValidatorDirective(schema, validate)
|
|
25
|
+
|
|
26
|
+
export default requireUserAuth
|
|
@@ -20,6 +20,16 @@ import { errorTelemetry } from "@cedarjs/telemetry";
|
|
|
20
20
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
21
21
|
import { isTypeScriptProject } from "../../../lib/project.js";
|
|
22
22
|
import { runTransform } from "../../../lib/runTransform.js";
|
|
23
|
+
import { TENANCY_AUTH_EXPORT } from "./authCodemod.js";
|
|
24
|
+
import {
|
|
25
|
+
isDefaultRequireAuthDirective,
|
|
26
|
+
renameRequireAuthDirective,
|
|
27
|
+
renameLibAuthImports
|
|
28
|
+
} from "./authDirectives.js";
|
|
29
|
+
import {
|
|
30
|
+
addPermissionsPackageTasks,
|
|
31
|
+
permissionsPackageName
|
|
32
|
+
} from "./permissionsPackage.js";
|
|
23
33
|
import {
|
|
24
34
|
authNotSetUpMessage,
|
|
25
35
|
hasWebAuthFile,
|
|
@@ -33,10 +43,15 @@ import {
|
|
|
33
43
|
ORGANIZATION_MODEL,
|
|
34
44
|
requiredUserFields
|
|
35
45
|
} from "./schemaPrisma.js";
|
|
46
|
+
import { renameUseAuthHasRole } from "./webHasRole.js";
|
|
36
47
|
const DEFAULT_TENANT_FIELD = "organizationId";
|
|
37
48
|
const AUTH_SNIPPET = `
|
|
49
|
+
import { createTenancyAuth } from '@cedarjs/tenancy'
|
|
50
|
+
|
|
51
|
+
import { withPermissions } from 'src/lib/permissions'
|
|
52
|
+
|
|
38
53
|
export const getCurrentUser = async (session) => {
|
|
39
|
-
return await db.user.findUnique({
|
|
54
|
+
return withPermissions(await db.user.findUnique({
|
|
40
55
|
where: { id: session.id },
|
|
41
56
|
select: {
|
|
42
57
|
// ...your existing fields...
|
|
@@ -49,11 +64,11 @@ const AUTH_SNIPPET = `
|
|
|
49
64
|
},
|
|
50
65
|
},
|
|
51
66
|
},
|
|
52
|
-
})
|
|
67
|
+
}))
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
`;
|
|
70
|
+
// In place of your hasRole and requireAuth
|
|
71
|
+
${TENANCY_AUTH_EXPORT}`;
|
|
57
72
|
const SIGNUP_SNIPPET = `
|
|
58
73
|
handler: async ({ username, hashedPassword, salt, userAttributes }) => {
|
|
59
74
|
const user = await db.user.create({
|
|
@@ -202,7 +217,7 @@ Re-run with --force to have this command append its own versions beside them ins
|
|
|
202
217
|
}
|
|
203
218
|
},
|
|
204
219
|
{
|
|
205
|
-
title: "Adding memberships to getCurrentUser and
|
|
220
|
+
title: "Adding memberships to getCurrentUser and the tenancy auth checks to api/src/lib/auth...",
|
|
206
221
|
task: async (_ctx, task) => {
|
|
207
222
|
if (!fs.existsSync(authPath)) {
|
|
208
223
|
task.skip("No api/src/lib/auth file found; skipping.");
|
|
@@ -212,7 +227,10 @@ Re-run with --force to have this command append its own versions beside them ins
|
|
|
212
227
|
transformPath: path.join(import.meta.dirname, "authCodemod.js"),
|
|
213
228
|
targetPaths: [authPath]
|
|
214
229
|
});
|
|
215
|
-
if (transformResult.error) {
|
|
230
|
+
if (transformResult.error?.includes("AUTH_CHECKS_CHANGED")) {
|
|
231
|
+
task.output = `api/src/lib/auth's requireAuth or hasRole has checks of its own, so it was left as it is. Please add this by hand, keeping your checks:
|
|
232
|
+
${AUTH_SNIPPET}`;
|
|
233
|
+
} else if (transformResult.error) {
|
|
216
234
|
task.output = `Could not update api/src/lib/auth automatically. Please add this by hand:
|
|
217
235
|
${AUTH_SNIPPET}`;
|
|
218
236
|
}
|
|
@@ -253,19 +271,94 @@ ${SIGNUP_SNIPPET}`;
|
|
|
253
271
|
skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
|
|
254
272
|
},
|
|
255
273
|
{
|
|
256
|
-
title: "
|
|
274
|
+
title: "Switching @requireAuth to @requireOrgAuth and @requireUserAuth, and hasRole to hasUserRole...",
|
|
275
|
+
task: async (ctx) => {
|
|
276
|
+
const oldDirectiveDir = path.join(paths.api.directives, "requireAuth");
|
|
277
|
+
const isInOldDirective = (file) => file.startsWith(oldDirectiveDir + path.sep);
|
|
278
|
+
const oldDirectivePath = path.join(
|
|
279
|
+
oldDirectiveDir,
|
|
280
|
+
`requireAuth.${ext}`
|
|
281
|
+
);
|
|
282
|
+
const keepOldDirective = fs.existsSync(oldDirectivePath) && !isDefaultRequireAuthDirective(
|
|
283
|
+
fs.readFileSync(oldDirectivePath, "utf-8")
|
|
284
|
+
);
|
|
285
|
+
const sourceFilesIn = (dir) => fs.readdirSync(dir, { recursive: true, withFileTypes: true }).filter(
|
|
286
|
+
(entry) => entry.isFile() && /\.[jt]sx?$/.test(entry.name)
|
|
287
|
+
).map((entry) => path.join(entry.parentPath, entry.name));
|
|
288
|
+
for (const file of sourceFilesIn(paths.api.src)) {
|
|
289
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
290
|
+
const renamed = renameLibAuthImports(
|
|
291
|
+
keepOldDirective || isInOldDirective(file) ? source : renameRequireAuthDirective(source)
|
|
292
|
+
);
|
|
293
|
+
if (renamed !== source) {
|
|
294
|
+
fs.writeFileSync(file, renamed);
|
|
295
|
+
writtenFiles.push(file);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const file of sourceFilesIn(paths.web.src)) {
|
|
299
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
300
|
+
const renamed = renameUseAuthHasRole(source);
|
|
301
|
+
if (renamed !== source) {
|
|
302
|
+
fs.writeFileSync(file, renamed);
|
|
303
|
+
writtenFiles.push(file);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (keepOldDirective) {
|
|
307
|
+
ctx.keptRequireAuthDirective = true;
|
|
308
|
+
} else if (fs.existsSync(oldDirectivePath)) {
|
|
309
|
+
fs.rmSync(oldDirectiveDir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
for (const name of ["requireOrgAuth", "requireUserAuth"]) {
|
|
312
|
+
const directiveDir = path.join(paths.api.directives, name);
|
|
313
|
+
await writeTemplateFile(
|
|
314
|
+
`${name}.ts.template`,
|
|
315
|
+
path.join(directiveDir, `${name}.${ext}`)
|
|
316
|
+
);
|
|
317
|
+
await writeTemplateFile(
|
|
318
|
+
`${name}.test.ts.template`,
|
|
319
|
+
path.join(directiveDir, `${name}.test.${ext}`)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
title: "Adding the permissions package...",
|
|
326
|
+
task: () => addPermissionsPackageTasks({
|
|
327
|
+
cedarVersion,
|
|
328
|
+
indexSource: fs.readFileSync(
|
|
329
|
+
path.resolve(
|
|
330
|
+
import.meta.dirname,
|
|
331
|
+
"templates",
|
|
332
|
+
"permissionsPackage.index.ts.template"
|
|
333
|
+
),
|
|
334
|
+
"utf-8"
|
|
335
|
+
),
|
|
336
|
+
force
|
|
337
|
+
})
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
title: "Adding permission checks and the requirePermission directive...",
|
|
257
341
|
task: async () => {
|
|
342
|
+
const replacements = {
|
|
343
|
+
__PERMISSIONS_PACKAGE__: permissionsPackageName()
|
|
344
|
+
};
|
|
345
|
+
await writeTemplateFile(
|
|
346
|
+
"permissions.ts.template",
|
|
347
|
+
path.join(paths.api.lib, `permissions.${ext}`),
|
|
348
|
+
replacements
|
|
349
|
+
);
|
|
258
350
|
const directiveDir = path.join(
|
|
259
351
|
paths.api.directives,
|
|
260
|
-
"
|
|
352
|
+
"requirePermission"
|
|
261
353
|
);
|
|
262
354
|
await writeTemplateFile(
|
|
263
|
-
"
|
|
264
|
-
path.join(directiveDir, `
|
|
355
|
+
"requirePermission.ts.template",
|
|
356
|
+
path.join(directiveDir, `requirePermission.${ext}`),
|
|
357
|
+
replacements
|
|
265
358
|
);
|
|
266
359
|
await writeTemplateFile(
|
|
267
|
-
"
|
|
268
|
-
path.join(directiveDir, `
|
|
360
|
+
"requirePermission.test.ts.template",
|
|
361
|
+
path.join(directiveDir, `requirePermission.test.${ext}`)
|
|
269
362
|
);
|
|
270
363
|
}
|
|
271
364
|
},
|
|
@@ -424,7 +517,8 @@ ${SIGNUP_SNIPPET}`;
|
|
|
424
517
|
User/Organization/Membership. For each existing model (e.g.
|
|
425
518
|
Contact) whose rows belong to one organization, add
|
|
426
519
|
${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
|
|
427
|
-
schema.prisma
|
|
520
|
+
schema.prisma, and switch its fields from ${c.highlight("@requireUserAuth")}
|
|
521
|
+
to ${c.highlight("@requireOrgAuth")}. For each one that stays global, add it
|
|
428
522
|
to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
|
|
429
523
|
3. Wrap your organization routes with the generated OrgScope in
|
|
430
524
|
web/src/Routes.tsx:
|
|
@@ -441,6 +535,26 @@ ${SIGNUP_SNIPPET}`;
|
|
|
441
535
|
tenant-owned models with ${c.highlight("withTenancy")} from ${c.highlight(
|
|
442
536
|
"@cedarjs/tenancy"
|
|
443
537
|
)}.
|
|
538
|
+
5. Your permissions, and what each role may do, are in
|
|
539
|
+
${c.highlight("packages/permissions")}, shared by the api and web sides${ctx.packagesWorkspaceTurnedOn ? ` (this
|
|
540
|
+
turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}. Guard
|
|
541
|
+
organization fields with ${c.highlight('@requirePermission(permission: "...")')}
|
|
542
|
+
(or ${c.highlight("@requireOrgAuth(roles: [...])")}), and check permissions on the
|
|
543
|
+
web with ${c.highlight("useAuth().hasPermission()")}.
|
|
544
|
+
6. ${c.highlight("@requireAuth")} is gone. Your fields use ${c.highlight("@requireUserAuth")} now,
|
|
545
|
+
which checks what ${c.highlight("@requireAuth")} did: there were no organizations
|
|
546
|
+
before. ${c.highlight("@requireOrgAuth")} needs a current organization, and checks
|
|
547
|
+
the user's role in it.${ctx.keptRequireAuthDirective ? `
|
|
548
|
+
api/src/directives/requireAuth was changed from Cedar's, so
|
|
549
|
+
it and your @requireAuth fields were left as they are: move
|
|
550
|
+
its checks to requireUserAuth or requireOrgAuth, switch the
|
|
551
|
+
fields over, then delete it.` : ""}
|
|
552
|
+
6. Every role check names its kind now. ${c.highlight("hasRole")} is
|
|
553
|
+
${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
|
|
554
|
+
checking ${c.highlight("currentUser.roles")}; ${c.highlight("hasOrgRole")} checks the role in
|
|
555
|
+
the organization. ${c.highlight("<PrivateSet roles>")} isn't available with
|
|
556
|
+
tenancy: guard routes by role with ${c.highlight("OrgScope")}'s ${c.highlight("roles")} and
|
|
557
|
+
${c.highlight("unauthorized")}, from a small wrapper component.
|
|
444
558
|
|
|
445
559
|
${keptFiles.length > 0 ? `
|
|
446
560
|
Kept these existing files rather than overwriting them (run with
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const HAS_ROLE = /(?<![\w$])hasRole(?![\w$])/;
|
|
2
|
+
const USE_AUTH_DESTRUCTURING = /\{([^{}]*)\}(\s*=\s*useAuth\(\))/g;
|
|
3
|
+
const USE_AUTH_VARIABLE = /(?:const|let|var)\s+([\w$]+)\s*=\s*useAuth\(\)/g;
|
|
4
|
+
const HAS_ROLE_ON_VARIABLE = /(?<![\w$.])([\w$]+)(\s*\??\.\s*)hasRole(?![\w$])/g;
|
|
5
|
+
function renameUseAuthHasRole(source) {
|
|
6
|
+
if (!source.includes("useAuth()") || !HAS_ROLE.test(source)) {
|
|
7
|
+
return source;
|
|
8
|
+
}
|
|
9
|
+
let renameUses = false;
|
|
10
|
+
let result = source.replace(
|
|
11
|
+
/(useAuth\(\)\s*\??\.\s*)hasRole(?![\w$])/g,
|
|
12
|
+
"$1hasUserRole"
|
|
13
|
+
);
|
|
14
|
+
result = result.replace(
|
|
15
|
+
USE_AUTH_DESTRUCTURING,
|
|
16
|
+
(_statement, specifiers, rest) => {
|
|
17
|
+
const renamed = specifiers.replace(
|
|
18
|
+
/(?<![\w$])hasRole(?![\w$])(\s*:)?/g,
|
|
19
|
+
(_specifier, alias) => {
|
|
20
|
+
if (!alias) {
|
|
21
|
+
renameUses = true;
|
|
22
|
+
}
|
|
23
|
+
return `hasUserRole${alias ?? ""}`;
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
return `{${renamed}}${rest}`;
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
if (renameUses) {
|
|
30
|
+
result = result.replace(/(?<![\w$.])hasRole(?![\w$])/g, "hasUserRole");
|
|
31
|
+
}
|
|
32
|
+
const authVariables = new Set(
|
|
33
|
+
Array.from(source.matchAll(USE_AUTH_VARIABLE), ([, name]) => name)
|
|
34
|
+
);
|
|
35
|
+
if (authVariables.size === 0) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
return result.replace(
|
|
39
|
+
HAS_ROLE_ON_VARIABLE,
|
|
40
|
+
(match, name, access) => authVariables.has(name) ? `${name}${access}hasUserRole` : match
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
renameUseAuthHasRole
|
|
45
|
+
};
|
|
@@ -14,6 +14,7 @@ import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display"
|
|
|
14
14
|
import { addEnvVar } from "@cedarjs/cli-helpers/project";
|
|
15
15
|
import { getSchemaPath } from "@cedarjs/project-config";
|
|
16
16
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
17
|
+
import { withAuthDirective } from "../../../lib/authDirective.js";
|
|
17
18
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
18
19
|
import { isTypeScriptProject } from "../../../lib/project.js";
|
|
19
20
|
import { generateSecret } from "../../generate/secret/secret.js";
|
|
@@ -112,6 +113,9 @@ const handler = async ({ targets, force }) => {
|
|
|
112
113
|
for (const [token, value] of Object.entries(replacements)) {
|
|
113
114
|
templateContent = templateContent.replaceAll(token, value);
|
|
114
115
|
}
|
|
116
|
+
templateContent = withAuthDirective(templateContent, {
|
|
117
|
+
belongsToOrg: false
|
|
118
|
+
});
|
|
115
119
|
const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
|
|
116
120
|
return writeFile(outputPath, content, { overwriteExisting: force });
|
|
117
121
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getPaths } from "./index.js";
|
|
4
|
+
function hasTenancyAuthDirectives() {
|
|
5
|
+
return fs.existsSync(path.join(getPaths().api.directives, "requireOrgAuth"));
|
|
6
|
+
}
|
|
7
|
+
function tenantField() {
|
|
8
|
+
for (const ext of ["ts", "js"]) {
|
|
9
|
+
const dbPath = path.join(getPaths().api.lib, `db.${ext}`);
|
|
10
|
+
if (fs.existsSync(dbPath)) {
|
|
11
|
+
const match = fs.readFileSync(dbPath, "utf-8").match(/\btenantField:\s*['"](\w+)['"]/);
|
|
12
|
+
return match?.[1] ?? "organizationId";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return "organizationId";
|
|
16
|
+
}
|
|
17
|
+
function belongsToOrganization(model) {
|
|
18
|
+
const field = tenantField();
|
|
19
|
+
return model.fields.some(
|
|
20
|
+
({ name, type }) => name === field || type === "Organization"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function withAuthDirective(source, { belongsToOrg }) {
|
|
24
|
+
if (!hasTenancyAuthDirectives()) {
|
|
25
|
+
return source;
|
|
26
|
+
}
|
|
27
|
+
return source.replace(
|
|
28
|
+
/@requireAuth(?![\w$])/g,
|
|
29
|
+
belongsToOrg ? "@requireOrgAuth" : "@requireUserAuth"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
belongsToOrganization,
|
|
34
|
+
hasTenancyAuthDirectives,
|
|
35
|
+
tenantField,
|
|
36
|
+
withAuthDirective
|
|
37
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3540",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.10",
|
|
38
38
|
"@babel/types": "7.29.11",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3540",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3540",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3540",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3540",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3540",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3540",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3540",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3540",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3540",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3540",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3540",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.4.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.0",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"yargs": "18.2.0"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
96
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3540",
|
|
97
97
|
"@prisma/dmmf": "7.10.1",
|
|
98
98
|
"@types/archiver": "^7.0.0",
|
|
99
99
|
"memfs": "4.80.0",
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
|
|
2
|
-
|
|
3
|
-
import requireMembership from './requireMembership.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `context.currentUser` is typed from this app's `getCurrentUser`, which
|
|
7
|
-
* selects more fields than these tests need.
|
|
8
|
-
*/
|
|
9
|
-
const asCurrentUser = (user: { id: string; memberships?: unknown[] }) =>
|
|
10
|
-
user as unknown as NonNullable<typeof context.currentUser>
|
|
11
|
-
|
|
12
|
-
describe('requireMembership directive', () => {
|
|
13
|
-
it('declares the directive sdl as schema, with the correct name', () => {
|
|
14
|
-
expect(requireMembership.schema).toBeTruthy()
|
|
15
|
-
expect(getDirectiveName(requireMembership.schema)).toBe(
|
|
16
|
-
'requireMembership',
|
|
17
|
-
)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('throws when there is no current user', () => {
|
|
21
|
-
const mockExecution = mockRedwoodDirective(requireMembership, {
|
|
22
|
-
context: {},
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(mockExecution).toThrowError()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('throws when there is a current user but no current organization', () => {
|
|
29
|
-
const mockExecution = mockRedwoodDirective(requireMembership, {
|
|
30
|
-
context: { currentUser: asCurrentUser({ id: '1' }) },
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
expect(mockExecution).toThrowError()
|
|
34
|
-
})
|
|
35
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { gql } from 'graphql-tag'
|
|
2
|
-
|
|
3
|
-
import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
|
|
4
|
-
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
5
|
-
|
|
6
|
-
import { requireMembership as applicationRequireMembership } from 'src/lib/auth'
|
|
7
|
-
|
|
8
|
-
export const schema = gql`
|
|
9
|
-
"""
|
|
10
|
-
Use to check whether or not the current user is a member of the current
|
|
11
|
-
organization, optionally restricted to a set of roles.
|
|
12
|
-
"""
|
|
13
|
-
directive @requireMembership(roles: [String]) on FIELD_DEFINITION
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
type RequireMembershipValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
|
17
|
-
|
|
18
|
-
const validate: RequireMembershipValidate = ({ directiveArgs }) => {
|
|
19
|
-
const { roles } = directiveArgs
|
|
20
|
-
applicationRequireMembership({ roles })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const requireMembership = createValidatorDirective(schema, validate)
|
|
24
|
-
|
|
25
|
-
export default requireMembership
|