Adding mkdocs and privileged tools
This commit is contained in:
274
docs/authentication.md
Normal file
274
docs/authentication.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Authentication Architecture
|
||||
|
||||
This document describes the authentication stack for SimbaRAG: LLDAP → Authelia → OAuth2/OIDC.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ LLDAP │────▶│ Authelia │────▶│ OAuth2/OIDC │────▶│ SimbaRAG │
|
||||
│ (Users) │ │ (IdP) │ │ (Flow) │ │ (App) │
|
||||
└─────────┘ └──────────┘ └──────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **LLDAP** | Lightweight LDAP server storing users and groups |
|
||||
| **Authelia** | Identity provider that authenticates against LLDAP and issues OIDC tokens |
|
||||
| **SimbaRAG** | Relying party that consumes OIDC tokens and manages sessions |
|
||||
|
||||
## OIDC Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `OIDC_ISSUER` | Authelia server URL | Required |
|
||||
| `OIDC_CLIENT_ID` | Client ID registered in Authelia | Required |
|
||||
| `OIDC_CLIENT_SECRET` | Client secret for token exchange | Required |
|
||||
| `OIDC_REDIRECT_URI` | Callback URL after authentication | Required |
|
||||
| `OIDC_USE_DISCOVERY` | Enable automatic discovery | `true` |
|
||||
| `JWT_SECRET_KEY` | Secret for signing backend JWTs | Required |
|
||||
|
||||
### Discovery
|
||||
|
||||
When `OIDC_USE_DISCOVERY=true`, the application fetches endpoints from:
|
||||
|
||||
```
|
||||
{OIDC_ISSUER}/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
This provides:
|
||||
|
||||
- Authorization endpoint
|
||||
- Token endpoint
|
||||
- JWKS URI for signature verification
|
||||
- Supported scopes and claims
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### 1. Login Initiation
|
||||
|
||||
```
|
||||
GET /api/user/oidc/login
|
||||
```
|
||||
|
||||
1. Generate PKCE code verifier and challenge (S256)
|
||||
2. Generate CSRF state token
|
||||
3. Store state in session storage
|
||||
4. Return authorization URL for frontend redirect
|
||||
|
||||
### 2. Authorization
|
||||
|
||||
User is redirected to Authelia where they:
|
||||
|
||||
1. Enter LDAP credentials
|
||||
2. Complete MFA if configured
|
||||
3. Consent to requested scopes
|
||||
|
||||
### 3. Callback
|
||||
|
||||
```
|
||||
GET /api/user/oidc/callback?code=...&state=...
|
||||
```
|
||||
|
||||
1. Validate state matches stored value (CSRF protection)
|
||||
2. Exchange authorization code for tokens using PKCE verifier
|
||||
3. Verify ID token signature using JWKS
|
||||
4. Validate claims (issuer, audience, expiration)
|
||||
5. Create or update user in database
|
||||
6. Issue backend JWT tokens (access + refresh)
|
||||
|
||||
### 4. Token Refresh
|
||||
|
||||
```
|
||||
POST /api/user/refresh
|
||||
Authorization: Bearer <refresh_token>
|
||||
```
|
||||
|
||||
Issues a new access token without re-authentication.
|
||||
|
||||
## User Model
|
||||
|
||||
```python
|
||||
class User(Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
username = CharField(max_length=255)
|
||||
password = BinaryField(null=True) # Nullable for OIDC-only users
|
||||
email = CharField(max_length=100, unique=True)
|
||||
|
||||
# OIDC fields
|
||||
oidc_subject = CharField(max_length=255, unique=True, null=True)
|
||||
auth_provider = CharField(max_length=50, default="local") # "local" or "oidc"
|
||||
ldap_groups = JSONField(default=[]) # LDAP groups from OIDC claims
|
||||
|
||||
created_at = DatetimeField(auto_now_add=True)
|
||||
updated_at = DatetimeField(auto_now=True)
|
||||
|
||||
def has_group(self, group: str) -> bool:
|
||||
"""Check if user belongs to a specific LDAP group."""
|
||||
return group in (self.ldap_groups or [])
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is an admin (member of lldap_admin group)."""
|
||||
return self.has_group("lldap_admin")
|
||||
```
|
||||
|
||||
### User Provisioning
|
||||
|
||||
The `OIDCUserService` handles automatic user creation:
|
||||
|
||||
1. Extract claims from ID token (`sub`, `email`, `preferred_username`)
|
||||
2. Check if user exists by `oidc_subject`
|
||||
3. If not, check by email for migration from local auth
|
||||
4. Create new user or update existing
|
||||
|
||||
## JWT Tokens
|
||||
|
||||
Backend issues its own JWTs after OIDC authentication:
|
||||
|
||||
| Token Type | Purpose | Typical Lifetime |
|
||||
|------------|---------|------------------|
|
||||
| Access Token | API authorization | 15 minutes |
|
||||
| Refresh Token | Obtain new access tokens | 7 days |
|
||||
|
||||
### Claims
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": "<user-uuid>",
|
||||
"type": "access|refresh",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
## Protected Endpoints
|
||||
|
||||
All API endpoints use the `@jwt_refresh_token_required` decorator for basic authentication:
|
||||
|
||||
```python
|
||||
@blueprint.route("/example")
|
||||
@jwt_refresh_token_required
|
||||
async def protected_endpoint():
|
||||
user_id = get_jwt_identity()
|
||||
# ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role-Based Access Control (RBAC)
|
||||
|
||||
RBAC is implemented using LDAP groups passed through Authelia as OIDC claims. Users in the `lldap_admin` group have admin privileges.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LLDAP │
|
||||
│ Groups: lldap_admin, lldap_user │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Authelia │
|
||||
│ Scope: groups → Claim: groups = ["lldap_admin"] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SimbaRAG │
|
||||
│ 1. Extract groups from ID token │
|
||||
│ 2. Store in User.ldap_groups │
|
||||
│ 3. Check membership with @admin_required decorator │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Authelia Configuration
|
||||
|
||||
Ensure Authelia is configured to pass the `groups` claim:
|
||||
|
||||
```yaml
|
||||
identity_providers:
|
||||
oidc:
|
||||
clients:
|
||||
- client_id: simbarag
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- groups # Required for RBAC
|
||||
```
|
||||
|
||||
### Admin-Only Endpoints
|
||||
|
||||
The `@admin_required` decorator protects privileged endpoints:
|
||||
|
||||
```python
|
||||
from blueprints.users.decorators import admin_required
|
||||
|
||||
@blueprint.post("/admin-action")
|
||||
@admin_required
|
||||
async def admin_only_endpoint():
|
||||
# Only users in lldap_admin group can access
|
||||
...
|
||||
```
|
||||
|
||||
**Protected endpoints:**
|
||||
|
||||
| Endpoint | Access | Description |
|
||||
|----------|--------|-------------|
|
||||
| `POST /api/rag/index` | Admin | Trigger document indexing |
|
||||
| `POST /api/rag/reindex` | Admin | Clear and reindex all documents |
|
||||
| `GET /api/rag/stats` | All users | View vector store statistics |
|
||||
|
||||
### User Response
|
||||
|
||||
The OIDC callback returns group information:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"groups": ["lldap_admin", "lldap_user"],
|
||||
"is_admin": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Current Gaps
|
||||
|
||||
| Issue | Risk | Mitigation |
|
||||
|-------|------|------------|
|
||||
| In-memory session storage | State lost on restart, not scalable | Use Redis for production |
|
||||
| No token revocation | Tokens valid until expiry | Implement blacklist or short expiry |
|
||||
| No audit logging | Cannot track auth events | Add event logging |
|
||||
| Single JWT secret | Compromise affects all tokens | Rotate secrets, use asymmetric keys |
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Use Redis** for OIDC state storage in production
|
||||
2. **Implement logout** with token blacklisting
|
||||
3. **Add audit logging** for authentication events
|
||||
4. **Rotate JWT secrets** regularly
|
||||
5. **Use short-lived access tokens** (15 min) with refresh
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `services/raggr/oidc_config.py` | OIDC client configuration and discovery |
|
||||
| `services/raggr/blueprints/users/models.py` | User model definition with group helpers |
|
||||
| `services/raggr/blueprints/users/oidc_service.py` | User provisioning from OIDC claims |
|
||||
| `services/raggr/blueprints/users/__init__.py` | Auth endpoints and flow |
|
||||
| `services/raggr/blueprints/users/decorators.py` | Auth decorators (`@admin_required`) |
|
||||
14
docs/index.md
Normal file
14
docs/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# SimbaRAG Documentation
|
||||
|
||||
SimbaRAG is a RAG-powered conversational AI system with enterprise authentication.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Backend**: Quart (async Python) with Tortoise ORM
|
||||
- **Vector Store**: LangChain with configurable embeddings
|
||||
- **Auth Stack**: LLDAP → Authelia → OAuth2/OIDC
|
||||
- **Database**: PostgreSQL
|
||||
|
||||
## Sections
|
||||
|
||||
- [Authentication](authentication.md) - OIDC flow, user management, and RBAC planning
|
||||
81
index.html
Normal file
81
index.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!doctype html>
|
||||
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="author" content="Paperless-ngx project and contributors">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
|
||||
<title>
|
||||
|
||||
Paperless-ngx sign in
|
||||
|
||||
</title>
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/base.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="text-center">
|
||||
<div class="position-absolute top-50 start-50 translate-middle">
|
||||
<form class="form-accounts" id="form-account" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="KLQ3mMraTFHfK9sMmc6DJcNIS6YixeHnSJiT3A12LYB49HeEXOpx5RnY9V6uPSrD">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" width='300' class='logo mb-4'>
|
||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||
<g class="text" style="fill:#000">
|
||||
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
|
||||
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
|
||||
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
|
||||
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
|
||||
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
|
||||
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
|
||||
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
|
||||
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
|
||||
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
|
||||
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
|
||||
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
Please sign in.
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="form-floating form-stacked-top">
|
||||
<input type="text" name="login" id="inputUsername" placeholder="Username" class="form-control" autocorrect="off" autocapitalize="none" required autofocus>
|
||||
<label for="inputUsername">Username</label>
|
||||
</div>
|
||||
<div class="form-floating form-stacked-bottom">
|
||||
<input type="password" name="password" id="inputPassword" placeholder="Password" class="form-control" required>
|
||||
<label for="inputPassword">Password</label>
|
||||
</div>
|
||||
<div class="d-grid mt-3">
|
||||
<button class="btn btn-lg btn-primary" type="submit">Sign in</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
25
mkdocs.yml
Normal file
25
mkdocs.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
site_name: SimbaRAG Documentation
|
||||
site_description: Documentation for SimbaRAG - RAG-powered conversational AI
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- content.code.copy
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- tables
|
||||
- toc:
|
||||
permalink: true
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Architecture:
|
||||
- Authentication: authentication.md
|
||||
@@ -2,6 +2,7 @@ from quart import Blueprint, jsonify
|
||||
from quart_jwt_extended import jwt_refresh_token_required
|
||||
|
||||
from .logic import get_vector_store_stats, index_documents, vector_store
|
||||
from blueprints.users.decorators import admin_required
|
||||
|
||||
rag_blueprint = Blueprint("rag_api", __name__, url_prefix="/api/rag")
|
||||
|
||||
@@ -15,9 +16,9 @@ async def get_stats():
|
||||
|
||||
|
||||
@rag_blueprint.post("/index")
|
||||
@jwt_refresh_token_required
|
||||
@admin_required
|
||||
async def trigger_index():
|
||||
"""Trigger indexing of documents from Paperless-NGX."""
|
||||
"""Trigger indexing of documents from Paperless-NGX. Admin only."""
|
||||
try:
|
||||
await index_documents()
|
||||
stats = get_vector_store_stats()
|
||||
@@ -27,9 +28,9 @@ async def trigger_index():
|
||||
|
||||
|
||||
@rag_blueprint.post("/reindex")
|
||||
@jwt_refresh_token_required
|
||||
@admin_required
|
||||
async def trigger_reindex():
|
||||
"""Clear and reindex all documents."""
|
||||
"""Clear and reindex all documents. Admin only."""
|
||||
try:
|
||||
# Clear existing documents
|
||||
collection = vector_store._collection
|
||||
|
||||
@@ -60,7 +60,7 @@ async def oidc_login():
|
||||
"client_id": oidc_config.client_id,
|
||||
"response_type": "code",
|
||||
"redirect_uri": oidc_config.redirect_uri,
|
||||
"scope": "openid email profile",
|
||||
"scope": "openid email profile groups",
|
||||
"state": state,
|
||||
"code_challenge": code_challenge,
|
||||
"code_challenge_method": "S256",
|
||||
@@ -115,7 +115,9 @@ async def oidc_callback():
|
||||
token_response = await client.post(token_endpoint, data=token_data)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
return jsonify({"error": f"Failed to exchange code for token: {token_response.text}"}), 400
|
||||
return jsonify(
|
||||
{"error": f"Failed to exchange code for token: {token_response.text}"}
|
||||
), 400
|
||||
|
||||
tokens = token_response.json()
|
||||
|
||||
@@ -141,7 +143,13 @@ async def oidc_callback():
|
||||
return jsonify(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user={"id": str(user.id), "username": user.username, "email": user.email},
|
||||
user={
|
||||
"id": str(user.id),
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"groups": user.ldap_groups,
|
||||
"is_admin": user.is_admin(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
26
services/raggr/blueprints/users/decorators.py
Normal file
26
services/raggr/blueprints/users/decorators.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Authentication decorators for role-based access control.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
from quart import jsonify
|
||||
from quart_jwt_extended import jwt_refresh_token_required, get_jwt_identity
|
||||
from .models import User
|
||||
|
||||
|
||||
def admin_required(fn):
|
||||
"""
|
||||
Decorator that requires the user to be an admin (member of lldap_admin group).
|
||||
Must be used on async route handlers.
|
||||
"""
|
||||
|
||||
@wraps(fn)
|
||||
@jwt_refresh_token_required
|
||||
async def wrapper(*args, **kwargs):
|
||||
user_id = get_jwt_identity()
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if not user or not user.is_admin():
|
||||
return jsonify({"error": "Admin access required"}), 403
|
||||
return await fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -12,8 +12,13 @@ class User(Model):
|
||||
email = fields.CharField(max_length=100, unique=True)
|
||||
|
||||
# OIDC fields
|
||||
oidc_subject = fields.CharField(max_length=255, unique=True, null=True, index=True) # "sub" claim from OIDC
|
||||
auth_provider = fields.CharField(max_length=50, default="local") # "local" or "oidc"
|
||||
oidc_subject = fields.CharField(
|
||||
max_length=255, unique=True, null=True, index=True
|
||||
) # "sub" claim from OIDC
|
||||
auth_provider = fields.CharField(
|
||||
max_length=50, default="local"
|
||||
) # "local" or "oidc"
|
||||
ldap_groups = fields.JSONField(default=[]) # LDAP groups from OIDC claims
|
||||
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
@@ -21,6 +26,14 @@ class User(Model):
|
||||
class Meta:
|
||||
table = "users"
|
||||
|
||||
def has_group(self, group: str) -> bool:
|
||||
"""Check if user belongs to a specific LDAP group."""
|
||||
return group in (self.ldap_groups or [])
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is an admin (member of lldap_admin group)."""
|
||||
return self.has_group("lldap_admin")
|
||||
|
||||
def set_password(self, plain_password: str):
|
||||
self.password = bcrypt.hashpw(
|
||||
plain_password.encode("utf-8"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
OIDC User Management Service
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from uuid import uuid4
|
||||
from .models import User
|
||||
@@ -31,10 +32,10 @@ class OIDCUserService:
|
||||
# Update user info from latest claims (optional)
|
||||
user.email = claims.get("email", user.email)
|
||||
user.username = (
|
||||
claims.get("preferred_username")
|
||||
or claims.get("name")
|
||||
or user.username
|
||||
claims.get("preferred_username") or claims.get("name") or user.username
|
||||
)
|
||||
# Update LDAP groups from claims
|
||||
user.ldap_groups = claims.get("groups", [])
|
||||
await user.save()
|
||||
return user
|
||||
|
||||
@@ -47,6 +48,7 @@ class OIDCUserService:
|
||||
user.oidc_subject = oidc_subject
|
||||
user.auth_provider = "oidc"
|
||||
user.password = None # Clear password
|
||||
user.ldap_groups = claims.get("groups", [])
|
||||
await user.save()
|
||||
return user
|
||||
|
||||
@@ -58,14 +60,17 @@ class OIDCUserService:
|
||||
or f"user_{oidc_subject[:8]}"
|
||||
)
|
||||
|
||||
# Extract LDAP groups from claims
|
||||
groups = claims.get("groups", [])
|
||||
|
||||
user = await User.create(
|
||||
id=uuid4(),
|
||||
username=username,
|
||||
email=email
|
||||
or f"{oidc_subject}@oidc.local", # Fallback if no email claim
|
||||
email=email or f"{oidc_subject}@oidc.local", # Fallback if no email claim
|
||||
oidc_subject=oidc_subject,
|
||||
auth_provider="oidc",
|
||||
password=None,
|
||||
ldap_groups=groups,
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
RUN_IN_TRANSACTION = True
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "users" ADD COLUMN "ldap_groups" JSONB DEFAULT '[]';
|
||||
"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "users" DROP COLUMN "ldap_groups";
|
||||
"""
|
||||
Reference in New Issue
Block a user