# Zitadel Integration Plan

This plan is split into stages. Each stage is self-contained and can be implemented and tested independently.

---

## Stage 1: Infrastructure Setup [COMPLETED]

**Goal**: Add Zitadel to docker-compose with first-boot initialization (single container, SQLite).

### Design Principles
- **KISS**: Single Zitadel container with SQLite (no separate PostgreSQL or init containers)
- **DRY**: Leverage Zitadel's built-in first-instance configuration via environment variables
- **Clean**: Credentials generated in configure.sh and printed directly

### Files Created

#### `infrastructure_files/Caddyfile.tmpl`
Reverse proxy configuration routing:
- `/api/*`, `/management.*` → Management server
- `/relay*` → Relay
- `/signalexchange.*`, `/signal*` → Signal
- `/oauth/*`, `/oidc/*`, `/.well-known/*`, `/ui/*` → Zitadel
- `/*` → Dashboard

### Files Modified

#### `infrastructure_files/docker-compose.yml.tmpl`
Added single Zitadel service using SQLite:
```yaml
zitadel:
  image: ghcr.io/zitadel/zitadel:$ZITADEL_TAG
  command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
  environment:
    - ZITADEL_DATABASE_SQLITE_PATH=/data/zitadel.db
    - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME=$ZITADEL_ADMIN_USERNAME
    - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=$ZITADEL_ADMIN_PASSWORD
    # ... service account config via env vars
```

#### `infrastructure_files/management.json.tmpl`
Updated IdpManagerConfig to default to Zitadel.

#### `infrastructure_files/base.setup.env`
Added Zitadel-specific environment variables:
- `ZITADEL_TAG` (default: v4.7.6)
- `ZITADEL_MASTERKEY` (auto-generated)
- `ZITADEL_ADMIN_USERNAME`, `ZITADEL_ADMIN_PASSWORD` (auto-generated)
- `ZITADEL_EXTERNALSECURE`, `ZITADEL_EXTERNALPORT`, `ZITADEL_TLS_MODE`

#### `infrastructure_files/configure.sh`
- Generates Zitadel masterkey if not set
- Generates admin credentials if not set
- Prints credentials to stdout during configuration

### Deliverable
Running `./configure.sh && cd artifacts && docker compose up -d` starts full stack with Zitadel. Admin credentials printed during configuration.

---

## Stage 2: Simplify IdP Integration

**Goal**: Make NetBird the single source of truth for user authorization data. Zitadel handles authentication only.

### Design Principles
- **Single source of truth**: NetBird DB stores all authorization data (roles, account membership, invite status)
- **Clean separation**: Zitadel stores identity only (email, name, password)
- **No duplicate data**: Remove `wt_account_id` and `wt_pending_invite` from IdP metadata

### Files to Modify

#### `management/server/types/user.go`
Add `PendingInvite` field to User struct:
```go
type User struct {
    // ... existing fields ...
    PendingInvite bool  // NEW: tracks if user has accepted invite
}
```

Update `ToUserInfo()` to use local field instead of IdP metadata:
```go
// Before
if userData.AppMetadata.WTPendingInvite != nil && *userData.AppMetadata.WTPendingInvite {
    userStatus = UserStatusInvited
}

// After
if u.PendingInvite {
    userStatus = UserStatusInvited
}
```

#### `management/server/idp/idp.go`
Simplify `Manager` interface:
```go
type Manager interface {
    // Simplified - no more accountID or invitedByEmail params
    CreateUser(ctx context.Context, email, name string) (*UserData, error)
    GetUserDataByID(ctx context.Context, userId string) (*UserData, error)
    GetUserByEmail(ctx context.Context, email string) ([]*UserData, error)
    InviteUserByID(ctx context.Context, userID string) error
    DeleteUser(ctx context.Context, userID string) error
}
```

Remove:
- `UpdateUserAppMetadata()` - no longer needed
- `GetAccount()` - account membership now in NetBird DB
- `GetAllAccounts()` - account membership now in NetBird DB
- `AppMetadata` struct fields (`WTAccountID`, `WTPendingInvite`, `WTInvitedBy`)

#### `management/server/idp/zitadel.go`
- Update `CreateUser()` to not set metadata
- Remove `UpdateUserAppMetadata()` implementation
- Simplify `GetUserDataByID()` to not expect metadata

#### `management/server/user.go`
Update user creation flow:
1. `inviteNewUser()`: Set `PendingInvite = true` when creating user
2. On first login: Set `PendingInvite = false`

### Data Flow (After)

```
Invite User:
1. Admin invites user@example.com via NetBird UI
2. NetBird calls Zitadel: CreateUser(email, name)  // No metadata
3. Zitadel creates user, sends invite email
4. NetBird creates User in its DB:
   - Id = Zitadel user ID
   - AccountID = current account
   - Role = invited role
   - PendingInvite = true

First Login:
1. User clicks invite, sets password in Zitadel
2. User logs into NetBird Dashboard
3. NetBird updates User in its DB:
   - PendingInvite = false
   - LastLogin = now
```

### Deliverable
- NetBird is single source of truth for authorization
- Zitadel stores identity only (email, name, credentials)
- No metadata sync issues between systems

---

## Stage 3: Remove Legacy IdP Managers

**Goal**: Remove all non-Zitadel IdP implementations.

### Files to Delete
- `management/server/idp/auth0.go`
- `management/server/idp/auth0_test.go`
- `management/server/idp/azure.go`
- `management/server/idp/azure_test.go`
- `management/server/idp/keycloak.go`
- `management/server/idp/keycloak_test.go`
- `management/server/idp/okta.go`
- `management/server/idp/okta_test.go`
- `management/server/idp/google_workspace.go`
- `management/server/idp/google_workspace_test.go`
- `management/server/idp/jumpcloud.go`
- `management/server/idp/jumpcloud_test.go`
- `management/server/idp/authentik.go`
- `management/server/idp/authentik_test.go`
- `management/server/idp/pocketid.go`
- `management/server/idp/pocketid_test.go`

### Files to Modify

#### `management/server/idp/idp.go`
Simplify `NewManager()` to only support Zitadel:
```go
func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetrics) (Manager, error) {
    switch strings.ToLower(config.ManagerType) {
    case "none", "":
        return nil, nil
    case "zitadel":
        return NewZitadelManager(...)
    default:
        return nil, fmt.Errorf("unsupported IdP manager type: %s (only 'zitadel' is supported)", config.ManagerType)
    }
}
```

#### `management/server/idp/idp.go`
Remove unused config structs:
- `Auth0ClientConfig`
- `AzureClientConfig`
- `KeycloakClientConfig`
- etc.

### Deliverable
Only Zitadel IdP manager remains. Build passes. Tests pass.

---

## Stage 4: External IdP Connector API

**Goal**: API for users to add external IdPs (Okta, Google, LDAP) as Zitadel connectors.

### New Files

#### `management/server/idp/connectors.go`
Wrapper for Zitadel IdP connector API:
```go
type Connector struct {
    ID       string
    Name     string
    Type     string // "oidc", "ldap", "saml"
    Issuer   string // for OIDC
    ClientID string // for OIDC
}

type ConnectorManager interface {
    AddOIDCConnector(ctx, name, issuer, clientID, clientSecret string, scopes []string) (*Connector, error)
    AddLDAPConnector(ctx, name, host string, port int, baseDN, bindDN, bindPassword string) (*Connector, error)
    ListConnectors(ctx) ([]Connector, error)
    DeleteConnector(ctx, connectorID string) error
}
```

#### `management/server/http/handlers/idp_connectors_handler.go`
REST API handlers:
```
POST   /api/idp/connectors        - Add connector
GET    /api/idp/connectors        - List connectors
DELETE /api/idp/connectors/{id}   - Delete connector
```

### Zitadel API Endpoints Used
- `POST /management/v1/idps/generic_oidc` - Add OIDC connector
- `POST /management/v1/idps/ldap` - Add LDAP connector
- `GET /management/v1/idps` - List all IdPs
- `DELETE /management/v1/idps/{id}` - Remove IdP

### Deliverable
Admin users can add/remove external IdP connectors via NetBird API.

---

## Stage 5: User Role Permissions

**Goal**: Enforce admin/user permissions throughout the application.

### Files to Modify

#### `management/server/types/user.go`
Simplify roles to:
```go
const (
    UserRoleAdmin UserRole = "admin"
    UserRoleUser  UserRole = "user"
)
```

Keep `owner` as alias for `admin` for backwards compatibility.

#### Permission checks (various files)
Update permission checks to use simplified role model:
- Admin: Full access to all resources
- User: View/manage own peers only, no user/policy management

### Permission Matrix
| Resource | Admin | User |
|----------|-------|------|
| All peers | read/write | - |
| Own peers | read/write | read/write |
| Users | read/write | - |
| Groups | read/write | read (own) |
| Policies | read/write | - |
| IdP Connectors | read/write | - |
| Account settings | read/write | - |

### Deliverable
Role-based access control enforced. User role has limited dashboard access.

---

## Architecture Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│                     NETBIRD DEPLOYMENT                          │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                      CADDY (Reverse Proxy)                │  │
│  │  :80/:443 → routes to appropriate service                 │  │
│  └───────────────────────────────────────────────────────────┘  │
│         │           │           │            │                  │
│         ▼           ▼           ▼            ▼                  │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐   │
│  │ Dashboard│ │Management│ │  Signal  │ │     Zitadel      │   │
│  │   :80    │ │   :80    │ │   :80    │ │  :8080 (SQLite)  │   │
│  └──────────┘ └────┬─────┘ └──────────┘ └──────────────────┘   │
│                    │                          │                 │
│                    │    OIDC + Mgmt API       │                 │
│                    └──────────────────────────┘                 │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │          Relay           │        Coturn (TURN)           │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
```

---

## Implementation Order

1. **Stage 1** - Infrastructure (can be tested standalone)
2. **Stage 2** - Role management (requires Stage 1)
3. **Stage 3** - Remove legacy IdPs (can be done in parallel with Stage 2)
4. **Stage 4** - Connector API (requires Stage 1)
5. **Stage 5** - Permissions (requires Stage 2)

Stages 3 and 4 can be worked on in parallel after Stage 1 is complete.

---

## Security Considerations

### First-Boot Credentials
- Generated with `openssl rand -base64 32` in configure.sh
- Printed to stdout during configuration (save them!)
- Marked as "must change on first login" in Zitadel via `PASSWORDCHANGEREQUIRED=true`

### Zitadel Masterkey
- Auto-generated if not provided (32 bytes)
- Stored in environment variable (docker secret in production)
- Used for Zitadel's internal encryption

### Service Account
- Machine user created via `ZITADEL_FIRSTINSTANCE_ORG_MACHINE_*` env vars
- PAT (Personal Access Token) generated with 1-year expiration
- Used by NetBird management to call Zitadel API

---

## Migration Notes

Existing deployments using Auth0/Azure/Keycloak will need to:
1. Deploy Zitadel alongside existing IdP
2. Add existing IdP as Zitadel connector
3. Migrate users (or let them re-authenticate via connector)
4. Switch NetBird config to use Zitadel
5. Remove old IdP

A migration guide should be provided as documentation.
