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.
|
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
|
### 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.
|
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):
|
async def watch(req, ws):
|
||||||
# Build user info from either built-in auth or SSO
|
# Build user info from either built-in auth or SSO
|
||||||
user_info = None
|
user_info = None
|
||||||
if sso_user := getattr(req.ctx, "sso_user", None):
|
if sso.paskia_enabled():
|
||||||
# SSO auth (paskia mode): extract from validation response
|
# SSO auth: call validation to get user info (don't enforce auth in public mode)
|
||||||
ctx = sso_user.get("ctx", {})
|
try:
|
||||||
perms = ctx.get("permissions", [])
|
await sso.validate_sso_request(req)
|
||||||
user_info = {
|
except Exception:
|
||||||
"username": ctx.get("user", {}).get("display_name", ""),
|
pass # Ignore auth errors, user_info stays None
|
||||||
"privileged": "cista:admin" in perms,
|
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:
|
elif req.ctx.user:
|
||||||
# Built-in auth: use local user database
|
# Built-in auth: use local user database
|
||||||
user_info = {
|
user_info = {
|
||||||
|
|||||||
+1
-4
@@ -257,10 +257,7 @@ def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]:
|
|||||||
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
@app.get("/zip/<keys>/<zipfile:ext=zip>")
|
||||||
async def zip_download(req, keys, zipfile, ext):
|
async def zip_download(req, keys, zipfile, ext):
|
||||||
"""Download a zip archive of the given keys"""
|
"""Download a zip archive of the given keys"""
|
||||||
if config.config.authentication == "paskia":
|
await auth.verify(req)
|
||||||
await auth.verify_sso(req)
|
|
||||||
else:
|
|
||||||
auth.verify(req)
|
|
||||||
|
|
||||||
wanted = set(keys.split("+"))
|
wanted = set(keys.split("+"))
|
||||||
files = get_files(wanted)
|
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 paskia mode (PASKIA_BACKEND_URL set), validates against the SSO backend.
|
||||||
For built-in mode, checks session-based authentication.
|
For built-in mode, checks session-based authentication.
|
||||||
For public mode (config.public=True), allows all requests.
|
For public mode (config.public=True), skips auth unless privileged is required.
|
||||||
|
|
||||||
All 401/403 responses include auth.iframe URL for consistent frontend handling
|
|
||||||
via the paskia library's showAuthIframe().
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: The Sanic request object
|
request: The Sanic request object
|
||||||
privileged: If True, requires admin privileges
|
privileged: If True, requires admin privileges (always enforced even in public mode)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Unauthorized: If authentication is required
|
Unauthorized: If authentication is required
|
||||||
Forbidden: If access is denied
|
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()
|
sso = _get_sso()
|
||||||
if sso.paskia_enabled():
|
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"
|
perm = "cista:admin" if privileged else "cista:login"
|
||||||
await sso.validate_sso_request(request, perm=perm)
|
await sso.validate_sso_request(request, perm=perm)
|
||||||
return
|
return
|
||||||
|
|
||||||
user = getattr(request.ctx, "user", None)
|
user = getattr(request.ctx, "user", None)
|
||||||
if privileged:
|
if privileged:
|
||||||
if user:
|
if user and user.privileged:
|
||||||
if user.privileged:
|
return
|
||||||
return
|
raise Forbidden(
|
||||||
raise Forbidden(
|
"Access Forbidden: Only for privileged users",
|
||||||
"Access Forbidden: Only for privileged users",
|
quiet=True,
|
||||||
quiet=True,
|
)
|
||||||
)
|
if user:
|
||||||
elif config.config.public or user:
|
|
||||||
return
|
return
|
||||||
# Return iframe URL for paskia library to show login dialog
|
|
||||||
raise Unauthorized(
|
raise Unauthorized(
|
||||||
f"Login required for {request.path}",
|
f"Login required for {request.path}",
|
||||||
"cookie",
|
"cookie",
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ const settingsMenu = (e: Event) => {
|
|||||||
|
|
||||||
if (store.user.isLoggedIn) {
|
if (store.user.isLoggedIn) {
|
||||||
items.push({ label: '🚪 Logout', onClick: () => store.logout() })
|
items.push({ label: '🚪 Logout', onClick: () => store.logout() })
|
||||||
} else if (!ssoStore.isExternalAuth) {
|
} else if (store.server.public) {
|
||||||
// Show login in paskia iframe overlay
|
// Show login option only in public mode (non-public modes trigger auth automatically)
|
||||||
items.push({ label: '🔐 Login', onClick: async () => {
|
items.push({ label: '🔐 Login', onClick: async () => {
|
||||||
try {
|
try {
|
||||||
await showAuthIframe('/auth/restricted')
|
await showAuthIframe('/auth/restricted#theme=light')
|
||||||
resumeWatching()
|
resumeWatching()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Login cancelled')
|
console.log('Login cancelled')
|
||||||
|
|||||||
Reference in New Issue
Block a user