Status: in-progress
OpenSpec changes:
implement-application-mgmt(2026-04-01) — Full implementation: registration, CSR/generated key pair, approval queue, JWT auth, write-without-read, cascade deletionadmin-application-request-visibility(2026-08-18) — An application can ask humans for credentials that no person can see:created_byisapplication:<id>and the user listing matches the acting user's id, the target Secrets are application-owned so they appear in no vault, and the only lister is the application's own Bearer endpoint (6 such requests measured on development). Adds an admin-scoped listing and revoke on the application's detail page, leaving the user-side listing untouched
@e2e exclude No application-management UI is built in v0.1; all scenarios exercise registration, CSR/key-pair handling, and JWT-auth API flows — covered by integration tests, not Playwright UI flows.
External and internal applications can be registered in Keepiq so that secrets can be attributed to them. An application gets its own EncryptionSuite, allowing secrets to be encrypted specifically for that application. The application can then retrieve and decrypt its own secrets via the API.
Registration is open (anyone can register, even anonymously), but non-admin registrations go into a pending queue for approval by the vault administrator. Secrets cannot be attributed to a pending application.
| Field | Type | Encrypted | Notes |
|---|---|---|---|
id |
UUID | No | Primary key |
name |
string | No | Human-readable name |
type |
enum | No | internal (Nextcloud app) or external |
status |
enum | No | pending, active |
registered_by |
string | No | Nextcloud user ID or null (anonymous) |
approved_by |
string | No | Admin user ID; null if pending |
created_at |
datetime | No | |
approved_at |
datetime | No |
The Application's encryption identity is provided via its EncryptionSuite(s), linked via the polymorphic owner_type = application pattern (see ADR-002).
The system MUST allow any user (including anonymous) to register an application.
- GIVEN the registrant is a Nextcloud administrator or vault app administrator
- WHEN they submit a registration
- THEN the application MUST be created with status
activeimmediately
- GIVEN the registrant is a regular user or anonymous
- WHEN they submit a registration
- THEN the application MUST be created with status
pending - AND no secrets can be attributed to it until it is approved
The system MUST allow vault administrators to view and approve or reject pending applications.
- GIVEN an application is pending
- WHEN a vault administrator approves it
- THEN the application status MUST be set to
active
- GIVEN an application is pending
- WHEN a vault administrator rejects it
- THEN the application MUST be removed from the queue and deleted
When a CSR is uploaded during registration, the system MUST use the public key from the CSR to create an EncryptionSuite for the application.
- GIVEN a registrant uploads a valid CSR
- WHEN the application is approved (or auto-approved for admin)
- THEN the system MUST extract the public key from the CSR, sign it with the CA intermediate, and create an EncryptionSuite for the application
- AND the private key is NOT stored — the application manages it externally
- GIVEN no CSR is uploaded
- WHEN the application is approved
- THEN the system MUST generate a 4096-bit RSA key pair, sign the certificate, and create an EncryptionSuite
- AND the private key MUST be returned to the registrant once (never stored in plaintext)
A vault administrator MUST be able to delete an active application. Deletion is permanent — there is no deactivation or soft-delete state.
On deletion:
- The application record is removed
- The application's EncryptionSuite is removed
- All secrets attributed to the application are hard-deleted
- GIVEN an application is active
- WHEN a vault administrator deletes it
- THEN the application, its EncryptionSuite, and all its secrets MUST be permanently deleted
- AND this action MUST NOT be reversible
When a non-admin submits an application registration, all vault administrators MUST be notified via the Nextcloud built-in notification system (bell icon).
Notification content:
- Title: "New application pending approval"
- Body: "Application {name} is awaiting approval."
- Action link: opens the approval queue in Keepiq
- GIVEN a non-admin submits a registration
- WHEN the application is created with status
pending - THEN a Nextcloud notification MUST be dispatched to all vault administrators
The Keepiq dashboard MUST display a visible counter of pending application registrations to vault administrators. Non-administrators MUST NOT see this counter.
- GIVEN one or more applications are in
pendingstatus - WHEN a vault administrator views the Keepiq dashboard
- THEN the dashboard MUST show the count of pending registrations
- AND the counter MUST link to the approval queue
- GIVEN no applications are pending
- WHEN a vault administrator views the Keepiq dashboard
- THEN the counter MUST NOT be shown (or shown as zero, implementation choice)
Once an application is active, users with appropriate permission MUST be able to write secrets into the application's vault (encrypted with the application's public certificate).
- GIVEN an application is active and has an EncryptionSuite
- WHEN a user with permission writes a secret for the application
- THEN the secret MUST be encrypted with the application's public certificate
- AND the writing user MUST NOT be able to read it back (write-without-read)
An administrator MUST be able to see the secret requests an application has created, and MUST be able to revoke them.
Today they cannot. created_by for an application request is application:<id>, and the user-facing listing matches created_by against the acting user's id, so no person can enumerate them; the target Secrets are application-owned and appear in no user's vault; and the only lister is the application's own Bearer-authenticated endpoint. Creation is recorded in the audit trail, so the events exist with nowhere to read them as state.
This matters because a pending fill link is a bearer credential in a URL. An administrator accountable for an approved application MUST be able to answer what credentials it is currently asking humans for, and MUST be able to end a circulating link without the application's cooperation.
Visibility MUST be scoped to administrators, not to whoever registered the application: an application's vault belongs to no single user, and registration is a historical act rather than continuing responsibility.
The listing MUST NOT render a request's full token, and MUST NOT expose any submitted value — write-without-read is unaffected by who is looking.
@e2e exclude No Playwright coverage of the admin application page yet; driven by ApplicationRequestAdminControllerTest::testAnAdministratorSeesTheApplicationsRequests, SecretRequestServiceTest::testListForApplicationReturnsThatApplicationsRequestsNewestFirst and the SecretRequestList vitest "renders the application rows". Verified live on 2026-08-19: the endpoint returned each application's own two requests with their status and requested field names.
- GIVEN an application has created pending secret requests
- WHEN an administrator opens that application
- THEN those requests MUST be listed with their status, requested field names and expiry
@e2e exclude Authorization, asserted at both layers rather than through a browser: ApplicationRequestAdminControllerTest::testANonAdministratorIsRefused (and ::testAnAnonymousCallerIsRefused) plus SecretRequestServiceTest::testListForApplicationRefusesANonAdmin, which fails when the service guard is removed. Registrar identity is not an input to either check. Verified live: alice received 403.
- WHEN a user who is not an administrator requests an application's secret requests
- THEN the system MUST refuse, regardless of whether that user registered the application
@e2e exclude Regression guard on a query, not a UI flow; driven by SecretRequestServiceTest::testListByUserStillQueriesTheRawUid, which pins that the uid is passed with no application prefix — the reason a user listing can never match an application's rows.
- GIVEN an instance with both user-created and application-created requests
- WHEN a user lists their own requests
- THEN only requests they created MUST be returned, exactly as before
@e2e exclude Driven by SecretRequestServiceTest::testAdminRevokeDeletesTheUnfilledApplicationPlaceholder, ::testAdminRevokeNeverDeletesAFilledApplicationSecret, ::testAdminRevokeWillNotDeleteAnotherApplicationsSecret and ::testRevokeForApplicationRefusesARequestOfAnotherActor (which fails when the created_by check is removed), plus the vitest "asks before revoking, and revokes through the application endpoint". NOT verified live: doing so would hard-delete a seeded placeholder Secret on the development instance, and the request row cannot be restored.
- GIVEN an application has a pending request whose link is in circulation
- WHEN an administrator revokes it
- THEN the token MUST stop being fillable
- AND the unfilled placeholder Secret MUST be deleted, per the Revoke Request requirement
@e2e exclude Driven by the vitest "never renders a full token", which asserts the truncated form is shown and neither full token appears anywhere in the rendered output. The API deliberately still returns the token — the copy-link action needs it — so truncation is the view's job, exactly as on the user side. No submitted value is readable from a listing because the response carries only metadata (write-without-read, ADR-003).
- WHEN an application's requests are listed for an administrator
- THEN each row MUST show the token truncated
- AND no submitted value MUST be readable from the listing
- As a developer, I want to register my application so that I can store and retrieve its secrets from Keepiq
- As an administrator, I want to approve or reject application registrations so that I control which applications can use the vault
- As a user, I want to write a secret for an application without being able to read it so that sensitive values are never in my hands
- As an application, I want to retrieve my secrets via the API using my private key so that I can configure myself securely
- Any user (including anonymous) can submit an application registration
- Admin-registered applications are immediately active
- Non-admin registrations are placed in a pending queue
- Vault administrators can approve or reject pending applications
- An approved application gets an EncryptionSuite (via CSR or generated)
- If a CSR is uploaded, the private key is not stored — only the signed certificate
- If no CSR is uploaded, a key pair is generated and the private key returned once
- Secrets cannot be attributed to pending applications
- Writing a secret for an application encrypts it with the app's public certificate
- All vault administrators receive a Nextcloud notification when a new application registration is pending
- The Keepiq dashboard shows a pending application counter to vault administrators (hidden from non-admins)
- Vault administrators can delete an active application
- Deletion permanently removes the application, its EncryptionSuite, and all its secrets (hard delete, no soft-delete or deactivation state)
Decision: RFC 7523 — OAuth2 client credentials flow where the application authenticates by presenting a JWT signed with its own RSA private key.
Why: Standard pattern (used by Google service accounts), uses the existing key infrastructure (the application already has an RSA key pair from registration), produces short-lived access tokens, and introduces no new credential to manage. Keepiq owns the /oauth2/token endpoint since Nextcloud's oauth2 app does not support this grant type.
Recovery: If the private key is lost, re-registration is the recovery path (new key pair, new EncryptionSuite; existing secrets encrypted with the old public key become inaccessible).
Alternatives considered:
- Static API token (
X-Vault-Token) — simple but the token is a credential that needs protecting separately - Custom private-key challenge-response — pure but non-standard
Status: Chosen route. May be revisited if implementation reveals blockers with Nextcloud's request lifecycle or JWT library availability in PHP 8.3+.
- "Internal vs external" application distinction does not currently affect functionality — both types go through the same registration flow. The type field is informational for now.
- The master password challenge for internal applications (how does a Nextcloud app running a cronjob authenticate?) is a known open problem documented in the Vault-app.docx. It is not scoped in this spec.
- Related ADRs: ADR-002 (polymorphic ownership), ADR-003 (encryption architecture)