Usage Guide

A comprehensive guide to using KoaRoleManager for role-based access control in koa.js applications.

Introduction

The KoaRoleManager is a powerful tool for implementing role-based access control (RBAC) in Koa.js applications. It integrates seamlessly with the @iamjs/core package to provide robust authorization capabilities. This guide will walk you through setting up, configuring, and using the KoaRoleManager in various scenarios.

Basic Setup

Here's a basic setup to get you started with @iamjs/koa:

import Koa from 'koa';
import Router from 'koa-router';
import { Role, Schema } from '@iamjs/core';
import { KoaRoleManager } from '@iamjs/koa';
 
// Define roles
const userRole = new Role({
  name: 'user',
  config: {
    posts: {
      base: 'cr--l',
      custom: {
        like: true,
        comment: true
      }
    },
    profile: {
      base: 'crud-',
      custom: {
        changePassword: true
      }
    }
  }
});
 
// Create schema
const schema = new Schema({
  roles: { user: userRole }
});
 
// Initialize KoaRoleManager
const roleManager = new KoaRoleManager({
  schema,
  async onSuccess(ctx, next) {
    ctx.status = 200;
    ctx.body = { message: 'Access granted' };
    await next();
  },
  async onError(err, ctx, next) {
    console.error('Authorization error:', err);
    ctx.status = 403;
    ctx.body = { error: 'Access denied' };
    await next();
  }
});
 
const app = new Koa();
const router = new Router();
 
// Example route with authorization
router.get('/posts', 
  roleManager.check({
    resources: 'posts',
    actions: ['read', 'list'],
    role: 'user'
  }),
  async (ctx) => {
    ctx.body = { posts: ['Post 1', 'Post 2'] };
  }
);
 
app.use(router.routes()).use(router.allowedMethods());
 
app.listen(3000, () => console.log('Server running on port 3000'));

This setup demonstrates how to define roles, create a schema, initialize the KoaRoleManager, and use it in a Koa route.

Advanced Usage

For more complex scenarios, you might need to dynamically determine the role or perform additional checks:

import Koa from 'koa';
import Router from 'koa-router';
import { Role, Schema } from '@iamjs/core';
import { KoaRoleManager } from '@iamjs/koa';
 
// ... (previous role and schema setup)
 
const roleManager = new KoaRoleManager({ schema });
 
// Middleware to fetch user permissions
async function authMiddleware(ctx: Koa.Context, next: Koa.Next) {
  // In a real app, you'd fetch this from a database or authentication service
  ctx.state.user = { id: 1, role: 'user' };
  ctx.state.permissions = userRole.toObject();
  await next();
}
 
const router = new Router();
 
router.post('/posts',
  authMiddleware,
  roleManager.check({
    resources: 'posts',
    actions: ['create'],
    strict: true,
    construct: true,
    data: async (ctx) => ctx.state.permissions
  }),
  async (ctx) => {
    ctx.body = { message: 'Post created successfully' };
  }
);
 
const app = new Koa();
app.use(router.routes()).use(router.allowedMethods());

This advanced example shows how to use middleware for authentication and fetching user permissions, and how to use the construct and data options for dynamic role checking.

Success and Error Handling

Customize success and error handling to tailor the behavior of your application:

const roleManager = new KoaRoleManager({
  schema,
  async onSuccess(ctx, next) {
    console.log('Authorization successful');
    ctx.set('X-Auth-Status', 'success');
    ctx.status = 200;
    ctx.body = { message: 'Access granted' };
    await next();
  },
  async onError(err, ctx, next) {
    console.error('Authorization error:', err);
    ctx.status = 403;
    ctx.body = {
      error: 'Access denied',
      details: err.message
    };
    await next();
  }
});

Logging User Activity

Use the onActivity handler to log or process user activities:

const roleManager = new KoaRoleManager({
  schema,
  async onSuccess(ctx, next) { /* ... */ },
  async onError(err, ctx, next) { /* ... */ },
  async onActivity(data) {
    console.log('User activity:', {
      timestamp: new Date(),
      userId: data.ctx?.state.user?.id,
      role: data.role,
      resources: data.resources,
      actions: data.actions,
      success: data.success
    });
    // In a real app, you might save this to a database or send to a logging service
  }
});

TypeScript Support

@iamjs/koa provides full TypeScript support. You can create custom interfaces to extend the Koa context:

import Koa from 'koa';
import Router from 'koa-router';
import { Role, Schema } from '@iamjs/core';
import { KoaRoleManager } from '@iamjs/koa';
 
interface CustomState extends Koa.DefaultState {
  user: { id: number; role: string };
  permissions: any;
}
 
interface CustomContext extends Koa.Context {
  state: CustomState;
}
 
const roleManager = new KoaRoleManager({
  schema,
  async onSuccess(ctx: CustomContext, next) {
    console.log('Authorized user:', ctx.state.user.id);
    ctx.status = 200;
    ctx.body = { message: 'Access granted' };
    await next();
  },
  async onError(err: Error, ctx: CustomContext, next) {
    console.error('Authorization failed for user:', ctx.state.user.id);
    ctx.status = 403;
    ctx.body = { error: 'Access denied' };
    await next();
  },
});
 
const router = new Router<CustomState, CustomContext>();
 
router.get(
  '/protected',
  roleManager.check({
    resources: 'protectedResource',
    actions: ['read'],
    construct: true,
    data: async (ctx: CustomContext) => ctx.state.permissions,
  }),
  async (ctx) => {
    ctx.body = {
      message: 'Protected resource accessed',
      userId: ctx.state.user.id,
    };
  },
);
 

Headless Checks Using checkFn

The checkFn method allows you to perform authorization checks without the Koa middleware context. This is particularly useful for:

  • Background jobs
  • API-based applications
  • Serverless functions
  • Any scenario where you need programmatic access control outside of a Koa request/response cycle

Basic Usage

import { KoaRoleManager, Schema, Role } from '@iamjs/koa';
 
// Define roles and schema
const userRole = new Role({
  name: 'user',
  config: {
    documents: {
      base: 'r--l',
      custom: {
        download: true
      }
    }
  }
});
 
const adminRole = new Role({
  name: 'admin',
  config: {
    documents: {
      base: 'crudl',
      custom: {
        delete: true
      }
    }
  }
});
 
const schema = new Schema({
  roles: { user: userRole, admin: adminRole }
});
 
// Initialize KoaRoleManager
const roleManager = new KoaRoleManager({ schema });
 
// Headless check function
async function canAccessDocument(userId: string, documentId: string, action: string): Promise<boolean> {
  const userRole = await getUserRole(userId); // Implement this function to fetch user's role
  
  return await roleManager.checkFn({
    role: userRole,
    resources: 'documents',
    actions: action
  });
}
 
// Usage in a background job
async function processDocuments() {
  const documents = await fetchDocumentsToProcess();
  for (const doc of documents) {
    if (await canAccessDocument(doc.ownerId, doc.id, 'read')) {
      await processDocument(doc);
    } else {
      console.log(`User ${doc.ownerId} does not have permission to read document ${doc.id}`);
    }
  }
}

With Dynamic Role Construction

import { KoaRoleManager, Schema } from '@iamjs/koa';
 
const schema = new Schema({ /* ... */ });
const roleManager = new KoaRoleManager({ schema });
 
async function checkDynamicPermission(userId: string, resource: string, action: string): Promise<boolean> {
  const userPermissions = await fetchUserPermissions(userId);
  
  return await roleManager.checkFn({
    resources: resource,
    actions: action,
    construct: true,
    data: userPermissions
  });
}
 
// Usage in an API endpoint (outside of Koa middleware)
async function handleApiRequest(userId: string, resource: string, action: string) {
  const canAccess = await checkDynamicPermission(userId, resource, action);
  
  if (canAccess) {
    return { status: 'success', message: 'Access granted' };
  } else {
    return { status: 'error', message: 'Access denied' };
  }
}

In Serverless Functions

import { KoaRoleManager, Schema, Role } from '@iamjs/koa';
 
const schema = new Schema({ /* ... */ });
const roleManager = new KoaRoleManager({ schema });
 
export async function handleDocumentAccess(event, context) {
  const { userId, documentId, action } = JSON.parse(event.body);
  
  const userRole = await getUserRoleFromDatabase(userId);
  
  const canAccess = await roleManager.checkFn({
    role: userRole,
    resources: 'documents',
    actions: action
  });
  
  if (canAccess) {
    // Perform the action
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Access granted' })
    };
  } else {
    return {
      statusCode: 403,
      body: JSON.stringify({ error: 'Access denied' })
    };
  }
}

Batch Operations

async function batchProcessDocuments(userIds: string[], action: string) {
  const results = await Promise.all(
    userIds.map(async (userId) => {
      const userRole = await getUserRole(userId);
      const canAccess = await roleManager.checkFn({
        role: userRole,
        resources: 'documents',
        actions: action
      });
      return { userId, canAccess };
    })
  );
 
  return results.filter(result => result.canAccess).map(result => result.userId);
}
 
// Usage
const userIdsWithAccess = await batchProcessDocuments(['user1', 'user2', 'user3'], 'read');
console.log('Users with read access:', userIdsWithAccess);

These examples demonstrate how checkFn can be used in various "headless" scenarios, providing flexible and powerful authorization checks outside of the typical Koa middleware flow. This approach allows you to implement consistent access control across different parts of your application, from web routes to background jobs and serverless functions. By leveraging checkFn, you can extend your authorization logic beyond just HTTP requests handled by Koa, ensuring a consistent authorization model throughout your entire application ecosystem.

Best Practices and Tips

  1. Granular Permissions: Define roles with specific, granular permissions for flexible access control.

  2. Dynamic Role Assignment: Utilize the construct and data options for dynamic role assignment based on user data.

  3. Error Handling: Provide clear error messages in your onError handler for better debugging and user experience.

  4. Logging: Use the onActivity handler for comprehensive audit trails of authorization attempts.

  5. Performance: Consider caching role data or authorization results for improved performance with complex permission structures.

  6. Security: Always validate and sanitize user input, especially when constructing roles dynamically.

  7. Testing: Implement thorough unit and integration tests for your authorization logic.

  8. Middleware Composition: Leverage Koa's middleware composition to combine authorization checks with other middlewares seamlessly.

  9. Context Usage: Make effective use of Koa's context object to pass data between middlewares and your authorization checks.