Implements Issue #11: Silo origin adapter
This commit creates the SiloOrigin class that implements the FileOrigin
interface introduced in Issue #9, enabling Silo to be used as a document
origin in the unified file origin system.
## SiloOrigin Class (silo_origin.py)
New Python module providing the FileOrigin implementation for Silo PLM:
### Identity Methods
- id(): Returns 'silo' as unique identifier
- name(): Returns 'Kindred Silo' for UI display
- nickname(): Returns 'Silo' for compact UI elements
- icon(): Returns 'silo' icon name
- type(): Returns OriginType.PLM (1)
### Workflow Characteristics
- tracksExternally(): True - Silo tracks documents in database
- requiresAuthentication(): True - Silo requires login
### Capabilities
- supportsRevisions(): True
- supportsBOM(): True
- supportsPartNumbers(): True
- supportsAssemblies(): True
### Connection State
- connectionState(): Checks auth status and API connectivity
- connect(): Triggers Silo_Auth dialog if needed
- disconnect(): Calls _client.logout()
### Document Identity (UUID-based tracking)
- documentIdentity(): Returns SiloItemId (UUID) as primary identity
- documentDisplayId(): Returns SiloPartNumber for human display
- ownsDocument(): True if document has SiloItemId or SiloPartNumber
### Core Operations (delegate to existing commands)
- newDocument(): Delegates to Silo_New command
- openDocument(): Uses find_file_by_part_number or _sync.open_item
- saveDocument(): Saves locally + uploads via _client._upload_file
- saveDocumentAs(): Triggers migration workflow for local docs
### Extended Operations
- commitDocument(): Delegates to Silo_Commit
- pullDocument(): Delegates to Silo_Pull
- pushDocument(): Delegates to Silo_Push
- showInfo(): Delegates to Silo_Info
- showBOM(): Delegates to Silo_BOM
### Module Functions
- get_silo_origin(): Returns singleton instance
- register_silo_origin(): Registers with FreeCADGui.addOrigin()
- unregister_silo_origin(): Cleanup function
## UUID Tracking (silo_commands.py)
Added SiloItemId property to all locations where Silo properties are set:
1. create_document_for_item() - Assembly objects (line 1115)
2. create_document_for_item() - Fallback Part objects (line 1131)
3. create_document_for_item() - Part objects (line 1145)
4. Silo_New.Activated() - Tagged existing objects (line 1471)
The SiloItemId stores the database UUID (Item.ID) which is immutable,
while SiloPartNumber remains the human-readable identifier that could
theoretically change.
Property structure on tracked objects:
- SiloItemId: UUID from database (primary tracking key)
- SiloPartNumber: Human-readable part number
- SiloRevision: Current revision number
- SiloItemType: 'part' or 'assembly'
## Workbench Integration (InitGui.py)
SiloOrigin is automatically registered when the Silo workbench
initializes:
def Initialize(self):
import silo_commands
try:
import silo_origin
silo_origin.register_silo_origin()
except Exception as e:
FreeCAD.Console.PrintWarning(...)
This makes Silo available as a file origin via:
- FreeCADGui.listOrigins() -> includes 'silo'
- FreeCADGui.getOrigin('silo') -> returns origin info dict
- FreeCADGui.setActiveOrigin('silo') -> sets Silo as active
## Design Decisions
1. **Delegation Pattern**: SiloOrigin delegates to existing Silo
commands rather than reimplementing logic, ensuring consistency
and easier maintenance.
2. **UUID as Primary Identity**: documentIdentity() returns UUID
(SiloItemId) for immutable tracking, while documentDisplayId()
returns part number for user display.
3. **Graceful Fallback**: If SiloItemId is not present (legacy docs),
falls back to SiloPartNumber for identity/ownership checks.
4. **Exception Handling**: All operations wrapped in try/except to
prevent origin system failures from breaking FreeCAD.
Refs: #11
- Add SiloEventListener class for SSE-based real-time notifications
- Add SiloPullDialog for revision selection when pulling
- Add conflict detection before pull (unsaved changes, stale revision, mtime)
- Add progress callback to _download_file() for UI feedback
- Integrate SSE listener into SiloAuthDockWidget with status indicator
- Refresh activity panel on remote change events
- Add top-level PySide.QtCore import for QThread/Signal usage
The sequence counter (sequences_by_name table) can get out of sync
with the items table if items were seeded/imported directly or if a
previous create failed after incrementing the sequence but before
the insert committed. This causes Generate() to return a part number
that already exists, hitting the unique constraint on items.part_number.
Add a retry loop (up to 5 attempts) in HandleCreateItem that detects
PostgreSQL unique violation errors (SQLSTATE 23505) via pgconn.PgError
and retries with the next sequence value. Non-duplicate errors still
fail immediately. If all retries are exhausted, returns 409 Conflict
instead of 500.
BOMEntryResponse was missing the ParentPartNumber field. The database
query populates it correctly, but the API serialization dropped it.
This caused the Calc extension to never populate the hidden
_silo_parent_pn column during pull, so push never called
_update_bom_relationship and no BOM entries were created.
- New /audit page with completeness scoring engine
- Weighted scoring by sourcing type (purchased vs manufactured)
- Batch DB queries for items+properties, BOM existence, project codes
- API endpoints: GET /api/audit/completeness, GET /api/audit/completeness/{pn}
- Audit UI: tier summary bar, filterable table, split-panel inline editing
- Create item form now shows category-specific property fields on category select
- Properties collected and submitted with item creation
- Fix FreeCAD Push command: compare local file mtime against server's
latest file revision timestamp instead of just checking file existence.
Previously, any file already uploaded (even at an older revision) was
skipped, causing 'All local files are already uploaded' when local
files were newer.
- Add SiloClient.latest_file_revision() helper method
- Renumber 009_item_extended_fields.sql to 010 to avoid collision
with 009_auth.sql from the auth system
Rework the FreeCAD client authentication to match the Silo backend's
actual auth implementation (local, LDAP, OIDC/Keycloak, API tokens).
Auth model change:
- Remove the old AuthToken/AuthTokenExpiry session-token approach
- Unify on API tokens (ApiToken preference / SILO_API_TOKEN env var)
as the single auth mechanism for the desktop client
- _get_auth_token() now delegates to _get_api_token() directly
- All requests use Bearer token auth via _get_auth_headers()
SiloClient.login() rewrite:
- POST form-encoded credentials to /login (matching the backend's
form-based login handler, works with local and LDAP backends)
- Use the resulting session cookie to call GET /api/auth/me to get
user info (username, role, auth_source)
- Create a persistent API token via POST /api/auth/tokens named
'FreeCAD (hostname)' with 90-day expiry
- Store the raw token in ApiToken preference for all future requests
- No more ephemeral session tokens — API tokens survive restarts
New SiloClient methods:
- get_current_user(): GET /api/auth/me, returns user dict or None
- refresh_auth_info(): fetches /api/auth/me and updates cached prefs
- auth_role(): returns stored role (admin/editor/viewer)
- auth_source(): returns stored auth source (local/ldap/oidc)
- list_tokens(): GET /api/auth/tokens
- create_token(name, expires_in_days): POST /api/auth/tokens
- revoke_token(token_id): DELETE /api/auth/tokens/{id}
New preference keys:
- AuthRole: cached user role from server
- AuthSource: cached auth source (local, ldap, oidc)
Removed preference keys:
- AuthToken: replaced by ApiToken (was duplicative)
- AuthTokenExpiry: API tokens have server-side expiry
Auth helper changes:
- _save_auth_info(): stores username, role, source, and optionally token
- _clear_auth(): clears ApiToken, AuthUsername, AuthRole, AuthSource
- _get_auth_role(), _get_auth_source(): new accessors
Dock widget updates:
- New Role row showing role and auth source (e.g. 'editor (ldap)')
- Status refresh validates token against /api/auth/me on each poll
- Four status states: Connected (green), Token invalid (orange),
Connected no auth (yellow), Disconnected (red)
- Caches validated user info back to preferences
Login dialog updates:
- Info text explains the flow (creates persistent API token)
- Placeholder text updated (removed LDAP-specific wording)
- Shows 'Logging in...' status during auth
- Displays role and auth source on success
- Disables login button during request
Settings dialog updates:
- API Token input field with show/hide toggle
- Token can be pasted directly from Silo web UI
- Hint text explains token sources (web UI, Login, env var)
- 'Clear Token and Logout' button replaces old clear credentials
- Save handler persists token changes
- Status summary shows role and auth source
check_connection() fix:
- Uses origin /health (not /api/health) matching actual route
Fixes the endpoint mismatch where client was calling a non-existent
POST /api/auth/login JSON endpoint. Now uses the actual form-based
POST /login + session cookie flow that the backend implements.
Client-side authentication support for the Silo database API. Adds a
compact dock widget for auth status and login, bearer token management
on all HTTP requests, and an enhanced settings dialog.
Auth helper functions:
- _get_auth_token(): reads token from FreeCAD preferences, checks expiry
- _get_auth_username(): reads stored username from preferences
- _get_auth_headers(): returns Authorization bearer header dict
- _clear_auth(): clears token, username, and expiry from preferences
SiloClient auth methods:
- login(username, password): POSTs to /auth/login, stores token/user/expiry
- logout(): clears stored credentials via _clear_auth()
- is_authenticated(): checks for valid stored token
- auth_username(): returns stored username
- check_connection(): GETs /health to test server reachability
Auth header injection:
- _request(), _download_file(), _upload_file(), delete_bom_entry() all
merge _get_auth_headers() into outgoing HTTP requests
- _request() clears stored token on 401 response
SiloAuthDockWidget:
- Status indicator dot (green=authenticated, yellow=no auth, red=disconnected)
- Displays current username and server URL
- Login/Logout toggle button
- Gear button opens Silo_Settings dialog
- 30-second QTimer polls connection and auth status
- Login dialog with username/password fields and inline error display
Silo_Auth command:
- Shows/focuses the auth dock panel from menu or toolbar
- Registered in workbench toolbar alongside existing commands
Silo_Settings enhancements:
- New Authentication section showing current auth status
- Clear Saved Credentials button
- Active values summary now includes authentication state
New icon:
- silo-auth.svg padlock icon in Catppuccin Mocha style
Graceful degradation: when backend auth endpoint does not yet exist,
login fails with a clear error and all existing unauthenticated
requests continue to work as before (empty auth headers are a no-op).
- Add migration 009: sourcing_type (manufactured/purchased), sourcing_link,
long_description, and standard_cost columns on items table
- Update Item struct, repository queries, and API handlers for new fields
- Add sourcing badge, long description block, standard cost, and sourcing
link display to item detail panel
- Add inline project tag editor in detail panel (add/remove via dropdown)
- Add new fields to create and edit modals
- Update CSV import/export for new columns
- Merge with auth CreatedBy/UpdatedBy changes from stash
Add run_migrations function to deploy.sh that automatically applies
pending SQL migrations during deployment. Migrations are run after
config installation and before service restart.
Migration runner:
- Sources /etc/silo/silod.env for SILO_DB_PASSWORD
- Reads DB host/port/name/user from production config.yaml
- Waits for database connectivity (5 retries)
- Applies each migration file in order, skipping already-applied ones
- Gracefully degrades if psql is missing or DB password is not set
This fixes the missing migration 009 (auth tables) that caused:
- 'column created_by of relation projects does not exist'
- 'relation api_tokens does not exist'
Also adds auth environment variables to silod.env.example:
- SILO_SESSION_SECRET
- SILO_ADMIN_USERNAME / SILO_ADMIN_PASSWORD
- SILO_OIDC_CLIENT_SECRET, SILO_LDAP_BIND_PASSWORD
Add a complete authentication and authorization system to Silo with
three pluggable backends (local bcrypt, LDAP/FreeIPA, OIDC/Keycloak),
session management, API token support, and role-based access control.
Authentication backends:
- Local: bcrypt (cost 12) password verification against users table
- LDAP: FreeIPA simple bind with group-to-role mapping
- OIDC: Keycloak redirect flow with realm role mapping
- Backends are tried in order; users upserted to DB on first login
Session and token management:
- PostgreSQL-backed sessions via alexedwards/scs + pgxstore
- Opaque API tokens (silo_ prefix, SHA-256 hashed, shown once)
- 24h session lifetime, HttpOnly/SameSite=Lax/Secure cookies
Role-based access control (admin > editor > viewer):
- RequireAuth middleware: Bearer token -> session -> redirect/401
- RequireRole middleware: per-route-group minimum role enforcement
- CSRF protection via justinas/nosurf on web forms, API exempt
- CORS locked to configured origins when auth enabled
Route restructuring:
- Public: /health, /ready, /login, /auth/oidc, /auth/callback
- Web (auth + CSRF): /, /projects, /schemas, /settings
- API read (viewer): GET /api/**
- API write (editor): POST/PUT/PATCH/DELETE /api/**
User context wiring:
- created_by/updated_by columns on items, projects, relationships
- All create/update handlers populate tracking fields from context
- CSV and BOM import handlers pass authenticated username
- Revision creation tracks user across all code paths
Default admin account:
- Configurable via auth.local.default_admin_username/password
- Env var overrides: SILO_ADMIN_USERNAME, SILO_ADMIN_PASSWORD
- Idempotent: created on first startup, skipped if exists
CLI and FreeCAD plugin:
- silo token create/list/revoke subcommands (HTTP API client)
- FreeCAD SiloClient sends Bearer token on all requests
- Token read from ApiToken preference or SILO_API_TOKEN env var
Web UI:
- Login page (Catppuccin Mocha themed, OIDC button conditional)
- Settings page with account info and API token management
- User display name, role badge, and logout button in header
- One-time token display banner with copy-to-clipboard
Database (migration 009):
- users table with role, auth_source, oidc_subject, password_hash
- api_tokens table with SHA-256 hash, prefix, expiry, scopes
- sessions table (scs pgxstore schema)
- audit_log table (schema ready for future use)
- created_by/updated_by ALTER on items, relationships, projects
New dependencies: scs/v2, scs/pgxstore, go-oidc/v3, go-ldap/v3,
justinas/nosurf, golang.org/x/oauth2
- Silo_ToggleMode: checkable toolbar button that swaps Ctrl+O/S/N
between standard FreeCAD file commands and Silo equivalents
- _swap_shortcuts() helper stores/restores original QAction shortcuts
- SSL settings: add CA certificate file browser (SslCertPath preference)
with QFileDialog for .pem/.crt/.cer, loaded in _get_ssl_context()
- Integrate Silo_BOM into workbench toolbar (after upstream BOM merge)
- Add Silo_ToggleMode to toolbar as first item with separator
Implement the full Bill of Materials stack on top of the existing
relationships table and bom_single_level view from migration 001.
API endpoints (6 new routes under /api/items/{partNumber}/bom):
- GET /bom Single-level BOM for an item
- GET /bom/expanded Multi-level BOM via recursive CTE (depth param)
- GET /bom/where-used Reverse lookup: which parents use this item
- POST /bom Add child to BOM with quantity, ref designators
- PUT /bom/{child} Update relationship type, quantity, ref des
- DELETE /bom/{child} Remove child from BOM
Database layer (internal/db/relationships.go):
- RelationshipRepository with full CRUD operations
- Single-level BOM query joining relationships with items
- Multi-level BOM expansion via recursive CTE (max depth 20)
- Where-used reverse lookup query
- Cycle detection at insert time to prevent circular BOMs
- BOMEntry and BOMTreeEntry types for denormalized query results
Server wiring:
- Added RelationshipRepository to Server struct in handlers.go
- Registered BOM routes in routes.go under /{partNumber} subrouter
FreeCAD workbench (pkg/freecad/silo_commands.py):
- 9 new BOM methods on SiloClient (get, expanded, where-used, add,
update, delete)
- Silo_BOM command class with two-tab dialog:
- BOM tab: table of children with Add/Edit/Remove buttons
- Where Used tab: read-only table of parent assemblies
- Add sub-dialog with fields for part number, type, qty, unit, ref des
- Edit sub-dialog pre-populated with current values
- Remove with confirmation prompt
- silo-bom.svg icon matching existing toolbar style
- Command registered in InitGui.py toolbar
No new migrations required - uses existing relationships table and
bom_single_level view from 001_initial.sql.
Opening documents (especially assemblies) inside the QDialog nested
event loop can crash FreeCAD when the Assembly solver triggers during
document restore. Move FreeCAD.openDocument() to after dialog.exec_()
returns.
Use urllib.parse to check if the URL has a path component. Only append
/api when the path is empty (e.g. https://silo.kindred.internal).
URLs that already include a path (e.g. http://localhost:8080/api) are
left unchanged. Restore localhost placeholder as the default.
- Auto-append /api to base URL if not already present, so users can
enter just the hostname (e.g. https://silo.kindred.internal)
- Load system CA certificate bundle in SSL context so the bundled
Python trusts internal CAs (FreeIPA) without disabling verification
- Update settings dialog placeholder and hint text to clarify expected
URL format
Replace hardcoded FreeCAD addon path searches with __file__-relative
resolution for icons in both InitGui.py and silo_commands.py. Icons now
load correctly regardless of install location.
Add Silo_Settings command with URL and SSL verification fields. Settings
persist via FreeCAD preferences and take priority over env vars. Wire
SSL context into all SiloClient HTTP methods.