OIDC groups implementation
- Add Groups field to User struct with JSON storage - Include GetGroups() and SetGroups() helper methods - Extract groups from OIDC claims in FromClaim() - Add database migration 202509161200 for groups column - Update config-example.yaml with groups scope - Add comprehensive documentation and testing
This commit is contained in:
parent
30d12dafed
commit
5abc3c87b2
29 changed files with 5088 additions and 3 deletions
309
docs/ref/api-groups.md
Normal file
309
docs/ref/api-groups.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# API Reference: OIDC Groups
|
||||
|
||||
This document describes the API changes related to OIDC group storage and management introduced in Headscale v0.24.0.
|
||||
|
||||
## Overview
|
||||
|
||||
Headscale now stores OIDC group membership information extracted from authentication claims. This enables external integrations to implement role-based access control based on a user's group membership.
|
||||
|
||||
## User API Changes
|
||||
|
||||
### User Object Schema
|
||||
|
||||
The User object now includes group information when users authenticate via OIDC:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"name": "alice",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"displayName": "Alice Smith",
|
||||
"email": "alice@example.com",
|
||||
"providerId": "https://provider.com/alice",
|
||||
"provider": "oidc",
|
||||
"profilePicUrl": "https://provider.com/avatar/alice.jpg",
|
||||
"groups": ["admin", "developers", "security-team"]
|
||||
}
|
||||
```
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `groups` | `string[]` | Array of group names extracted from OIDC claims. Empty array for non-OIDC users. |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### List Users
|
||||
|
||||
Returns all users including their group membership.
|
||||
|
||||
**Request:**
|
||||
```http
|
||||
GET /api/v1/user
|
||||
Authorization: Bearer <api-key>
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "alice",
|
||||
"email": "alice@example.com",
|
||||
"provider": "oidc",
|
||||
"groups": ["admin", "developers"]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "bob",
|
||||
"email": "",
|
||||
"provider": "cli",
|
||||
"groups": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get User
|
||||
|
||||
Returns a specific user including group membership.
|
||||
|
||||
**Request:**
|
||||
```http
|
||||
GET /api/v1/user/{name}
|
||||
Authorization: Bearer <api-key>
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": "1",
|
||||
"name": "alice",
|
||||
"email": "alice@example.com",
|
||||
"provider": "oidc",
|
||||
"groups": ["admin", "developers", "security-team"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Group Management
|
||||
|
||||
### Automatic Group Updates
|
||||
|
||||
Groups are automatically updated when OIDC users authenticate:
|
||||
|
||||
1. User logs in via OIDC
|
||||
2. Groups are extracted from ID token and/or UserInfo endpoint
|
||||
3. User's group membership is updated in the database
|
||||
4. API responses include updated group information
|
||||
|
||||
### Group Sources
|
||||
|
||||
Groups can be extracted from multiple sources in OIDC claims:
|
||||
|
||||
- **Standard `groups` claim**: Most common format
|
||||
- **`roles` claim**: Alternative role-based claim
|
||||
- **Provider-specific claims**: e.g., `cognito:groups` for AWS Cognito
|
||||
- **Nested claims**: e.g., `resource_access.client.roles` for Keycloak
|
||||
|
||||
### Group Validation
|
||||
|
||||
- Only string values are accepted as group names
|
||||
- Empty or null groups are filtered out
|
||||
- Group names are stored as-is (case-sensitive)
|
||||
- Maximum reasonable limit of groups per user (no strict limit enforced)
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
External applications can query user groups for access control:
|
||||
|
||||
```bash
|
||||
# Get user groups via API
|
||||
curl -H "Authorization: Bearer $API_KEY" \
|
||||
https://headscale.example.com/api/v1/user/alice | \
|
||||
jq '.user.groups[]'
|
||||
|
||||
# Check if user has admin group
|
||||
curl -H "Authorization: Bearer $API_KEY" \
|
||||
https://headscale.example.com/api/v1/user/alice | \
|
||||
jq '.user.groups | contains(["admin"])'
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
For direct database access:
|
||||
|
||||
```sql
|
||||
-- Get all users with their groups
|
||||
SELECT name, email, groups FROM users WHERE provider = 'oidc';
|
||||
|
||||
-- Find users in specific group
|
||||
SELECT name FROM users
|
||||
WHERE provider = 'oidc'
|
||||
AND JSON_EXTRACT(groups, '$') LIKE '%"admin"%';
|
||||
|
||||
-- Count users by group membership
|
||||
SELECT
|
||||
json_each.value as group_name,
|
||||
COUNT(*) as user_count
|
||||
FROM users, json_each(users.groups)
|
||||
WHERE provider = 'oidc'
|
||||
GROUP BY json_each.value;
|
||||
```
|
||||
|
||||
## WebUI Integration
|
||||
|
||||
### Headplane Integration
|
||||
|
||||
When using Headplane as a web interface, the groups information enables:
|
||||
|
||||
- **Automatic Role Assignment**: Map OIDC groups to Headplane roles
|
||||
- **Dynamic Permissions**: Update user capabilities based on current group membership
|
||||
- **Audit Trails**: Track role assignments based on group changes
|
||||
|
||||
Example Headplane API usage:
|
||||
|
||||
```javascript
|
||||
// Fetch user with groups
|
||||
const response = await fetch('/api/v1/user/alice', {
|
||||
headers: { 'Authorization': `Bearer ${apiKey}` }
|
||||
});
|
||||
const user = await response.json();
|
||||
|
||||
// Map groups to roles
|
||||
const roles = mapGroupsToRoles(user.groups);
|
||||
console.log(`User ${user.name} has roles:`, roles);
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
### CLI Users
|
||||
|
||||
- Users created via CLI (`headscale users create`) have empty groups array
|
||||
- No changes to existing CLI user management
|
||||
- Groups field is optional and defaults to empty array
|
||||
|
||||
### API Compatibility
|
||||
|
||||
- All existing API endpoints continue to work unchanged
|
||||
- New `groups` field is additive (doesn't break existing clients)
|
||||
- Clients can safely ignore the groups field if not needed
|
||||
|
||||
### Migration
|
||||
|
||||
- Existing OIDC users will have empty groups until next login
|
||||
- No database migration required for basic functionality
|
||||
- Groups are populated automatically on subsequent OIDC logins
|
||||
|
||||
## Configuration
|
||||
|
||||
### Headscale Configuration
|
||||
|
||||
Ensure groups scope is included for group extraction:
|
||||
|
||||
```yaml
|
||||
oidc:
|
||||
issuer: "https://your-provider.com"
|
||||
client_id: "headscale"
|
||||
client_secret: "your-secret"
|
||||
scope: ["openid", "profile", "email", "groups"]
|
||||
```
|
||||
|
||||
### Provider-Specific Configuration
|
||||
|
||||
=== "Keycloak"
|
||||
```yaml
|
||||
# Keycloak automatically includes groups in standard format
|
||||
oidc:
|
||||
scope: ["openid", "profile", "email", "groups"]
|
||||
```
|
||||
|
||||
=== "Azure AD"
|
||||
```yaml
|
||||
# Azure AD requires group claims configuration
|
||||
oidc:
|
||||
scope: ["openid", "profile", "email"]
|
||||
# Groups included via claims configuration in Azure AD
|
||||
```
|
||||
|
||||
=== "Okta"
|
||||
```yaml
|
||||
# Okta supports groups in tokens
|
||||
oidc:
|
||||
scope: ["openid", "profile", "email", "groups"]
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Groups not appearing:**
|
||||
- Verify `groups` scope is included in OIDC configuration
|
||||
- Check identity provider group claim configuration
|
||||
- Ensure user is member of groups in identity provider
|
||||
|
||||
**Invalid group data:**
|
||||
- Non-string group values are automatically filtered out
|
||||
- Empty arrays are valid (user has no groups)
|
||||
- Malformed JSON is handled gracefully with empty array fallback
|
||||
|
||||
### Error Responses
|
||||
|
||||
Standard HTTP error codes apply:
|
||||
|
||||
- `401 Unauthorized`: Invalid or missing API key
|
||||
- `404 Not Found`: User does not exist
|
||||
- `500 Internal Server Error`: Server-side processing error
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Access Control
|
||||
|
||||
- Groups information is available to any client with valid API key
|
||||
- Consider creating read-only API keys for external integrations
|
||||
- Audit API key usage for compliance requirements
|
||||
|
||||
### Data Privacy
|
||||
|
||||
- Group names may contain sensitive organizational information
|
||||
- Consider data classification for group membership information
|
||||
- Implement appropriate access controls for group data
|
||||
|
||||
### Token Security
|
||||
|
||||
- Groups are extracted from verified OIDC tokens only
|
||||
- Token validation ensures groups cannot be spoofed
|
||||
- Groups are updated only during successful authentication
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Metrics
|
||||
|
||||
Monitor group extraction and updates:
|
||||
|
||||
- Number of OIDC logins with groups extracted
|
||||
- Distribution of group membership across users
|
||||
- Failed group extractions or parsing errors
|
||||
|
||||
### Logging
|
||||
|
||||
Key log events to monitor:
|
||||
|
||||
- Group extraction from OIDC claims
|
||||
- Group updates during user authentication
|
||||
- Group parsing errors or validation failures
|
||||
|
||||
Example log entries:
|
||||
|
||||
```
|
||||
INFO Groups extracted from OIDC claims user=alice groups=["admin","developers"]
|
||||
WARN Invalid group value filtered out user=bob value=123 type=number
|
||||
ERROR Failed to parse groups from OIDC claims user=charlie error="invalid JSON"
|
||||
```
|
||||
|
||||
This API enhancement provides the foundation for implementing sophisticated role-based access control systems while maintaining backward compatibility with existing deployments.
|
||||
Loading…
Add table
Add a link
Reference in a new issue