auth.e2e-spec.ts 13 KB

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