auth.e2e-spec.ts 16 KB

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