auth.e2e-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
  3. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  4. import { DocumentNode } from 'graphql';
  5. import gql from 'graphql-tag';
  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 * as Codegen from './graphql/generated-e2e-admin-types';
  13. import { ErrorCode, Permission } from './graphql/generated-e2e-admin-types';
  14. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  15. import {
  16. ATTEMPT_LOGIN,
  17. CREATE_ADMINISTRATOR,
  18. CREATE_CUSTOMER,
  19. CREATE_CUSTOMER_GROUP,
  20. CREATE_PRODUCT,
  21. CREATE_ROLE,
  22. GET_CUSTOMER_LIST,
  23. GET_PRODUCT_LIST,
  24. GET_TAX_RATES_LIST,
  25. ME,
  26. UPDATE_PRODUCT,
  27. UPDATE_TAX_RATE,
  28. } from './graphql/shared-definitions';
  29. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  30. describe('Authorization & permissions', () => {
  31. const { server, adminClient, shopClient } = createTestEnvironment({
  32. ...testConfig(),
  33. plugins: [ProtectedFieldsPlugin, Issue2097Plugin],
  34. });
  35. beforeAll(async () => {
  36. await server.init({
  37. initialData,
  38. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  39. customerCount: 5,
  40. });
  41. await adminClient.asSuperAdmin();
  42. }, TEST_SETUP_TIMEOUT_MS);
  43. afterAll(async () => {
  44. await server.destroy();
  45. });
  46. describe('admin permissions', () => {
  47. describe('Anonymous user', () => {
  48. beforeAll(async () => {
  49. await adminClient.asAnonymousUser();
  50. });
  51. it(
  52. 'me is not permitted',
  53. assertThrowsWithMessage(async () => {
  54. await adminClient.query<Codegen.MeQuery>(ME);
  55. }, 'You are not currently authorized to perform this action'),
  56. );
  57. it('can attempt login', async () => {
  58. await assertRequestAllowed<Codegen.MutationLoginArgs>(ATTEMPT_LOGIN, {
  59. username: SUPER_ADMIN_USER_IDENTIFIER,
  60. password: SUPER_ADMIN_USER_PASSWORD,
  61. rememberMe: false,
  62. });
  63. });
  64. });
  65. describe('Customer user', () => {
  66. let customerEmailAddress: string;
  67. beforeAll(async () => {
  68. await adminClient.asSuperAdmin();
  69. const { customers } =
  70. await adminClient.query<Codegen.GetCustomerListQuery>(GET_CUSTOMER_LIST);
  71. customerEmailAddress = customers.items[0].emailAddress;
  72. });
  73. it('cannot login', async () => {
  74. const result = await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
  75. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  76. });
  77. });
  78. describe('ReadCatalog permission', () => {
  79. beforeAll(async () => {
  80. await adminClient.asSuperAdmin();
  81. const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
  82. Permission.ReadCatalog,
  83. ]);
  84. await adminClient.asUserWithCredentials(identifier, password);
  85. });
  86. it('me returns correct permissions', async () => {
  87. const { me } = await adminClient.query<Codegen.MeQuery>(ME);
  88. expect(me!.channels[0].permissions).toEqual([
  89. Permission.Authenticated,
  90. Permission.ReadCatalog,
  91. ]);
  92. });
  93. it('can read', async () => {
  94. await assertRequestAllowed(GET_PRODUCT_LIST);
  95. });
  96. it('cannot update', async () => {
  97. await assertRequestForbidden<Codegen.MutationUpdateProductArgs>(UPDATE_PRODUCT, {
  98. input: {
  99. id: '1',
  100. translations: [],
  101. },
  102. });
  103. });
  104. it('cannot create', async () => {
  105. await assertRequestForbidden<Codegen.MutationCreateProductArgs>(CREATE_PRODUCT, {
  106. input: {
  107. translations: [],
  108. },
  109. });
  110. });
  111. });
  112. describe('CRUD on Customers permissions', () => {
  113. beforeAll(async () => {
  114. await adminClient.asSuperAdmin();
  115. const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
  116. Permission.CreateCustomer,
  117. Permission.ReadCustomer,
  118. Permission.UpdateCustomer,
  119. Permission.DeleteCustomer,
  120. ]);
  121. await adminClient.asUserWithCredentials(identifier, password);
  122. });
  123. it('me returns correct permissions', async () => {
  124. const { me } = await adminClient.query<Codegen.MeQuery>(ME);
  125. expect(me!.channels[0].permissions).toEqual([
  126. Permission.Authenticated,
  127. Permission.CreateCustomer,
  128. Permission.ReadCustomer,
  129. Permission.UpdateCustomer,
  130. Permission.DeleteCustomer,
  131. ]);
  132. });
  133. it('can create', async () => {
  134. await assertRequestAllowed(
  135. gql(`mutation CanCreateCustomer($input: CreateCustomerInput!) {
  136. createCustomer(input: $input) {
  137. ... on Customer {
  138. id
  139. }
  140. }
  141. }
  142. `),
  143. { input: { emailAddress: '', firstName: '', lastName: '' } },
  144. );
  145. });
  146. it('can read', async () => {
  147. await assertRequestAllowed(gql`
  148. query GetCustomerCount {
  149. customers {
  150. totalItems
  151. }
  152. }
  153. `);
  154. });
  155. });
  156. });
  157. describe('administrator and customer users with the same email address', () => {
  158. const emailAddress = 'same-email@test.com';
  159. const adminPassword = 'admin-password';
  160. const customerPassword = 'customer-password';
  161. const loginErrorGuard: ErrorResultGuard<Codegen.CurrentUserFragment> = createErrorResultGuard(
  162. input => !!input.identifier,
  163. );
  164. beforeAll(async () => {
  165. await adminClient.asSuperAdmin();
  166. await adminClient.query<
  167. Codegen.CreateAdministratorMutation,
  168. Codegen.CreateAdministratorMutationVariables
  169. >(CREATE_ADMINISTRATOR, {
  170. input: {
  171. emailAddress,
  172. firstName: 'First',
  173. lastName: 'Last',
  174. password: adminPassword,
  175. roleIds: ['1'],
  176. },
  177. });
  178. await adminClient.query<Codegen.CreateCustomerMutation, Codegen.CreateCustomerMutationVariables>(
  179. CREATE_CUSTOMER,
  180. {
  181. input: {
  182. emailAddress,
  183. firstName: 'First',
  184. lastName: 'Last',
  185. },
  186. password: customerPassword,
  187. },
  188. );
  189. });
  190. beforeEach(async () => {
  191. await adminClient.asAnonymousUser();
  192. await shopClient.asAnonymousUser();
  193. });
  194. it('can log in as an administrator', async () => {
  195. const loginResult = await adminClient.query<
  196. CodegenShop.AttemptLoginMutation,
  197. CodegenShop.AttemptLoginMutationVariables
  198. >(ATTEMPT_LOGIN, {
  199. username: emailAddress,
  200. password: adminPassword,
  201. });
  202. loginErrorGuard.assertSuccess(loginResult.login);
  203. expect(loginResult.login.identifier).toEqual(emailAddress);
  204. });
  205. it('can log in as a customer', async () => {
  206. const loginResult = await shopClient.query<
  207. CodegenShop.AttemptLoginMutation,
  208. CodegenShop.AttemptLoginMutationVariables
  209. >(ATTEMPT_LOGIN, {
  210. username: emailAddress,
  211. password: customerPassword,
  212. });
  213. loginErrorGuard.assertSuccess(loginResult.login);
  214. expect(loginResult.login.identifier).toEqual(emailAddress);
  215. });
  216. it('cannot log in as an administrator using a customer password', async () => {
  217. const loginResult = await adminClient.query<
  218. CodegenShop.AttemptLoginMutation,
  219. CodegenShop.AttemptLoginMutationVariables
  220. >(ATTEMPT_LOGIN, {
  221. username: emailAddress,
  222. password: customerPassword,
  223. });
  224. loginErrorGuard.assertErrorResult(loginResult.login);
  225. expect(loginResult.login.errorCode).toEqual(ErrorCode.INVALID_CREDENTIALS_ERROR);
  226. });
  227. it('cannot log in as a customer using an administrator password', async () => {
  228. const loginResult = await shopClient.query<
  229. CodegenShop.AttemptLoginMutation,
  230. CodegenShop.AttemptLoginMutationVariables
  231. >(ATTEMPT_LOGIN, {
  232. username: emailAddress,
  233. password: adminPassword,
  234. });
  235. loginErrorGuard.assertErrorResult(loginResult.login);
  236. expect(loginResult.login.errorCode).toEqual(ErrorCode.INVALID_CREDENTIALS_ERROR);
  237. });
  238. });
  239. describe('protected field resolvers', () => {
  240. let readCatalogAdmin: { identifier: string; password: string };
  241. let transactionsAdmin: { identifier: string; password: string };
  242. const GET_PRODUCT_WITH_TRANSACTIONS = `
  243. query GetProductWithTransactions($id: ID!) {
  244. product(id: $id) {
  245. id
  246. transactions {
  247. id
  248. amount
  249. description
  250. }
  251. }
  252. }
  253. `;
  254. beforeAll(async () => {
  255. await adminClient.asSuperAdmin();
  256. transactionsAdmin = await createAdministratorWithPermissions('Transactions', [
  257. Permission.ReadCatalog,
  258. transactions.Permission,
  259. ]);
  260. readCatalogAdmin = await createAdministratorWithPermissions('ReadCatalog', [
  261. Permission.ReadCatalog,
  262. ]);
  263. });
  264. it('protected field not resolved without permissions', async () => {
  265. await adminClient.asUserWithCredentials(readCatalogAdmin.identifier, readCatalogAdmin.password);
  266. try {
  267. const status = await adminClient.query(gql(GET_PRODUCT_WITH_TRANSACTIONS), { id: 'T_1' });
  268. fail('Should have thrown');
  269. } catch (e: any) {
  270. expect(getErrorCode(e)).toBe('FORBIDDEN');
  271. }
  272. });
  273. it('protected field is resolved with permissions', async () => {
  274. await adminClient.asUserWithCredentials(transactionsAdmin.identifier, transactionsAdmin.password);
  275. const { product } = await adminClient.query(gql(GET_PRODUCT_WITH_TRANSACTIONS), { id: 'T_1' });
  276. expect(product.id).toBe('T_1');
  277. expect(product.transactions).toEqual([
  278. { id: 'T_1', amount: 100, description: 'credit' },
  279. { id: 'T_2', amount: -50, description: 'debit' },
  280. ]);
  281. });
  282. // https://github.com/vendurehq/vendure/issues/730
  283. it('protects against deep query data leakage', async () => {
  284. await adminClient.asSuperAdmin();
  285. const { createCustomerGroup } = await adminClient.query<
  286. Codegen.CreateCustomerGroupMutation,
  287. Codegen.CreateCustomerGroupMutationVariables
  288. >(CREATE_CUSTOMER_GROUP, {
  289. input: {
  290. name: 'Test group',
  291. customerIds: ['T_1', 'T_2', 'T_3', 'T_4'],
  292. },
  293. });
  294. const taxRateName = `Standard Tax ${initialData.defaultZone}`;
  295. const { taxRates } = await adminClient.query<
  296. Codegen.GetTaxRatesQuery,
  297. Codegen.GetTaxRatesQueryVariables
  298. >(GET_TAX_RATES_LIST, {
  299. options: {
  300. filter: {
  301. name: { eq: taxRateName },
  302. },
  303. },
  304. });
  305. const standardTax = taxRates.items[0];
  306. expect(standardTax.name).toBe(taxRateName);
  307. await adminClient.query<Codegen.UpdateTaxRateMutation, Codegen.UpdateTaxRateMutationVariables>(
  308. UPDATE_TAX_RATE,
  309. {
  310. input: {
  311. id: standardTax.id,
  312. customerGroupId: createCustomerGroup.id,
  313. },
  314. },
  315. );
  316. try {
  317. const status = await shopClient.query(
  318. gql(`
  319. query DeepFieldResolutionTestQuery{
  320. product(id: "T_1") {
  321. variants {
  322. taxRateApplied {
  323. customerGroup {
  324. customers {
  325. items {
  326. id
  327. emailAddress
  328. }
  329. }
  330. }
  331. }
  332. }
  333. }
  334. }`),
  335. { id: 'T_1' },
  336. );
  337. fail('Should have thrown');
  338. } catch (e: any) {
  339. expect(getErrorCode(e)).toBe('FORBIDDEN');
  340. }
  341. });
  342. // https://github.com/vendurehq/vendure/issues/2097
  343. it('does not overwrite ctx.authorizedAsOwnerOnly with multiple parallel top-level queries', async () => {
  344. // We run this multiple times since the error is based on a race condition that does not
  345. // show up consistently.
  346. for (let i = 0; i < 10; i++) {
  347. const result = await shopClient.query(
  348. gql(`
  349. query Issue2097 {
  350. ownerProtectedThing
  351. publicThing
  352. }
  353. `),
  354. );
  355. expect(result.ownerProtectedThing).toBe(true);
  356. expect(result.publicThing).toBe(true);
  357. }
  358. });
  359. });
  360. async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
  361. try {
  362. const status = await adminClient.queryStatus(operation, variables);
  363. expect(status).toBe(200);
  364. } catch (e: any) {
  365. const errorCode = getErrorCode(e);
  366. if (!errorCode) {
  367. fail(`Unexpected failure: ${JSON.stringify(e)}`);
  368. } else {
  369. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  370. }
  371. }
  372. }
  373. async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
  374. try {
  375. const status = await adminClient.query(operation, variables);
  376. fail('Should have thrown');
  377. } catch (e: any) {
  378. expect(getErrorCode(e)).toBe('FORBIDDEN');
  379. }
  380. }
  381. function getErrorCode(err: any): string {
  382. return err.response.errors[0].extensions.code;
  383. }
  384. async function createAdministratorWithPermissions(
  385. code: string,
  386. permissions: Permission[],
  387. ): Promise<{ identifier: string; password: string }> {
  388. const roleResult = await adminClient.query<
  389. Codegen.CreateRoleMutation,
  390. Codegen.CreateRoleMutationVariables
  391. >(CREATE_ROLE, {
  392. input: {
  393. code,
  394. description: '',
  395. permissions,
  396. },
  397. });
  398. const role = roleResult.createRole;
  399. const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
  400. const password = 'test';
  401. await adminClient.query<
  402. Codegen.CreateAdministratorMutation,
  403. Codegen.CreateAdministratorMutationVariables
  404. >(CREATE_ADMINISTRATOR, {
  405. input: {
  406. emailAddress: identifier,
  407. firstName: code,
  408. lastName: 'Admin',
  409. password,
  410. roleIds: [role.id],
  411. },
  412. });
  413. return {
  414. identifier,
  415. password,
  416. };
  417. }
  418. });