Forráskód Böngészése

feat(core): Expose active user permissions in Admin API

Relates to #94
Michael Bromley 6 éve
szülő
commit
b7cd6e57ce

+ 10 - 2
packages/admin-ui/src/app/common/generated-types.ts

@@ -879,7 +879,14 @@ export type CurrentUser = {
   __typename?: 'CurrentUser',
   id: Scalars['ID'],
   identifier: Scalars['String'],
-  channelTokens: Array<Scalars['String']>,
+  channels: Array<CurrentUserChannel>,
+};
+
+export type CurrentUserChannel = {
+  __typename?: 'CurrentUserChannel',
+  token: Scalars['String'],
+  code: Scalars['String'],
+  permissions: Array<Permission>,
 };
 
 export type Customer = Node & {
@@ -3519,7 +3526,7 @@ export type AssignRoleToAdministratorMutationVariables = {
 
 export type AssignRoleToAdministratorMutation = ({ __typename?: 'Mutation' } & { assignRoleToAdministrator: ({ __typename?: 'Administrator' } & AdministratorFragment) });
 
-export type CurrentUserFragment = ({ __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier' | 'channelTokens'>);
+export type CurrentUserFragment = ({ __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier'> & { channels: Array<({ __typename?: 'CurrentUserChannel' } & Pick<CurrentUserChannel, 'code' | 'token' | 'permissions'>)> });
 
 export type AttemptLoginMutationVariables = {
   username: Scalars['String'],
@@ -4403,6 +4410,7 @@ export namespace AssignRoleToAdministrator {
 
 export namespace CurrentUser {
   export type Fragment = CurrentUserFragment;
+  export type Channels = (NonNullable<CurrentUserFragment['channels'][0]>);
 }
 
 export namespace AttemptLogin {

+ 10 - 5
packages/admin-ui/src/app/core/providers/auth/auth.service.ts

@@ -1,8 +1,9 @@
 import { Injectable } from '@angular/core';
 import { Observable, of } from 'rxjs';
 import { catchError, mapTo, mergeMap, switchMap } from 'rxjs/operators';
+import { DEFAULT_CHANNEL_CODE } from 'shared/shared-constants';
 
-import { SetAsLoggedIn } from '../../../common/generated-types';
+import { CurrentUserChannel, CurrentUserFragment, SetAsLoggedIn } from '../../../common/generated-types';
 import { DataService } from '../../../data/providers/data.service';
 import { ServerConfigService } from '../../../data/server-config';
 import { LocalStorageService } from '../local-storage/local-storage.service';
@@ -25,7 +26,7 @@ export class AuthService {
     logIn(username: string, password: string, rememberMe: boolean): Observable<SetAsLoggedIn.Mutation> {
         return this.dataService.auth.attemptLogin(username, password, rememberMe).pipe(
             switchMap(response => {
-                this.setChannelToken(response.login.user.channelTokens[0]);
+                this.setChannelToken(response.login.user.channels);
                 return this.serverConfigService.getServerConfig();
             }),
             switchMap(() => {
@@ -78,7 +79,7 @@ export class AuthService {
                 if (!result.me) {
                     return of(false) as any;
                 }
-                this.setChannelToken(result.me.channelTokens[0]);
+                this.setChannelToken(result.me.channels);
                 return this.dataService.client.loginSuccess(result.me.identifier);
             }),
             mapTo(true),
@@ -86,7 +87,11 @@ export class AuthService {
         );
     }
 
-    private setChannelToken(channelToken: string) {
-        this.localStorageService.set('activeChannelToken', channelToken);
+    private setChannelToken(userChannels: CurrentUserFragment['channels']) {
+        const defaultChannel = userChannels.find(c => c.code === DEFAULT_CHANNEL_CODE);
+        this.localStorageService.set(
+            'activeChannelToken',
+            defaultChannel ? defaultChannel.token : userChannels[0].token,
+        );
     }
 }

+ 5 - 1
packages/admin-ui/src/app/data/definitions/auth-definitions.ts

@@ -4,7 +4,11 @@ export const CURRENT_USER_FRAGMENT = gql`
     fragment CurrentUser on CurrentUser {
         id
         identifier
-        channelTokens
+        channels {
+            code
+            token
+            permissions
+        }
     }
 `;
 

+ 8 - 1
packages/common/src/generated-shop-types.ts

@@ -623,7 +623,14 @@ export type CurrentUser = {
     __typename?: 'CurrentUser';
     id: Scalars['ID'];
     identifier: Scalars['String'];
-    channelTokens: Array<Scalars['String']>;
+    channels: Array<CurrentUserChannel>;
+};
+
+export type CurrentUserChannel = {
+    __typename?: 'CurrentUserChannel';
+    token: Scalars['String'];
+    code: Scalars['String'];
+    permissions: Array<Permission>;
 };
 
 export type Customer = Node & {

+ 8 - 1
packages/common/src/generated-types.ts

@@ -878,7 +878,14 @@ export type CurrentUser = {
   __typename?: 'CurrentUser',
   id: Scalars['ID'],
   identifier: Scalars['String'],
-  channelTokens: Array<Scalars['String']>,
+  channels: Array<CurrentUserChannel>,
+};
+
+export type CurrentUserChannel = {
+  __typename?: 'CurrentUserChannel',
+  token: Scalars['String'],
+  code: Scalars['String'],
+  permissions: Array<Permission>,
 };
 
 export type Customer = Node & {

+ 45 - 1
packages/core/e2e/auth.e2e-spec.ts

@@ -1,20 +1,31 @@
+/* tslint:disable:no-non-null-assertion */
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import path from 'path';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
     CreateAdministrator,
     CreateRole,
+    Me,
     MutationCreateProductArgs,
     MutationLoginArgs,
     MutationUpdateProductArgs,
     Permission,
 } from './graphql/generated-e2e-admin-types';
-import { ATTEMPT_LOGIN, CREATE_ADMINISTRATOR, CREATE_PRODUCT, CREATE_ROLE, GET_PRODUCT_LIST, UPDATE_PRODUCT } from './graphql/shared-definitions';
+import {
+    ATTEMPT_LOGIN,
+    CREATE_ADMINISTRATOR,
+    CREATE_PRODUCT,
+    CREATE_ROLE,
+    GET_PRODUCT_LIST,
+    UPDATE_PRODUCT,
+} from './graphql/shared-definitions';
 import { TestAdminClient } from './test-client';
 import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 describe('Authorization & permissions', () => {
     const client = new TestAdminClient();
@@ -38,6 +49,13 @@ describe('Authorization & permissions', () => {
                 await client.asAnonymousUser();
             });
 
+            it(
+                'me is not permitted',
+                assertThrowsWithMessage(async () => {
+                    await client.query<Me.Query>(ME);
+                }, 'You are not currently authorized to perform this action'),
+            );
+
             it('can attempt login', async () => {
                 await assertRequestAllowed<MutationLoginArgs>(ATTEMPT_LOGIN, {
                     username: SUPER_ADMIN_USER_IDENTIFIER,
@@ -56,6 +74,12 @@ describe('Authorization & permissions', () => {
                 await client.asUserWithCredentials(identifier, password);
             });
 
+            it('me returns correct permissions', async () => {
+                const { me } = await client.query<Me.Query>(ME);
+
+                expect(me!.channels[0].permissions).toEqual(['ReadCatalog']);
+            });
+
             it('can read', async () => {
                 await assertRequestAllowed(GET_PRODUCT_LIST);
             });
@@ -90,6 +114,17 @@ describe('Authorization & permissions', () => {
                 await client.asUserWithCredentials(identifier, password);
             });
 
+            it('me returns correct permissions', async () => {
+                const { me } = await client.query<Me.Query>(ME);
+
+                expect(me!.channels[0].permissions).toEqual([
+                    'CreateCustomer',
+                    'ReadCustomer',
+                    'UpdateCustomer',
+                    'DeleteCustomer',
+                ]);
+            });
+
             it('can create', async () => {
                 await assertRequestAllowed(
                     gql`
@@ -181,3 +216,12 @@ describe('Authorization & permissions', () => {
         };
     }
 });
+
+export const ME = gql`
+    query Me {
+        me {
+            ...CurrentUser
+        }
+    }
+    ${CURRENT_USER_FRAGMENT}
+`;

+ 5 - 1
packages/core/e2e/graphql/fragments.ts

@@ -448,7 +448,11 @@ export const CURRENT_USER_FRAGMENT = gql`
     fragment CurrentUser on CurrentUser {
         id
         identifier
-        channelTokens
+        channels {
+            code
+            token
+            permissions
+        }
     }
 `;
 export const VARIANT_WITH_STOCK_FRAGMENT = gql`

+ 26 - 5
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -878,7 +878,14 @@ export type CurrentUser = {
     __typename?: 'CurrentUser';
     id: Scalars['ID'];
     identifier: Scalars['String'];
-    channelTokens: Array<Scalars['String']>;
+    channels: Array<CurrentUserChannel>;
+};
+
+export type CurrentUserChannel = {
+    __typename?: 'CurrentUserChannel';
+    token: Scalars['String'];
+    code: Scalars['String'];
+    permissions: Array<Permission>;
 };
 
 export type Customer = Node & {
@@ -3352,6 +3359,12 @@ export type GetCustomerCountQuery = { __typename?: 'Query' } & {
     customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'>;
 };
 
+export type MeQueryVariables = {};
+
+export type MeQuery = { __typename?: 'Query' } & {
+    me: Maybe<{ __typename?: 'CurrentUser' } & CurrentUserFragment>;
+};
+
 export type GetCollectionsWithAssetsQueryVariables = {};
 
 export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
@@ -4041,10 +4054,11 @@ export type TaxRateFragment = { __typename?: 'TaxRate' } & Pick<
         customerGroup: Maybe<{ __typename?: 'CustomerGroup' } & Pick<CustomerGroup, 'id' | 'name'>>;
     };
 
-export type CurrentUserFragment = { __typename?: 'CurrentUser' } & Pick<
-    CurrentUser,
-    'id' | 'identifier' | 'channelTokens'
->;
+export type CurrentUserFragment = { __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier'> & {
+        channels: Array<
+            { __typename?: 'CurrentUserChannel' } & Pick<CurrentUserChannel, 'code' | 'token' | 'permissions'>
+        >;
+    };
 
 export type VariantWithStockFragment = { __typename?: 'ProductVariant' } & Pick<
     ProductVariant,
@@ -4955,6 +4969,12 @@ export namespace GetCustomerCount {
     export type Customers = GetCustomerCountQuery['customers'];
 }
 
+export namespace Me {
+    export type Variables = MeQueryVariables;
+    export type Query = MeQuery;
+    export type Me = CurrentUserFragment;
+}
+
 export namespace GetCollectionsWithAssets {
     export type Variables = GetCollectionsWithAssetsQueryVariables;
     export type Query = GetCollectionsWithAssetsQuery;
@@ -5405,6 +5425,7 @@ export namespace TaxRate {
 
 export namespace CurrentUser {
     export type Fragment = CurrentUserFragment;
+    export type Channels = NonNullable<CurrentUserFragment['channels'][0]>;
 }
 
 export namespace VariantWithStock {

+ 8 - 1
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -623,7 +623,14 @@ export type CurrentUser = {
     __typename?: 'CurrentUser';
     id: Scalars['ID'];
     identifier: Scalars['String'];
-    channelTokens: Array<Scalars['String']>;
+    channels: Array<CurrentUserChannel>;
+};
+
+export type CurrentUserChannel = {
+    __typename?: 'CurrentUserChannel';
+    token: Scalars['String'];
+    code: Scalars['String'];
+    permissions: Array<Permission>;
 };
 
 export type Customer = Node & {

+ 3 - 1
packages/core/mock-data/simple-graphql-client.ts

@@ -18,7 +18,9 @@ const LOGIN = gql`
             user {
                 id
                 identifier
-                channelTokens
+                channels {
+                    token
+                }
             }
         }
     }

+ 7 - 5
packages/core/src/api/resolvers/admin/auth.resolver.ts

@@ -1,5 +1,5 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { MutationLoginArgs, LoginResult, Permission } from '@vendure/common/lib/generated-types';
+import { LoginResult, MutationLoginArgs, Permission } from '@vendure/common/lib/generated-types';
 import { Request, Response } from 'express';
 
 import { ConfigService } from '../../../config/config.service';
@@ -31,14 +31,16 @@ export class AuthResolver extends BaseAuthResolver {
 
     @Mutation()
     @Allow(Permission.Public)
-    logout(@Ctx() ctx: RequestContext,
-           @Context('req') req: Request,
-           @Context('res') res: Response): Promise<boolean> {
+    logout(
+        @Ctx() ctx: RequestContext,
+        @Context('req') req: Request,
+        @Context('res') res: Response,
+    ): Promise<boolean> {
         return super.logout(ctx, req, res);
     }
 
     @Query()
-    @Allow(Permission.Authenticated)
+    @Allow(Permission.Authenticated, Permission.Owner)
     me(@Ctx() ctx: RequestContext) {
         return super.me(ctx);
     }

+ 34 - 7
packages/core/src/api/resolvers/base/base-auth.resolver.ts

@@ -1,7 +1,13 @@
-import { MutationLoginArgs, LoginResult } from '@vendure/common/lib/generated-types';
+import {
+    CurrentUser,
+    CurrentUserChannel,
+    LoginResult,
+    MutationLoginArgs,
+} from '@vendure/common/lib/generated-types';
+import { unique } from '@vendure/common/lib/unique';
 import { Request, Response } from 'express';
 
-import { InternalServerError } from '../../../common/error/errors';
+import { ForbiddenError, InternalServerError } from '../../../common/error/errors';
 import { ConfigService } from '../../../config/config.service';
 import { User } from '../../../entity/user/user.entity';
 import { AuthService } from '../../../service/services/auth.service';
@@ -51,6 +57,9 @@ export class BaseAuthResolver {
      */
     async me(ctx: RequestContext) {
         const userId = ctx.activeUserId;
+        if (!userId) {
+            throw new ForbiddenError();
+        }
         const user = userId && (await this.userService.getUserById(userId));
         return user ? this.publiclyAccessibleUser(user) : null;
     }
@@ -95,15 +104,33 @@ export class BaseAuthResolver {
     /**
      * Exposes a subset of the User properties which we want to expose to the public API.
      */
-    private publiclyAccessibleUser(user: User): any {
+    private publiclyAccessibleUser(user: User): CurrentUser {
         return {
-            id: user.id,
+            id: user.id as string,
             identifier: user.identifier,
-            channelTokens: this.getAvailableChannelTokens(user),
+            channels: this.getCurrentUserChannels(user),
         };
     }
 
-    private getAvailableChannelTokens(user: User): string[] {
-        return user.roles.reduce((tokens, role) => role.channels.map(c => c.token), [] as string[]);
+    private getCurrentUserChannels(user: User): CurrentUserChannel[] {
+        const channelsMap: { [code: string]: CurrentUserChannel } = {};
+
+        for (const role of user.roles) {
+            for (const channel of role.channels) {
+                if (!channelsMap[channel.code]) {
+                    channelsMap[channel.code] = {
+                        token: channel.token,
+                        code: channel.code,
+                        permissions: [],
+                    };
+                }
+                channelsMap[channel.code].permissions = unique([
+                    ...channelsMap[channel.code].permissions,
+                    ...role.permissions,
+                ]);
+            }
+        }
+
+        return Object.values(channelsMap);
     }
 }

+ 7 - 1
packages/core/src/api/schema/type/auth.type.graphql

@@ -5,5 +5,11 @@ type LoginResult {
 type CurrentUser {
     id: ID!
     identifier: String!
-    channelTokens: [String!]!
+    channels: [CurrentUserChannel!]!
+}
+
+type CurrentUserChannel {
+    token: String!
+    code: String!
+    permissions: [Permission!]!
 }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
schema-admin.json


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
schema-shop.json


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott