Hone logo
Hone
Problems

TypeScript Decorator Factory for Method Authorization

This challenge focuses on implementing a TypeScript decorator factory that can be used to add authorization logic to class methods. Decorators are a powerful feature for meta-programming in TypeScript, allowing you to annotate and modify classes, methods, properties, and parameters. This exercise will help you understand how to create reusable decorator logic that can be applied across your codebase to enforce access control.

Problem Description

You need to create a TypeScript decorator factory named authorize. This factory should accept an array of roles (strings) as an argument. The decorator, when applied to a class method, should intercept method calls. Before executing the actual method logic, it should check if the current user (represented by a mock currentUser object) has at least one of the required roles.

Key Requirements:

  1. Decorator Factory: Create a function authorize that acts as a decorator factory. It should accept roles: string[] as its parameter.
  2. Method Decorator: The authorize factory must return a method decorator.
  3. Authorization Check: Inside the method decorator, before invoking the original method, check if currentUser.roles contains any of the roles passed to the authorize factory.
  4. Mock currentUser: For this challenge, assume a global or accessible currentUser object exists with a roles: string[] property.
    interface User {
        roles: string[];
    }
    declare const currentUser: User;
    
  5. Access Denied: If the currentUser does not have any of the required roles, the decorator should throw an Error with the message "Access Denied."
  6. Access Granted: If the currentUser has at least one of the required roles, the original method should be executed, and its return value should be returned by the decorator.
  7. Method Signature Preservation: The decorator should correctly handle method parameters and the return type of the decorated method.

Expected Behavior:

When a method decorated with @authorize([...]) is called:

  • If the current user's roles satisfy the authorization requirements, the method executes normally.
  • If the current user's roles do not satisfy the requirements, an "Access Denied." error is thrown.

Edge Cases to Consider:

  • An empty array of roles passed to the authorize factory. In this case, all users (or no users, depending on interpretation) should be allowed. For this challenge, consider an empty role list as allowing all.
  • Methods with no parameters.
  • Methods with parameters.
  • Methods returning different types (e.g., void, string, number, Promise).

Examples

Example 1:

Input:

interface User {
    roles: string[];
}

// Mock current user for demonstration
const currentUser: User = { roles: ['admin', 'editor'] };

function authorize(roles: string[]): MethodDecorator {
    // Your implementation of authorize factory and method decorator
    return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            if (roles.length === 0) { // Allow all if no roles specified
                return originalMethod.apply(this, args);
            }

            const hasPermission = roles.some(requiredRole => currentUser.roles.includes(requiredRole));

            if (!hasPermission) {
                throw new Error("Access Denied.");
            }

            return originalMethod.apply(this, args);
        };

        return descriptor;
    };
}


class DocumentService {
    @authorize(['admin'])
    deleteDocument(id: number): void {
        console.log(`Document ${id} deleted.`);
    }

    @authorize(['editor', 'viewer'])
    viewDocument(id: number): string {
        console.log(`Viewing document ${id}.`);
        return `Document content for ${id}.`;
    }
}

const service = new DocumentService();

// Test case 1: User has 'admin' role, can delete
try {
    service.deleteDocument(123); // Output: Document 123 deleted.
} catch (e: any) {
    console.error(e.message);
}

// Test case 2: User has 'editor' role, can view
try {
    const content = service.viewDocument(456); // Output: Viewing document 456.
    console.log(content); // Output: Document content for 456.
} catch (e: any) {
    console.error(e.message);
}

Output:

Document 123 deleted.
Viewing document 456.
Document content for 456.

Explanation:

The currentUser has roles ['admin', 'editor'].

  • deleteDocument requires ['admin'], which the user has. The method executes.
  • viewDocument requires ['editor', 'viewer']. The user has ['editor'], which is sufficient. The method executes.

Example 2:

Input:

interface User {
    roles: string[];
}

// Mock current user for demonstration
const currentUser: User = { roles: ['viewer'] };

function authorize(roles: string[]): MethodDecorator {
    // Your implementation of authorize factory and method decorator
    return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            if (roles.length === 0) {
                return originalMethod.apply(this, args);
            }

            const hasPermission = roles.some(requiredRole => currentUser.roles.includes(requiredRole));

            if (!hasPermission) {
                throw new Error("Access Denied.");
            }

            return originalMethod.apply(this, args);
        };

        return descriptor;
    };
}

class AdminPanel {
    @authorize(['admin', 'manager'])
    manageUsers(): void {
        console.log("Managing users...");
    }

    @authorize(['admin'])
    systemSettings(): void {
        console.log("Accessing system settings...");
    }
}

const adminPanel = new AdminPanel();

// Test case 1: User has 'viewer' role, cannot manage users
try {
    adminPanel.manageUsers();
} catch (e: any) {
    console.error(e.message); // Expected Output: Access Denied.
}

// Test case 2: User has 'viewer' role, cannot access system settings
try {
    adminPanel.systemSettings();
} catch (e: any) {
    console.error(e.message); // Expected Output: Access Denied.
}

Output:

Access Denied.
Access Denied.

Explanation:

The currentUser has roles ['viewer'].

  • manageUsers requires ['admin', 'manager']. The user has neither. An error is thrown.
  • systemSettings requires ['admin']. The user does not have this role. An error is thrown.

Example 3: (Edge case: Empty roles array)

Input:

interface User {
    roles: string[];
}

// Mock current user for demonstration
const currentUser: User = { roles: ['viewer'] };

function authorize(roles: string[]): MethodDecorator {
    // Your implementation of authorize factory and method decorator
    return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            if (roles.length === 0) { // Allow all if no roles specified
                return originalMethod.apply(this, args);
            }

            const hasPermission = roles.some(requiredRole => currentUser.roles.includes(requiredRole));

            if (!hasPermission) {
                throw new Error("Access Denied.");
            }

            return originalMethod.apply(this, args);
        };

        return descriptor;
    };
}

class PublicApi {
    @authorize([]) // No roles required
    getPublicData(): string {
        console.log("Fetching public data...");
        return "Public data.";
    }
}

const publicApi = new PublicApi();

// Test case: Method with empty role requirement
try {
    const data = publicApi.getPublicData(); // Output: Fetching public data...
    console.log(data); // Output: Public data.
} catch (e: any) {
    console.error(e.message);
}

Output:

Fetching public data...
Public data.

Explanation:

The getPublicData method is decorated with authorize([]). Since the roles array is empty, the condition roles.length === 0 is true, and the original method is executed without any role checks, regardless of the currentUser's roles.

Constraints

  • The solution must be written in TypeScript.
  • The authorize decorator factory must accept an array of strings representing required roles.
  • The decorator must correctly handle methods with zero or more arguments.
  • The decorator must preserve the this context of the decorated method.
  • The decorator should not introduce significant performance overhead for typical use cases.

Notes

  • You will need to define the User interface and the currentUser variable (as shown in the examples) for testing purposes.
  • Consider how you will access and modify the descriptor.value to wrap the original method.
  • The apply method of functions will be useful for calling the original method with the correct this context and arguments.
  • Think about the return type of the original method and how to ensure the decorator returns it correctly.
Loading editor...
typescript