@cedarjs/cli 8.0.0-canary.3565 → 8.0.0-canary.3568
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/setup/realtime/realtimeHandler.js +18 -43
- package/dist/commands/setup/tenancy/authCodemod.js +1 -129
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +5 -6
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +13 -0
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +18 -41
- package/dist/commands/setup/tenancy/templates/permissions.ts.template +4 -2
- package/dist/commands/setup/tenancy/templates/permissionsPackage.index.ts.template +13 -7
- package/dist/commands/setup/tenancy/templates/requirePermission.ts.template +1 -3
- package/dist/commands/setup/tenancy/templates/tenancy.tsx.template +2 -6
- package/dist/commands/setup/tenancy/tenancyHandler.js +23 -94
- package/dist/commands/setup/uploads/uploadsHandler.js +0 -4
- package/dist/lib/authDirective.js +15 -5
- package/package.json +13 -13
- package/dist/commands/setup/tenancy/authDirectives.js +0 -101
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.test.ts.template +0 -56
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.ts.template +0 -25
- package/dist/commands/setup/tenancy/templates/requireUserAuth.test.ts.template +0 -26
- package/dist/commands/setup/tenancy/templates/requireUserAuth.ts.template +0 -26
- package/dist/commands/setup/tenancy/webHasRole.js +0 -45
|
@@ -7,7 +7,6 @@ import { addApiPackages } from "@cedarjs/cli-helpers/installHelpers";
|
|
|
7
7
|
import { runBinSync } from "@cedarjs/cli-helpers/packageManager/exec";
|
|
8
8
|
import { generate as generateTypes } from "@cedarjs/internal/dist/generate/generate";
|
|
9
9
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
10
|
-
import { withAuthDirective } from "../../../lib/authDirective.js";
|
|
11
10
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
12
11
|
import { isTypeScriptProject, serverFileExists } from "../../../lib/project.js";
|
|
13
12
|
import { setupServerFileTasks } from "../server-file/serverFileHandler.js";
|
|
@@ -98,13 +97,9 @@ async function handler(args) {
|
|
|
98
97
|
exampleSubscriptionTemplateContent
|
|
99
98
|
);
|
|
100
99
|
return [
|
|
101
|
-
writeFile(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
overwriteExisting: force
|
|
106
|
-
}
|
|
107
|
-
)
|
|
100
|
+
writeFile(exampleFile, setupScriptContent, {
|
|
101
|
+
overwriteExisting: force
|
|
102
|
+
})
|
|
108
103
|
];
|
|
109
104
|
}
|
|
110
105
|
},
|
|
@@ -163,23 +158,15 @@ async function handler(args) {
|
|
|
163
158
|
exampleSubscriptionTemplateContent
|
|
164
159
|
);
|
|
165
160
|
return [
|
|
166
|
-
writeFile(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
overwriteExisting: force
|
|
171
|
-
}
|
|
172
|
-
),
|
|
161
|
+
writeFile(sdlFile, sdlContent, {
|
|
162
|
+
overwriteExisting: force
|
|
163
|
+
}),
|
|
173
164
|
writeFile(serviceFile, serviceContent, {
|
|
174
165
|
overwriteExisting: force
|
|
175
166
|
}),
|
|
176
|
-
writeFile(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
overwriteExisting: force
|
|
181
|
-
}
|
|
182
|
-
)
|
|
167
|
+
writeFile(exampleFile, setupScriptContent, {
|
|
168
|
+
overwriteExisting: force
|
|
169
|
+
})
|
|
183
170
|
];
|
|
184
171
|
}
|
|
185
172
|
},
|
|
@@ -219,13 +206,9 @@ async function handler(args) {
|
|
|
219
206
|
);
|
|
220
207
|
const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
|
|
221
208
|
return [
|
|
222
|
-
writeFile(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
overwriteExisting: force
|
|
227
|
-
}
|
|
228
|
-
),
|
|
209
|
+
writeFile(sdlFile, sdlContent, {
|
|
210
|
+
overwriteExisting: force
|
|
211
|
+
}),
|
|
229
212
|
writeFile(serviceFile, serviceContent, {
|
|
230
213
|
overwriteExisting: force
|
|
231
214
|
})
|
|
@@ -268,13 +251,9 @@ async function handler(args) {
|
|
|
268
251
|
);
|
|
269
252
|
const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
|
|
270
253
|
return [
|
|
271
|
-
writeFile(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
overwriteExisting: force
|
|
276
|
-
}
|
|
277
|
-
),
|
|
254
|
+
writeFile(sdlFile, sdlContent, {
|
|
255
|
+
overwriteExisting: force
|
|
256
|
+
}),
|
|
278
257
|
writeFile(serviceFile, serviceContent, {
|
|
279
258
|
overwriteExisting: force
|
|
280
259
|
})
|
|
@@ -317,13 +296,9 @@ async function handler(args) {
|
|
|
317
296
|
);
|
|
318
297
|
const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
|
|
319
298
|
return [
|
|
320
|
-
writeFile(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{
|
|
324
|
-
overwriteExisting: force
|
|
325
|
-
}
|
|
326
|
-
),
|
|
299
|
+
writeFile(sdlFile, sdlContent, {
|
|
300
|
+
overwriteExisting: force
|
|
301
|
+
}),
|
|
327
302
|
writeFile(serviceFile, serviceContent, {
|
|
328
303
|
overwriteExisting: force
|
|
329
304
|
})
|
|
@@ -18,59 +18,6 @@ function buildMembershipsSelectProperty() {
|
|
|
18
18
|
}
|
|
19
19
|
return property;
|
|
20
20
|
}
|
|
21
|
-
const TENANCY_AUTH_EXPORT = `export const { hasUserRole, hasOrgRole, requireUserAuth, requireOrgAuth } =
|
|
22
|
-
createTenancyAuth()
|
|
23
|
-
`;
|
|
24
|
-
const DEFAULT_REQUIRE_AUTH_STATEMENTS = [
|
|
25
|
-
/^if \(!isAuthenticated\(\)\) \{ throw new AuthenticationError\([^)]*\);? \}$/,
|
|
26
|
-
/^if \(roles && !hasRole\(roles\)\) \{ throw new ForbiddenError\([^)]*\);? \}$/,
|
|
27
|
-
/^return isAuthenticated\(\);?$/
|
|
28
|
-
];
|
|
29
|
-
const DEFAULT_HAS_ROLE_NAMES = /* @__PURE__ */ new Set([
|
|
30
|
-
"AllowedRoles",
|
|
31
|
-
"Array",
|
|
32
|
-
"allowedRole",
|
|
33
|
-
"allowedRoles",
|
|
34
|
-
"context",
|
|
35
|
-
"currentUser",
|
|
36
|
-
"currentUserRoles",
|
|
37
|
-
"hasRole",
|
|
38
|
-
"includes",
|
|
39
|
-
"isArray",
|
|
40
|
-
"isAuthenticated",
|
|
41
|
-
"role",
|
|
42
|
-
"roles",
|
|
43
|
-
"some",
|
|
44
|
-
"undefined"
|
|
45
|
-
]);
|
|
46
|
-
function exportedValue(declaration) {
|
|
47
|
-
const node = declaration.nodes()[0];
|
|
48
|
-
return j.VariableDeclaration.check(node?.declaration) ? node.declaration.declarations[0] : void 0;
|
|
49
|
-
}
|
|
50
|
-
function isDefaultHasRole(hasRole) {
|
|
51
|
-
return hasRole.find(j.Identifier).nodes().every((identifier) => DEFAULT_HAS_ROLE_NAMES.has(identifier.name));
|
|
52
|
-
}
|
|
53
|
-
function isDefaultRequireAuth(requireAuth) {
|
|
54
|
-
const declarator = exportedValue(requireAuth);
|
|
55
|
-
const fn = j.VariableDeclarator.check(declarator) ? declarator.init : null;
|
|
56
|
-
if (!j.ArrowFunctionExpression.check(fn) || !j.BlockStatement.check(fn.body)) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
return fn.body.body.every((statement) => {
|
|
60
|
-
const source = j(statement).toSource().replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
61
|
-
return DEFAULT_REQUIRE_AUTH_STATEMENTS.some(
|
|
62
|
-
(pattern) => pattern.test(source)
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function findExportedConst(root, name) {
|
|
67
|
-
return root.find(j.ExportNamedDeclaration, {
|
|
68
|
-
declaration: {
|
|
69
|
-
type: "VariableDeclaration",
|
|
70
|
-
declarations: [{ id: { type: "Identifier", name } }]
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
21
|
const USER_LOOKUP = {
|
|
75
22
|
callee: {
|
|
76
23
|
type: "MemberExpression",
|
|
@@ -82,33 +29,6 @@ const USER_LOOKUP = {
|
|
|
82
29
|
property: { name: "findUnique" }
|
|
83
30
|
}
|
|
84
31
|
};
|
|
85
|
-
function addContextImport(root) {
|
|
86
|
-
const usesContext = root.find(j.Identifier, { name: "context" }).filter(
|
|
87
|
-
(path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node && !path.parent.node.computed) && !(j.ObjectProperty.check(path.parent.node) && path.parent.node.key === path.node)
|
|
88
|
-
).length > 0;
|
|
89
|
-
const declaresContext = root.find(j.ImportDeclaration, {
|
|
90
|
-
source: { value: "@cedarjs/graphql-server" }
|
|
91
|
-
}).length > 0 || root.find(j.ImportSpecifier, { local: { name: "context" } }).length > 0 || root.find(j.VariableDeclarator, { id: { name: "context" } }).length > 0;
|
|
92
|
-
if (!usesContext || declaresContext) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
const contextImport = j.withParser("ts")(`import { context } from '@cedarjs/context'
|
|
96
|
-
`).find(j.ImportDeclaration).nodes()[0];
|
|
97
|
-
const cedarImports = root.find(j.ImportDeclaration).filter((path) => String(path.node.source.value).startsWith("@cedarjs/"));
|
|
98
|
-
const before = cedarImports.filter(
|
|
99
|
-
(path) => String(path.node.source.value) > "@cedarjs/context"
|
|
100
|
-
);
|
|
101
|
-
if (before.length > 0) {
|
|
102
|
-
before.at(0).insertBefore(contextImport);
|
|
103
|
-
} else if (cedarImports.length > 0) {
|
|
104
|
-
cedarImports.at(cedarImports.length - 1).insertAfter(contextImport);
|
|
105
|
-
} else {
|
|
106
|
-
root.find(j.Program).forEach((path) => {
|
|
107
|
-
path.node.body.unshift(contextImport);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
32
|
function addWithPermissions(root, lookups) {
|
|
113
33
|
const isWithPermissionsCall = (node) => j.CallExpression.check(node) && j.Identifier.check(node.callee) && node.callee.name === "withPermissions";
|
|
114
34
|
let wrapped = false;
|
|
@@ -193,56 +113,8 @@ function transform(fileInfo) {
|
|
|
193
113
|
if (addWithPermissions(root, membershipLookups)) {
|
|
194
114
|
changed = true;
|
|
195
115
|
}
|
|
196
|
-
|
|
197
|
-
if (alreadyWired) {
|
|
198
|
-
if (addContextImport(root)) {
|
|
199
|
-
changed = true;
|
|
200
|
-
}
|
|
201
|
-
return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
|
|
202
|
-
}
|
|
203
|
-
const hasRole = findExportedConst(root, "hasRole");
|
|
204
|
-
const requireAuth = findExportedConst(root, "requireAuth");
|
|
205
|
-
if (hasRole.length !== 1 || requireAuth.length !== 1) {
|
|
206
|
-
throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_NOT_FOUND");
|
|
207
|
-
}
|
|
208
|
-
if (!isDefaultRequireAuth(requireAuth) || !isDefaultHasRole(hasRole)) {
|
|
209
|
-
throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_CHANGED");
|
|
210
|
-
}
|
|
211
|
-
const tenancyAuth = j.withParser("ts")(TENANCY_AUTH_EXPORT).find(j.ExportNamedDeclaration).nodes()[0];
|
|
212
|
-
requireAuth.replaceWith(tenancyAuth);
|
|
213
|
-
hasRole.remove();
|
|
214
|
-
root.find(j.Identifier, { name: "hasRole" }).filter(
|
|
215
|
-
(path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node)
|
|
216
|
-
).forEach((path) => {
|
|
217
|
-
path.node.name = "hasUserRole";
|
|
218
|
-
});
|
|
219
|
-
const isUnused = (name) => root.find(j.Identifier, { name }).filter(
|
|
220
|
-
(path) => !j.ImportSpecifier.check(path.parent.node) && !j.TSTypeAliasDeclaration.check(path.parent.node)
|
|
221
|
-
).length === 0;
|
|
222
|
-
root.find(j.TSTypeAliasDeclaration, { id: { name: "AllowedRoles" } }).filter(() => isUnused("AllowedRoles")).remove();
|
|
223
|
-
root.find(j.ImportDeclaration, { source: { value: "@cedarjs/graphql-server" } }).forEach((path) => {
|
|
224
|
-
path.node.specifiers = (path.node.specifiers ?? []).filter(
|
|
225
|
-
(specifier) => !j.ImportSpecifier.check(specifier) || !j.Identifier.check(specifier.local) || !isUnused(specifier.local.name)
|
|
226
|
-
);
|
|
227
|
-
}).filter((path) => (path.node.specifiers ?? []).length === 0).remove();
|
|
228
|
-
const tenancyImport = j.withParser("ts")(`import { createTenancyAuth } from '@cedarjs/tenancy'
|
|
229
|
-
`).find(j.ImportDeclaration).nodes()[0];
|
|
230
|
-
const imports = root.find(j.ImportDeclaration);
|
|
231
|
-
const cedarImports = imports.filter(
|
|
232
|
-
(path) => String(path.node.source.value).startsWith("@cedarjs/")
|
|
233
|
-
);
|
|
234
|
-
const anchor = cedarImports.length > 0 ? cedarImports : imports;
|
|
235
|
-
if (anchor.length > 0) {
|
|
236
|
-
anchor.at(anchor.length - 1).insertAfter(tenancyImport);
|
|
237
|
-
} else {
|
|
238
|
-
root.find(j.Program).forEach((path) => {
|
|
239
|
-
path.node.body.unshift(tenancyImport);
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
addContextImport(root);
|
|
243
|
-
return addPermissionsImport(root.toSource());
|
|
116
|
+
return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
|
|
244
117
|
}
|
|
245
118
|
export {
|
|
246
|
-
TENANCY_AUTH_EXPORT,
|
|
247
119
|
transform as default
|
|
248
120
|
};
|
|
@@ -45,7 +45,7 @@ export const schema = gql`
|
|
|
45
45
|
|
|
46
46
|
type Query {
|
|
47
47
|
"Every membership belonging to the signed-in user, across all organizations they belong to."
|
|
48
|
-
myMemberships: [Membership!]! @
|
|
48
|
+
myMemberships: [Membership!]! @requireAuth
|
|
49
49
|
|
|
50
50
|
"The current organization, resolved by resolveCurrentOrg for this request."
|
|
51
51
|
organization(id: String!): Organization
|
|
@@ -55,13 +55,12 @@ export const schema = gql`
|
|
|
55
55
|
type Mutation {
|
|
56
56
|
"Creates a new organization with the caller as its owner."
|
|
57
57
|
createOrganization(input: CreateOrganizationInput!): Organization!
|
|
58
|
-
@
|
|
58
|
+
@requireAuth
|
|
59
59
|
|
|
60
60
|
"""
|
|
61
61
|
Creates a pending membership (an invitation) in the current organization,
|
|
62
|
-
for a role the caller
|
|
63
|
-
|
|
64
|
-
owner. The app is responsible for emailing input.email a link built from
|
|
62
|
+
for a role whose permissions the caller all holds -- an admin can invite
|
|
63
|
+
an admin or a member, but not an owner. The app is responsible for emailing input.email a link built from
|
|
65
64
|
the returned invitationToken; Membership has no email column of its own.
|
|
66
65
|
The invitation can be claimed until the returned invitationExpiresAt.
|
|
67
66
|
Web callers must call reauthenticate() from useAuth() in onCompleted so
|
|
@@ -78,7 +77,7 @@ export const schema = gql`
|
|
|
78
77
|
the new membership is reflected in currentUser.memberships.
|
|
79
78
|
"""
|
|
80
79
|
acceptInvitation(token: String!): Organization!
|
|
81
|
-
@
|
|
80
|
+
@requireAuth
|
|
82
81
|
|
|
83
82
|
"""
|
|
84
83
|
Removes a membership from the current organization, for a role the
|
|
@@ -120,6 +120,19 @@ describe('organizations service', () => {
|
|
|
120
120
|
},
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
+
scenario(
|
|
124
|
+
'an admin can invite an admin',
|
|
125
|
+
async (scenario: StandardScenario) => {
|
|
126
|
+
asMember(scenario, scenario.membership.admin)
|
|
127
|
+
|
|
128
|
+
const result = await inviteMember({
|
|
129
|
+
input: { email: 'new-admin@example.com', role: 'admin' },
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(result.invitationToken).toBeTruthy()
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
123
136
|
scenario(
|
|
124
137
|
'an admin cannot invite an owner',
|
|
125
138
|
async (scenario: StandardScenario) => {
|
|
@@ -6,10 +6,13 @@ import type {
|
|
|
6
6
|
MembershipRelationResolvers,
|
|
7
7
|
} from 'types/graphql'
|
|
8
8
|
|
|
9
|
+
import { permissions } from '__PERMISSIONS_PACKAGE__'
|
|
10
|
+
|
|
9
11
|
import { ForbiddenError, ValidationError } from '@cedarjs/graphql-server'
|
|
10
12
|
import { requireCurrentOrg } from '@cedarjs/tenancy'
|
|
11
13
|
|
|
12
14
|
import { Prisma, db } from 'src/lib/db'
|
|
15
|
+
import { canGrantRole } from 'src/lib/permissions'
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* How long an invitation stays claimable after `inviteMember` creates it.
|
|
@@ -37,38 +40,12 @@ const membershipsForUser = (userId: string): Promise<MembershipSummary[]> =>
|
|
|
37
40
|
select: { id: true, organizationId: true, role: true },
|
|
38
41
|
})
|
|
39
42
|
|
|
40
|
-
/**
|
|
41
|
-
* Roles this generated service knows how to check, and which of them a
|
|
42
|
-
* membership with that role may grant to someone else (`inviteMember`) or
|
|
43
|
-
* revoke (`removeMember`). `owner` may grant or revoke any known role;
|
|
44
|
-
* `admin` may grant or revoke `admin` and `member`, but never `owner`, so
|
|
45
|
-
* an admin can never create or remove an owner. `member` grants nothing;
|
|
46
|
-
* it's listed anyway so `KNOWN_ROLES` below covers every role a membership
|
|
47
|
-
* can actually hold, which is what makes the "unknown role" check on
|
|
48
|
-
* `inviteMember` meaningful.
|
|
49
|
-
*
|
|
50
|
-
* Add a role here when your app adds one. A role missing from this map can
|
|
51
|
-
* never be granted to, or removed from, anyone by `inviteMember` /
|
|
52
|
-
* `removeMember` -- including by an owner.
|
|
53
|
-
*/
|
|
54
|
-
export const ROLE_GRANTS: Readonly<Record<string, readonly string[]>> = {
|
|
55
|
-
owner: ['owner', 'admin', 'member'],
|
|
56
|
-
admin: ['admin', 'member'],
|
|
57
|
-
member: [],
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const KNOWN_ROLES = Object.keys(ROLE_GRANTS)
|
|
61
|
-
|
|
62
|
-
/** Whether a membership with `granterRole` may grant or revoke `targetRole`. */
|
|
63
|
-
const canGrantRole = (granterRole: string, targetRole: string): boolean =>
|
|
64
|
-
ROLE_GRANTS[granterRole]?.includes(targetRole) ?? false
|
|
65
|
-
|
|
66
43
|
/**
|
|
67
44
|
* Every membership belonging to the signed-in user, across all
|
|
68
45
|
* organizations. `Membership` is a global model, so this reads plain `db`.
|
|
69
46
|
*/
|
|
70
47
|
export const myMemberships: QueryResolvers['myMemberships'] = () => {
|
|
71
|
-
// @
|
|
48
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
72
49
|
return db.membership.findMany({
|
|
73
50
|
where: { userId: context.currentUser!.id },
|
|
74
51
|
orderBy: { createdAt: 'asc' },
|
|
@@ -95,7 +72,7 @@ export const organization: QueryResolvers['organization'] = async ({ id }) => {
|
|
|
95
72
|
export const createOrganization: MutationResolvers['createOrganization'] =
|
|
96
73
|
async ({ input }) => {
|
|
97
74
|
const baseSlug = slugify(input.name)
|
|
98
|
-
// @
|
|
75
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
99
76
|
const userId = context.currentUser!.id
|
|
100
77
|
|
|
101
78
|
let slug = baseSlug
|
|
@@ -139,12 +116,12 @@ export const createOrganization: MutationResolvers['createOrganization'] =
|
|
|
139
116
|
|
|
140
117
|
/**
|
|
141
118
|
* Creates a pending membership (an invitation) in the current organization,
|
|
142
|
-
* for a role the caller
|
|
143
|
-
* `admin` can invite an `admin` or a `member`, but not an
|
|
144
|
-
* though `@requirePermission(permission: "members.manage")`
|
|
145
|
-
* this mutation at all. A role that isn't in
|
|
146
|
-
* outright, so a typo in `input.role`
|
|
147
|
-
* can
|
|
119
|
+
* for a role the caller may grant (`canGrantRole`): one whose permissions
|
|
120
|
+
* they all hold. An `admin` can invite an `admin` or a `member`, but not an
|
|
121
|
+
* `owner`, even though `@requirePermission(permission: "members.manage")`
|
|
122
|
+
* lets an admin call this mutation at all. A role that isn't in the
|
|
123
|
+
* permission definitions is rejected outright, so a typo in `input.role`
|
|
124
|
+
* can never create a membership with no permissions.
|
|
148
125
|
*
|
|
149
126
|
* `Membership` has no email column: the caller is responsible for emailing
|
|
150
127
|
* `input.email` a link built from the returned `invitationToken`, with its
|
|
@@ -158,14 +135,14 @@ export const inviteMember: MutationResolvers['inviteMember'] = async ({
|
|
|
158
135
|
}) => {
|
|
159
136
|
const currentOrg = requireCurrentOrg()
|
|
160
137
|
|
|
161
|
-
if (!
|
|
138
|
+
if (!Object.hasOwn(permissions.roles, input.role)) {
|
|
162
139
|
throw new ValidationError(
|
|
163
|
-
`"${input.role}" is not a known role. Add it to
|
|
164
|
-
'
|
|
140
|
+
`"${input.role}" is not a known role. Add it to your permission ` +
|
|
141
|
+
'definitions before inviting with it.',
|
|
165
142
|
)
|
|
166
143
|
}
|
|
167
144
|
|
|
168
|
-
if (!canGrantRole(
|
|
145
|
+
if (!canGrantRole(input.role)) {
|
|
169
146
|
throw new ForbiddenError(
|
|
170
147
|
`A "${currentOrg.role}" cannot invite a "${input.role}".`,
|
|
171
148
|
)
|
|
@@ -267,7 +244,7 @@ const isExpiredInvitation = async (token: string): Promise<boolean> => {
|
|
|
267
244
|
export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
|
|
268
245
|
token,
|
|
269
246
|
}) => {
|
|
270
|
-
// @
|
|
247
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
271
248
|
const userId = context.currentUser!.id
|
|
272
249
|
|
|
273
250
|
const claim = await claimInvitation(token, userId)
|
|
@@ -288,7 +265,7 @@ export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
|
|
|
288
265
|
/**
|
|
289
266
|
* Removes a membership from the current organization. A caller may only
|
|
290
267
|
* remove a membership whose role they could grant via `inviteMember` (the
|
|
291
|
-
* same `
|
|
268
|
+
* same `canGrantRole` rule), so an admin can never remove an owner, and the
|
|
292
269
|
* organization's last owner can never be removed -- including by
|
|
293
270
|
* themselves -- so an organization always keeps at least one owner who has
|
|
294
271
|
* accepted their invitation.
|
|
@@ -306,7 +283,7 @@ export const removeMember: MutationResolvers['removeMember'] = async ({
|
|
|
306
283
|
throw new ForbiddenError("You don't have access to that membership.")
|
|
307
284
|
}
|
|
308
285
|
|
|
309
|
-
if (!canGrantRole(
|
|
286
|
+
if (!canGrantRole(membership.role)) {
|
|
310
287
|
throw new ForbiddenError(
|
|
311
288
|
`A "${currentOrg.role}" cannot remove a "${membership.role}" member.`,
|
|
312
289
|
)
|
|
@@ -9,12 +9,14 @@ import { createPermissions } from '@cedarjs/tenancy'
|
|
|
9
9
|
* `useAuth().hasPermission()`. The web only decides what to show; these
|
|
10
10
|
* checks are the guard.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* `canGrantRole` is who may give someone a role, or take it away: whoever
|
|
13
|
+
* holds every permission it has (`inviteMember` and `removeMember` in
|
|
14
|
+
* api/src/services/organizations).
|
|
14
15
|
*/
|
|
15
16
|
export const {
|
|
16
17
|
hasPermission,
|
|
17
18
|
requirePermission,
|
|
19
|
+
canGrantRole,
|
|
18
20
|
permissionsOf,
|
|
19
21
|
withPermissions,
|
|
20
22
|
} = createPermissions(permissions)
|
|
@@ -2,24 +2,30 @@ import { definePermissions } from '@cedarjs/tenancy/permissions'
|
|
|
2
2
|
import type { PermissionName } from '@cedarjs/tenancy/permissions'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Every permission the app checks, and what each role may do
|
|
6
|
-
* for a permission
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Every permission the app checks, and what each role may do: a role is a
|
|
6
|
+
* named bundle of permissions. Fields ask for a permission
|
|
7
|
+
* (`@requirePermission(permission: "members.manage")`, or
|
|
8
|
+
* `requirePermission('members.manage')` in a service), never a role, so a
|
|
9
|
+
* new role is one entry here: it can do exactly what its list says, and a
|
|
9
10
|
* field added later is closed to it unless its permission is on the list.
|
|
10
11
|
*
|
|
12
|
+
* `organization.read` is "any member": every role lists it, and fields any
|
|
13
|
+
* member may use ask for it. `organization.manage` is the owner's alone,
|
|
14
|
+
* which also keeps admins from making someone an owner: a role can only be
|
|
15
|
+
* given by someone who holds every permission it has.
|
|
16
|
+
*
|
|
11
17
|
* `roles` are per organization: a membership's role decides what the user
|
|
12
18
|
* may do in that organization. Fields about the user rather than an
|
|
13
|
-
* organization (their own account, say) use `@
|
|
19
|
+
* organization (their own account, say) use `@requireAuth` instead.
|
|
14
20
|
*
|
|
15
21
|
* This package is shared: the api checks these permissions
|
|
16
22
|
* (api/src/lib/permissions), and the web can import the names. The web gets each
|
|
17
23
|
* membership's permissions from the api, through `getCurrentUser`.
|
|
18
24
|
*/
|
|
19
25
|
export const permissions = definePermissions({
|
|
20
|
-
permissions: ['organization.read', 'members.manage'],
|
|
26
|
+
permissions: ['organization.read', 'organization.manage', 'members.manage'],
|
|
21
27
|
roles: {
|
|
22
|
-
owner: ['organization.read', 'members.manage'],
|
|
28
|
+
owner: ['organization.read', 'organization.manage', 'members.manage'],
|
|
23
29
|
admin: ['organization.read', 'members.manage'],
|
|
24
30
|
member: ['organization.read'],
|
|
25
31
|
},
|
|
@@ -22,9 +22,7 @@ type RequirePermissionValidate = ValidatorDirectiveFunc<{
|
|
|
22
22
|
const validate: RequirePermissionValidate = ({ directiveArgs }) => {
|
|
23
23
|
// A name the permissions map doesn't know throws, so a typo in the SDL is
|
|
24
24
|
// an error rather than a field nobody can use
|
|
25
|
-
applicationRequirePermission(
|
|
26
|
-
permission: directiveArgs.permission as Permission,
|
|
27
|
-
})
|
|
25
|
+
applicationRequirePermission(directiveArgs.permission as Permission)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
const requirePermission = createValidatorDirective(schema, validate)
|
|
@@ -16,12 +16,8 @@ import { createTenancy } from '@cedarjs/tenancy/web'
|
|
|
16
16
|
*
|
|
17
17
|
* A user who isn't signed in is sent to log in, and one who isn't a member
|
|
18
18
|
* of the organization sees `notAMember`. Pages only some members may open
|
|
19
|
-
* take `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* <PrivateSet unauthenticated="login" unauthorized="projects" orgRoles="owner">
|
|
23
|
-
* <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
|
|
24
|
-
* </PrivateSet>
|
|
19
|
+
* take `orgPermission` (see packages/permissions), and the one a member
|
|
20
|
+
* without it is sent to is `unauthorized`:
|
|
25
21
|
*
|
|
26
22
|
* <PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="members.manage">
|
|
27
23
|
* <Route path="/org/{orgSlug}/members" page={MembersPage} name="members" />
|
|
@@ -20,12 +20,6 @@ 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
23
|
import {
|
|
30
24
|
addPermissionsPackageTasks,
|
|
31
25
|
permissionsPackageName
|
|
@@ -43,11 +37,8 @@ import {
|
|
|
43
37
|
ORGANIZATION_MODEL,
|
|
44
38
|
requiredUserFields
|
|
45
39
|
} from "./schemaPrisma.js";
|
|
46
|
-
import { renameUseAuthHasRole } from "./webHasRole.js";
|
|
47
40
|
const DEFAULT_TENANT_FIELD = "organizationId";
|
|
48
41
|
const AUTH_SNIPPET = `
|
|
49
|
-
import { createTenancyAuth } from '@cedarjs/tenancy'
|
|
50
|
-
|
|
51
42
|
import { withPermissions } from 'src/lib/permissions'
|
|
52
43
|
|
|
53
44
|
export const getCurrentUser = async (session) => {
|
|
@@ -65,10 +56,7 @@ const AUTH_SNIPPET = `
|
|
|
65
56
|
},
|
|
66
57
|
},
|
|
67
58
|
}))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// In place of your hasRole and requireAuth
|
|
71
|
-
${TENANCY_AUTH_EXPORT}`;
|
|
59
|
+
}`;
|
|
72
60
|
const SIGNUP_SNIPPET = `
|
|
73
61
|
handler: async ({ username, hashedPassword, salt, userAttributes }) => {
|
|
74
62
|
const user = await db.user.create({
|
|
@@ -217,7 +205,7 @@ Re-run with --force to have this command append its own versions beside them ins
|
|
|
217
205
|
}
|
|
218
206
|
},
|
|
219
207
|
{
|
|
220
|
-
title: "Adding memberships
|
|
208
|
+
title: "Adding memberships and their permissions to getCurrentUser...",
|
|
221
209
|
task: async (_ctx, task) => {
|
|
222
210
|
if (!fs.existsSync(authPath)) {
|
|
223
211
|
task.skip("No api/src/lib/auth file found; skipping.");
|
|
@@ -227,10 +215,7 @@ Re-run with --force to have this command append its own versions beside them ins
|
|
|
227
215
|
transformPath: path.join(import.meta.dirname, "authCodemod.js"),
|
|
228
216
|
targetPaths: [authPath]
|
|
229
217
|
});
|
|
230
|
-
if (transformResult.error
|
|
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) {
|
|
218
|
+
if (transformResult.error) {
|
|
234
219
|
task.output = `Could not update api/src/lib/auth automatically. Please add this by hand:
|
|
235
220
|
${AUTH_SNIPPET}`;
|
|
236
221
|
}
|
|
@@ -270,57 +255,6 @@ ${SIGNUP_SNIPPET}`;
|
|
|
270
255
|
},
|
|
271
256
|
skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
|
|
272
257
|
},
|
|
273
|
-
{
|
|
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
258
|
{
|
|
325
259
|
title: "Adding the permissions package...",
|
|
326
260
|
task: () => addPermissionsPackageTasks({
|
|
@@ -375,7 +309,8 @@ ${SIGNUP_SNIPPET}`;
|
|
|
375
309
|
const withUserFields = (firstNumber) => (content) => addRequiredUserFields(content, userFields, { firstNumber });
|
|
376
310
|
await writeTemplateFile(
|
|
377
311
|
"organizations.ts.template",
|
|
378
|
-
path.join(serviceDir, `organizations.${ext}`)
|
|
312
|
+
path.join(serviceDir, `organizations.${ext}`),
|
|
313
|
+
{ __PERMISSIONS_PACKAGE__: permissionsPackageName() }
|
|
379
314
|
);
|
|
380
315
|
await writeTemplateFile(
|
|
381
316
|
"organizations.test.ts.template",
|
|
@@ -535,8 +470,9 @@ ${SIGNUP_SNIPPET}`;
|
|
|
535
470
|
User/Organization/Membership. For each existing model (e.g.
|
|
536
471
|
Contact) whose rows belong to one organization, add
|
|
537
472
|
${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
|
|
538
|
-
schema.prisma, and switch its fields from ${c.highlight("@
|
|
539
|
-
to ${c.highlight(
|
|
473
|
+
schema.prisma, and switch its fields from ${c.highlight("@requireAuth")}
|
|
474
|
+
to ${c.highlight('@requirePermission(permission: "organization.read")')}, or
|
|
475
|
+
a permission of its own. For each one that stays global, add it
|
|
540
476
|
to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
|
|
541
477
|
3. Put your organization routes in a PrivateSet in web/src/Routes.tsx.
|
|
542
478
|
The Router renders the organization for it, using the generated
|
|
@@ -567,29 +503,22 @@ ${ctx.routerNeedsTenancy ? `
|
|
|
567
503
|
)}.
|
|
568
504
|
5. Your permissions, and what each role may do, are in
|
|
569
505
|
${c.highlight("packages/permissions")}, shared by the api and web sides${ctx.packagesWorkspaceTurnedOn ? ` (this
|
|
570
|
-
turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}.
|
|
571
|
-
organization fields with
|
|
572
|
-
|
|
573
|
-
web with ${c.highlight("useAuth().hasPermission()")}.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
${c.highlight("
|
|
584
|
-
checking ${c.highlight("currentUser.roles")}; ${c.highlight("hasOrgRole")} checks the role in
|
|
585
|
-
the organization. ${c.highlight("<PrivateSet roles>")} isn't available with
|
|
586
|
-
tenancy: guard routes with ${c.highlight("<PrivateSet userRoles>")} for
|
|
587
|
-
${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet orgRoles>")} for the
|
|
588
|
-
role in the route's organization. A member who lacks it is sent
|
|
589
|
-
to ${c.highlight("unauthorized")}, and ${c.highlight("<PrivateSet orgPermission>")} checks a
|
|
590
|
-
permission of their membership the same way:
|
|
506
|
+
turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}. A role is a bundle of
|
|
507
|
+
permissions: guard organization fields with
|
|
508
|
+
${c.highlight('@requirePermission(permission: "...")')}, never a role, and check
|
|
509
|
+
permissions on the web with ${c.highlight("useAuth().hasPermission()")}. Fields
|
|
510
|
+
any member may use ask for ${c.highlight("organization.read")}, which every
|
|
511
|
+
role lists.
|
|
512
|
+
6. ${c.highlight("@requireAuth")} is for fields about the signed-in user.
|
|
513
|
+
${c.highlight("@requirePermission")} needs a current organization, and checks
|
|
514
|
+
what the user's role there may do.
|
|
515
|
+
7. Roles are the user's own: ${c.highlight("hasRole")} and ${c.highlight("<PrivateSet roles>")}
|
|
516
|
+
check ${c.highlight("currentUser.roles")}. In an organization, check a
|
|
517
|
+
permission instead: guard routes with ${c.highlight("<PrivateSet orgPermission>")}
|
|
518
|
+
for a permission of their membership in the route's organization.
|
|
519
|
+
A member who lacks it is sent to ${c.highlight("unauthorized")}:
|
|
591
520
|
|
|
592
|
-
<PrivateSet unauthenticated="login" unauthorized="projects"
|
|
521
|
+
<PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="organization.manage">
|
|
593
522
|
<Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
|
|
594
523
|
</PrivateSet>
|
|
595
524
|
|
|
@@ -14,7 +14,6 @@ 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";
|
|
18
17
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
19
18
|
import { isTypeScriptProject } from "../../../lib/project.js";
|
|
20
19
|
import { generateSecret } from "../../generate/secret/secret.js";
|
|
@@ -113,9 +112,6 @@ const handler = async ({ targets, force }) => {
|
|
|
113
112
|
for (const [token, value] of Object.entries(replacements)) {
|
|
114
113
|
templateContent = templateContent.replaceAll(token, value);
|
|
115
114
|
}
|
|
116
|
-
templateContent = withAuthDirective(templateContent, {
|
|
117
|
-
belongsToOrg: false
|
|
118
|
-
});
|
|
119
115
|
const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
|
|
120
116
|
return writeFile(outputPath, content, { overwriteExisting: force });
|
|
121
117
|
};
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getPaths } from "./index.js";
|
|
4
|
-
function
|
|
5
|
-
|
|
4
|
+
function hasTenancy() {
|
|
5
|
+
let packageJson;
|
|
6
|
+
try {
|
|
7
|
+
packageJson = JSON.parse(
|
|
8
|
+
fs.readFileSync(path.join(getPaths().api.base, "package.json"), "utf-8")
|
|
9
|
+
);
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return !!packageJson.dependencies?.["@cedarjs/tenancy"] || !!packageJson.devDependencies?.["@cedarjs/tenancy"];
|
|
6
14
|
}
|
|
15
|
+
const MEMBER_PERMISSION = "organization.read";
|
|
7
16
|
function tenantField() {
|
|
8
17
|
for (const ext of ["ts", "js"]) {
|
|
9
18
|
const dbPath = path.join(getPaths().api.lib, `db.${ext}`);
|
|
@@ -21,17 +30,18 @@ function belongsToOrganization(model) {
|
|
|
21
30
|
);
|
|
22
31
|
}
|
|
23
32
|
function withAuthDirective(source, { belongsToOrg }) {
|
|
24
|
-
if (!
|
|
33
|
+
if (!belongsToOrg || !hasTenancy()) {
|
|
25
34
|
return source;
|
|
26
35
|
}
|
|
27
36
|
return source.replace(
|
|
28
37
|
/@requireAuth(?![\w$])/g,
|
|
29
|
-
|
|
38
|
+
`@requirePermission(permission: "${MEMBER_PERMISSION}")`
|
|
30
39
|
);
|
|
31
40
|
}
|
|
32
41
|
export {
|
|
42
|
+
MEMBER_PERMISSION,
|
|
33
43
|
belongsToOrganization,
|
|
34
|
-
|
|
44
|
+
hasTenancy,
|
|
35
45
|
tenantField,
|
|
36
46
|
withAuthDirective
|
|
37
47
|
};
|
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.3568",
|
|
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.3568",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3568",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3568",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3568",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3568",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3568",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3568",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3568",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3568",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3568",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3568",
|
|
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.3568",
|
|
97
97
|
"@prisma/dmmf": "7.10.1",
|
|
98
98
|
"@types/archiver": "^7.0.0",
|
|
99
99
|
"memfs": "4.80.0",
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
function renameRequireAuthDirective(source) {
|
|
2
|
-
return source.replace(/@requireAuth(?![\w$])/g, "@requireUserAuth");
|
|
3
|
-
}
|
|
4
|
-
const LIB_AUTH_IMPORT = /import\s*\{([^}]*)\}\s*from\s*(['"])((?:src|\.{1,2})(?:\/[^'"]*)?\/lib\/auth(?:\.[jt]s)?)\2/g;
|
|
5
|
-
const LIB_AUTH_RENAMES = {
|
|
6
|
-
requireAuth: "requireUserAuth",
|
|
7
|
-
hasRole: "hasUserRole"
|
|
8
|
-
};
|
|
9
|
-
function renameLibAuthImports(source) {
|
|
10
|
-
let result = source;
|
|
11
|
-
for (const [from, to] of Object.entries(LIB_AUTH_RENAMES)) {
|
|
12
|
-
let renameUses = false;
|
|
13
|
-
result = result.replace(
|
|
14
|
-
LIB_AUTH_IMPORT,
|
|
15
|
-
(statement, specifiers) => {
|
|
16
|
-
const renamed = specifiers.replace(
|
|
17
|
-
new RegExp(`(?<![\\w$])${from}(?![\\w$])(\\s+as\\s+[\\w$]+)?`, "g"),
|
|
18
|
-
(_specifier, alias) => {
|
|
19
|
-
if (!alias) {
|
|
20
|
-
renameUses = true;
|
|
21
|
-
}
|
|
22
|
-
return `${to}${alias ?? ""}`;
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
return statement.replace(specifiers, renamed);
|
|
26
|
-
}
|
|
27
|
-
);
|
|
28
|
-
if (renameUses) {
|
|
29
|
-
result = result.replace(
|
|
30
|
-
new RegExp(`(?<![\\w$@.])${from}(?![\\w$])`, "g"),
|
|
31
|
-
to
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
37
|
-
const DEFAULT_REQUIRE_AUTH_DIRECTIVES = [
|
|
38
|
-
`import { gql } from 'graphql-tag'
|
|
39
|
-
|
|
40
|
-
import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
|
|
41
|
-
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
42
|
-
|
|
43
|
-
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
|
|
44
|
-
|
|
45
|
-
export const schema = gql\`
|
|
46
|
-
"""
|
|
47
|
-
Use to check whether or not a user is authenticated and is associated
|
|
48
|
-
with an optional set of roles.
|
|
49
|
-
"""
|
|
50
|
-
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
|
51
|
-
\`
|
|
52
|
-
|
|
53
|
-
type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
|
54
|
-
|
|
55
|
-
const validate: RequireAuthValidate = ({ directiveArgs }) => {
|
|
56
|
-
const { roles } = directiveArgs
|
|
57
|
-
applicationRequireAuth({ roles })
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const requireAuth = createValidatorDirective(schema, validate)
|
|
61
|
-
|
|
62
|
-
export default requireAuth
|
|
63
|
-
`,
|
|
64
|
-
`import { gql } from 'graphql-tag'
|
|
65
|
-
|
|
66
|
-
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
67
|
-
|
|
68
|
-
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
|
|
69
|
-
|
|
70
|
-
export const schema = gql\`
|
|
71
|
-
"""
|
|
72
|
-
Use to check whether or not a user is authenticated and is associated
|
|
73
|
-
with an optional set of roles.
|
|
74
|
-
"""
|
|
75
|
-
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
|
76
|
-
\`
|
|
77
|
-
|
|
78
|
-
const validate = ({ directiveArgs }) => {
|
|
79
|
-
const { roles } = directiveArgs
|
|
80
|
-
applicationRequireAuth({ roles })
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const requireAuth = createValidatorDirective(schema, validate)
|
|
84
|
-
|
|
85
|
-
export default requireAuth
|
|
86
|
-
`
|
|
87
|
-
];
|
|
88
|
-
function withoutFormatting(source) {
|
|
89
|
-
return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").replace(/["']/g, "'").replace(/[;\s]/g, "").replace(/,([)\]}>])/g, "$1");
|
|
90
|
-
}
|
|
91
|
-
function isDefaultRequireAuthDirective(source) {
|
|
92
|
-
return DEFAULT_REQUIRE_AUTH_DIRECTIVES.some(
|
|
93
|
-
(directive) => withoutFormatting(directive) === withoutFormatting(source)
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
export {
|
|
97
|
-
DEFAULT_REQUIRE_AUTH_DIRECTIVES,
|
|
98
|
-
isDefaultRequireAuthDirective,
|
|
99
|
-
renameLibAuthImports,
|
|
100
|
-
renameRequireAuthDirective
|
|
101
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
|
|
2
|
-
|
|
3
|
-
import requireOrgAuth from './requireOrgAuth.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('requireOrgAuth directive', () => {
|
|
13
|
-
it('declares the directive sdl as schema, with the correct name', () => {
|
|
14
|
-
expect(requireOrgAuth.schema).toBeTruthy()
|
|
15
|
-
expect(getDirectiveName(requireOrgAuth.schema)).toBe('requireOrgAuth')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('throws when there is no current user', () => {
|
|
19
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, { context: {} })
|
|
20
|
-
|
|
21
|
-
expect(mockExecution).toThrowError("You don't have permission to do that.")
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('throws when there is no current organization', () => {
|
|
25
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
26
|
-
context: { currentUser: { id: 'user-1' } },
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
expect(mockExecution).toThrowError(
|
|
30
|
-
'You must be a member of an organization to do that.',
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('does not throw for a member of the current organization', () => {
|
|
35
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
36
|
-
context: {
|
|
37
|
-
currentUser: { id: 'user-1' },
|
|
38
|
-
currentOrg: currentOrg('member'),
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
expect(mockExecution).not.toThrowError()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it("throws when the role in the current organization isn't listed", () => {
|
|
46
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
47
|
-
context: {
|
|
48
|
-
currentUser: { id: 'user-1' },
|
|
49
|
-
currentOrg: currentOrg('member'),
|
|
50
|
-
},
|
|
51
|
-
directiveArgs: { roles: ['owner'] },
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
expect(mockExecution).toThrowError("You don't have access to do that.")
|
|
55
|
-
})
|
|
56
|
-
})
|
|
@@ -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 { requireOrgAuth as applicationRequireOrgAuth } from 'src/lib/auth'
|
|
7
|
-
|
|
8
|
-
export const schema = gql`
|
|
9
|
-
"""
|
|
10
|
-
Use to check that the signed-in user is a member of the current
|
|
11
|
-
organization, with one of an optional set of roles there.
|
|
12
|
-
"""
|
|
13
|
-
directive @requireOrgAuth(roles: [String]) on FIELD_DEFINITION
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
type RequireOrgAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
|
17
|
-
|
|
18
|
-
const validate: RequireOrgAuthValidate = ({ directiveArgs }) => {
|
|
19
|
-
const { roles } = directiveArgs
|
|
20
|
-
applicationRequireOrgAuth({ roles })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const requireOrgAuth = createValidatorDirective(schema, validate)
|
|
24
|
-
|
|
25
|
-
export default requireOrgAuth
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,26 +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 { 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
|
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
};
|