Browse Source

refactor access token model to simplify scope extraction and add unit tests for scope utilities

reiji-h 1 year ago
parent
commit
19a1f85618

+ 2 - 2
apps/app/src/server/models/access-token.ts

@@ -12,7 +12,7 @@ import type { Scope } from '~/interfaces/scope';
 import loggerFactory from '~/utils/logger';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
-import { extractAllScope, extractScopes } from '../util/scope-utils';
+import { extractScopes } from '../util/scope-utils';
 
 const logger = loggerFactory('growi:models:access-token');
 
@@ -108,7 +108,7 @@ accessTokenSchema.statics.findUserIdByToken = async function(token: string, requ
   if (requiredScopes.length === 0) {
     return;
   }
-  const extractedScopes = requiredScopes.map(scope => extractAllScope(scope)).flat();
+  const extractedScopes = extractScopes(requiredScopes);
   return this.findOne({ tokenHash, expiredAt: { $gt: now }, scopes: { $all: extractedScopes } }).select('user');
 };
 

+ 90 - 0
apps/app/src/server/util/scope-util.spec.ts

@@ -0,0 +1,90 @@
+import { describe, it, expect } from 'vitest';
+
+import { SCOPE } from '../../interfaces/scope';
+
+import {
+  isValidScope, hasAllScope, extractAllScope, extractScopes,
+} from './scope-utils';
+
+describe('scope-utils', () => {
+  describe('isValidScope', () => {
+    it('should return true for valid scopes', () => {
+      expect(isValidScope(SCOPE.READ.USER.API.API_TOKEN)).toBe(true);
+      expect(isValidScope(SCOPE.WRITE.USER.API.ACCESS_TOKEN)).toBe(true);
+      expect(isValidScope(SCOPE.READ.ADMIN.APP)).toBe(true);
+    });
+
+    it('should return false for invalid scopes', () => {
+      expect(isValidScope('invalid:scope' as any)).toBe(false);
+      expect(isValidScope('read:invalid:path' as any)).toBe(false);
+      expect(isValidScope('write:user:invalid' as any)).toBe(false);
+    });
+  });
+
+  describe('hasAllScope', () => {
+    it('should return true for scopes ending with *', () => {
+      expect(hasAllScope(SCOPE.READ.USER.API.ALL)).toBe(true);
+      expect(hasAllScope(SCOPE.WRITE.ADMIN.ALL)).toBe(true);
+    });
+
+    it('should return false for specific scopes', () => {
+      expect(hasAllScope(SCOPE.READ.USER.API.API_TOKEN)).toBe(false);
+      expect(hasAllScope(SCOPE.WRITE.USER.API.ACCESS_TOKEN)).toBe(false);
+    });
+  });
+
+  describe('extractAllScope', () => {
+    it('should extract all specific scopes from ALL scope', () => {
+      const extracted = extractAllScope(SCOPE.READ.USER.API.ALL);
+      expect(extracted).toContain(SCOPE.READ.USER.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
+      expect(extracted).not.toContain(SCOPE.READ.USER.API.ALL);
+    });
+
+    it('should return array with single scope for specific scope', () => {
+      const scope = SCOPE.READ.USER.API.API_TOKEN;
+      const extracted = extractAllScope(scope);
+      expect(extracted).toEqual([scope]);
+    });
+  });
+
+  describe('extractScopes', () => {
+    it('should return empty array for undefined input', () => {
+      expect(extractScopes()).toEqual([]);
+    });
+
+    it('should extract all implied scopes including READ permission for WRITE scopes', () => {
+      const scopes = [SCOPE.WRITE.USER.API.ACCESS_TOKEN];
+      const extracted = extractScopes(scopes);
+
+      expect(extracted).toContain(SCOPE.WRITE.USER.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
+    });
+
+    it('should extract all specific scopes from ALL scope with implied permissions', () => {
+      const scopes = [SCOPE.WRITE.USER.API.ALL];
+      const extracted = extractScopes(scopes);
+
+      // Should include both WRITE and READ permissions for all specific scopes
+      expect(extracted).toContain(SCOPE.WRITE.USER.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.WRITE.USER.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
+
+      // Should not include ALL scopes
+      expect(extracted).not.toContain(SCOPE.WRITE.USER.API.ALL);
+      expect(extracted).not.toContain(SCOPE.READ.USER.API.ALL);
+    });
+
+    it('should remove duplicate scopes', () => {
+      const scopes = [
+        SCOPE.WRITE.USER.API.ACCESS_TOKEN,
+        SCOPE.READ.USER.API.ACCESS_TOKEN, // This is implied by WRITE
+      ];
+      const extracted = extractScopes(scopes);
+
+      const accessTokenScopes = extracted.filter(s => s.endsWith('access_token'));
+      expect(accessTokenScopes).toHaveLength(2); // Only READ and WRITE, no duplicates
+    });
+  });
+});