directus 9.19.2 → 9.20.1

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.
@@ -2,3 +2,4 @@ export * from './local';
2
2
  export * from './oauth2';
3
3
  export * from './openid';
4
4
  export * from './ldap';
5
+ export * from './saml';
@@ -18,3 +18,4 @@ __exportStar(require("./local"), exports);
18
18
  __exportStar(require("./oauth2"), exports);
19
19
  __exportStar(require("./openid"), exports);
20
20
  __exportStar(require("./ldap"), exports);
21
+ __exportStar(require("./saml"), exports);
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
@@ -6,7 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
29
  exports.createOAuth2AuthRouter = exports.OAuth2AuthDriver = void 0;
7
30
  const exceptions_1 = require("@directus/shared/exceptions");
8
31
  const utils_1 = require("@directus/shared/utils");
9
- const express_1 = require("express");
32
+ const express_1 = __importStar(require("express"));
10
33
  const flat_1 = __importDefault(require("flat"));
11
34
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
12
35
  const ms_1 = __importDefault(require("ms"));
@@ -81,8 +104,8 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
81
104
  return user === null || user === void 0 ? void 0 : user.id;
82
105
  }
83
106
  async getUserID(payload) {
84
- if (!payload.code || !payload.codeVerifier) {
85
- logger_1.default.trace('[OAuth2] No code or codeVerifier in payload');
107
+ if (!payload.code || !payload.codeVerifier || !payload.state) {
108
+ logger_1.default.warn('[OAuth2] No code, codeVerifier or state in payload');
86
109
  throw new exceptions_2.InvalidCredentialsException();
87
110
  }
88
111
  let tokenSet;
@@ -116,7 +139,7 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
116
139
  }
117
140
  // Is public registration allowed?
118
141
  if (!allowPublicRegistration) {
119
- logger_1.default.trace(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${provider}"`);
142
+ logger_1.default.warn(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${provider}"`);
120
143
  throw new exceptions_2.InvalidCredentialsException();
121
144
  }
122
145
  try {
@@ -207,6 +230,9 @@ function createOAuth2AuthRouter(providerName) {
207
230
  });
208
231
  return res.redirect(provider.generateAuthUrl(codeVerifier, prompt));
209
232
  }, respond_1.respond);
233
+ router.post('/callback', express_1.default.urlencoded({ extended: false }), (req, res) => {
234
+ res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
235
+ }, respond_1.respond);
210
236
  router.get('/callback', (0, async_handler_1.default)(async (req, res, next) => {
211
237
  var _a;
212
238
  let tokenData;
@@ -230,9 +256,6 @@ function createOAuth2AuthRouter(providerName) {
230
256
  let authResponse;
231
257
  try {
232
258
  res.clearCookie(`oauth2.${providerName}`);
233
- if (!req.query.code || !req.query.state) {
234
- logger_1.default.warn(`[OAuth2] Couldn't extract OAuth2 code or state from query: ${JSON.stringify(req.query)}`);
235
- }
236
259
  authResponse = await authenticationService.login(providerName, {
237
260
  code: req.query.code,
238
261
  codeVerifier: verifier,
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
@@ -6,7 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
29
  exports.createOpenIDAuthRouter = exports.OpenIDAuthDriver = void 0;
7
30
  const exceptions_1 = require("@directus/shared/exceptions");
8
31
  const utils_1 = require("@directus/shared/utils");
9
- const express_1 = require("express");
32
+ const express_1 = __importStar(require("express"));
10
33
  const flat_1 = __importDefault(require("flat"));
11
34
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
12
35
  const ms_1 = __importDefault(require("ms"));
@@ -91,8 +114,8 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
91
114
  return user === null || user === void 0 ? void 0 : user.id;
92
115
  }
93
116
  async getUserID(payload) {
94
- if (!payload.code || !payload.codeVerifier) {
95
- logger_1.default.trace('[OpenID] No code or codeVerifier in payload');
117
+ if (!payload.code || !payload.codeVerifier || !payload.state) {
118
+ logger_1.default.warn('[OpenID] No code, codeVerifier or state in payload');
96
119
  throw new exceptions_2.InvalidCredentialsException();
97
120
  }
98
121
  let tokenSet;
@@ -134,7 +157,7 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
134
157
  const isEmailVerified = !requireVerifiedEmail || userInfo.email_verified;
135
158
  // Is public registration allowed?
136
159
  if (!allowPublicRegistration || !isEmailVerified) {
137
- logger_1.default.trace(`[OpenID] User doesn't exist, and public registration not allowed for provider "${provider}"`);
160
+ logger_1.default.warn(`[OpenID] User doesn't exist, and public registration not allowed for provider "${provider}"`);
138
161
  throw new exceptions_2.InvalidCredentialsException();
139
162
  }
140
163
  try {
@@ -226,6 +249,9 @@ function createOpenIDAuthRouter(providerName) {
226
249
  });
227
250
  return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
228
251
  }), respond_1.respond);
252
+ router.post('/callback', express_1.default.urlencoded({ extended: false }), (req, res) => {
253
+ res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
254
+ }, respond_1.respond);
229
255
  router.get('/callback', (0, async_handler_1.default)(async (req, res, next) => {
230
256
  var _a;
231
257
  let tokenData;
@@ -249,9 +275,6 @@ function createOpenIDAuthRouter(providerName) {
249
275
  let authResponse;
250
276
  try {
251
277
  res.clearCookie(`openid.${providerName}`);
252
- if (!req.query.code || !req.query.state) {
253
- logger_1.default.warn(`[OpenID] Couldn't extract OpenID code or state from query: ${JSON.stringify(req.query)}`);
254
- }
255
278
  authResponse = await authenticationService.login(providerName, {
256
279
  code: req.query.code,
257
280
  codeVerifier: verifier,
@@ -0,0 +1,14 @@
1
+ import { UsersService } from '../../services';
2
+ import { AuthDriverOptions, User } from '../../types';
3
+ import { LocalAuthDriver } from './local';
4
+ export declare class SAMLAuthDriver extends LocalAuthDriver {
5
+ idp: any;
6
+ sp: any;
7
+ usersService: UsersService;
8
+ config: Record<string, any>;
9
+ constructor(options: AuthDriverOptions, config: Record<string, any>);
10
+ fetchUserID(identifier: string): Promise<any>;
11
+ getUserID(payload: Record<string, any>): Promise<any>;
12
+ login(_user: User): Promise<void>;
13
+ }
14
+ export declare function createSAMLAuthRouter(providerName: string): import("express-serve-static-core").Router;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.createSAMLAuthRouter = exports.SAMLAuthDriver = void 0;
30
+ const validator = __importStar(require("@authenio/samlify-node-xmllint"));
31
+ const exceptions_1 = require("@directus/shared/exceptions");
32
+ const express_1 = __importStar(require("express"));
33
+ const samlify = __importStar(require("samlify"));
34
+ const auth_1 = require("../../auth");
35
+ const constants_1 = require("../../constants");
36
+ const env_1 = __importDefault(require("../../env"));
37
+ const exceptions_2 = require("../../exceptions");
38
+ const record_not_unique_1 = require("../../exceptions/database/record-not-unique");
39
+ const logger_1 = __importDefault(require("../../logger"));
40
+ const respond_1 = require("../../middleware/respond");
41
+ const services_1 = require("../../services");
42
+ const async_handler_1 = __importDefault(require("../../utils/async-handler"));
43
+ const get_config_from_env_1 = require("../../utils/get-config-from-env");
44
+ const local_1 = require("./local");
45
+ // tell samlify to use validator...
46
+ samlify.setSchemaValidator(validator);
47
+ class SAMLAuthDriver extends local_1.LocalAuthDriver {
48
+ constructor(options, config) {
49
+ super(options, config);
50
+ this.config = config;
51
+ this.usersService = new services_1.UsersService({ knex: this.knex, schema: this.schema });
52
+ this.sp = samlify.ServiceProvider((0, get_config_from_env_1.getConfigFromEnv)(`AUTH_${config.provider.toUpperCase()}_SP`));
53
+ this.idp = samlify.IdentityProvider((0, get_config_from_env_1.getConfigFromEnv)(`AUTH_${config.provider.toUpperCase()}_IDP`));
54
+ }
55
+ async fetchUserID(identifier) {
56
+ const user = await this.knex
57
+ .select('id')
58
+ .from('directus_users')
59
+ .whereRaw('LOWER(??) = ?', ['external_identifier', identifier.toLowerCase()])
60
+ .first();
61
+ return user === null || user === void 0 ? void 0 : user.id;
62
+ }
63
+ async getUserID(payload) {
64
+ const { provider, emailKey, identifierKey, givenNameKey, familyNameKey, allowPublicRegistration } = this.config;
65
+ const email = payload[emailKey !== null && emailKey !== void 0 ? emailKey : 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'];
66
+ const identifier = payload[identifierKey !== null && identifierKey !== void 0 ? identifierKey : 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'];
67
+ const userID = await this.fetchUserID(identifier);
68
+ if (userID)
69
+ return userID;
70
+ if (!allowPublicRegistration) {
71
+ logger_1.default.trace(`[SAML] User doesn't exist, and public registration not allowed for provider "${provider}"`);
72
+ throw new exceptions_2.InvalidCredentialsException();
73
+ }
74
+ const firstName = payload[givenNameKey !== null && givenNameKey !== void 0 ? givenNameKey : 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'];
75
+ const lastName = payload[familyNameKey !== null && familyNameKey !== void 0 ? familyNameKey : 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'];
76
+ try {
77
+ return await this.usersService.createOne({
78
+ provider,
79
+ first_name: firstName,
80
+ last_name: lastName,
81
+ email: email,
82
+ external_identifier: identifier.toLowerCase(),
83
+ role: this.config.defaultRoleId,
84
+ });
85
+ }
86
+ catch (error) {
87
+ if (error instanceof record_not_unique_1.RecordNotUniqueException) {
88
+ logger_1.default.warn(error, '[SAML] Failed to register user. User not unique');
89
+ throw new exceptions_2.InvalidProviderException();
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+ // There's no local checks to be done when the user is authenticated in the IDP
95
+ async login(_user) {
96
+ return;
97
+ }
98
+ }
99
+ exports.SAMLAuthDriver = SAMLAuthDriver;
100
+ function createSAMLAuthRouter(providerName) {
101
+ const router = (0, express_1.Router)();
102
+ router.get('/metadata', (0, async_handler_1.default)(async (_req, res) => {
103
+ const { sp } = (0, auth_1.getAuthProvider)(providerName);
104
+ return res.header('Content-Type', 'text/xml').send(sp.getMetadata());
105
+ }));
106
+ router.get('/', (0, async_handler_1.default)(async (req, res) => {
107
+ const { sp, idp } = (0, auth_1.getAuthProvider)(providerName);
108
+ const { context: url } = await sp.createLoginRequest(idp, 'redirect');
109
+ const parsedUrl = new URL(url);
110
+ if (req.query.redirect) {
111
+ parsedUrl.searchParams.append('RelayState', req.query.redirect);
112
+ }
113
+ return res.redirect(parsedUrl.toString());
114
+ }));
115
+ router.post('/logout', (0, async_handler_1.default)(async (req, res) => {
116
+ const { sp, idp } = (0, auth_1.getAuthProvider)(providerName);
117
+ const { context } = await sp.createLogoutRequest(idp, 'redirect', req.body);
118
+ const authService = new services_1.AuthenticationService({ accountability: req.accountability, schema: req.schema });
119
+ if (req.cookies[env_1.default.REFRESH_TOKEN_COOKIE_NAME]) {
120
+ const currentRefreshToken = req.cookies[env_1.default.REFRESH_TOKEN_COOKIE_NAME];
121
+ if (currentRefreshToken) {
122
+ await authService.logout(currentRefreshToken);
123
+ res.clearCookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, constants_1.COOKIE_OPTIONS);
124
+ }
125
+ }
126
+ return res.redirect(context);
127
+ }));
128
+ router.post('/acs', express_1.default.urlencoded({ extended: false }), (0, async_handler_1.default)(async (req, res, next) => {
129
+ var _a;
130
+ const relayState = (_a = req.body) === null || _a === void 0 ? void 0 : _a.RelayState;
131
+ try {
132
+ const { sp, idp } = (0, auth_1.getAuthProvider)(providerName);
133
+ const { extract } = await sp.parseLoginResponse(idp, 'post', req);
134
+ const authService = new services_1.AuthenticationService({ accountability: req.accountability, schema: req.schema });
135
+ const { accessToken, refreshToken, expires } = await authService.login(providerName, extract.attributes);
136
+ res.locals.payload = {
137
+ data: {
138
+ access_token: accessToken,
139
+ refresh_token: refreshToken,
140
+ expires,
141
+ },
142
+ };
143
+ if (relayState) {
144
+ res.cookie(env_1.default.REFRESH_TOKEN_COOKIE_NAME, refreshToken, constants_1.COOKIE_OPTIONS);
145
+ return res.redirect(relayState);
146
+ }
147
+ return next();
148
+ }
149
+ catch (error) {
150
+ if (relayState) {
151
+ let reason = 'UNKNOWN_EXCEPTION';
152
+ if (error instanceof exceptions_1.BaseException) {
153
+ reason = error.code;
154
+ }
155
+ else {
156
+ logger_1.default.warn(error, `[SAML] Unexpected error during SAML login`);
157
+ }
158
+ return res.redirect(`${relayState.split('?')[0]}?reason=${reason}`);
159
+ }
160
+ logger_1.default.warn(error, `[SAML] Unexpected error during SAML login`);
161
+ throw error;
162
+ }
163
+ }), respond_1.respond);
164
+ return router;
165
+ }
166
+ exports.createSAMLAuthRouter = createSAMLAuthRouter;
package/dist/auth.js CHANGED
@@ -63,5 +63,7 @@ function getProviderInstance(driver, options, config = {}) {
63
63
  return new drivers_1.OpenIDAuthDriver(options, config);
64
64
  case 'ldap':
65
65
  return new drivers_1.LDAPAuthDriver(options, config);
66
+ case 'saml':
67
+ return new drivers_1.SAMLAuthDriver(options, config);
66
68
  }
67
69
  }
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const path_1 = __importDefault(require("path"));
3
7
  const index_1 = require("./index");
4
8
  jest.mock('../../src/env', () => ({
5
9
  ...jest.requireActual('../../src/env').default,
@@ -16,7 +20,8 @@ jest.mock('@directus/shared/utils/node/get-extensions', () => ({
16
20
  getPackageExtensions: jest.fn(() => Promise.resolve([])),
17
21
  getLocalExtensions: jest.fn(() => Promise.resolve([customCliExtension])),
18
22
  }));
19
- jest.mock(`/hooks/custom-cli/index.js`, () => customCliHook, { virtual: true });
23
+ const customHookPath = path_1.default.resolve('/hooks/custom-cli', 'index.js');
24
+ jest.doMock(customHookPath, () => customCliHook, { virtual: true });
20
25
  const customCliExtension = {
21
26
  path: `/hooks/custom-cli`,
22
27
  name: 'custom-cli',
@@ -32,6 +32,9 @@ for (const authProvider of authProviders) {
32
32
  case 'ldap':
33
33
  authRouter = (0, drivers_1.createLDAPAuthRouter)(authProvider.name);
34
34
  break;
35
+ case 'saml':
36
+ authRouter = (0, drivers_1.createSAMLAuthRouter)(authProvider.name);
37
+ break;
35
38
  }
36
39
  if (!authRouter) {
37
40
  logger_1.default.warn(`Couldn't create login router for auth provider "${authProvider.name}"`);
@@ -101,9 +101,6 @@ const multipartHandler = (req, res, next) => {
101
101
  };
102
102
  exports.multipartHandler = multipartHandler;
103
103
  router.post('/', (0, async_handler_1.default)(exports.multipartHandler), (0, async_handler_1.default)(async (req, res, next) => {
104
- if (req.is('multipart/form-data') === false) {
105
- throw new exceptions_1.UnsupportedMediaTypeException(`Unsupported Content-Type header`);
106
- }
107
104
  const service = new services_1.FilesService({
108
105
  accountability: req.accountability,
109
106
  schema: req.schema,
@@ -9,9 +9,6 @@ fields:
9
9
  options:
10
10
  iconRight: title
11
11
  placeholder: $t:field_options.directus_settings.project_name_placeholder
12
- translations:
13
- language: en-US
14
- translations: Name
15
12
  width: half
16
13
 
17
14
  - field: project_descriptor
@@ -19,9 +16,6 @@ fields:
19
16
  options:
20
17
  iconRight: title
21
18
  placeholder: $t:field_options.directus_settings.project_name_placeholder
22
- translations:
23
- language: en-US
24
- translations: Name
25
19
  width: half
26
20
 
27
21
  - field: project_url
@@ -29,9 +23,6 @@ fields:
29
23
  options:
30
24
  iconRight: link
31
25
  placeholder: https://example.com
32
- translations:
33
- language: en-US
34
- translations: Website
35
26
  width: half
36
27
 
37
28
  - field: default_language
@@ -39,9 +30,6 @@ fields:
39
30
  options:
40
31
  iconRight: language
41
32
  placeholder: en-US
42
- translations:
43
- language: en-US
44
- translations: Default Language
45
33
  width: half
46
34
 
47
35
  - field: branding_divider
@@ -57,31 +45,20 @@ fields:
57
45
  - field: project_color
58
46
  interface: select-color
59
47
  note: $t:field_options.directus_settings.project_color_note
60
- translations:
61
- language: en-US
62
- translations: Brand Color
63
48
  width: half
64
49
 
65
50
  - field: project_logo
66
51
  interface: file
67
52
  note: $t:field_options.directus_settings.project_logo_note
68
- translations:
69
- language: en-US
70
- translations: Brand Logo
71
53
  width: half
72
54
 
73
55
  - field: public_foreground
74
56
  interface: file
75
- translations:
76
- language: en-US
77
- translations: Login Foreground
78
57
  width: half
79
58
 
80
59
  - field: public_background
81
60
  interface: file
82
61
  translations:
83
- language: en-US
84
- translations: Login Background
85
62
  width: half
86
63
 
87
64
  - field: public_note
package/dist/env.js CHANGED
@@ -140,6 +140,8 @@ const allowedEnvironmentVars = [
140
140
  'AUTH_.+_GROUP_DN',
141
141
  'AUTH_.+_GROUP_ATTRIBUTE',
142
142
  'AUTH_.+_GROUP_SCOPE',
143
+ 'AUTH_.+_IDP.+',
144
+ 'AUTH_.+_SP.+',
143
145
  // extensions
144
146
  'EXTENSIONS_PATH',
145
147
  'EXTENSIONS_AUTO_RELOAD',
@@ -290,8 +290,10 @@ class AuthorizationService {
290
290
  for (const field of requiredPermissions[collection]) {
291
291
  if (field.startsWith('$FOLLOW'))
292
292
  continue;
293
- if (!allowedFields.includes(field))
293
+ const fieldName = (0, strip_function_1.stripFunction)(field);
294
+ if (!allowedFields.includes(fieldName)) {
294
295
  throw new exceptions_2.ForbiddenException();
296
+ }
295
297
  }
296
298
  }
297
299
  }
@@ -303,6 +303,11 @@ class FieldsService {
303
303
  const record = field.meta
304
304
  ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()
305
305
  : null;
306
+ if ((hookAdjustedField.type === 'alias' ||
307
+ this.schema.collections[collection].fields[field.field].type === 'alias') &&
308
+ hookAdjustedField.type !== this.schema.collections[collection].fields[field.field].type) {
309
+ throw new exceptions_1.InvalidPayloadException('Alias type cannot be changed');
310
+ }
306
311
  if (hookAdjustedField.schema) {
307
312
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
308
313
  if (!(0, lodash_1.isEqual)(existingColumn, hookAdjustedField.schema)) {
@@ -390,7 +395,10 @@ class FieldsService {
390
395
  // If the current field is a m2o, delete the related o2m if it exists and remove the relationship
391
396
  if (isM2O) {
392
397
  await relationsService.deleteOne(collection, field);
393
- if (relation.related_collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field)) {
398
+ if (relation.related_collection &&
399
+ ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) &&
400
+ relation.related_collection !== collection &&
401
+ relation.meta.one_field !== field) {
394
402
  await fieldsService.deleteField(relation.related_collection, relation.meta.one_field);
395
403
  }
396
404
  }
@@ -18,6 +18,11 @@ export declare class FilesService extends ItemsService {
18
18
  * Import a single file from an external URL
19
19
  */
20
20
  importOne(importURL: string, body: Partial<File>): Promise<PrimaryKey>;
21
+ /**
22
+ * Create a file (only applicable when it is not a multipart/data POST request)
23
+ * Useful for associating metadata with existing file in storage
24
+ */
25
+ createOne(data: Partial<File>, opts?: MutationOptions): Promise<PrimaryKey>;
21
26
  /**
22
27
  * Delete a file
23
28
  */
@@ -251,6 +251,17 @@ class FilesService extends items_1.ItemsService {
251
251
  };
252
252
  return await this.uploadOne(fileResponse.data, payload);
253
253
  }
254
+ /**
255
+ * Create a file (only applicable when it is not a multipart/data POST request)
256
+ * Useful for associating metadata with existing file in storage
257
+ */
258
+ async createOne(data, opts) {
259
+ if (!data.type) {
260
+ throw new exceptions_1.InvalidPayloadException(`"type" is required`);
261
+ }
262
+ const key = await super.createOne(data, opts);
263
+ return key;
264
+ }
254
265
  /**
255
266
  * Delete a file
256
267
  */
@@ -7,6 +7,7 @@ const exifr_1 = __importDefault(require("exifr"));
7
7
  const knex_1 = __importDefault(require("knex"));
8
8
  const knex_mock_client_1 = require("knex-mock-client");
9
9
  const _1 = require(".");
10
+ const exceptions_1 = require("../exceptions");
10
11
  jest.mock('exifr');
11
12
  jest.mock('../../src/database/index', () => {
12
13
  return { getDatabaseClient: jest.fn().mockReturnValue('postgres') };
@@ -21,8 +22,43 @@ describe('Integration Tests', () => {
21
22
  });
22
23
  afterEach(() => {
23
24
  tracker.reset();
25
+ jest.clearAllMocks();
24
26
  });
25
27
  describe('Services / Files', () => {
28
+ describe('createOne', () => {
29
+ let service;
30
+ let superCreateOne;
31
+ beforeEach(() => {
32
+ service = new _1.FilesService({
33
+ knex: db,
34
+ schema: { collections: {}, relations: [] },
35
+ });
36
+ superCreateOne = jest.spyOn(_1.ItemsService.prototype, 'createOne').mockImplementation(jest.fn());
37
+ });
38
+ it('throws InvalidPayloadException when "type" is not provided', async () => {
39
+ try {
40
+ await service.createOne({
41
+ title: 'Test File',
42
+ storage: 'local',
43
+ filename_download: 'test_file',
44
+ });
45
+ }
46
+ catch (err) {
47
+ expect(err).toBeInstanceOf(exceptions_1.InvalidPayloadException);
48
+ expect(err.message).toBe('"type" is required');
49
+ }
50
+ expect(superCreateOne).not.toHaveBeenCalled();
51
+ });
52
+ it('creates a file entry when "type" is provided', async () => {
53
+ await service.createOne({
54
+ title: 'Test File',
55
+ storage: 'local',
56
+ filename_download: 'test_file',
57
+ type: 'application/octet-stream',
58
+ });
59
+ expect(superCreateOne).toHaveBeenCalled();
60
+ });
61
+ });
26
62
  describe('getMetadata', () => {
27
63
  let service;
28
64
  let exifrParseSpy;
@@ -51,6 +51,7 @@ const string_or_float_1 = require("./types/string-or-float");
51
51
  const void_1 = require("./types/void");
52
52
  const add_path_to_validation_error_1 = require("./utils/add-path-to-validation-error");
53
53
  const hash_1 = require("./types/hash");
54
+ const bigint_1 = require("./types/bigint");
54
55
  const validationRules = Array.from(graphql_1.specifiedRules);
55
56
  if (env_1.default.GRAPHQL_INTROSPECTION === false) {
56
57
  validationRules.push(graphql_1.NoSchemaIntrospectionCustomRule);
@@ -668,6 +669,7 @@ class GraphQLService {
668
669
  case graphql_1.GraphQLBoolean:
669
670
  filterOperatorType = BooleanFilterOperators;
670
671
  break;
672
+ case bigint_1.GraphQLBigInt:
671
673
  case graphql_1.GraphQLInt:
672
674
  case graphql_1.GraphQLFloat:
673
675
  filterOperatorType = NumberFilterOperators;
@@ -717,6 +719,7 @@ class GraphQLService {
717
719
  fields: Object.values(collection.fields).reduce((acc, field) => {
718
720
  const graphqlType = (0, get_graphql_type_1.getGraphQLType)(field.type, field.special);
719
721
  switch (graphqlType) {
722
+ case bigint_1.GraphQLBigInt:
720
723
  case graphql_1.GraphQLInt:
721
724
  case graphql_1.GraphQLFloat:
722
725
  acc[field.field] = {
@@ -0,0 +1,2 @@
1
+ import { GraphQLScalarType } from 'graphql';
2
+ export declare const GraphQLBigInt: GraphQLScalarType<string | number, string>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphQLBigInt = void 0;
4
+ const graphql_1 = require("graphql");
5
+ exports.GraphQLBigInt = new graphql_1.GraphQLScalarType({
6
+ name: 'GraphQLBigInt',
7
+ description: 'BigInt value',
8
+ serialize(value) {
9
+ if (typeof value !== 'number') {
10
+ throw new Error('Value must be a Number');
11
+ }
12
+ return value.toString();
13
+ },
14
+ parseValue(value) {
15
+ if (typeof value !== 'string') {
16
+ throw new Error('Value must be a String');
17
+ }
18
+ return parseNumberValue(value);
19
+ },
20
+ parseLiteral(ast) {
21
+ if (ast.kind !== graphql_1.Kind.STRING) {
22
+ throw new Error('Value must be a String');
23
+ }
24
+ return parseNumberValue(ast.value);
25
+ },
26
+ });
27
+ function parseNumberValue(input) {
28
+ if (!/[+-]?([0-9]+[.])?[0-9]+/.test(input))
29
+ return input;
30
+ const value = parseInt(input);
31
+ if (isNaN(value) || value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
32
+ throw new Error('Invalid GraphQLBigInt');
33
+ }
34
+ return value;
35
+ }
@@ -6,6 +6,7 @@ const graphql_compose_1 = require("graphql-compose");
6
6
  const date_1 = require("../services/graphql/types/date");
7
7
  const geojson_1 = require("../services/graphql/types/geojson");
8
8
  const hash_1 = require("../services/graphql/types/hash");
9
+ const bigint_1 = require("../services/graphql/types/bigint");
9
10
  function getGraphQLType(localType, special) {
10
11
  if (special.includes('conceal')) {
11
12
  return hash_1.GraphQLHash;
@@ -14,7 +15,7 @@ function getGraphQLType(localType, special) {
14
15
  case 'boolean':
15
16
  return graphql_1.GraphQLBoolean;
16
17
  case 'bigInteger':
17
- return graphql_1.GraphQLString;
18
+ return bigint_1.GraphQLBigInt;
18
19
  case 'integer':
19
20
  return graphql_1.GraphQLInt;
20
21
  case 'decimal':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.19.2",
3
+ "version": "9.20.1",
4
4
  "license": "GPL-3.0-only",
5
5
  "homepage": "https://github.com/directus/directus#readme",
6
6
  "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -65,17 +65,18 @@
65
65
  "README.md"
66
66
  ],
67
67
  "dependencies": {
68
+ "@authenio/samlify-node-xmllint": "2.0.0",
68
69
  "@aws-sdk/client-ses": "3.190.0",
69
- "@directus/app": "9.19.2",
70
- "@directus/drive": "9.19.2",
71
- "@directus/drive-azure": "9.19.2",
72
- "@directus/drive-gcs": "9.19.2",
73
- "@directus/drive-s3": "9.19.2",
74
- "@directus/extensions-sdk": "9.19.2",
70
+ "@directus/app": "9.20.1",
71
+ "@directus/drive": "9.20.1",
72
+ "@directus/drive-azure": "9.20.1",
73
+ "@directus/drive-gcs": "9.20.1",
74
+ "@directus/drive-s3": "9.20.0",
75
+ "@directus/extensions-sdk": "9.20.1",
75
76
  "@directus/format-title": "9.15.0",
76
- "@directus/schema": "9.19.2",
77
- "@directus/shared": "9.19.2",
78
- "@directus/specs": "9.19.2",
77
+ "@directus/schema": "9.20.1",
78
+ "@directus/shared": "9.20.1",
79
+ "@directus/specs": "9.20.1",
79
80
  "@godaddy/terminus": "4.11.2",
80
81
  "@rollup/plugin-alias": "4.0.0",
81
82
  "@rollup/plugin-virtual": "3.0.0",
@@ -142,6 +143,7 @@
142
143
  "rate-limiter-flexible": "2.3.12",
143
144
  "resolve-cwd": "3.0.0",
144
145
  "rollup": "3.2.3",
146
+ "samlify": "2.8.6",
145
147
  "sanitize-html": "2.7.2",
146
148
  "sharp": "0.31.1",
147
149
  "snappy": "7.2.0",
@@ -169,7 +171,6 @@
169
171
  "devDependencies": {
170
172
  "@otplib/preset-default": "12.0.1",
171
173
  "@types/async": "3.2.15",
172
- "@types/body-parser": "1.19.2",
173
174
  "@types/busboy": "1.5.0",
174
175
  "@types/bytes": "3.1.1",
175
176
  "@types/cookie-parser": "1.4.3",