Authentication, Users & Permissions¶
haystack-py includes a complete authentication and authorization system with user management, SCRAM-SHA-256 credentials, and role-based access control.
See also
HTTP Server for general server setup, Security for the full API reference.
Roles¶
Every user is assigned a Role that determines what they
can do. Roles form a strict hierarchy: Admin > Operator > Viewer.
Role |
Capabilities |
|---|---|
Admin |
Full access: user management CRUD, all read and write Haystack ops |
Operator |
Read + write Haystack ops (hisWrite, pointWrite, invokeAction, watchSub/Unsub/Poll) but no user management |
Viewer |
Read-only Haystack ops (read, nav, hisRead, defs, libs, filetypes) and informational GET ops (about, ops, formats) |
from hs_py.user import Role
Role.ADMIN > Role.OPERATOR # True
Role.OPERATOR > Role.VIEWER # True
User Model¶
User is a frozen dataclass that stores SCRAM-SHA-256
credentials — the plaintext password is never retained.
from hs_py.user import Role, create_user
admin = create_user("admin", "s3cret", role=Role.ADMIN)
op = create_user("operator", "pass", role=Role.OPERATOR,
first_name="Jane", email="jane@example.com")
viewer = create_user("viewer", "readonly", role=Role.VIEWER)
Fields:
username(required) — unique login identifierpassword(required, used only during creation) — plaintext, discarded after credential derivationfirst_name,last_name,email— optional profile fieldsrole—Roleenum (defaultVIEWER)enabled— boolean, disabled users cannot authenticatecredentials—ScramCredentials(derived from password)
UserStore Protocol¶
The UserStore protocol defines five async
methods for user persistence. All three storage backends implement it:
class UserStore(Protocol):
async def get_user(self, username: str) -> User | None: ...
async def list_users(self) -> list[User]: ...
async def create_user(self, user: User) -> None: ...
async def update_user(self, username: str, **fields) -> User: ...
async def delete_user(self, username: str) -> bool: ...
Each backend (InMemoryAdapter,
RedisAdapter,
TimescaleAdapter) implements both
StorageAdapter and UserStore, so a single instance serves as a unified
storage layer.
StorageAuthenticator¶
StorageAuthenticator bridges the
Authenticator protocol to a UserStore. It
reads SCRAM credentials from the store and returns None for disabled or
missing users (blocking authentication).
from hs_py.auth_types import StorageAuthenticator
from hs_py.storage.memory import InMemoryAdapter
storage = InMemoryAdapter()
auth = StorageAuthenticator(storage)
Wire this into create_fastapi_app():
from hs_py.fastapi_server import create_fastapi_app
app = create_fastapi_app(
storage=storage,
authenticator=auth,
user_store=storage,
)
Admin Bootstrap¶
On startup, the server calls ensure_superuser() to
verify that at least one enabled Admin user exists. If none is found, it
attempts to seed one from environment variables:
Variable |
Description |
|---|---|
|
Username for the seeded admin account |
|
Password for the seeded admin account |
If neither an Admin user nor environment variables are present, the server exits with a clear error message.
export HS_SUPERUSER_USERNAME=admin
export HS_SUPERUSER_PASSWORD=supersecret
uvicorn myapp:app --host 0.0.0.0 --port 8080
User Management API¶
Admin users can manage users via REST JSON endpoints under /api/users/:
Method |
Endpoint |
Description |
|---|---|---|
|
|
Create a user ( |
|
|
List all users |
|
|
Get a single user |
|
|
Update fields (password, role, enabled, email, etc.) |
|
|
Delete a user (self-delete prevented) |
Example — create an operator user:
curl -X POST http://localhost:8080/api/users/ \
-H "Authorization: BEARER authToken=<token>" \
-H "Content-Type: application/json" \
-d '{
"username": "operator1",
"password": "pass123",
"role": "operator",
"first_name": "Jane",
"email": "jane@example.com"
}'
Example — promote a user to admin:
curl -X PUT http://localhost:8080/api/users/operator1 \
-H "Authorization: BEARER authToken=<token>" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
Responses are plain JSON (not Haystack grids). Passwords are write-only and never included in API responses.
Permission Enforcement¶
Roles are enforced on every Haystack operation, on both HTTP and WebSocket transports.
Write ops — require Operator or Admin role:
hisWrite,pointWrite,invokeActionwatchSub,watchUnsub,watchPoll
Read ops — require any authenticated role (Viewer+):
read,nav,hisReaddefs,libs,filetypes
GET ops (about, ops, formats, close) — accessible to any authenticated user.
User management — requires Admin role only.
When a user lacks sufficient permissions, the server returns a Haystack error grid with a descriptive message:
Insufficient permissions: hisWrite requires operator or admin role
Complete Example¶
import asyncio
import uvicorn
from hs_py import MARKER, Ref
from hs_py.user import Role, create_user
from hs_py.auth_types import StorageAuthenticator
from hs_py.fastapi_server import create_fastapi_app
from hs_py.storage.memory import InMemoryAdapter
async def main():
storage = InMemoryAdapter()
await storage.start()
# Seed entities
await storage.load_entities([
{"id": Ref("s1"), "site": MARKER, "dis": "Main Office"},
])
# Create users with different roles
await storage.create_user(
create_user("admin", "admin-pass", role=Role.ADMIN)
)
await storage.create_user(
create_user("operator", "op-pass", role=Role.OPERATOR)
)
await storage.create_user(
create_user("viewer", "view-pass", role=Role.VIEWER)
)
# Wire auth and create the app
auth = StorageAuthenticator(storage)
app = create_fastapi_app(
storage=storage,
authenticator=auth,
user_store=storage,
)
uvicorn.run(app, host="0.0.0.0", port=8080)
asyncio.run(main())