1
0

auth.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { ErrorCode, Permission } from '@vendure/common/lib/generated-types';
  3. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
  4. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  5. import { DocumentNode } from 'graphql';
  6. import path from 'path';
  7. import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
  8. import { initialData } from '../../../e2e-common/e2e-initial-data';
  9. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  10. import { Issue2097Plugin } from './fixtures/test-plugins/issue-2097-plugin';
  11. import { ProtectedFieldsPlugin, transactions } from './fixtures/test-plugins/with-protected-field-resolver';
  12. import {
  13. canCreateCustomerDocument,
  14. deepFieldResolutionTestQueryDocument,
  15. getCustomerCountDocument,
  16. getProductWithTransactionsDocument,
  17. issue2097QueryDocument,
  18. } from './graphql/admin-definitions';
  19. import { currentUserFragment } from './graphql/fragments-admin';
  20. import { ResultOf } from './graphql/graphql-admin';
  21. import {
  22. attemptLoginDocument,
  23. createAdministratorDocument,
  24. createCustomerDocument,
  25. createCustomerGroupDocument,
  26. createProductDocument,
  27. createRoleDocument,
  28. getCustomerListDocument,
  29. getProductListDocument,
  30. getTaxRatesListDocument,
  31. MeDocument,
  32. updateProductDocument,
  33. updateTaxRateDocument,
  34. } from './graphql/shared-definitions';
  35. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  36. const productResultGuard: ErrorResultGuard<
  37. NonNullable<ResultOf<typeof getProductWithTransactionsDocument>['product']>
  38. > = createErrorResultGuard(input => !!input && 'id' in input);
  39. describe('Authorization & permissions', () => {
  40. const { server, adminClient, shopClient } = createTestEnvironment({
  41. ...testConfig(),
  42. plugins: [ProtectedFieldsPlugin, Issue2097Plugin],
  43. });
  44. beforeAll(async () => {
  45. await server.init({
  46. initialData,
  47. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  48. customerCount: 5,
  49. });
  50. await adminClient.asSuperAdmin();
  51. }, TEST_SETUP_TIMEOUT_MS);
  52. afterAll(async () => {
  53. await server.destroy();
  54. });
  55. describe('admin permissions', () => {
  56. describe('Anonymous user', () => {
  57. beforeAll(async () => {
  58. await adminClient.asAnonymousUser();
  59. });
  60. it(
  61. 'me is not permitted',
  62. assertThrowsWithMessage(async () => {
  63. await adminClient.query(MeDocument);
  64. }, 'You are not currently authorized to perform this action'),
  65. );
  66. it('can attempt login', async () => {
  67. await assertRequestAllowed(attemptLoginDocument, {
  68. username: SUPER_ADMIN_USER_IDENTIFIER,
  69. password: SUPER_ADMIN_USER_PASSWORD,
  70. rememberMe: false,
  71. });
  72. });
  73. });
  74. describe('Customer user', () => {
  75. let customerEmailAddress: string;
  76. beforeAll(async () => {
  77. await adminClient.asSuperAdmin();
  78. const { customers } = await adminClient.query(getCustomerListDocument);
  79. customerEmailAddress = customers.items[0].emailAddress;
  80. });
  81. it('cannot login', async () => {
  82. const result = await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
  83. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  84. });
  85. });
  86. describe('ReadCatalog permission', () => {
  87. beforeAll(async () => {
  88. await adminClient.asSuperAdmin();
  89. const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
  90. Permission.ReadCatalog,
  91. ]);
  92. await adminClient.asUserWithCredentials(identifier, password);
  93. });
  94. it('me returns correct permissions', async () => {
  95. const result = await adminClient.query(MeDocument);
  96. expect(result.me!.channels[0].permissions).toEqual([
  97. Permission.Authenticated,
  98. Permission.ReadCatalog,
  99. ]);
  100. });
  101. it('can read', async () => {
  102. await assertRequestAllowed(getProductListDocument);
  103. });
  104. it('cannot update', async () => {
  105. await assertRequestForbidden(updateProductDocument, {
  106. input: {
  107. id: '1',
  108. translations: [],
  109. },
  110. });
  111. });
  112. it('cannot create', async () => {
  113. await assertRequestForbidden(createProductDocument, {
  114. input: {
  115. translations: [],
  116. },
  117. });
  118. });
  119. });
  120. describe('CRUD on Customers permissions', () => {
  121. beforeAll(async () => {
  122. await adminClient.asSuperAdmin();
  123. const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
  124. Permission.CreateCustomer,
  125. Permission.ReadCustomer,
  126. Permission.UpdateCustomer,
  127. Permission.DeleteCustomer,
  128. ]);
  129. await adminClient.asUserWithCredentials(identifier, password);
  130. });
  131. it('me returns correct permissions', async () => {
  132. const result = await adminClient.query(MeDocument);
  133. expect(result.me!.channels[0].permissions).toEqual([
  134. Permission.Authenticated,
  135. Permission.CreateCustomer,
  136. Permission.ReadCustomer,
  137. Permission.UpdateCustomer,
  138. Permission.DeleteCustomer,
  139. ]);
  140. });
  141. it('can create', async () => {
  142. await assertRequestAllowed(canCreateCustomerDocument, {
  143. input: { emailAddress: '', firstName: '', lastName: '' },
  144. });
  145. });
  146. it('can read', async () => {
  147. await assertRequestAllowed(getCustomerCountDocument);
  148. });
  149. });
  150. });
  151. describe('administrator and customer users with the same email address', () => {
  152. const emailAddress = 'same-email@test.com';
  153. const adminPassword = 'admin-password';
  154. const customerPassword = 'customer-password';
  155. const loginErrorGuard: ErrorResultGuard<ResultOf<typeof currentUserFragment>> =
  156. createErrorResultGuard(input => !!input.identifier);
  157. beforeAll(async () => {
  158. await adminClient.asSuperAdmin();
  159. await adminClient.query(createAdministratorDocument, {
  160. input: {
  161. emailAddress,
  162. firstName: 'First',
  163. lastName: 'Last',
  164. password: adminPassword,
  165. roleIds: ['1'],
  166. },
  167. });
  168. await adminClient.query(createCustomerDocument, {
  169. input: {
  170. emailAddress,
  171. firstName: 'First',
  172. lastName: 'Last',
  173. },
  174. password: customerPassword,
  175. });
  176. });
  177. beforeEach(async () => {
  178. await adminClient.asAnonymousUser();
  179. await shopClient.asAnonymousUser();
  180. });
  181. it('can log in as an administrator', async () => {
  182. const loginResult = await adminClient.query(attemptLoginDocument, {
  183. username: emailAddress,
  184. password: adminPassword,
  185. });
  186. loginErrorGuard.assertSuccess(loginResult.login);
  187. expect(loginResult.login.identifier).toEqual(emailAddress);
  188. });
  189. it('can log in as a customer', async () => {
  190. const loginResult = await shopClient.query(attemptLoginDocument, {
  191. username: emailAddress,
  192. password: customerPassword,
  193. });
  194. loginErrorGuard.assertSuccess(loginResult.login);
  195. expect(loginResult.login.identifier).toEqual(emailAddress);
  196. });
  197. it('cannot log in as an administrator using a customer password', async () => {
  198. const loginResult = await adminClient.query(attemptLoginDocument, {
  199. username: emailAddress,
  200. password: customerPassword,
  201. });
  202. loginErrorGuard.assertErrorResult(loginResult.login);
  203. expect(loginResult.login.errorCode).toEqual(ErrorCode.INVALID_CREDENTIALS_ERROR);
  204. });
  205. it('cannot log in as a customer using an administrator password', async () => {
  206. const loginResult = await shopClient.query(attemptLoginDocument, {
  207. username: emailAddress,
  208. password: adminPassword,
  209. });
  210. loginErrorGuard.assertErrorResult(loginResult.login);
  211. expect(loginResult.login.errorCode).toEqual(ErrorCode.INVALID_CREDENTIALS_ERROR);
  212. });
  213. });
  214. describe('protected field resolvers', () => {
  215. let readCatalogAdmin: { identifier: string; password: string };
  216. let transactionsAdmin: { identifier: string; password: string };
  217. beforeAll(async () => {
  218. await adminClient.asSuperAdmin();
  219. transactionsAdmin = await createAdministratorWithPermissions('Transactions', [
  220. Permission.ReadCatalog,
  221. transactions.Permission,
  222. ]);
  223. readCatalogAdmin = await createAdministratorWithPermissions('ReadCatalog', [
  224. Permission.ReadCatalog,
  225. ]);
  226. });
  227. it('protected field not resolved without permissions', async () => {
  228. await adminClient.asUserWithCredentials(readCatalogAdmin.identifier, readCatalogAdmin.password);
  229. try {
  230. const status = await adminClient.query(getProductWithTransactionsDocument, { id: 'T_1' });
  231. fail('Should have thrown');
  232. } catch (e: any) {
  233. expect(getErrorCode(e)).toBe('FORBIDDEN');
  234. }
  235. });
  236. it('protected field is resolved with permissions', async () => {
  237. await adminClient.asUserWithCredentials(transactionsAdmin.identifier, transactionsAdmin.password);
  238. const { product } = await adminClient.query(getProductWithTransactionsDocument, { id: 'T_1' });
  239. productResultGuard.assertSuccess(product);
  240. expect(product.id).toBe('T_1');
  241. expect(product.transactions).toEqual([
  242. { id: 'T_1', amount: 100, description: 'credit' },
  243. { id: 'T_2', amount: -50, description: 'debit' },
  244. ]);
  245. });
  246. // https://github.com/vendure-ecommerce/vendure/issues/730
  247. it('protects against deep query data leakage', async () => {
  248. await adminClient.asSuperAdmin();
  249. const { createCustomerGroup } = await adminClient.query(createCustomerGroupDocument, {
  250. input: {
  251. name: 'Test group',
  252. customerIds: ['T_1', 'T_2', 'T_3', 'T_4'],
  253. },
  254. });
  255. const taxRateName = `Standard Tax ${initialData.defaultZone}`;
  256. const { taxRates } = await adminClient.query(getTaxRatesListDocument, {
  257. options: {
  258. filter: {
  259. name: { eq: taxRateName },
  260. },
  261. },
  262. });
  263. const standardTax = taxRates.items[0];
  264. expect(standardTax.name).toBe(taxRateName);
  265. await adminClient.query(updateTaxRateDocument, {
  266. input: {
  267. id: standardTax.id,
  268. customerGroupId: createCustomerGroup.id,
  269. },
  270. });
  271. try {
  272. const status = await shopClient.query(deepFieldResolutionTestQueryDocument, { id: 'T_1' });
  273. fail('Should have thrown');
  274. } catch (e: any) {
  275. expect(getErrorCode(e)).toBe('FORBIDDEN');
  276. }
  277. });
  278. // https://github.com/vendure-ecommerce/vendure/issues/2097
  279. it('does not overwrite ctx.authorizedAsOwnerOnly with multiple parallel top-level queries', async () => {
  280. // We run this multiple times since the error is based on a race condition that does not
  281. // show up consistently.
  282. for (let i = 0; i < 10; i++) {
  283. const result = await shopClient.query(issue2097QueryDocument);
  284. expect(result.ownerProtectedThing).toBe(true);
  285. expect(result.publicThing).toBe(true);
  286. }
  287. });
  288. });
  289. async function assertRequestAllowed(operation: DocumentNode, variables?: any) {
  290. try {
  291. const status = await adminClient.queryStatus(operation, variables);
  292. expect(status).toBe(200);
  293. } catch (e: any) {
  294. const errorCode = getErrorCode(e);
  295. if (!errorCode) {
  296. fail(`Unexpected failure: ${JSON.stringify(e)}`);
  297. } else {
  298. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  299. }
  300. }
  301. }
  302. async function assertRequestForbidden(operation: DocumentNode, variables?: any) {
  303. try {
  304. const status = await adminClient.query(operation, variables);
  305. fail('Should have thrown');
  306. } catch (e: any) {
  307. expect(getErrorCode(e)).toBe('FORBIDDEN');
  308. }
  309. }
  310. function getErrorCode(err: any): string {
  311. return err.response.errors[0].extensions.code;
  312. }
  313. async function createAdministratorWithPermissions(
  314. code: string,
  315. permissions: Permission[],
  316. ): Promise<{ identifier: string; password: string }> {
  317. const roleResult = await adminClient.query(createRoleDocument, {
  318. input: {
  319. code,
  320. description: '',
  321. permissions,
  322. },
  323. });
  324. const role = roleResult.createRole;
  325. const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
  326. const password = 'test';
  327. await adminClient.query(createAdministratorDocument, {
  328. input: {
  329. emailAddress: identifier,
  330. firstName: code,
  331. lastName: 'Admin',
  332. password,
  333. roleIds: [role.id],
  334. },
  335. });
  336. return {
  337. identifier,
  338. password,
  339. };
  340. }
  341. });