-
Notifications
You must be signed in to change notification settings - Fork 0
Redesign SurrealDB auth model for virtual keys #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
91f722d
Redesign SurrealDB auth model for virtual keys
CLoaKY233 d4e030b
Feat: add wildcard models route and alias trim
CLoaKY233 1245454
Migrate to model_definition and add routing models
CLoaKY233 670d97e
Improve error mapping for virtual keys and proxy
CLoaKY233 4b5c2a0
Add retry backoff for provider credential delete
CLoaKY233 6747b4c
Secure DB permissions and hide encrypted keys
CLoaKY233 a25e991
Chore: Add SurrealDB setup scripts and README
CLoaKY233 698eada
delete setup scripts
CLoaKY233 d871b74
Add atomic provider sync locking
CLoaKY233 e161ac3
Improve DB sync, permissions, and proxy error handling
CLoaKY233 48442f1
Validate virtual keys and record model updates
CLoaKY233 27cd9d1
Fix: Set sync_started_at when syncing
CLoaKY233 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,3 +4,5 @@ | |
| /skills | ||
| .claude | ||
| documents | ||
| surrealdbdocs | ||
| .codex | ||
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,44 @@ | ||
| DEFINE TABLE IF NOT EXISTS virtual_api_key SCHEMAFULL | ||
| DEFINE TABLE OVERWRITE virtual_api_key SCHEMAFULL | ||
| PERMISSIONS | ||
| FOR select, update, delete | ||
| WHERE user = $auth.id | ||
| FOR select | ||
| WHERE (user = $auth.id AND $access = "account") | ||
| OR (id = $auth AND $access = "virtual_key_access") | ||
| -- account: full update; virtual_key_access: only its own record (field-level perms restrict what) | ||
| FOR update | ||
| WHERE (user = $auth.id AND $access = "account") | ||
| OR (id = $auth AND $access = "virtual_key_access") | ||
| FOR delete | ||
| WHERE user = $auth.id AND $access = "account" | ||
| FOR create | ||
| WHERE user = $auth.id; | ||
| WHERE user = $auth.id AND $access = "account"; | ||
|
|
||
| DEFINE FIELD IF NOT EXISTS user ON TABLE virtual_api_key TYPE record<user>; | ||
| DEFINE FIELD IF NOT EXISTS name ON TABLE virtual_api_key TYPE string; | ||
| DEFINE FIELD IF NOT EXISTS key_prefix ON TABLE virtual_api_key TYPE string; | ||
| DEFINE FIELD IF NOT EXISTS key_hash ON TABLE virtual_api_key TYPE string; | ||
| DEFINE FIELD IF NOT EXISTS allowed_models ON TABLE virtual_api_key TYPE array; | ||
| DEFINE FIELD IF NOT EXISTS tags ON TABLE virtual_api_key TYPE array; | ||
| DEFINE FIELD OVERWRITE enabled ON TABLE virtual_api_key TYPE bool DEFAULT true; | ||
| DEFINE FIELD IF NOT EXISTS expires_at ON TABLE virtual_api_key TYPE option<datetime>; | ||
| DEFINE FIELD IF NOT EXISTS last_used_at ON TABLE virtual_api_key TYPE option<datetime>; | ||
| DEFINE FIELD IF NOT EXISTS created_at ON TABLE virtual_api_key TYPE datetime VALUE time::now() READONLY; | ||
| DEFINE FIELD IF NOT EXISTS updated_at ON TABLE virtual_api_key TYPE datetime VALUE time::now(); | ||
| -- Sensitive fields: only the owning account session may update these. | ||
| DEFINE FIELD OVERWRITE user ON TABLE virtual_api_key TYPE record<user> | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE name ON TABLE virtual_api_key TYPE string | ||
| ASSERT string::len($value) >= 1 AND string::len($value) <= 256 | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE key_prefix ON TABLE virtual_api_key TYPE string | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE key_hash ON TABLE virtual_api_key TYPE string | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE allowed_models ON TABLE virtual_api_key TYPE array | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE tags ON TABLE virtual_api_key TYPE array | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE enabled ON TABLE virtual_api_key TYPE bool DEFAULT true | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| DEFINE FIELD OVERWRITE expires_at ON TABLE virtual_api_key TYPE option<datetime> | ||
| PERMISSIONS FOR update WHERE user = $auth.id AND $access = "account"; | ||
| -- last_used_at: account can update it manually; virtual_key_access updates it on each proxy call. | ||
| DEFINE FIELD OVERWRITE last_used_at ON TABLE virtual_api_key TYPE option<datetime> | ||
| PERMISSIONS | ||
| FOR select WHERE (user = $auth.id AND $access = "account") OR (id = $auth AND $access = "virtual_key_access") | ||
| FOR update WHERE (user = $auth.id AND $access = "account") OR (id = $auth AND $access = "virtual_key_access"); | ||
| DEFINE FIELD OVERWRITE created_at ON TABLE virtual_api_key TYPE datetime VALUE time::now() READONLY; | ||
| DEFINE FIELD OVERWRITE updated_at ON TABLE virtual_api_key TYPE datetime VALUE time::now(); | ||
|
|
||
| DEFINE INDEX IF NOT EXISTS virtual_api_key_hash_unique ON TABLE virtual_api_key FIELDS key_hash UNIQUE; | ||
|
|
||
| -- Performance index: key lookups by owner (list, cascade delete, usage queries) | ||
| DEFINE INDEX IF NOT EXISTS idx_virtual_api_key_user ON TABLE virtual_api_key FIELDS user; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| DEFINE ACCESS OVERWRITE virtual_key_access ON DATABASE TYPE RECORD | ||
| SIGNIN ( | ||
| SELECT * FROM virtual_api_key | ||
| WHERE key_hash = crypto::sha256($virtual_key) | ||
| AND enabled = true | ||
| AND (expires_at = NONE OR expires_at > time::now()) | ||
| LIMIT 1 | ||
| ) | ||
| DURATION FOR TOKEN 15m, FOR SESSION NONE; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| -- backend_service is used by the ValyMux proxy to authenticate directly to SurrealDB. | ||
| -- This access type only allows reading encrypted provider credentials via fn::backend_fetch_secret. | ||
| -- The encrypted values still require SURREAL_ENCRYPTION_KEY to decrypt (held in the Rust process). | ||
| -- | ||
| -- Grant duration: 30 days. Rotate before expiry using scripts/generate_backend_grant.sh: | ||
| -- 1. Run ./scripts/generate_backend_grant.sh → new VALYMUX_DB_SERVICE_KEY | ||
| -- 2. Update .env and restart the server | ||
| -- 3. Revoke the old grant: ACCESS backend_service ON DATABASE REVOKE GRANT <old-id>; | ||
| DEFINE ACCESS OVERWRITE backend_service ON DATABASE TYPE BEARER FOR USER | ||
| DURATION FOR GRANT 30d, FOR TOKEN 1h, FOR SESSION 24h; |
21 changes: 21 additions & 0 deletions
21
crates/surrealdb/schema/functions/001_user_functions.surql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| -- User Functions | ||
| -- PERMISSIONS WHERE $auth != NONE: $auth.id IS available inside the function body | ||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::get_current_user() { | ||
| -- Explicit column list intentionally excludes the password hash. | ||
| LET $user = ( | ||
| SELECT id, name, email, enabled, created_at, updated_at | ||
| FROM $auth.id | ||
| )[0]; | ||
| IF $user = NONE { | ||
| THROW "authenticated user not found"; | ||
| }; | ||
| IF $user.enabled = false { | ||
| THROW "account is disabled"; | ||
| }; | ||
| RETURN $user; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::update_profile($changed_name: string) { | ||
| RETURN (UPDATE user SET name = $changed_name WHERE id = $auth.id RETURN AFTER)[0]; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
96 changes: 96 additions & 0 deletions
96
crates/surrealdb/schema/functions/002_provider_credential_functions.surql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| -- Provider Credential Functions | ||
| -- PERMISSIONS WHERE $auth != NONE: $auth.id IS available inside the function body | ||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::list_provider_credentials() { | ||
| RETURN SELECT * OMIT encrypted_api_key FROM provider_credential | ||
| WHERE user = $auth.id | ||
| ORDER BY created_at ASC; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::get_provider_credential($id: record<provider_credential>) { | ||
| RETURN (SELECT * OMIT encrypted_api_key FROM $id WHERE user = $auth.id)[0]; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
|
||
| -- encrypted_api_key must be encrypted by Rust before calling this | ||
| DEFINE FUNCTION OVERWRITE fn::create_provider_credential( | ||
| $provider: string, | ||
| $label: string, | ||
| $encrypted_api_key: string, | ||
| $tags: array | ||
| ) { | ||
| RETURN CREATE provider_credential CONTENT { | ||
| user: $auth.id, | ||
| provider: $provider, | ||
| label: $label, | ||
| encrypted_api_key: $encrypted_api_key, | ||
| tags: $tags, | ||
| enabled: true, | ||
| sync_status: "pending", | ||
| model_count: 0 | ||
| }; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::update_provider_credential( | ||
| $id: record<provider_credential>, | ||
| $label: string, | ||
| $tags: array, | ||
| $enabled: bool, | ||
| $encrypted_api_key: option<string> | ||
| ) { | ||
| LET $existing = (SELECT * FROM $id WHERE user = $auth.id)[0]; | ||
| IF $existing = NONE { | ||
| THROW "provider credential not found or access denied"; | ||
| }; | ||
|
|
||
| IF $encrypted_api_key != NONE { | ||
| LET $updated = (UPDATE $id MERGE { | ||
| label: $label, | ||
| tags: $tags, | ||
| enabled: $enabled, | ||
| encrypted_api_key: $encrypted_api_key, | ||
| sync_status: "pending", | ||
| sync_error: NONE | ||
| } WHERE user = $auth.id RETURN AFTER)[0]; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| RETURN $updated; | ||
| }; | ||
|
|
||
| RETURN (UPDATE $id MERGE { | ||
| label: $label, | ||
| tags: $tags, | ||
| enabled: $enabled | ||
| } WHERE user = $auth.id RETURN AFTER)[0]; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
|
||
| -- Atomically transitions a credential to "syncing" only if it is not already syncing, | ||
| -- or if the existing sync lock is stale (started more than 10 minutes ago). | ||
| -- Returns true if the caller acquired the sync lock, false if already syncing. | ||
| DEFINE FUNCTION OVERWRITE fn::start_provider_sync($id: record<provider_credential>) { | ||
| LET $updated = ( | ||
| UPDATE $id SET sync_status = "syncing", sync_error = NONE, sync_started_at = time::now() | ||
| WHERE user = $auth.id | ||
| AND sync_status != "deleting" | ||
| AND (sync_status != "syncing" | ||
| OR sync_started_at = NONE | ||
| OR sync_started_at < time::now() - 10m) | ||
| RETURN AFTER | ||
| )[0]; | ||
| RETURN $updated != NONE; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| DEFINE FUNCTION OVERWRITE fn::delete_provider_credential($id: record<provider_credential>) { | ||
| LET $before = (SELECT * FROM $id WHERE user = $auth.id)[0]; | ||
| IF $before = NONE { | ||
| RETURN NONE; | ||
| }; | ||
|
|
||
| UPDATE $id MERGE { | ||
| sync_status: "deleting", | ||
| sync_error: NONE | ||
| } WHERE user = $auth.id; | ||
|
|
||
| DELETE supports WHERE in = $id; | ||
| DELETE virtual_key_route WHERE out = $id; | ||
| DELETE $id WHERE user = $auth.id; | ||
|
|
||
| RETURN $before; | ||
| } PERMISSIONS WHERE $auth != NONE AND $access = "account"; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.