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:
- Decorator Factory: Create a function
authorizethat acts as a decorator factory. It should acceptroles: string[]as its parameter. - Method Decorator: The
authorizefactory must return a method decorator. - Authorization Check: Inside the method decorator, before invoking the original method, check if
currentUser.rolescontains any of the roles passed to theauthorizefactory. - Mock
currentUser: For this challenge, assume a global or accessiblecurrentUserobject exists with aroles: string[]property.interface User { roles: string[]; } declare const currentUser: User; - Access Denied: If the
currentUserdoes not have any of the required roles, the decorator should throw anErrorwith the message "Access Denied." - Access Granted: If the
currentUserhas at least one of the required roles, the original method should be executed, and its return value should be returned by the decorator. - 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
authorizefactory. 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'].
deleteDocumentrequires['admin'], which the user has. The method executes.viewDocumentrequires['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'].
manageUsersrequires['admin', 'manager']. The user has neither. An error is thrown.systemSettingsrequires['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
authorizedecorator 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
thiscontext of the decorated method. - The decorator should not introduce significant performance overhead for typical use cases.
Notes
- You will need to define the
Userinterface and thecurrentUservariable (as shown in the examples) for testing purposes. - Consider how you will access and modify the
descriptor.valueto wrap the original method. - The
applymethod of functions will be useful for calling the original method with the correctthiscontext and arguments. - Think about the return type of the original method and how to ensure the decorator returns it correctly.