[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F89C8WGGFHEZTH604ZX6H6":3,"skill-same:01M6F89C8WGGFHEZTH604ZX6H6":75},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":52,"commitSha":53,"skillDirectory":54,"forksCount":55,"repositoryHtmlUrl":56,"repositoryHomepageUrl":6,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":74},"01M6F89C8WGGFHEZTH604ZX6H6","obsidian-enterprise-rbac",null,"Implement team vault access patterns and role-based controls.\nUse when managing shared vaults, implementing access controls,\nor building team collaboration features for Obsidian.\nTrigger with phrases like \"obsidian team\", \"obsidian access control\",\n\"obsidian enterprise\", \"shared vault permissions\".","Dicklesworthstone\u002Fpi_agent_rust","Dicklesworthstone","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F35050222?v=4","01M6CG26AR942CSVMN1M5E3DSE","Jeff Emanuel","01M6CG26EPP71PSHW1VDS71121",1986,"MIT","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fobsidian-pack\u002Fskills\u002Fobsidian-enterprise-rbac\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"rbac","Enterprise","obsidian enterprise rbac","obsidian","patterns","when","features","for","implementing","controls","trigger","io","Access Control","shared","vaults","use","edit","write","read","access","team","permissions","collaboration","implement","2026-11-10 10:12:30",false,"---\nname: obsidian-enterprise-rbac\ndescription: |\n  Implement team vault access patterns and role-based controls.\n  Use when managing shared vaults, implementing access controls,\n  or building team collaboration features for Obsidian.\n  Trigger with phrases like \"obsidian team\", \"obsidian access control\",\n  \"obsidian enterprise\", \"shared vault permissions\".\nallowed-tools: Read, Write, Edit\nversion: 1.0.0\nlicense: MIT\nauthor: Jeremy Longshore \u003Cjeremy@intentsolutions.io>\n---\n\n# Obsidian Enterprise RBAC\n\n## Overview\nImplement role-based access control patterns for team vaults and shared Obsidian environments.\n\n## Prerequisites\n- Understanding of RBAC concepts\n- Multi-user vault setup\n- Backend service for authentication (optional)\n\n## Access Control Concepts\n\n### Role Hierarchy\n| Role | Read | Write | Delete | Admin | Settings |\n|------|------|-------|--------|-------|----------|\n| Viewer | Yes | No | No | No | No |\n| Editor | Yes | Yes | No | No | No |\n| Contributor | Yes | Yes | Own | No | No |\n| Manager | Yes | Yes | Yes | No | View |\n| Admin | Yes | Yes | Yes | Yes | Full |\n\n### Folder-Based Permissions\n```\nvault\u002F\n├── public\u002F           # All roles can read\n├── team\u002F             # Editors+ can read\u002Fwrite\n├── projects\u002F\n│   ├── project-a\u002F    # Project members only\n│   └── project-b\u002F    # Project members only\n├── management\u002F       # Managers+ only\n└── admin\u002F            # Admins only\n```\n\n## Instructions\n\n### Step 1: Permission System\n```typescript\n\u002F\u002F src\u002Frbac\u002Fpermissions.ts\nexport type Role = 'viewer' | 'editor' | 'contributor' | 'manager' | 'admin';\nexport type Permission = 'read' | 'write' | 'delete' | 'admin' | 'settings';\n\nexport interface User {\n  id: string;\n  name: string;\n  email: string;\n  role: Role;\n  teamIds: string[];\n  projectIds: string[];\n}\n\nexport interface FolderPermission {\n  path: string;\n  allowedRoles: Role[];\n  allowedTeams?: string[];\n  allowedUsers?: string[];\n}\n\nconst rolePermissions: Record\u003CRole, Permission[]> = {\n  viewer: ['read'],\n  editor: ['read', 'write'],\n  contributor: ['read', 'write', 'delete'], \u002F\u002F delete own only\n  manager: ['read', 'write', 'delete', 'settings'],\n  admin: ['read', 'write', 'delete', 'admin', 'settings'],\n};\n\nexport class PermissionService {\n  private currentUser: User | null = null;\n  private folderPermissions: FolderPermission[] = [];\n\n  setCurrentUser(user: User): void {\n    this.currentUser = user;\n  }\n\n  setFolderPermissions(permissions: FolderPermission[]): void {\n    this.folderPermissions = permissions;\n  }\n\n  hasPermission(permission: Permission): boolean {\n    if (!this.currentUser) return false;\n    return rolePermissions[this.currentUser.role].includes(permission);\n  }\n\n  canAccessFolder(path: string): boolean {\n    if (!this.currentUser) return false;\n\n    \u002F\u002F Admin can access everything\n    if (this.currentUser.role === 'admin') return true;\n\n    \u002F\u002F Check folder permissions\n    const folderPerm = this.findFolderPermission(path);\n    if (!folderPerm) return true; \u002F\u002F No restrictions\n\n    \u002F\u002F Check role\n    if (folderPerm.allowedRoles.includes(this.currentUser.role)) {\n      return true;\n    }\n\n    \u002F\u002F Check team membership\n    if (folderPerm.allowedTeams) {\n      const hasTeam = folderPerm.allowedTeams.some(\n        teamId => this.currentUser!.teamIds.includes(teamId)\n      );\n      if (hasTeam) return true;\n    }\n\n    \u002F\u002F Check user allowlist\n    if (folderPerm.allowedUsers) {\n      if (folderPerm.allowedUsers.includes(this.currentUser.id)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  canReadFile(path: string): boolean {\n    return this.hasPermission('read') && this.canAccessFolder(path);\n  }\n\n  canWriteFile(path: string): boolean {\n    return this.hasPermission('write') && this.canAccessFolder(path);\n  }\n\n  canDeleteFile(path: string, ownerId?: string): boolean {\n    if (!this.currentUser) return false;\n\n    \u002F\u002F Contributor can only delete own files\n    if (this.currentUser.role === 'contributor') {\n      return ownerId === this.currentUser.id && this.canAccessFolder(path);\n    }\n\n    return this.hasPermission('delete') && this.canAccessFolder(path);\n  }\n\n  private findFolderPermission(path: string): FolderPermission | null {\n    \u002F\u002F Find most specific matching folder permission\n    let match: FolderPermission | null = null;\n    let matchLength = 0;\n\n    for (const perm of this.folderPermissions) {\n      if (path.startsWith(perm.path) && perm.path.length > matchLength) {\n        match = perm;\n        matchLength = perm.path.length;\n      }\n    }\n\n    return match;\n  }\n}\n```\n\n### Step 2: Protected Operations Wrapper\n```typescript\n\u002F\u002F src\u002Frbac\u002Fprotected-vault.ts\nimport { App, TFile, Notice } from 'obsidian';\nimport { PermissionService } from '.\u002Fpermissions';\n\nexport class ProtectedVault {\n  constructor(\n    private app: App,\n    private permissions: PermissionService\n  ) {}\n\n  async readFile(file: TFile): Promise\u003Cstring | null> {\n    if (!this.permissions.canReadFile(file.path)) {\n      new Notice('Permission denied: Cannot read this file');\n      return null;\n    }\n    return this.app.vault.read(file);\n  }\n\n  async writeFile(file: TFile, content: string): Promise\u003Cboolean> {\n    if (!this.permissions.canWriteFile(file.path)) {\n      new Notice('Permission denied: Cannot write to this file');\n      return false;\n    }\n    await this.app.vault.modify(file, content);\n    return true;\n  }\n\n  async createFile(path: string, content: string): Promise\u003CTFile | null> {\n    if (!this.permissions.canWriteFile(path)) {\n      new Notice('Permission denied: Cannot create files in this folder');\n      return null;\n    }\n    return this.app.vault.create(path, content);\n  }\n\n  async deleteFile(file: TFile, ownerId?: string): Promise\u003Cboolean> {\n    if (!this.permissions.canDeleteFile(file.path, ownerId)) {\n      new Notice('Permission denied: Cannot delete this file');\n      return false;\n    }\n    await this.app.vault.delete(file);\n    return true;\n  }\n\n  getAccessibleFiles(): TFile[] {\n    return this.app.vault.getMarkdownFiles().filter(\n      file => this.permissions.canReadFile(file.path)\n    );\n  }\n}\n```\n\n### Step 3: User Management\n```typescript\n\u002F\u002F src\u002Frbac\u002Fuser-manager.ts\nimport { Plugin } from 'obsidian';\n\nexport interface TeamConfig {\n  id: string;\n  name: string;\n  members: string[];\n  folders: string[];\n}\n\nexport interface RBACConfig {\n  enabled: boolean;\n  users: Record\u003Cstring, {\n    role: Role;\n    teams: string[];\n  }>;\n  teams: TeamConfig[];\n  folderPermissions: FolderPermission[];\n}\n\nexport class UserManager {\n  private config: RBACConfig;\n  private plugin: Plugin;\n\n  constructor(plugin: Plugin) {\n    this.plugin = plugin;\n    this.config = this.loadConfig();\n  }\n\n  private loadConfig(): RBACConfig {\n    \u002F\u002F Load from plugin settings or external config\n    return {\n      enabled: true,\n      users: {},\n      teams: [],\n      folderPermissions: [],\n    };\n  }\n\n  getCurrentUser(): User | null {\n    \u002F\u002F Get current user from auth system\n    \u002F\u002F This could be from local storage, external auth, etc.\n    const userId = this.getCurrentUserId();\n    if (!userId) return null;\n\n    const userConfig = this.config.users[userId];\n    if (!userConfig) return null;\n\n    return {\n      id: userId,\n      name: this.getUserName(userId),\n      email: this.getUserEmail(userId),\n      role: userConfig.role,\n      teamIds: userConfig.teams,\n      projectIds: this.getProjectsForUser(userId),\n    };\n  }\n\n  private getCurrentUserId(): string | null {\n    \u002F\u002F Implementation depends on auth system\n    \u002F\u002F Could be from: local file, external service, etc.\n    return localStorage.getItem('obsidian-user-id');\n  }\n\n  private getUserName(userId: string): string {\n    \u002F\u002F Fetch from user directory\n    return userId;\n  }\n\n  private getUserEmail(userId: string): string {\n    \u002F\u002F Fetch from user directory\n    return `${userId}@example.com`;\n  }\n\n  private getProjectsForUser(userId: string): string[] {\n    \u002F\u002F Get projects user has access to\n    return [];\n  }\n\n  getTeams(): TeamConfig[] {\n    return this.config.teams;\n  }\n\n  getFolderPermissions(): FolderPermission[] {\n    return this.config.folderPermissions;\n  }\n\n  \u002F\u002F Admin functions\n  async addUser(userId: string, role: Role, teams: string[]): Promise\u003Cvoid> {\n    this.config.users[userId] = { role, teams };\n    await this.saveConfig();\n  }\n\n  async updateUserRole(userId: string, role: Role): Promise\u003Cvoid> {\n    if (this.config.users[userId]) {\n      this.config.users[userId].role = role;\n      await this.saveConfig();\n    }\n  }\n\n  async addFolderPermission(permission: FolderPermission): Promise\u003Cvoid> {\n    this.config.folderPermissions.push(permission);\n    await this.saveConfig();\n  }\n\n  private async saveConfig(): Promise\u003Cvoid> {\n    \u002F\u002F Save to plugin data or external storage\n  }\n}\n```\n\n### Step 4: Audit Logging\n```typescript\n\u002F\u002F src\u002Frbac\u002Faudit-log.ts\nexport interface AuditEntry {\n  timestamp: string;\n  userId: string;\n  action: 'read' | 'write' | 'delete' | 'permission_denied' | 'login' | 'logout';\n  resource: string;\n  details?: Record\u003Cstring, any>;\n  success: boolean;\n}\n\nexport class AuditLogger {\n  private entries: AuditEntry[] = [];\n  private maxEntries = 1000;\n\n  log(entry: Omit\u003CAuditEntry, 'timestamp'>): void {\n    const fullEntry: AuditEntry = {\n      ...entry,\n      timestamp: new Date().toISOString(),\n    };\n\n    this.entries.push(fullEntry);\n\n    \u002F\u002F Trim old entries\n    if (this.entries.length > this.maxEntries) {\n      this.entries = this.entries.slice(-this.maxEntries);\n    }\n\n    \u002F\u002F Console log for debugging\n    console.log(`[Audit] ${fullEntry.action}: ${fullEntry.resource}`, {\n      user: fullEntry.userId,\n      success: fullEntry.success,\n    });\n  }\n\n  logAccess(userId: string, path: string, action: 'read' | 'write' | 'delete', success: boolean): void {\n    this.log({\n      userId,\n      action,\n      resource: path,\n      success,\n    });\n  }\n\n  logPermissionDenied(userId: string, path: string, requestedAction: string): void {\n    this.log({\n      userId,\n      action: 'permission_denied',\n      resource: path,\n      details: { requestedAction },\n      success: false,\n    });\n  }\n\n  getEntries(filter?: {\n    userId?: string;\n    action?: AuditEntry['action'];\n    since?: Date;\n  }): AuditEntry[] {\n    let results = [...this.entries];\n\n    if (filter?.userId) {\n      results = results.filter(e => e.userId === filter.userId);\n    }\n\n    if (filter?.action) {\n      results = results.filter(e => e.action === filter.action);\n    }\n\n    if (filter?.since) {\n      results = results.filter(e =>\n        new Date(e.timestamp) >= filter.since!\n      );\n    }\n\n    return results;\n  }\n\n  getPermissionDenials(): AuditEntry[] {\n    return this.entries.filter(e => e.action === 'permission_denied');\n  }\n\n  export(): string {\n    return JSON.stringify(this.entries, null, 2);\n  }\n}\n```\n\n### Step 5: UI Integration\n```typescript\n\u002F\u002F src\u002Frbac\u002Frbac-ui.ts\nimport { App, Modal, Setting } from 'obsidian';\nimport { Role, User, PermissionService } from '.\u002Fpermissions';\n\nexport class UserRoleModal extends Modal {\n  private user: User;\n  private onSave: (role: Role) => void;\n\n  constructor(app: App, user: User, onSave: (role: Role) => void) {\n    super(app);\n    this.user = user;\n    this.onSave = onSave;\n  }\n\n  onOpen() {\n    const { contentEl } = this;\n    contentEl.empty();\n\n    contentEl.createEl('h2', { text: `Edit User: ${this.user.name}` });\n\n    new Setting(contentEl)\n      .setName('Role')\n      .setDesc('User role determines permissions')\n      .addDropdown(dropdown => {\n        dropdown\n          .addOption('viewer', 'Viewer')\n          .addOption('editor', 'Editor')\n          .addOption('contributor', 'Contributor')\n          .addOption('manager', 'Manager')\n          .addOption('admin', 'Admin')\n          .setValue(this.user.role)\n          .onChange(value => {\n            this.user.role = value as Role;\n          });\n      });\n\n    new Setting(contentEl)\n      .addButton(btn => btn\n        .setButtonText('Save')\n        .setCta()\n        .onClick(() => {\n          this.onSave(this.user.role);\n          this.close();\n        }))\n      .addButton(btn => btn\n        .setButtonText('Cancel')\n        .onClick(() => this.close()));\n  }\n}\n\n\u002F\u002F Status bar indicator\nexport function addRoleIndicator(plugin: Plugin, permissions: PermissionService): void {\n  const user = permissions.getCurrentUser();\n  if (!user) return;\n\n  const statusBar = plugin.addStatusBarItem();\n  statusBar.setText(`${user.role.toUpperCase()}`);\n  statusBar.addClass(`role-indicator-${user.role}`);\n}\n```\n\n## Output\n- Role-based permission system\n- Folder-level access control\n- Protected vault operations\n- Audit logging\n- User management UI\n\n## Error Handling\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| Permission denied | Insufficient role | Check role assignment |\n| User not found | Auth not configured | Set up user management |\n| Folder access blocked | Team not assigned | Add user to team |\n| Audit log full | No rotation | Implement log rotation |\n\n## Examples\n\n### Configuration File\n```yaml\n# .obsidian\u002Fplugins\u002Fmy-plugin\u002Frbac-config.yaml\nenabled: true\n\nusers:\n  alice:\n    role: admin\n    teams: [management, engineering]\n  bob:\n    role: editor\n    teams: [engineering]\n  charlie:\n    role: viewer\n    teams: [support]\n\nteams:\n  - id: management\n    name: Management\n    folders: [management\u002F, projects\u002F]\n  - id: engineering\n    name: Engineering\n    folders: [engineering\u002F, projects\u002F]\n  - id: support\n    name: Support\n    folders: [support\u002F, public\u002F]\n\nfolderPermissions:\n  - path: public\u002F\n    allowedRoles: [viewer, editor, contributor, manager, admin]\n  - path: management\u002F\n    allowedRoles: [manager, admin]\n    allowedTeams: [management]\n  - path: admin\u002F\n    allowedRoles: [admin]\n```\n\n## Resources\n- [RBAC Concepts](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRole-based_access_control)\n- [Obsidian Sync Teams](https:\u002F\u002Fobsidian.md\u002Fsync)\n\n## Next Steps\nFor major migrations, see `obsidian-migration-deep-dive`.\n","# Obsidian Enterprise RBAC\n\n## Overview\nImplement role-based access control patterns for team vaults and shared Obsidian environments.\n\n## Prerequisites\n- Understanding of RBAC concepts\n- Multi-user vault setup\n- Backend service for authentication (optional)\n\n## Access Control Concepts\n\n### Role Hierarchy\n| Role | Read | Write | Delete | Admin | Settings |\n|------|------|-------|--------|-------|----------|\n| Viewer | Yes | No | No | No | No |\n| Editor | Yes | Yes | No | No | No |\n| Contributor | Yes | Yes | Own | No | No |\n| Manager | Yes | Yes | Yes | No | View |\n| Admin | Yes | Yes | Yes | Yes | Full |\n\n### Folder-Based Permissions\n```\nvault\u002F\n├── public\u002F           # All roles can read\n├── team\u002F             # Editors+ can read\u002Fwrite\n├── projects\u002F\n│   ├── project-a\u002F    # Project members only\n│   └── project-b\u002F    # Project members only\n├── management\u002F       # Managers+ only\n└── admin\u002F            # Admins only\n```\n\n## Instructions\n\n### Step 1: Permission System\n```typescript\n\u002F\u002F src\u002Frbac\u002Fpermissions.ts\nexport type Role = 'viewer' | 'editor' | 'contributor' | 'manager' | 'admin';\nexport type Permission = 'read' | 'write' | 'delete' | 'admin' | 'settings';\n\nexport interface User {\n  id: string;\n  name: string;\n  email: string;\n  role: Role;\n  teamIds: string[];\n  projectIds: string[];\n}\n\nexport interface FolderPermission {\n  path: string;\n  allowedRoles: Role[];\n  allowedTeams?: string[];\n  allowedUsers?: string[];\n}\n\nconst rolePermissions: Record\u003CRole, Permission[]> = {\n  viewer: ['read'],\n  editor: ['read', 'write'],\n  contributor: ['read', 'write', 'delete'], \u002F\u002F delete own only\n  manager: ['read', 'write', 'delete', 'settings'],\n  admin: ['read', 'write', 'delete', 'admin', 'settings'],\n};\n\nexport class PermissionService {\n  private currentUser: User | null = null;\n  private folderPermissions: FolderPermission[] = [];\n\n  setCurrentUser(user: User): void {\n    this.currentUser = user;\n  }\n\n  setFolderPermissions(permissions: FolderPermission[]): void {\n    this.folderPermissions = permissions;\n  }\n\n  hasPermission(permission: Permission): boolean {\n    if (!this.currentUser) return false;\n    return rolePermissions[this.currentUser.role].includes(permission);\n  }\n\n  canAccessFolder(path: string): boolean {\n    if (!this.currentUser) return false;\n\n    \u002F\u002F Admin can access everything\n    if (this.currentUser.role === 'admin') return true;\n\n    \u002F\u002F Check folder permissions\n    const folderPerm = this.findFolderPermission(path);\n    if (!folderPerm) return true; \u002F\u002F No restrictions\n\n    \u002F\u002F Check role\n    if (folderPerm.allowedRoles.includes(this.currentUser.role)) {\n      return true;\n    }\n\n    \u002F\u002F Check team membership\n    if (folderPerm.allowedTeams) {\n      const hasTeam = folderPerm.allowedTeams.some(\n        teamId => this.currentUser!.teamIds.includes(teamId)\n      );\n      if (hasTeam) return true;\n    }\n\n    \u002F\u002F Check user allowlist\n    if (folderPerm.allowedUsers) {\n      if (folderPerm.allowedUsers.includes(this.currentUser.id)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  canReadFile(path: string): boolean {\n    return this.hasPermission('read') && this.canAccessFolder(path);\n  }\n\n  canWriteFile(path: string): boolean {\n    return this.hasPermission('write') && this.canAccessFolder(path);\n  }\n\n  canDeleteFile(path: string, ownerId?: string): boolean {\n    if (!this.currentUser) return false;\n\n    \u002F\u002F Contributor can only delete own files\n    if (this.currentUser.role === 'contributor') {\n      return ownerId === this.currentUser.id && this.canAccessFolder(path);\n    }\n\n    return this.hasPermission('delete') && this.canAccessFolder(path);\n  }\n\n  private findFolderPermission(path: string): FolderPermission | null {\n    \u002F\u002F Find most specific matching folder permission\n    let match: FolderPermission | null = null;\n    let matchLength = 0;\n\n    for (const perm of this.folderPermissions) {\n      if (path.startsWith(perm.path) && perm.path.length > matchLength) {\n        match = perm;\n        matchLength = perm.path.length;\n      }\n    }\n\n    return match;\n  }\n}\n```\n\n### Step 2: Protected Operations Wrapper\n```typescript\n\u002F\u002F src\u002Frbac\u002Fprotected-vault.ts\nimport { App, TFile, Notice } from 'obsidian';\nimport { PermissionService } from '.\u002Fpermissions';\n\nexport class ProtectedVault {\n  constructor(\n    private app: App,\n    private permissions: PermissionService\n  ) {}\n\n  async readFile(file: TFile): Promise\u003Cstring | null> {\n    if (!this.permissions.canReadFile(file.path)) {\n      new Notice('Permission denied: Cannot read this file');\n      return null;\n    }\n    return this.app.vault.read(file);\n  }\n\n  async writeFile(file: TFile, content: string): Promise\u003Cboolean> {\n    if (!this.permissions.canWriteFile(file.path)) {\n      new Notice('Permission denied: Cannot write to this file');\n      return false;\n    }\n    await this.app.vault.modify(file, content);\n    return true;\n  }\n\n  async createFile(path: string, content: string): Promise\u003CTFile | null> {\n    if (!this.permissions.canWriteFile(path)) {\n      new Notice('Permission denied: Cannot create files in this folder');\n      return null;\n    }\n    return this.app.vault.create(path, content);\n  }\n\n  async deleteFile(file: TFile, ownerId?: string): Promise\u003Cboolean> {\n    if (!this.permissions.canDeleteFile(file.path, ownerId)) {\n      new Notice('Permission denied: Cannot delete this file');\n      return false;\n    }\n    await this.app.vault.delete(file);\n    return true;\n  }\n\n  getAccessibleFiles(): TFile[] {\n    return this.app.vault.getMarkdownFiles().filter(\n      file => this.permissions.canReadFile(file.path)\n    );\n  }\n}\n```\n\n### Step 3: User Management\n```typescript\n\u002F\u002F src\u002Frbac\u002Fuser-manager.ts\nimport { Plugin } from 'obsidian';\n\nexport interface TeamConfig {\n  id: string;\n  name: string;\n  members: string[];\n  folders: string[];\n}\n\nexport interface RBACConfig {\n  enabled: boolean;\n  users: Record\u003Cstring, {\n    role: Role;\n    teams: string[];\n  }>;\n  teams: TeamConfig[];\n  folderPermissions: FolderPermission[];\n}\n\nexport class UserManager {\n  private config: RBACConfig;\n  private plugin: Plugin;\n\n  constructor(plugin: Plugin) {\n    this.plugin = plugin;\n    this.config = this.loadConfig();\n  }\n\n  private loadConfig(): RBACConfig {\n    \u002F\u002F Load from plugin settings or external config\n    return {\n      enabled: true,\n      users: {},\n      teams: [],\n      folderPermissions: [],\n    };\n  }\n\n  getCurrentUser(): User | null {\n    \u002F\u002F Get current user from auth system\n    \u002F\u002F This could be from local storage, external auth, etc.\n    const userId = this.getCurrentUserId();\n    if (!userId) return null;\n\n    const userConfig = this.config.users[userId];\n    if (!userConfig) return null;\n\n    return {\n      id: userId,\n      name: this.getUserName(userId),\n      email: this.getUserEmail(userId),\n      role: userConfig.role,\n      teamIds: userConfig.teams,\n      projectIds: this.getProjectsForUser(userId),\n    };\n  }\n\n  private getCurrentUserId(): string | null {\n    \u002F\u002F Implementation depends on auth system\n    \u002F\u002F Could be from: local file, external service, etc.\n    return localStorage.getItem('obsidian-user-id');\n  }\n\n  private getUserName(userId: string): string {\n    \u002F\u002F Fetch from user directory\n    return userId;\n  }\n\n  private getUserEmail(userId: string): string {\n    \u002F\u002F Fetch from user directory\n    return `${userId}@example.com`;\n  }\n\n  private getProjectsForUser(userId: string): string[] {\n    \u002F\u002F Get projects user has access to\n    return [];\n  }\n\n  getTeams(): TeamConfig[] {\n    return this.config.teams;\n  }\n\n  getFolderPermissions(): FolderPermission[] {\n    return this.config.folderPermissions;\n  }\n\n  \u002F\u002F Admin functions\n  async addUser(userId: string, role: Role, teams: string[]): Promise\u003Cvoid> {\n    this.config.users[userId] = { role, teams };\n    await this.saveConfig();\n  }\n\n  async updateUserRole(userId: string, role: Role): Promise\u003Cvoid> {\n    if (this.config.users[userId]) {\n      this.config.users[userId].role = role;\n      await this.saveConfig();\n    }\n  }\n\n  async addFolderPermission(permission: FolderPermission): Promise\u003Cvoid> {\n    this.config.folderPermissions.push(permission);\n    await this.saveConfig();\n  }\n\n  private async saveConfig(): Promise\u003Cvoid> {\n    \u002F\u002F Save to plugin data or external storage\n  }\n}\n```\n\n### Step 4: Audit Logging\n```typescript\n\u002F\u002F src\u002Frbac\u002Faudit-log.ts\nexport interface AuditEntry {\n  timestamp: string;\n  userId: string;\n  action: 'read' | 'write' | 'delete' | 'permission_denied' | 'login' | 'logout';\n  resource: string;\n  details?: Record\u003Cstring, any>;\n  success: boolean;\n}\n\nexport class AuditLogger {\n  private entries: AuditEntry[] = [];\n  private maxEntries = 1000;\n\n  log(entry: Omit\u003CAuditEntry, 'timestamp'>): void {\n    const fullEntry: AuditEntry = {\n      ...entry,\n      timestamp: new Date().toISOString(),\n    };\n\n    this.entries.push(fullEntry);\n\n    \u002F\u002F Trim old entries\n    if (this.entries.length > this.maxEntries) {\n      this.entries = this.entries.slice(-this.maxEntries);\n    }\n\n    \u002F\u002F Console log for debugging\n    console.log(`[Audit] ${fullEntry.action}: ${fullEntry.resource}`, {\n      user: fullEntry.userId,\n      success: fullEntry.success,\n    });\n  }\n\n  logAccess(userId: string, path: string, action: 'read' | 'write' | 'delete', success: boolean): void {\n    this.log({\n      userId,\n      action,\n      resource: path,\n      success,\n    });\n  }\n\n  logPermissionDenied(userId: string, path: string, requestedAction: string): void {\n    this.log({\n      userId,\n      action: 'permission_denied',\n      resource: path,\n      details: { requestedAction },\n      success: false,\n    });\n  }\n\n  getEntries(filter?: {\n    userId?: string;\n    action?: AuditEntry['action'];\n    since?: Date;\n  }): AuditEntry[] {\n    let results = [...this.entries];\n\n    if (filter?.userId) {\n      results = results.filter(e => e.userId === filter.userId);\n    }\n\n    if (filter?.action) {\n      results = results.filter(e => e.action === filter.action);\n    }\n\n    if (filter?.since) {\n      results = results.filter(e =>\n        new Date(e.timestamp) >= filter.since!\n      );\n    }\n\n    return results;\n  }\n\n  getPermissionDenials(): AuditEntry[] {\n    return this.entries.filter(e => e.action === 'permission_denied');\n  }\n\n  export(): string {\n    return JSON.stringify(this.entries, null, 2);\n  }\n}\n```\n\n### Step 5: UI Integration\n```typescript\n\u002F\u002F src\u002Frbac\u002Frbac-ui.ts\nimport { App, Modal, Setting } from 'obsidian';\nimport { Role, User, PermissionService } from '.\u002Fpermissions';\n\nexport class UserRoleModal extends Modal {\n  private user: User;\n  private onSave: (role: Role) => void;\n\n  constructor(app: App, user: User, onSave: (role: Role) => void) {\n    super(app);\n    this.user = user;\n    this.onSave = onSave;\n  }\n\n  onOpen() {\n    const { contentEl } = this;\n    contentEl.empty();\n\n    contentEl.createEl('h2', { text: `Edit User: ${this.user.name}` });\n\n    new Setting(contentEl)\n      .setName('Role')\n      .setDesc('User role determines permissions')\n      .addDropdown(dropdown => {\n        dropdown\n          .addOption('viewer', 'Viewer')\n          .addOption('editor', 'Editor')\n          .addOption('contributor', 'Contributor')\n          .addOption('manager', 'Manager')\n          .addOption('admin', 'Admin')\n          .setValue(this.user.role)\n          .onChange(value => {\n            this.user.role = value as Role;\n          });\n      });\n\n    new Setting(contentEl)\n      .addButton(btn => btn\n        .setButtonText('Save')\n        .setCta()\n        .onClick(() => {\n          this.onSave(this.user.role);\n          this.close();\n        }))\n      .addButton(btn => btn\n        .setButtonText('Cancel')\n        .onClick(() => this.close()));\n  }\n}\n\n\u002F\u002F Status bar indicator\nexport function addRoleIndicator(plugin: Plugin, permissions: PermissionService): void {\n  const user = permissions.getCurrentUser();\n  if (!user) return;\n\n  const statusBar = plugin.addStatusBarItem();\n  statusBar.setText(`${user.role.toUpperCase()}`);\n  statusBar.addClass(`role-indicator-${user.role}`);\n}\n```\n\n## Output\n- Role-based permission system\n- Folder-level access control\n- Protected vault operations\n- Audit logging\n- User management UI\n\n## Error Handling\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| Permission denied | Insufficient role | Check role assignment |\n| User not found | Auth not configured | Set up user management |\n| Folder access blocked | Team not assigned | Add user to team |\n| Audit log full | No rotation | Implement log rotation |\n\n## Examples\n\n### Configuration File\n```yaml\n# .obsidian\u002Fplugins\u002Fmy-plugin\u002Frbac-config.yaml\nenabled: true\n\nusers:\n  alice:\n    role: admin\n    teams: [management, engineering]\n  bob:\n    role: editor\n    teams: [engineering]\n  charlie:\n    role: viewer\n    teams: [support]\n\nteams:\n  - id: management\n    name: Management\n    folders: [management\u002F, projects\u002F]\n  - id: engineering\n    name: Engineering\n    folders: [engineering\u002F, projects\u002F]\n  - id: support\n    name: Support\n    folders: [support\u002F, public\u002F]\n\nfolderPermissions:\n  - path: public\u002F\n    allowedRoles: [viewer, editor, contributor, manager, admin]\n  - path: management\u002F\n    allowedRoles: [manager, admin]\n    allowedTeams: [management]\n  - path: admin\u002F\n    allowedRoles: [admin]\n```\n\n## Resources\n- [RBAC Concepts](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRole-based_access_control)\n- [Obsidian Sync Teams](https:\u002F\u002Fobsidian.md\u002Fsync)\n\n## Next Steps\nFor major migrations, see `obsidian-migration-deep-dive`.\n",{"name":5,"author":48,"license":15,"version":49,"description":50,"allowed-tools":51},"Jeremy Longshore \u003Cjeremy@intentsolutions.io>","1.0.0","Implement team vault access patterns and role-based controls.\nUse when managing shared vaults, implementing access controls,\nor building team collaboration features for Obsidian.\nTrigger with phrases like \"obsidian team\", \"obsidian access control\",\n\"obsidian enterprise\", \"shared vault permissions\".\n","Read, Write, Edit","main","90b50ed0778745a80fd5bfb13b1843d45b9e82f7","tests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fobsidian-pack\u002Fskills\u002Fobsidian-enterprise-rbac",233,"https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust","https:\u002F\u002Fgithub.com\u002FDicklesworthstone","https:\u002F\u002Fwww.jeffreyemanuel.com\u002F","Building in NY","2026-11-13 03:33:43",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add Dicklesworthstone\u002Fpi_agent_rust --skill obsidian-enterprise-rbac",{"tool":67,"command":68},"bunx","bunx skills add Dicklesworthstone\u002Fpi_agent_rust --skill obsidian-enterprise-rbac",{"tool":70,"command":71},"pnpm","pnpm dlx skills add Dicklesworthstone\u002Fpi_agent_rust --skill obsidian-enterprise-rbac","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fobsidian-pack\u002Fskills\u002Fobsidian-enterprise-rbac\u002FSKILL.md",true,[],[76,115,140,166,193,215],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":80,"licenseFileUrl":81,"sourceHtmlUrl":82,"categories":83,"keywords":89,"publishedAt":43},"01M6FEGPPK0ZTKCCM0R3E3KS33","web-artifacts-builder","Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn\u002Fui). Use for complex artifacts requiring state management, routing, or shadcn\u002Fui components - not for simple single-file HTML\u002FJSX artifacts.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FSKILL.md",[84],{"categoryNo":85,"categoryName":86,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",[90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,34,108,109,110,111,112,26,113,114],"web artifacts builder","web","artifacts","artifacts builder","builder","frontend","css","components","react","component","routing","state management","state","shadcn","file","ui","Tools","Tailwind","html","single-file","simple","ui components","multi","jsx","suite",{"skillNo":116,"name":117,"nameZh":6,"description":118,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":80,"licenseFileUrl":119,"sourceHtmlUrl":120,"categories":121,"keywords":126,"publishedAt":43},"01M6FEE9VTK1W4GY21AKREJXRG","webapp-testing","Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FSKILL.md",[122,125],{"categoryNo":123,"categoryName":124,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":85,"categoryName":86,"taxonomyType":87,"taxonomyTypeDesc":88},[127,128,129,95,130,131,91,132,133,134,105,135,136,137,138,139,26],"testing","webapp testing","webapp","screenshots","playwright","debugging","toolkit","browser","logs","applications","behavior","local","with",{"skillNo":141,"name":142,"nameZh":6,"description":143,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":144,"sourceHtmlUrl":145,"categories":146,"keywords":147,"publishedAt":43},"01M6FE20F7ZTQBFWKF4MPJ2SBX","neuropixels-analysis","Neuropixels neural recording analysis. Load SpikeGLX\u002FOpenEphys data, preprocess, motion correction, Kilosort4 spike sorting, quality metrics, Allen\u002FIBL curation, AI-assisted visual analysis, for Neuropixels 1.0\u002F2.0 extracellular electrophysiology. Use when working with neural recordings, spike sorting, extracellular electrophysiology, or when the user mentions Neuropixels, SpikeGLX, Open Ephys, Kilosort, quality metrics, or unit curation.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fscientific\u002Fneuropixels-analysis\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fscientific\u002Fneuropixels-analysis\u002FSKILL.md",[],[148,149,150,151,152,153,154,155,34,156,157,158,159,160,161,162,139,163,164,165,26,24],"analysis","neuropixels analysis","spike","metrics","data","open","quality","user","recordings","Neural","motion","visual","quality metrics","unit","sorting","curation","recording","load",{"skillNo":167,"name":168,"nameZh":6,"description":169,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":80,"licenseFileUrl":170,"sourceHtmlUrl":171,"categories":172,"keywords":176,"publishedAt":43},"01M6FDP0PDK6F1GGX395C7625Z","skill-creator","Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FSKILL.md",[173],{"categoryNo":174,"categoryName":175,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000023","Skill与智能体开发",[177,178,179,180,181,182,183,184,185,186,187,188,189,190,139,26,191,24,192],"skill creator","creator","workflows","create","update","capabilities","knowledge","tool","integrations","specialized","guide","new skill","existing skill","users","Existing","new",{"skillNo":194,"name":195,"nameZh":6,"description":196,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":197,"sourceHtmlUrl":198,"categories":199,"keywords":201,"publishedAt":43},"01M6FDM8Y7VK7NYG9SM7QPA3PW","notebooklm","Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced hallucinations through document-only responses.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fnotebooklm\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fnotebooklm\u002FSKILL.md",[200],{"categoryNo":174,"categoryName":175,"taxonomyType":87,"taxonomyTypeDesc":88},[195,202,203,204,205,206,207,134,208,209,34,210,211,212,213,26,214],"google","browser automation","gemini","automation","auth","code","library","management","only","document","query","Citation","source",{"skillNo":216,"name":217,"nameZh":6,"description":218,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":80,"licenseFileUrl":219,"sourceHtmlUrl":220,"categories":221,"keywords":228,"publishedAt":43},"01M6FDHPXE10J3XGTCH5HBHS1M","slack-gif-creator","Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like \"make me a GIF of X doing Y for Slack.\"","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FSKILL.md",[222,225],{"categoryNo":223,"categoryName":224,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000005","邮件与沟通",{"categoryNo":226,"categoryName":227,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000016","图像与多媒体",[229,230,231,178,232,106,233,234,183,34,235,190,236,237,238,26,239,24],"slack gif creator","slack","gif","validation","constraints","Animation","me","make","Utilities","concepts","request"]