Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98949e6b30 | ||
|
|
8f76d770ee |
@@ -38,6 +38,39 @@ pip install cista --break-system-packages
|
||||
|
||||
The server remembers its settings in the config folder (default `~/.local/share/cista/`), including the listen port and directory, for future runs without arguments.
|
||||
|
||||
## Authentication
|
||||
|
||||
Cista supports three authentication modes:
|
||||
|
||||
### Built-in Authentication (default)
|
||||
|
||||
User accounts are managed directly by Cista. Create users with the `--user` flag:
|
||||
|
||||
```fish
|
||||
uvx cista --user admin --privileged # Create admin user
|
||||
uvx cista --user guest # Create regular user
|
||||
```
|
||||
|
||||
Privileged users can manage other users and change settings via the Admin Settings menu.
|
||||
|
||||
### Public Mode
|
||||
|
||||
In public mode, anyone can read, send and even delete files without without logging in. Privileged users can still log in via the menu to access admin settings, from where the public mode can be toggled on or off.
|
||||
|
||||
### Paskia SSO Authentication
|
||||
|
||||
For centralized authentication, Cista can integrate with [Paskia](https://git.zi.fi/LeoVasanko/paskia) SSO server. Set the `PASKIA_BACKEND_URL` environment variable:
|
||||
|
||||
```fish
|
||||
PASKIA_BACKEND_URL=http://localhost:4401 uvx cista
|
||||
```
|
||||
|
||||
In Paskia mode:
|
||||
- All `/auth/*` requests are proxied to the Paskia backend
|
||||
- Users with `cista:login` permission can access files
|
||||
- Users with `cista:admin` permission get privileged access (Admin Settings)
|
||||
- Public mode works with Paskia: unauthenticated users can browse, while the menu has option to login
|
||||
|
||||
### Internet Access
|
||||
|
||||
Most admins find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
|
||||
|
||||
+13
-8
@@ -95,14 +95,19 @@ async def control(req, ws):
|
||||
async def watch(req, ws):
|
||||
# Build user info from either built-in auth or SSO
|
||||
user_info = None
|
||||
if sso_user := getattr(req.ctx, "sso_user", None):
|
||||
# SSO auth (paskia mode): extract from validation response
|
||||
ctx = sso_user.get("ctx", {})
|
||||
perms = ctx.get("permissions", [])
|
||||
user_info = {
|
||||
"username": ctx.get("user", {}).get("display_name", ""),
|
||||
"privileged": "cista:admin" in perms,
|
||||
}
|
||||
if sso.paskia_enabled():
|
||||
# SSO auth: call validation to get user info (don't enforce auth in public mode)
|
||||
try:
|
||||
await sso.validate_sso_request(req)
|
||||
except Exception:
|
||||
pass # Ignore auth errors, user_info stays None
|
||||
if sso_user := getattr(req.ctx, "sso_user", None):
|
||||
ctx = sso_user.get("ctx", {})
|
||||
perms = ctx.get("permissions", [])
|
||||
user_info = {
|
||||
"username": ctx.get("user", {}).get("display_name", ""),
|
||||
"privileged": "cista:admin" in perms,
|
||||
}
|
||||
elif req.ctx.user:
|
||||
# Built-in auth: use local user database
|
||||
user_info = {
|
||||
|
||||
+1
-4
@@ -257,10 +257,7 @@ def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]:
|
||||
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
||||
async def zip_download(req, keys, zipfile, ext):
|
||||
"""Download a zip archive of the given keys"""
|
||||
if config.config.authentication == "paskia":
|
||||
await auth.verify_sso(req)
|
||||
else:
|
||||
auth.verify(req)
|
||||
await auth.verify(req)
|
||||
|
||||
wanted = set(keys.split("+"))
|
||||
files = get_files(wanted)
|
||||
|
||||
+13
-16
@@ -236,39 +236,36 @@ async def verify(request, *, privileged=False):
|
||||
|
||||
For paskia mode (PASKIA_BACKEND_URL set), validates against the SSO backend.
|
||||
For built-in mode, checks session-based authentication.
|
||||
For public mode (config.public=True), allows all requests.
|
||||
|
||||
All 401/403 responses include auth.iframe URL for consistent frontend handling
|
||||
via the paskia library's showAuthIframe().
|
||||
For public mode (config.public=True), skips auth unless privileged is required.
|
||||
|
||||
Args:
|
||||
request: The Sanic request object
|
||||
privileged: If True, requires admin privileges
|
||||
privileged: If True, requires admin privileges (always enforced even in public mode)
|
||||
|
||||
Raises:
|
||||
Unauthorized: If authentication is required
|
||||
Forbidden: If access is denied
|
||||
"""
|
||||
# Public mode: skip auth unless privileged access is required
|
||||
if config.config.public and not privileged:
|
||||
return
|
||||
|
||||
sso = _get_sso()
|
||||
if sso.paskia_enabled():
|
||||
# SSO validation against auth backend
|
||||
# Always check cista:login; privileged flag comes from response perm list
|
||||
perm = "cista:admin" if privileged else "cista:login"
|
||||
await sso.validate_sso_request(request, perm=perm)
|
||||
return
|
||||
|
||||
user = getattr(request.ctx, "user", None)
|
||||
if privileged:
|
||||
if user:
|
||||
if user.privileged:
|
||||
return
|
||||
raise Forbidden(
|
||||
"Access Forbidden: Only for privileged users",
|
||||
quiet=True,
|
||||
)
|
||||
elif config.config.public or user:
|
||||
if user and user.privileged:
|
||||
return
|
||||
raise Forbidden(
|
||||
"Access Forbidden: Only for privileged users",
|
||||
quiet=True,
|
||||
)
|
||||
if user:
|
||||
return
|
||||
# Return iframe URL for paskia library to show login dialog
|
||||
raise Unauthorized(
|
||||
f"Login required for {request.path}",
|
||||
"cookie",
|
||||
|
||||
@@ -94,11 +94,11 @@ const settingsMenu = (e: Event) => {
|
||||
|
||||
if (store.user.isLoggedIn) {
|
||||
items.push({ label: '🚪 Logout', onClick: () => store.logout() })
|
||||
} else if (!ssoStore.isExternalAuth) {
|
||||
// Show login in paskia iframe overlay
|
||||
} else if (store.server.public) {
|
||||
// Show login option only in public mode (non-public modes trigger auth automatically)
|
||||
items.push({ label: '🔐 Login', onClick: async () => {
|
||||
try {
|
||||
await showAuthIframe('/auth/restricted')
|
||||
await showAuthIframe('/auth/restricted#theme=light')
|
||||
resumeWatching()
|
||||
} catch (e) {
|
||||
console.log('Login cancelled')
|
||||
|
||||
Reference in New Issue
Block a user