Browse Source

fix(core): Prevent removal of sole SuperAdmin

Relates to #1307
Michael Bromley 4 years ago
parent
commit
a1debffb66

+ 43 - 0
packages/core/e2e/administrator.e2e-spec.ts

@@ -1,5 +1,6 @@
 import { SUPER_ADMIN_USER_IDENTIFIER } from '@vendure/common/lib/shared-constants';
 import { createTestEnvironment } from '@vendure/testing';
+import { fail } from 'assert';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -150,6 +151,48 @@ describe('Administrator resolver', () => {
         expect(after.totalItems).toBe(1);
     });
 
+    it('cannot delete sole SuperAdmin', async () => {
+        const { administrators: before } = await adminClient.query<
+            GetAdministrators.Query,
+            GetAdministrators.Variables
+        >(GET_ADMINISTRATORS);
+        expect(before.totalItems).toBe(1);
+        expect(before.items[0].emailAddress).toBe('superadmin');
+
+        try {
+            const { deleteAdministrator } = await adminClient.query<
+                DeleteAdministrator.Mutation,
+                DeleteAdministrator.Variables
+            >(DELETE_ADMINISTRATOR, {
+                id: before.items[0].id,
+            });
+            fail('Should have thrown');
+        } catch (e) {
+            expect(e.message).toBe('The sole SuperAdmin cannot be deleted');
+        }
+
+        const { administrators: after } = await adminClient.query<
+            GetAdministrators.Query,
+            GetAdministrators.Variables
+        >(GET_ADMINISTRATORS);
+        expect(after.totalItems).toBe(1);
+    });
+
+    it(
+        'cannot remove SuperAdmin role from sole SuperAdmin',
+        assertThrowsWithMessage(async () => {
+            const result = await adminClient.query<
+                UpdateAdministrator.Mutation,
+                UpdateAdministrator.Variables
+            >(UPDATE_ADMINISTRATOR, {
+                input: {
+                    id: 'T_1',
+                    roleIds: [],
+                },
+            });
+        }, 'Cannot remove the SuperAdmin role from the sole SuperAdmin'),
+    );
+
     it('cannot query a deleted Administrator', async () => {
         const { administrator } = await adminClient.query<GetAdministrator.Query, GetAdministrator.Variables>(
             GET_ADMINISTRATOR,

+ 2 - 0
packages/core/src/i18n/messages/en.json

@@ -1,6 +1,7 @@
 {
   "error": {
     "cannot-delete-role": "The role '{ roleCode }' cannot be deleted",
+    "cannot-delete-sole-superadmin": "The sole SuperAdmin cannot be deleted",
     "cannot-locate-customer-for-user": "Cannot locate a Customer for the user",
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
     "cannot-create-sales-for-active-order": "Cannot create a Sale for an Order which is still active",
@@ -43,6 +44,7 @@
     "product-variant-options-combination-already-exists": "A ProductVariant with the selected options already exists: {variantName}",
     "promotion-channels-can-only-be-changed-from-default-channel": "Promotions channels may only be changed from the Default Channel",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
+    "superadmin-must-have-superadmin-role": "Cannot remove the SuperAdmin role from the sole SuperAdmin",
     "unauthorized": "The credentials did not match. Please check and try again"
   },
   "errorResult": {

+ 34 - 1
packages/core/src/service/services/administrator.service.ts

@@ -4,10 +4,12 @@ import {
     DeletionResult,
     UpdateAdministratorInput,
 } from '@vendure/common/lib/generated-types';
+import { SUPER_ADMIN_ROLE_CODE } from '@vendure/common/lib/shared-constants';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
-import { EntityNotFoundError } from '../../common/error/errors';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import { idsAreEqual } from '../../common/index';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ConfigService } from '../../config';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -144,6 +146,13 @@ export class AdministratorService {
             }
         }
         if (input.roleIds) {
+            const isSoleSuperAdmin = await this.isSoleSuperadmin(ctx, input.id);
+            if (isSoleSuperAdmin) {
+                const superAdminRole = await this.roleService.getSuperAdminRole();
+                if (!input.roleIds.find(id => idsAreEqual(id, superAdminRole.id))) {
+                    throw new InternalServerError('error.superadmin-must-have-superadmin-role');
+                }
+            }
             const removeIds = administrator.user.roles
                 .map(role => role.id)
                 .filter(roleId => (input.roleIds as ID[]).indexOf(roleId) === -1);
@@ -196,6 +205,10 @@ export class AdministratorService {
         const administrator = await this.connection.getEntityOrThrow(ctx, Administrator, id, {
             relations: ['user'],
         });
+        const isSoleSuperadmin = await this.isSoleSuperadmin(ctx, id);
+        if (isSoleSuperadmin) {
+            throw new InternalServerError('error.cannot-delete-sole-superadmin');
+        }
         await this.connection.getRepository(ctx, Administrator).update({ id }, { deletedAt: new Date() });
         // tslint:disable-next-line:no-non-null-assertion
         await this.userService.softDelete(ctx, administrator.user!.id);
@@ -205,6 +218,26 @@ export class AdministratorService {
         };
     }
 
+    /**
+     * @description
+     * Resolves to `true` if the administrator ID belongs to the only Administrator
+     * with SuperAdmin permissions.
+     */
+    private async isSoleSuperadmin(ctx: RequestContext, id: ID) {
+        const superAdminRole = await this.roleService.getSuperAdminRole();
+        const allAdmins = await this.connection.getRepository(ctx, Administrator).find({
+            relations: ['user', 'user.roles'],
+        });
+        const superAdmins = allAdmins.filter(
+            admin => !!admin.user.roles.find(r => r.id === superAdminRole.id),
+        );
+        if (1 < superAdmins.length) {
+            return false;
+        } else {
+            return idsAreEqual(superAdmins[0].id, id);
+        }
+    }
+
     /**
      * @description
      * There must always exist a SuperAdmin, otherwise full administration via API will