@semapps/auth 1.1.0 → 1.1.2

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/index.js CHANGED
@@ -3,7 +3,6 @@ module.exports = {
3
3
  AuthLocalService: require('./services/auth.local'),
4
4
  AuthOIDCService: require('./services/auth.oidc'),
5
5
  AuthAccountService: require('./services/account'),
6
- AuthCapabilitiesService: require('./services/capabilities'),
7
6
  AuthJWTService: require('./services/jwt'),
8
7
  AuthMigrationService: require('./services/migration'),
9
8
  AuthMailService: require('./services/mail')
package/mixins/auth.js CHANGED
@@ -1,12 +1,62 @@
1
1
  const passport = require('passport');
2
2
  const { Errors: E } = require('moleculer-web');
3
3
  const { TripleStoreAdapter } = require('@semapps/triplestore');
4
- const urlJoin = require('url-join');
5
4
  const AuthAccountService = require('../services/account');
6
5
  const AuthJWTService = require('../services/jwt');
7
- const CapabilitiesService = require('../services/capabilities');
8
6
 
9
- /** @type {import('moleculer').ServiceSchema} */
7
+ /**
8
+ * Auth Mixin that handles authentication and authorization for routes
9
+ * that requested this.
10
+ *
11
+ * The authorization and authentication actions check for a valid `authorization` header.
12
+ * If the bearer token is a server-signed JWT identifying the user, `ctx.meta.tokenPayload` and
13
+ * `ctx.meta.webId` are set. Setting either `authorization` or `authentication` suffices.
14
+ *
15
+ * # Authentication
16
+ * In the `authenticate` action, the webId is set to `anon`, if no `authorization` header is present.
17
+ *
18
+ * # Authorization
19
+ * In contrast, the `authorize` action throws an unauthorized error,
20
+ * if no `authorization` header is present.
21
+ * @see https://moleculer.services/docs/0.13/moleculer-web.html#Authentication
22
+ *
23
+ * ## Capability Authorization
24
+ * Additionally, the `authorize` action supports capability authorization based on
25
+ * Verifiable Credentials (VCs), if `opts.authorizeWithCapability` is set to `true`.
26
+ *
27
+ * **WARNING**: This does not make any assertions about the validity of the capabilities'
28
+ * content (`credentialSubject`). What *is* checked:
29
+ * - the delegation chain was correct
30
+ * - all signatures are valid
31
+ * - the `controller`s of the keys (`verificationMethod`) used in the proofs to sign
32
+ * the VC capabilities and presentation are correct. I.e. the controller resolves to
33
+ * the WebId/controller identifier document (CID) which lists the key.\
34
+ * This means that *you know who signed the presentation and capabilities* on the way.
35
+ *
36
+ * NO BUSINESS LOGIC IS CHECKED.\
37
+ * It is still necessary to verify if the request itself is valid.
38
+ * There would be no error when the `credentialSubject` says: "A is allowed to read B"
39
+ * while the statement is actually made by "C" and not by "B".\
40
+ *
41
+ * @see https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
42
+ *
43
+ * @example Configuration for a new route
44
+ * ```js
45
+ * ctx.call('api.addRoute', {
46
+ * path: path.join(basePath, '/your/route'),
47
+ * name: 'your-route-name',
48
+ * aliases: {
49
+ * 'GET /': 'your.action.here',
50
+ * },
51
+ * // Set to true, to run authorization action.
52
+ * authorization: true,
53
+ * // Set to true, to run authenticate action.
54
+ * authentication: false,
55
+ * });
56
+ * ```
57
+ *
58
+ * @type {import('moleculer').ServiceSchema}
59
+ */
10
60
  const AuthMixin = {
11
61
  settings: {
12
62
  baseUrl: null,
@@ -14,6 +64,8 @@ const AuthMixin = {
14
64
  capabilitiesPath: undefined,
15
65
  registrationAllowed: true,
16
66
  reservedUsernames: [],
67
+ minPasswordLength: 1,
68
+ minUsernameLength: 1,
17
69
  webIdSelection: [],
18
70
  accountSelection: [],
19
71
  accountsDataset: 'settings',
@@ -21,7 +73,8 @@ const AuthMixin = {
21
73
  },
22
74
  dependencies: ['api'],
23
75
  async created() {
24
- const { jwtPath, reservedUsernames, accountsDataset, podProvider } = this.settings;
76
+ const { jwtPath, reservedUsernames, minPasswordLength, minUsernameLength, accountsDataset, podProvider } =
77
+ this.settings;
25
78
 
26
79
  this.broker.createService({
27
80
  mixins: [AuthJWTService],
@@ -30,13 +83,9 @@ const AuthMixin = {
30
83
 
31
84
  this.broker.createService({
32
85
  mixins: [AuthAccountService],
33
- settings: { reservedUsernames },
86
+ settings: { reservedUsernames, minPasswordLength, minUsernameLength },
34
87
  adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: accountsDataset })
35
88
  });
36
-
37
- if (podProvider) {
38
- this.broker.createService({ mixins: [CapabilitiesService], settings: { path: this.settings.capabilitiesPath } });
39
- }
40
89
  },
41
90
  async started() {
42
91
  if (!this.passportId) throw new Error('this.passportId must be set in the service creation.');
@@ -73,33 +122,28 @@ const AuthMixin = {
73
122
  }
74
123
 
75
124
  if (method === 'Bearer') {
76
- const payload = await ctx.call('auth.jwt.verifyToken', { token });
125
+ const payload = await ctx.call('auth.jwt.verifyServerSignedToken', { token });
77
126
  if (payload) {
78
127
  ctx.meta.tokenPayload = payload;
79
128
  ctx.meta.webId = payload.webId;
80
129
  return Promise.resolve(payload);
81
130
  }
131
+
132
+ // Check if token is a capability.
133
+ if (route.opts.authorizeWithCapability) {
134
+ return this.validateCapability(ctx, token);
135
+ }
136
+
82
137
  // Invalid token
83
138
  // TODO make sure token is deleted client-side
84
- ctx.meta.webId = 'anon';
85
139
  return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
86
- } else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
87
- const capabilityUri = token || req.$params.capability;
88
- const capability = await this.actions.getValidateCapability({
89
- capabilityUri,
90
- username: req.parsedUrl.match(/[^/]+/)[0]
91
- });
92
-
93
- ctx.meta.webId = 'anon';
94
- req.$ctx.meta.webId = 'anon';
95
- req.$ctx.meta.authorization = { capability };
96
- return Promise.resolve(null);
97
140
  }
98
141
 
99
142
  // No valid auth method given.
100
143
  ctx.meta.webId = 'anon';
101
144
  return Promise.resolve(null);
102
145
  },
146
+
103
147
  // See https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
104
148
  async authorize(ctx) {
105
149
  const { route, req, res } = ctx.params;
@@ -108,97 +152,31 @@ const AuthMixin = {
108
152
  const [method, token] = req.headers.authorization && req.headers.authorization.split(' ');
109
153
 
110
154
  if (!token) {
111
- ctx.meta.webId = 'anon';
112
155
  return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
113
156
  }
157
+ if (method !== 'Bearer') {
158
+ return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
159
+ }
114
160
 
115
- if (method === 'Bearer') {
116
- const payload = await ctx.call('auth.jwt.verifyToken', { token });
117
- if (payload) {
118
- ctx.meta.tokenPayload = payload;
119
- ctx.meta.webId = payload.webId;
120
- return Promise.resolve(payload);
121
- }
122
- } else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
123
- const capabilityUri = token || req.$params.capability;
124
- const capability = await this.actions.getValidateCapability({
125
- capabilityUri,
126
- username: req.parsedUrl.match(/[^/]+/)[0]
127
- });
128
-
129
- ctx.meta.webId = 'anon';
161
+ // Validate if the token was signed by server (registered user).
162
+ const serverSignedPayload = await ctx.call('auth.jwt.verifyServerSignedToken', { token });
163
+ if (serverSignedPayload) {
164
+ ctx.meta.tokenPayload = serverSignedPayload;
165
+ ctx.meta.webId = serverSignedPayload.webId;
166
+ return Promise.resolve(serverSignedPayload);
167
+ }
130
168
 
131
- req.$ctx.meta.webId = 'anon';
132
- req.$ctx.meta.authorization = { capability };
133
- if (capability) {
134
- return Promise.resolve(null);
135
- }
169
+ // Check if token is a capability.
170
+ if (route.opts.authorizeWithCapability) {
171
+ return this.validateCapability(ctx, token);
136
172
  }
137
173
 
138
174
  return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
139
175
  },
140
- getValidateCapability: {
141
- params: {
142
- capabilityUri: {
143
- type: 'string',
144
- required: true
145
- },
146
- webId: {
147
- type: 'string',
148
- optional: true
149
- },
150
- username: {
151
- type: 'string',
152
- optional: true
153
- }
154
- },
155
- /**
156
- * Checks, if the provided capabilityUri is a valid URI and within the resource owner's cap container.
157
- * @returns {Promise<object>} The stored capability object or undefined, if the capability is not valid.
158
- */
159
- async handler(ctx) {
160
- let { capabilityUri, webId, username } = ctx.params;
161
- /** @type {string} */
162
- const baseUrlTrailing = urlJoin(this.settings.baseUrl, '/');
163
- webId = webId || baseUrlTrailing + username;
164
-
165
- const podUrl = await ctx.call('solid-storage.getUrl', { webId });
166
-
167
- // Check if capabilityUri is within the resource owner's pod
168
- if (!webId?.startsWith(baseUrlTrailing) || !capabilityUri?.startsWith(podUrl)) {
169
- return undefined;
170
- }
171
-
172
- // Check, if capUri is a valid URI.
173
- try {
174
- // eslint-disable-next-line no-new
175
- new URL(capabilityUri);
176
- } catch {
177
- return undefined;
178
- }
179
-
180
- // Check if capabilityUri is within the resource owner's cap container.
181
- const resourceCapContainerUri = await ctx.call('capabilities.getContainerUri', {
182
- webId
183
- });
184
-
185
- if (
186
- !(await ctx.call('ldp.container.includes', {
187
- containerUri: resourceCapContainerUri,
188
- resourceUri: capabilityUri,
189
- webId: 'system'
190
- }))
191
- ) {
192
- return undefined;
193
- }
194
-
195
- return await ctx.call('capabilities.get', { resourceUri: capabilityUri });
196
- }
197
- },
198
176
 
199
177
  async impersonate(ctx) {
200
178
  const { webId } = ctx.params;
201
- return await ctx.call('auth.jwt.generateToken', {
179
+ return await ctx.call('auth.jwt.generateServerSignedToken', {
202
180
  payload: {
203
181
  webId
204
182
  }
@@ -206,6 +184,34 @@ const AuthMixin = {
206
184
  }
207
185
  },
208
186
  methods: {
187
+ async validateCapability(ctx, token) {
188
+ // We accept VC Presentations to invoke capabilities here. It must be encoded as JWT.
189
+ // We do not use the VC-JOSE spec to sign and envelop presentations. Instead we go
190
+ // with embedded signatures. This way, the signature persists within the resource.
191
+
192
+ // Decode JTW to JSON.
193
+ const decodedToken = await ctx.call('auth.jwt.decodeToken', { token });
194
+ if (!decodedToken) return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
195
+
196
+ // Verify that decoded JSON token is a valid VC presentation.
197
+ const {
198
+ verified: isCapSignatureVerified,
199
+ presentation: verifiedPresentation,
200
+ presentationResult
201
+ } = await ctx.call('crypto.vc.verifier.verifyCapabilityPresentation', {
202
+ verifiablePresentation: decodedToken,
203
+ options: {
204
+ maxChainLength: ctx.params.route.opts.maxChainLength
205
+ }
206
+ });
207
+ if (!isCapSignatureVerified) return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
208
+
209
+ // VC Capability is verified.
210
+ ctx.meta.webId = presentationResult?.results?.[0]?.purposeResult?.holder || 'anon';
211
+ ctx.meta.authorization = { capabilityPresentation: verifiedPresentation };
212
+
213
+ return Promise.resolve(verifiedPresentation);
214
+ },
209
215
  getStrategy() {
210
216
  throw new Error('getStrategy must be implemented by the service');
211
217
  },
@@ -60,7 +60,7 @@ const AuthSSOMixin = {
60
60
  );
61
61
  }
62
62
 
63
- const token = await ctx.call('auth.jwt.generateToken', { payload: { webId } });
63
+ const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId } });
64
64
 
65
65
  return { token, newUser };
66
66
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@semapps/auth",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Authentification module for SemApps",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Virtual Assembly",
7
7
  "dependencies": {
8
- "@semapps/ldp": "1.1.0",
9
- "@semapps/middlewares": "1.1.0",
10
- "@semapps/mime-types": "1.1.0",
11
- "@semapps/triplestore": "1.1.0",
8
+ "@semapps/ldp": "1.1.2",
9
+ "@semapps/middlewares": "1.1.2",
10
+ "@semapps/mime-types": "1.1.2",
11
+ "@semapps/triplestore": "1.1.2",
12
12
  "bcrypt": "^5.0.1",
13
13
  "express-session": "^1.17.0",
14
14
  "jsonwebtoken": "^8.5.1",
@@ -21,6 +21,7 @@
21
21
  "passport-cas2": "0.0.12",
22
22
  "passport-local": "^1.0.0",
23
23
  "pug": "^3.0.2",
24
+ "speakingurl": "^14.0.1",
24
25
  "url-join": "^4.0.1"
25
26
  },
26
27
  "publishConfig": {
@@ -29,5 +30,5 @@
29
30
  "engines": {
30
31
  "node": ">=14"
31
32
  },
32
- "gitHead": "950728b27cb5ef1c7a396482ec028c27b41db181"
33
+ "gitHead": "35ec922a369a84225e0b7761ef36dbeb8c327316"
33
34
  }
@@ -1,53 +1,88 @@
1
1
  const bcrypt = require('bcrypt');
2
+ const createSlug = require('speakingurl');
2
3
  const DbService = require('moleculer-db');
3
4
  const { TripleStoreAdapter } = require('@semapps/triplestore');
4
5
  const crypto = require('crypto');
5
6
 
7
+ // Taken from https://stackoverflow.com/a/9204568/7900695
8
+ const emailRegexp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+
6
10
  module.exports = {
7
11
  name: 'auth.account',
8
12
  mixins: [DbService],
9
13
  adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: 'settings' }),
10
14
  settings: {
11
15
  idField: '@id',
12
- reservedUsernames: ['relay']
16
+ reservedUsernames: ['relay'],
17
+ minPasswordLength: 1,
18
+ minUsernameLength: 1
13
19
  },
14
20
  dependencies: ['triplestore'],
15
21
  actions: {
16
22
  async create(ctx) {
17
23
  let { uuid, username, password, email, webId, ...rest } = ctx.params;
18
- const hashedPassword = password ? await this.hashPassword(password) : undefined;
19
24
 
20
- email = email && email.toLowerCase();
25
+ // FORMAT AND VERIFY PASSWORD
26
+
27
+ if (password) {
28
+ if (password.length < this.settings.minPasswordLength) {
29
+ throw new Error('password.too-short');
30
+ }
21
31
 
22
- const emailExists = !email ? false : await ctx.call('auth.account.emailExists', { email });
23
- if (emailExists) {
24
- throw new Error('email.already.exists');
32
+ password = await this.hashPassword(password);
25
33
  }
26
34
 
35
+ // FORMAT AND VERIFY EMAIL
36
+
37
+ if (email) {
38
+ email = email.toLowerCase();
39
+
40
+ const emailExists = await ctx.call('auth.account.emailExists', { email });
41
+ if (emailExists) {
42
+ throw new Error('email.already.exists');
43
+ }
44
+
45
+ if (!emailRegexp.test(email)) {
46
+ throw new Error('email.invalid');
47
+ }
48
+ }
49
+
50
+ // FORMAT AND VERIFY USERNAME
51
+
27
52
  if (username) {
28
- if (!ctx.meta.isSystemCall) await this.isValidUsername(ctx, username);
53
+ if (!ctx.meta.isSystemCall) {
54
+ const { isValid, error } = await this.isValidUsername(ctx, username);
55
+ if (!isValid) throw new Error(error);
56
+ }
29
57
  } else if (email) {
30
- // If username is not provided, find an username based on the email
31
- const usernameFromEmail = email.split('@')[0].toLowerCase();
32
- let usernameValid = false;
33
- let i = 0;
34
- do {
35
- username = i === 0 ? usernameFromEmail : usernameFromEmail + i;
36
- try {
37
- usernameValid = await this.isValidUsername(ctx, username);
38
- } catch (e) {
39
- // Do nothing, the loop will continue
58
+ // If username is not provided, find one automatically from the email (without errors)
59
+ username = createSlug(email.split('@')[0].toLowerCase());
60
+
61
+ let { isValid, error } = await this.isValidUsername(ctx, username);
62
+
63
+ if (!isValid) {
64
+ if (error === 'username.invalid' || error === 'username.too-short') {
65
+ // If username generated from email is invalid, use a generic name
66
+ username = 'user';
40
67
  }
41
- i++;
42
- } while (!usernameValid);
43
- } else throw new Error('you must provide at least a username or an email address');
68
+
69
+ // If necessary, add a number after the username
70
+ let i = 0;
71
+ do {
72
+ username = i === 0 ? username : username + i;
73
+ ({ isValid } = await this.isValidUsername(ctx, username));
74
+ } while (!isValid);
75
+ }
76
+ } else {
77
+ throw new Error('You must provide at least a username or an email address');
78
+ }
44
79
 
45
80
  return await this._create(ctx, {
46
81
  ...rest,
47
82
  uuid,
48
83
  username,
49
84
  email,
50
- hashedPassword,
85
+ hashedPassword: password,
51
86
  webId
52
87
  });
53
88
  },
@@ -151,7 +186,8 @@ module.exports = {
151
186
  return account?.username;
152
187
  },
153
188
  async findSettingsByWebId(ctx) {
154
- const webId = ctx.params.webId || ctx.meta.webId;
189
+ const webId = ctx.meta.webId;
190
+
155
191
  const account = await ctx.call('auth.account.findByWebId', { webId });
156
192
 
157
193
  return {
@@ -219,23 +255,29 @@ module.exports = {
219
255
  },
220
256
  methods: {
221
257
  async isValidUsername(ctx, username) {
258
+ let error;
259
+
222
260
  // Ensure the username has no space or special characters
223
261
  if (!/^[a-z0-9\-+_.]+$/.exec(username)) {
224
- throw new Error('username.invalid');
262
+ error = 'username.invalid';
263
+ }
264
+
265
+ if (username.length < this.settings.minUsernameLength) {
266
+ error = 'username.too-short';
225
267
  }
226
268
 
227
269
  // Ensure we don't use reservedUsernames
228
270
  if (this.settings.reservedUsernames.includes(username)) {
229
- throw new Error('username.already.exists');
271
+ error = 'username.reserved';
230
272
  }
231
273
 
232
274
  // Ensure username doesn't already exist
233
275
  const usernameExists = await ctx.call('auth.account.usernameExists', { username });
234
276
  if (usernameExists) {
235
- throw new Error('username.already.exists');
277
+ error = 'username.already.exists';
236
278
  }
237
279
 
238
- return true;
280
+ return { isValid: !error, error };
239
281
  },
240
282
  async hashPassword(password) {
241
283
  return new Promise((resolve, reject) => {
@@ -14,6 +14,8 @@ const AuthLocalService = {
14
14
  jwtPath: null,
15
15
  registrationAllowed: true,
16
16
  reservedUsernames: [],
17
+ minPasswordLength: 1,
18
+ minUsernameLength: 1,
17
19
  webIdSelection: [],
18
20
  accountSelection: [],
19
21
  formUrl: null,
@@ -47,13 +49,10 @@ const AuthLocalService = {
47
49
  actions: {
48
50
  async signup(ctx) {
49
51
  const { username, email, password, ...rest } = ctx.params;
52
+
50
53
  // This is going to get in our way otherwise when waiting for completions.
51
54
  ctx.meta.skipObjectsWatcher = true;
52
55
 
53
- if (username && username.length < 2) {
54
- throw new MoleculerError('The username must be at least 2 characters long', 400, 'BAD_REQUEST');
55
- }
56
-
57
56
  let accountData = await ctx.call('auth.account.create', {
58
57
  username,
59
58
  email,
@@ -62,7 +61,7 @@ const AuthLocalService = {
62
61
  });
63
62
 
64
63
  try {
65
- const profileData = { nick: username, email, ...rest };
64
+ const profileData = { nick: accountData.username, email: accountData.email, ...rest };
66
65
  const webId = await ctx.call('webid.createWebId', this.pickWebIdData(profileData), {
67
66
  meta: {
68
67
  isSignup: true // Allow services to handle directly the webId creation if it is generated by the AuthService
@@ -74,7 +73,7 @@ const AuthLocalService = {
74
73
 
75
74
  ctx.emit('auth.registered', { webId, profileData, accountData });
76
75
 
77
- const token = await ctx.call('auth.jwt.generateToken', { payload: { webId } });
76
+ const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId } });
78
77
 
79
78
  return { token, webId, newUser: true };
80
79
  } catch (e) {
@@ -90,7 +89,7 @@ const AuthLocalService = {
90
89
 
91
90
  ctx.emit('auth.connected', { webId: accountData.webId, accountData }, { meta: { webId: null, dataset: null } });
92
91
 
93
- const token = await ctx.call('auth.jwt.generateToken', { payload: { webId: accountData.webId } });
92
+ const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId: accountData.webId } });
94
93
 
95
94
  return { token, webId: accountData.webId, newUser: false };
96
95
  },
package/services/jwt.js CHANGED
@@ -3,6 +3,13 @@ const path = require('path');
3
3
  const jwt = require('jsonwebtoken');
4
4
  const crypto = require('crypto');
5
5
 
6
+ /**
7
+ * Service that creates and validates JSON web tokens(JWT).
8
+ * Tokens are signed against this server's keys.
9
+ * This is useful for generating/validating authentication tokens.
10
+ *
11
+ * TODO: Tokens do not expire.
12
+ */
6
13
  module.exports = {
7
14
  name: 'auth.jwt',
8
15
  settings: {
@@ -63,11 +70,12 @@ module.exports = {
63
70
  );
64
71
  });
65
72
  },
66
- async generateToken(ctx) {
73
+ async generateServerSignedToken(ctx) {
67
74
  const { payload } = ctx.params;
68
75
  return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });
69
76
  },
70
- async verifyToken(ctx) {
77
+ /** Verifies that the token was signed by this server. */
78
+ async verifyServerSignedToken(ctx) {
71
79
  const { token } = ctx.params;
72
80
  try {
73
81
  return jwt.verify(token, this.publicKey, { algorithms: ['RS256'] });
@@ -75,6 +83,11 @@ module.exports = {
75
83
  return false;
76
84
  }
77
85
  },
86
+ async generateUnsignedToken(ctx) {
87
+ const { payload } = ctx.params;
88
+ const token = jwt.sign(payload, null, { algorithm: 'none' });
89
+ return token;
90
+ },
78
91
  // Warning, this does NOT verify if signature is valid
79
92
  async decodeToken(ctx) {
80
93
  const { token } = ctx.params;
@@ -1,69 +0,0 @@
1
- const { ControlledContainerMixin } = require('@semapps/ldp');
2
- const { MIME_TYPES } = require('@semapps/mime-types');
3
-
4
- const CAPABILITIES_ROUTE = '/capabilities';
5
-
6
- /**
7
- * Service to host the capabilities container.
8
- * @type {import('moleculer').ServiceSchema}
9
- */
10
- const CapabilitiesService = {
11
- name: 'capabilities',
12
- mixins: [ControlledContainerMixin],
13
- settings: {
14
- path: CAPABILITIES_ROUTE,
15
- acceptedTypes: ['acl:Authorization'],
16
- excludeFromMirror: true,
17
- activateTombstones: false,
18
- permissions: {},
19
- newResourcesPermissions: {},
20
- typeIndex: 'private'
21
- },
22
- hooks: {
23
- before: {
24
- /**
25
- * Bypass authorization when getting the resource.
26
- *
27
- * The URI itself is considered the secret. So no more
28
- * authorization is necessary at this point.
29
- * If we decide to support capabilities with multiple
30
- * authorization factors, this will have to change in
31
- * the future.
32
- */
33
- get: ctx => {
34
- ctx.params.webId = 'system';
35
- }
36
- }
37
- },
38
- actions: {
39
- createCapability: {
40
- params: {
41
- webId: { type: 'string', optional: false },
42
- accessTo: { type: 'string', optional: false },
43
- mode: { type: 'string', optional: false }
44
- },
45
- async handler(ctx) {
46
- const { accessTo, mode, webId } = ctx.params;
47
-
48
- const capContainerUri = await this.actions.getContainerUri({ webId }, { parentCtx: ctx });
49
-
50
- const capUri = await this.actions.post(
51
- {
52
- containerUri: capContainerUri,
53
- resource: {
54
- '@type': 'acl:Authorization',
55
- 'acl:accessTo': accessTo,
56
- 'acl:mode': mode
57
- },
58
- contentType: MIME_TYPES.JSON,
59
- webId
60
- },
61
- { parentCtx: ctx }
62
- );
63
- return capUri;
64
- }
65
- }
66
- }
67
- };
68
-
69
- module.exports = CapabilitiesService;