Authorization

Comprehensive Guide to Authorization with Auth Manager

Introduction

The Auth Manager class in iamjs provides a robust framework for implementing and managing authorization in your application. It allows for fine-grained access control based on roles and their associated permissions. This guide will walk you through the process of setting up and using the Auth Manager for effective authorization management.

1. Defining Roles

The first step in setting up your authorization system is to define roles. Each role represents a set of permissions for various resources and actions.

import { Role, Schema, AuthManager } from '@iamjs/core';
 
const roles = {
  user: new Role({
    name: 'user',
    description: 'Standard user with limited permissions',
    config: {
      user: {
        base: '-r--l',
        custom: {
          updateOwnProfile: true,
          changePassword: true
        }
      },
      post: {
        base: 'cr-dl',
        custom: {
          like: true,
          comment: true
        }
      }
    }
  }),
  moderator: new Role({
    name: 'moderator',
    description: 'User with elevated permissions for content moderation',
    config: {
      user: {
        base: 'r---l',
        custom: {
          banUser: true,
          viewUserDetails: true
        }
      },
      post: {
        base: 'crudl',
        custom: {
          pinPost: true,
          flagInappropriate: true
        }
      }
    }
  }),
  admin: new Role({
    name: 'admin',
    description: 'Administrator with full system access',
    config: {
      user: {
        base: 'crudl',
        custom: {
          assignRoles: true,
          viewAuditLogs: true
        }
      },
      post: {
        base: 'crudl',
        custom: {
          deleteAnyPost: true,
          editAnyPost: true
        }
      },
      system: {
        base: 'crudl',
        custom: {
          configureSettings: true,
          viewAnalytics: true
        }
      }
    }
  })
};

In this example, we've defined three roles: user, moderator, and admin. Each role has specific permissions for different resources (user, post, system) and actions (create, read, update, delete, list, and custom actions).

2. Creating the Authorization Schema

Once roles are defined, we create an authorization schema using the Schema class:

const schema = new Schema({ roles });

This schema encapsulates all the defined roles and their permissions.

3. Initializing the Auth Manager

With the schema in place, we can now initialize the Auth Manager:

const authManager = new AuthManager(schema);

The Auth Manager will use this schema to perform authorization checks.

4. Performing Authorization Checks

The Auth Manager provides an authorize method for performing authorization checks. This method is highly flexible and can be used in various scenarios:

Basic Authorization Check

const canUserCreatePost = authManager.authorize({
  role: 'user',
  actions: ['create'],
  resources: 'post'
});
console.log('Can user create post?', canUserCreatePost); // true

Multiple Actions and Resources

const canModeratorManagePosts = authManager.authorize({
  role: 'moderator',
  actions: ['update', 'delete', 'pinPost'],
  resources: ['post']
});
console.log('Can moderator manage posts?', canModeratorManagePosts); // true

Strict Mode

Strict mode ensures that all specified actions are authorized for all specified resources:

const canAdminDoEverything = authManager.authorize({
  role: 'admin',
  actions: ['create', 'read', 'update', 'delete', 'list', 'assignRoles', 'configureSettings'],
  resources: ['user', 'post', 'system'],
  strict: true
});
console.log('Can admin do everything?', canAdminDoEverything); // true

Dynamic Role Construction

The Auth Manager can construct roles on the fly from provided data:

const dynamicRole = {
  name: 'dynamicRole',
  config: {
    customResource: {
      base: 'cr--l',
      custom: {
        specialAction: true
      }
    }
  }
};
 
const canDynamicRolePerformSpecialAction = authManager.authorize({
  construct: true,
  data: dynamicRole,
  actions: ['specialAction'],
  resources: 'customResource'
});
console.log('Can dynamic role perform special action?', canDynamicRolePerformSpecialAction); // true

5. Best Practices

  1. Granular Permissions: Define roles with fine-grained permissions to allow for flexible access control.
  2. Resource-Action Mapping: Clearly map out which actions are applicable to which resources.
  3. Custom Actions: Utilize custom actions for application-specific operations that don't fit into CRUD.
  4. Role Hierarchy: Consider implementing a role hierarchy where higher roles inherit permissions from lower roles.
  5. Regular Audits: Periodically review and update role permissions to ensure they align with current security requirements.
  6. Performance Optimization: Cache authorization results for frequently checked permissions to improve performance.

6. Creating Custom Wrappers

While the Auth Manager provides powerful out-of-the-box functionality, you may want to create custom wrappers to better integrate with your application's architecture or to add additional features. Here are some examples of custom wrappers you can create:

Express.js Middleware Wrapper

This wrapper creates an Express.js middleware for easy integration with Express applications:

import { Request, Response, NextFunction } from 'express';
import { AuthManager, Schema } from '@iamjs/core';
 
export function createAuthMiddleware(authManager: AuthManager, schema: Schema) {
  return function authMiddleware(req: Request, res: Response, next: NextFunction) {
    const { role, action, resource } = req.body; // Assume these are passed in the request body
 
    const isAuthorized = authManager.authorize({
      role,
      actions: [action],
      resources: [resource]
    });
 
    if (isAuthorized) {
      next();
    } else {
      res.status(403).json({ error: 'Unauthorized' });
    }
  };
}
 
// Usage
const authMiddleware = createAuthMiddleware(authManager, schema);
app.use('/protected-route', authMiddleware, (req, res) => {
  res.json({ message: 'Access granted' });
});

React Hook Wrapper

This wrapper creates a custom React hook for easy use in React components:

import { useContext } from 'react';
import { AuthManager } from '@iamjs/core';
 
// Assume you have an AuthContext that provides the AuthManager instance
const AuthContext = React.createContext<AuthManager | null>(null);
 
export function useAuthorization() {
  const authManager = useContext(AuthContext);
  if (!authManager) {
    throw new Error('useAuthorization must be used within an AuthProvider');
  }
 
  return {
    can: (role: string, action: string, resource: string) => {
      return authManager.authorize({
        role,
        actions: [action],
        resources: [resource]
      });
    },
    canAll: (role: string, actions: string[], resources: string[]) => {
      return authManager.authorize({
        role,
        actions,
        resources,
        strict: true
      });
    }
  };
}
 
// Usage in a React component
function ProtectedComponent() {
  const { can } = useAuthorization();
  const userRole = 'user'; // This should come from your authentication system
 
  if (can(userRole, 'read', 'secretData')) {
    return <div>Secret data here!</div>;
  } else {
    return <div>Access denied</div>;
  }
}

GraphQL Resolver Wrapper

This wrapper creates a higher-order function to wrap GraphQL resolvers with authorization checks:

import { AuthManager } from '@iamjs/core';
 
export function withAuthorization(
  authManager: AuthManager,
  requiredRole: string,
  requiredAction: string,
  resource: string
) {
  return function(resolver: Function) {
    return async function(parent: any, args: any, context: any, info: any) {
      const userRole = context.user?.role; // Assume role is available in the context
 
      const isAuthorized = authManager.authorize({
        role: userRole,
        actions: [requiredAction],
        resources: [resource]
      });
 
      if (!isAuthorized) {
        throw new Error('Unauthorized');
      }
 
      return resolver(parent, args, context, info);
    };
  };
}
 
// Usage in GraphQL resolvers
const resolvers = {
  Query: {
    secretData: withAuthorization(authManager, 'admin', 'read', 'secretData')(
      (parent, args, context) => {
        return 'This is secret data';
      }
    )
  }
};

Decorators for Class-based Architectures

If you're using TypeScript with experimental decorators, you can create an authorization decorator:

import { AuthManager } from '@iamjs/core';
 
export function Authorize(authManager: AuthManager, action: string, resource: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
 
    descriptor.value = function (...args: any[]) {
      const userRole = this.getUserRole(); // Assume this method exists to get the user's role
 
      const isAuthorized = authManager.authorize({
        role: userRole,
        actions: [action],
        resources: [resource]
      });
 
      if (!isAuthorized) {
        throw new Error('Unauthorized');
      }
 
      return originalMethod.apply(this, args);
    };
 
    return descriptor;
  };
}
 
// Usage in a class
class UserService {
  @Authorize(authManager, 'create', 'user')
  createUser(userData: any) {
    // Create user logic here
  }
}

These custom wrappers demonstrate how you can adapt the Auth Manager to various architectural patterns and frameworks. By creating these wrappers, you can provide a more intuitive and integrated authorization experience that fits seamlessly with your application's structure. Remember to adjust these examples to fit your specific use case and error handling requirements. Also, ensure that you're consistently retrieving the user's role from your authentication system in a secure manner.

Conclusion

The Auth Manager provides a powerful and flexible system for managing authorization in your application. By defining clear roles, creating a comprehensive schema, and utilizing the various authorization check options, you can implement a robust access control system that meets the specific needs of your application.

Remember to regularly review and update your authorization rules as your application evolves to maintain optimal security and functionality.