# Access Control Guide

> Quick reference for access control components and hooks

## Design Philosophy

This access control API is designed with **semantics-first** principle:

- **Name tells the story**: `useAccessCheck` checks permissions, `AccessGuard` guards content, `PageAccessGuard` guards pages, `AccessDeniedHoverHint` disables with hints
- **Single responsibility**: Each component/hook does one thing well, avoiding "Swiss Army knife" cognitive overhead
- **Self-documenting code**: The code speaks for itself—less time reading docs or diving into implementation details

This approach reduces cognitive load for developers and helps AI tools better understand and assist with your code.

---

## What should I use?

```mermaid
flowchart TD
    Start([Your scenario?]) --> Page[Page-level protection]
    Start --> Partial[Buttons / Menu items<br/>Partial UI]

    Page --> PageGuard[PageAccessGuard]

    Partial --> Disable[Disable + show reason]
    Partial --> DisableOnly[Disable only, no hint]
    Partial --> Hide[Hide or replace<br/>entire section]

    Disable --> SingleReason[Only permission check]
    Disable --> MultiReason[Multiple reasons<br/>permission/state/quota...]

    SingleReason --> HoverHint[AccessDeniedHoverHint]
    MultiReason --> ReasonHint[useAccessDeniedHintReason<br/>+ ReasonHoverHint]

    DisableOnly --> CanHook[Can use hook]
    DisableOnly --> NoHook[Hook not convenient]

    CanHook --> UseCheck[useAccessCheck]
    NoHook --> CheckComp[AccessCheck]

    Hide --> Guard[AccessGuard]

    style PageGuard fill:#e1f5e1
    style HoverHint fill:#e1f5e1
    style ReasonHint fill:#e1f5e1
    style UseCheck fill:#e1f5e1
    style CheckComp fill:#e1f5e1
    style Guard fill:#e1f5e1
```

<details>
<summary>Text version of decision tree</summary>

```
Your scenario?
│
├─ Page-level protection
│  └─> PageAccessGuard
│
└─ Buttons / Menu items / Partial UI
   │
   ├─ Disable + show reason
   │  ├─ Only permission check → AccessDeniedHoverHint
   │  └─ Multiple reasons (permission/state/quota...) → useAccessDeniedHintReason + ReasonHoverHint
   │
   ├─ Disable only, no hint
   │  ├─ Can use hook → useAccessCheck
   │  └─ Hook not convenient → AccessCheck
   │
   └─ Hide or replace entire section
      └─> AccessGuard
```

</details>

---

## Common Usage

### Content-Level Access Control

> Most common: Disable buttons/actions with hover hints when access is denied

#### [Most Common] Disable + show hint on access denied

```tsx
import { AccessDeniedHoverHint } from 'dbaas/contexts/access-control'

;<AccessDeniedHoverHint id="OrgUsersAction">
  {({ accessible }) => (
    <Button disabled={!accessible} onClick={handleAdd}>
      Add User
    </Button>
  )}
</AccessDeniedHoverHint>
```

> ⚠️ You must manually control `disabled`. The component won't automatically disable children.

**Using the same permission ID on multiple buttons**: Just wrap each one separately.

```tsx
<AccessDeniedHoverHint id="OrgUsersAction">
  {({ accessible }) => <Button disabled={!accessible}>Add</Button>}
</AccessDeniedHoverHint>

<AccessDeniedHoverHint id="OrgUsersAction">
  {({ accessible }) => <Button disabled={!accessible}>Edit</Button>}
</AccessDeniedHoverHint>
```

---

#### [Most Common] Disable only, no hint

```tsx
import { useAccessCheck } from 'dbaas/contexts/access-control'

const accessible = useAccessCheck({ id: 'ClusterDelete' })

return <Button disabled={!accessible}>Delete</Button>
```

**When hooks aren't convenient, use the component**:

```tsx
import { AccessCheck } from 'dbaas/contexts/access-control'

;<AccessCheck id="ClusterDelete">
  {({ accessible }) => <Menu.Item disabled={!accessible}>Delete Cluster</Menu.Item>}
</AccessCheck>
```

---

#### [Common] Multiple disabled reasons

When an action can be disabled for multiple reasons (permission + state + quota...), compose them together:

```tsx
import { useAccessDeniedHintReason } from 'dbaas/contexts/access-control'
import { ReasonHoverHint } from 'dbaas/components/HoverHint/ReasonHoverHint'

function ScaleButton({ cluster }) {
  const { reason: accessReason } = useAccessDeniedHintReason({
    id: 'ClusterScale'
  })

  return (
    <ReasonHoverHint
      reasons={[
        accessReason, // Highest priority
        {
          when: cluster.status === 'SCALING',
          content: 'Cluster is currently scaling'
        },
        {
          when: cluster.size >= cluster.maxSize,
          content: 'Quota limit reached'
        }
      ]}
    >
      {({ matched }) => <Button disabled={matched}>Scale</Button>}
    </ReasonHoverHint>
  )
}
```

`ReasonHoverHint` finds the first reason where `when=true` and shows its hint. `matched` tells you if any reason was triggered.

---

#### [Less Common] Hide or replace entire section

```tsx
import { AccessGuard } from 'dbaas/contexts/access-control'

;<AccessGuard id="AdvancedFeatures" fallback={<EmptyState message="Advanced features require higher permissions" />}>
  <AdvancedSettings />
</AccessGuard>
```

**Hide without replacing**: Just omit the `fallback` prop.

```tsx
<AccessGuard id="AdvancedFeatures">
  <AdvancedSettings />
</AccessGuard>
```

---

### Page-Level Access Control

> Most common: Use default denied page

#### [Most Common] Default denied page

```tsx
import { PageAccessGuard } from 'dbaas/contexts/access-control'

function UserManagementPage() {
  return (
    <PageAccessGuard id="OrgUsersAction">
      <PageHeader title="User Management" />
      <UserList />
    </PageAccessGuard>
  )
}
```

Shows a default "Access Denied" page when permission is denied.

---

#### [Common] Wrap denied page in layout

```tsx
<PageAccessGuard
  id="OrgSettingsGate"
  fallback={(defaultDenied) => (
    <PageLayout>
      <Sidebar />
      <MainContent>{defaultDenied}</MainContent>
    </PageLayout>
  )}
>
  <SettingsContent />
</PageAccessGuard>
```

---

#### [Less Common] Fully custom denied page

```tsx
<PageAccessGuard id="OrgBillingGate" fallback={<CustomDeniedPage title="Billing access required" />}>
  <BillingContent />
</PageAccessGuard>
```

---

## Migration Guide

### Hook Rename

`useAccessGuard` has been renamed to `useAccessCheck` for better semantics. The codebase has been fully migrated—no manual action needed.

---

### Migrating from `AccessGuard`

Choose your migration path based on how you're using it:

#### Scenario 1: Hiding/replacing partial content

If your `AccessGuard` has a `fallback` prop (or you want to hide content entirely), **no migration needed**. Keep using `AccessGuard`.

```tsx
// This is correct usage, no changes needed
<AccessGuard id="AdvancedFeatures" fallback={<EmptyState message="Higher permissions required" />}>
  <AdvancedSettings />
</AccessGuard>
```

---

#### Scenario 2: Page-level protection

If your `AccessGuard` is at the top level of a route component protecting an entire page, migrate to `PageAccessGuard`.

```tsx
// ❌ Old code
function UserManagementPage() {
  return (
    <AccessGuard id="OrgUsersAction">
      <PageContent />
    </AccessGuard>
  )
}

// ✅ New code
function UserManagementPage() {
  return (
    <PageAccessGuard id="OrgUsersAction">
      <PageContent />
    </PageAccessGuard>
  )
}
```

**If the denied page needs to go inside a layout**:

```tsx
// ❌ Old code
function SettingsPage() {
  return (
    <PageLayout>
      <Sidebar />
      <MainContent>
        <AccessGuard id="OrgSettingsGate">
          <SettingsContent />
        </AccessGuard>
      </MainContent>
    </PageLayout>
  )
}

// ✅ New code
function SettingsPage() {
  return (
    <PageAccessGuard
      id="OrgSettingsGate"
      fallback={(defaultDenied) => (
        <PageLayout>
          <Sidebar />
          <MainContent>{defaultDenied}</MainContent>
        </PageLayout>
      )}
    >
      <SettingsContent />
    </PageAccessGuard>
  )
}
```

---

#### Scenario 3: Just getting the `accessible` boolean

If you just need the `accessible` boolean to control a button's `disabled` state, migrate to `useAccessCheck` / `AccessCheck`.

```tsx
// ❌ Old code
function SomeComponent() {
  return (
    <AccessGuard id="ClusterDelete">
      <Button disabled={!accessible}>Delete</Button>
    </AccessGuard>
  )
}

// ✅ New code
import { useAccessCheck } from 'dbaas/contexts/access-control'

function SomeComponent() {
  const accessible = useAccessCheck({ id: 'ClusterDelete' })

  return <Button disabled={!accessible}>Delete</Button>
}
```

**If you need a hover hint explaining why**:

```tsx
// ✅ New code
import { AccessDeniedHoverHint } from 'dbaas/contexts/access-control'

;<AccessDeniedHoverHint id="ClusterDelete">
  {({ accessible }) => <Button disabled={!accessible}>Delete</Button>}
</AccessDeniedHoverHint>
```

---

### Migration Decision Flow

```mermaid
flowchart TD
    Start([How are you using<br/>AccessGuard?]) --> HasFallback[Has a fallback or<br/>want to hide entire section]
    Start --> PageLevel[At page/route top level]
    Start --> ButtonControl[Just controlling<br/>button disabled state]

    HasFallback --> KeepGuard[Keep using AccessGuard<br/>no migration needed ✓]

    PageLevel --> MigratePageGuard[Migrate to PageAccessGuard]

    ButtonControl --> NeedHint{Need hint?}
    NeedHint -->|No| UseCheck[useAccessCheck]
    NeedHint -->|Yes| HoverHint[AccessDeniedHoverHint]

    style KeepGuard fill:#e1f5e1
    style MigratePageGuard fill:#fff4e1
    style UseCheck fill:#fff4e1
    style HoverHint fill:#fff4e1
```

<details>
<summary>Text version of decision flow</summary>

```
How are you using AccessGuard?
│
├─ Has a fallback (or want to hide entire section)
│  └─> Keep using AccessGuard, no migration needed
│
├─ At page/route top level
│  └─> Migrate to PageAccessGuard
│
└─ Just controlling button disabled state
   ├─ No hint needed → useAccessCheck
   └─ Need hint → AccessDeniedHoverHint
```

</details>

---

## API Reference

### Exports

```tsx
// Permission checks (get boolean)
import { useAccessCheck } from 'dbaas/contexts/access-control'
import { AccessCheck } from 'dbaas/contexts/access-control'

// Content-level guard
import { AccessGuard } from 'dbaas/contexts/access-control'

// Page-level guard
import { PageAccessGuard } from 'dbaas/contexts/access-control'

// Action-level hover hints
import { AccessDeniedHoverHint } from 'dbaas/contexts/access-control'
import { useAccessDeniedHintReason } from 'dbaas/contexts/access-control'

// Other
import { AccessProvider } from 'dbaas/contexts/access-control'
import { RBACRoles, RBACRoleUINames } from 'dbaas/contexts/access-control'
```

---

### `useAccessCheck`

Hook to get permission check result.

**Parameters**

```ts
interface UseAccessCheckParams {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
}
```

**Returns**

`boolean` - `true` if user has permission, `false` otherwise

**Example**

```tsx
const accessible = useAccessCheck({ id: 'ClusterDelete' })

return <Button disabled={!accessible}>Delete</Button>
```

**Cross-resource check example**

```tsx
// Check delete permission for a different cluster
const accessible = useAccessCheck({
  id: 'ClusterDelete',
  scopeProps: { tidbId: anotherCluster.id }
})
```

---

### `<AccessCheck>`

Component form of permission check. Use when hooks aren't convenient.

**Props**

```ts
interface AccessCheckProps {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
  children: (params: { accessible: boolean }) => React.ReactNode
}
```

**Example**

```tsx
<AccessCheck id="ClusterDelete">{({ accessible }) => <Button disabled={!accessible}>Delete</Button>}</AccessCheck>
```

---

### `<AccessGuard>`

Content-level access control. Renders children when permitted, fallback or nothing when denied.

**Props**

```ts
interface AccessGuardProps {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
  children?: React.ReactNode // Content to render when permitted
  fallback?: React.ReactNode // Content to render when denied (optional, hides if omitted)
}
```

**Example**

```tsx
<AccessGuard id="AdvancedFeatures" fallback={<EmptyState message="Advanced features require higher permissions" />}>
  <AdvancedSettings />
</AccessGuard>
```

---

### `<PageAccessGuard>`

Page-level protection with automatic default denied page.

**Props**

```ts
interface PageAccessGuardProps {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
  children?: React.ReactNode // Content to render when permitted
  fallback?: // Content to render when denied (optional)
  | React.ReactNode // - ReactNode: fully custom
    | ((defaultFallback: React.ReactNode) => React.ReactNode) // - Function: wrap default page
}
```

**Example**

```tsx
// Default behavior
<PageAccessGuard id="OrgUsersAction">
  <UserManagementPage />
</PageAccessGuard>

// Wrap default page
<PageAccessGuard
  id="OrgUsersAction"
  fallback={(defaultDenied) => (
    <PageLayout>{defaultDenied}</PageLayout>
  )}
>
  <UserManagementPage />
</PageAccessGuard>

// Fully custom
<PageAccessGuard
  id="OrgUsersAction"
  fallback={<CustomDeniedPage />}
>
  <UserManagementPage />
</PageAccessGuard>
```

---

### `<AccessDeniedHoverHint>`

Action-level access control + hover hint. Keeps UI visible, only disables interaction with hover explanation.

**Props**

```ts
interface AccessDeniedHoverHintProps {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
  children: // Target element
  React.ReactNode | ((params: { accessible: boolean }) => React.ReactNode)
  reason?: Partial<HoverHintReason> // Custom HoverCard config (optional)
}
```

> ⚠️ Must use render prop pattern to manually control `disabled` state

**Example**

```tsx
<AccessDeniedHoverHint id="OrgUsersAction">
  {({ accessible }) => <Button disabled={!accessible}>Add User</Button>}
</AccessDeniedHoverHint>
```

---

### `useAccessDeniedHintReason`

Converts permission check into a `reason` object for `ReasonHoverHint`. Use for multi-reason scenarios.

**Parameters**

```ts
interface UseAccessDeniedHintReasonParams {
  id: string // Permission ID
  scopeProps?: {
    // Scope props for cross-resource checks (optional)
    orgId?: string
    projectId?: string
    tidbId?: string
  }
  content?: HoverHintContent // Custom hint content (optional)
  reason?: Partial<HoverHintReason> // Other reason config (optional)
}
```

**Returns**

```ts
{
  accessible: boolean // Permission check result
  reason: HoverHintReason // Reason object for ReasonHoverHint
}
```

**Example**

```tsx
const { reason: accessReason } = useAccessDeniedHintReason({
  id: 'ClusterScale'
})

return (
  <ReasonHoverHint reasons={[accessReason, otherReason]}>
    {({ matched }) => <Button disabled={matched}>Scale</Button>}
  </ReasonHoverHint>
)
```

---

## FAQ

**Q: Why doesn't `AccessDeniedHoverHint` automatically disable children?**

A: Auto-disabling would require deep tree traversal, which is slow and can't handle Fragments, conditional rendering, or other complex structures. Explicit control takes one extra line but makes behavior clear and predictable.

---

**Q: When to use `AccessDeniedHoverHint` vs `useAccessDeniedHintReason + ReasonHoverHint`?**

A:

- Only permission check → use `AccessDeniedHoverHint`
- Multiple reasons (permission + state + quota...) → use `useAccessDeniedHintReason + ReasonHoverHint`

---

**Q: What's the difference between `AccessGuard` and `PageAccessGuard`?**

A:

- `AccessGuard`: hide/replace partial content, you provide the fallback
- `PageAccessGuard`: page-level protection, automatic default denied page

---

**Q: Can I nest these components?**

A: Yes, but usually not needed. Use `PageAccessGuard` at the page level, then use the appropriate component for each partial UI within the page.
