Roles
RoleはZeltの認可システムの基盤です。ユーザーが何をできるかを定義します。
Roleとは何か?
roleは、権限レベルや能力を表すシンプルな文字列です。
const adminRoles = ['admin', 'editor', 'viewer'];
const teamRoles = ['owner', 'member', 'guest'];
const permissionRoles = ['read:users', 'write:users', 'delete:users'];
roleは、認証時に setUser() を通じて割り当てられます。
setUser(
{ id: user.id, name: user.name },
['admin', 'user'] // ← role
);
Role Types
currentRoles() は常に readonly string[] を返します — role値は型レベルで特定のUnionに絞り込まれません。アプリで使うroleのローカル型を定義し、roleを割り当てたり比較したりする箇所すべてでそれを参照することで、role名の一貫性を保ちます:
type Role = 'admin' | 'editor' | 'viewer';
Role設計パターン
Hierarchical Roles
他のroleを包含するroleを定義します。
type Role = 'admin' | 'editor' | 'viewer';
const roleHierarchy: Record<Role, Role[]> = {
admin: ['admin', 'editor', 'viewer'],
editor: ['editor', 'viewer'],
viewer: ['viewer'],
};
// userを設定する際、roleを展開する
setUser(user, roleHierarchy[user.primaryRole]);
Resource-Scoped Roles
role名にリソースのcontextを含めます。
type Role =
| 'admin'
| `project:${string}:owner`
| `project:${string}:member`
| `team:${string}:admin`;
// userはproject-123のowner、team-456のmember
setUser(user, ['project:123:owner', 'team:456:admin']);
Permission-Based Roles
きめ細かい権限文字列を使います。
type Permission =
| 'read:users'
| 'write:users'
| 'delete:users'
| 'read:posts'
| 'write:posts';
// roleはpermissionにマッピングされる
const rolePermissions: Record<string, Permission[]> = {
admin: ['read:users', 'write:users', 'delete:users', 'read:posts', 'write:posts'],
editor: ['read:users', 'read:posts', 'write:posts'],
viewer: ['read:users', 'read:posts'],
};
Roleはどこから来るのか
Database
roleをユーザーレコードと一緒に保存します。
@Middleware
class AuthMiddleware {
constructor(
private jwtService = inject(JwtService),
private userRepo = inject(UserRepository)
) {}
async use(c: RequestContext, next: Next): Promise<Response | undefined> {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (token) {
const payload = await this.jwtService.verify(token);
const user = await this.userRepo.findById(payload.sub!);
setUser({ id: user.id, name: user.name }, user.roles);
}
await next();
return undefined;
}
}
JWT Claims
JWTのpayloadにroleを含めます。
@Controller('/auth')
class AuthController {
constructor(private jwtService = inject(JwtService)) {}
@Post('/login')
async login(user: User) {
const token = await this.jwtService.sign({
sub: user.id,
roles: user.roles,
});
return { token };
}
}
@Config
class MyJwtConfig extends JwtConfig {
override get resolveUser(): (payload: JwtPayload) => Promise<ResolveUserResult> {
return async (payload) => ({
user: { id: payload.sub! },
roles: payload.roles as string[],
});
}
}
Session Data
roleをセッションに保存します。
@Middleware
class SessionAuthMiddleware {
constructor(
private sessionService = inject(SessionService),
private userRepo = inject(UserRepository)
) {}
async use(c: RequestContext, next: Next): Promise<Response | undefined> {
const session = this.sessionService.getSession();
if (session) {
const user = await this.userRepo.findById(session.userId);
setUser(user, session.roles);
}
await next();
return undefined;
}
}
External Service
identity providerからroleを取得します。
@Middleware
class ExternalAuthMiddleware {
constructor(private idp = inject(IdentityProviderService)) {}
async use(c: RequestContext, next: Next): Promise<Response | undefined> {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (token) {
const userInfo = await this.idp.getUserInfo(token);
const roles = await this.idp.getRoles(userInfo.sub);
setUser({ id: userInfo.sub, name: userInfo.name }, roles);
}
await next();
return undefined;
}
}
Role割り当ての戦略
Static Assignment
roleは一度設定されると、めったに変わりません。
@Controller('/users')
class UserRolesController {
constructor(private userRepo = inject(UserRepository)) {}
@Authorized(['admin'])
@Post('/:id/roles')
async assignRoles(req = request(RolesSchema)) {
const id = req.pathParam('id');
const data = await req.body();
await this.userRepo.updateRoles(id, data.roles);
return { success: true };
}
}
Dynamic Assignment
roleはcontextに応じて計算されます。
@Middleware
class ProjectRolesMiddleware {
constructor(private projectRepo = inject(ProjectRepository)) {}
async use(c: RequestContext, next: Next): Promise<Response | undefined> {
const user = currentUser() as User | undefined;
const projectId = c.req.param('projectId');
if (user && projectId) {
const project = await this.projectRepo.findById(projectId);
const roles: string[] = [];
if (project.ownerId === user.id) {
roles.push('project:owner');
}
if (project.memberIds.includes(user.id)) {
roles.push('project:member');
}
setUser(user, roles);
}
await next();
return undefined;
}
}
Time-Based Roles
roleが時間に応じて失効・有効化されます。
const roles = user.roles.filter(role => {
const grant = user.roleGrants.find(g => g.role === role);
if (!grant) return true;
const now = Date.now();
if (grant.startsAt && now < grant.startsAt) return false;
if (grant.expiresAt && now > grant.expiresAt) return false;
return true;
});
setUser(user, roles);
Roleへアクセスする
Handler内で
@Controller('/app')
class AppController {
@Get('/dashboard')
dashboard() {
const roles = currentRoles();
return {
canManageUsers: roles.includes('admin'),
canEditContent: roles.includes('editor') || roles.includes('admin'),
};
}
}
Service内で
class PostService {
canDelete(post: Post): boolean {
const roles = currentRoles();
const user = currentUser() as User | undefined;
if (roles.includes('admin')) return true;
if (post.authorId === user?.id) return true;
return false;
}
}
Best Practices
Roleをシンプルに保つ
ネストしたオブジェクトではなく、フラットな文字列を使います。
// ✅ Good
const goodRoles = ['admin', 'editor', 'viewer'];
// ❌ Avoid
const badRoles = [{ name: 'admin', level: 10, permissions: [] }];
Roleは粗いアクセス制御に使う
roleが答えるのは「このユーザーはこの機能領域にアクセスできるか?」であり、「このユーザーはこの特定のレコードを編集できるか?」ではありません。
// ✅ Role-based: "Can access admin section"
@Controller('/admin')
class AdminController {
@Authorized(['admin'])
@Get('/dashboard')
adminDashboard() {}
}
// ❌ Not a role: "Can edit post #123"
// → Handle in service logic instead
Roleの爆発を避ける
すべての操作ごとにroleを作らないでください。
// ❌ Too many roles
const tooManyRoles = ['can_view_users', 'can_create_users', 'can_edit_users', 'can_delete_users'];
// ✅ Group into meaningful roles
const meaningfulRoles = ['admin', 'user_manager', 'viewer'];
Roleを文書化する
中央のリファレンスを維持します。
/**
* Application Roles
*
* - admin: Full system access
* - editor: Can create and modify content
* - viewer: Read-only access
* - moderator: Can manage user-generated content
*/
type Role = 'admin' | 'editor' | 'viewer' | 'moderator';