[FEAT-DATSET-14] InvenioRDM Credential Management - #1491
Conversation
Implement Checkpoints 1-9 of the implementation plan: Database & Config: - New user_external_credential table (source-agnostic, per user/instance) - Vault config property for dedicated AES-256 master key alias Encryption (ADR-0002 S2): - CredentialEncryptor port (application layer) - AesGcmCredentialEncryptor impl (infrastructure, AES/GCM/NoPadding) - Per-token random 12-byte nonce, 128-bit GCM tag - 10 unit tests covering roundtrip, nonce uniqueness, wrong key, etc. Domain model: - UserExternalCredential entity (JPA + domain, same-class convention) - CredentialStatus enum (VALID / INVALIDATED) - UserExternalCredentialRepository port - Spring Data JPA repo + repository impl Validation layer (composite dispatcher pattern): - ExternalCredentialValidator port (application layer) - CredentialValidatorAdapter interface (infrastructure, per-provider) - InvenioRdmCredentialValidatorAdapter (GET /api/users, per OpenAPI spec) - SourceTypeDispatchingCredentialValidator (routes by SourceType) - CredentialValidationException Client enhancements: - InvenioRdmClient.getAuthenticatedUser() (new endpoint) - InvenioRdmClient.search()/getRecord() (auth-header overloads) - SourceInstanceDescriptor now carries sourceType Application service: - ExternalCredentialService interface + DefaultExternalCredentialService - Sealed AddCredentialResult (Success/InvalidToken/ServiceError/UnknownInstance) - Token zeroed in finally block after add DatasetSource adapter: - InvenioRdmDatasetSource now resolves per-user tokens for authenticated search/resolve (decryption boundary ADR-0002 D1 enforced) Spring wiring: - InvenioRdmConfiguration registers all new beans - Dispatcher configured for INVENIO_RDM (extensible for future providers) All 263 existing tests still pass (0 failures, 0 errors).
6394a35 to
7f2364a
Compare
Implement Checkpoint 10 of the implementation plan: - Route: /external-providers (alongside /profile and /personal-access-token) - Layout: heading + benefit text (AC-6), list of configured instances, Add/Remove buttons - Add Token dialog: PasswordField input with no echo, validate-then-save flow - Token field is cleared on dialog close (no stale secrets in component state) - Status indicators: green check for connected, red X for not configured - Help link to standard InvenioRDM token settings page - Toast notifications for success, invalid token, transient errors - Error messages are generic (no upstream details leaked) - Token zeroed by the service; field cleared on close
7f2364a to
34da266
Compare
…lidation - AesGcmCredentialEncryptor now validates that the master key material is exactly 32 bytes (AES-256) and rejects AES-128/AES-192 keys at construction time - InvenioRdmConfiguration reads vault entry as Base64-encoded string, decodes, and validates the decoded length is exactly 32 bytes before constructing the SecretKey - Vault provisioning instructions updated to document Base64 encoding requirement (e.g., openssl rand -base64 32) - Implementation plan and migration guides updated to reflect new key handling requirements - Checklist updated to include key size validation checkpoints This fixes the provisioning issue where the previous 32-byte raw string requirement was not well-supported
34da266 to
c1255c9
Compare
- Redesign External Providers page with card-based layout - Colored left border accents (green/gray/red) - Top-right action buttons (Connect/Disconnect) - Provider home page links - Configured date display - Add verification sidebar (right slide-in drawer) - Verify all tokens in parallel - Skip providers without tokens (not connected state) - Real-time status updates (Valid/Invalid/Error) - Inline Reconnect action for invalid tokens - Refreshes on close to sync with main view - Add manual token validation to domain layer - validateCredential() decrypts stored token and validates - Updates credential status to VALID or INVALIDATED per ADR-0002 - Only explicit user action triggers validation (no silent updates) - Handle invalidated credentials gracefully - Invalidated cards show two-button recovery: [Reconnect] [Disconnect] - Sidebar marks invalid tokens red with reason and Reconnect link - Successful reconnection from either surface refreshes both views - Fix provider URL rendering (use base-url field, not display name parsing) - Add toolbar with global 'Verify connections' button - Add security reassurance text and page padding
…atset-14-invenio-rdm-credentials
| String instanceBaseUrl, | ||
| boolean configured, | ||
| /** {@code "VALID"}, {@code "INVALIDATED"}, or {@code "NOT_CONFIGURED"} */ | ||
| String status, |
There was a problem hiding this comment.
I think this better be an enum, I dont see the benefit of having free text values passing around for a confined set of values.
| -alias external-credential-master-key \ | ||
| -keystore /path/to/shared/keystore.p12 \ | ||
| -storepass $DATAMANAGER_VAULT_KEY \ | ||
| -keypass $DATAMANAGER_VAULT_ENTRY_PASSWORD \ |
There was a problem hiding this comment.
Warning: Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value.
The listed command does not work and fails with the mentioned error. Afterwards, an alias for external-credential-master-key is in the keytool but with unknown content.
There was a problem hiding this comment.
❯ echo "$AES_KEY" | keytool -importpass \
-alias external-credential-master-key \
-keystore keystore.p12 \
-storepass $DATAMANAGER_VAULT_KEY \
-storetype PKCS12
Enter the password to be stored: Re-enter password: They don't match. Try again
Enter the password to be stored: Re-enter password:
When not choosing a keypass, the key stays empty and I get an java.lang.IllegalArgumentException: Empty key when starting the app. It is good that this is checked and highlights that the command provided here did not work as intended.
There was a problem hiding this comment.
the echo $AES_KEY command works
There was a problem hiding this comment.
Please replace the command by the correct one
keytool --importpass \
-alias external-credential-master.jey \
-keystore keystore.p12 \
-storepass $DATAMANAGER_VAULT_KEY \and then entering the value of the AES_KEY as password for the entry
| -alias external-credential-master-key \ | ||
| -keystore /path/to/shared/keystore.p12 \ | ||
| -storepass $DATAMANAGER_VAULT_KEY \ | ||
| -keypass $DATAMANAGER_VAULT_ENTRY_PASSWORD \ |
There was a problem hiding this comment.
Please replace the command by the correct one
keytool --importpass \
-alias external-credential-master.jey \
-keystore keystore.p12 \
-storepass $DATAMANAGER_VAULT_KEY \and then entering the value of the AES_KEY as password for the entry
KochTobi
left a comment
There was a problem hiding this comment.
Please refine the migration guide. The functionality provided by this PR is great!
KochTobi
left a comment
There was a problem hiding this comment.
Great improvements!
For me the command mentioned in the NEXT.md did not work. Did you verify that piping works in this case?
Summary
Implements two user stories from FEAT-DATASET-CONNECTION:
FEAT-DATSET-14: Add credentials for an InvenioRDM instance
FEAT-DATSET-15: Remove credentials for a configured InvenioRDM instance
These are prerequisites for FEAT-DATSET-05/06 (connecting/viewing restricted datasets). No dependency on other stories.
What is implemented
Commit 1 — Credential infrastructure (Checkpoints 1-9)
Schema & Config
user_external_credentialtable — source-agnostic (source_type+instance_id+user_id), supports future providers without schema changesqbic.security.vault.external-credential.key-aliasfor dedicated AES master keyEncryption (ADR-0002 S2)
CredentialEncryptorport (application layer) +AesGcmCredentialEncryptorimpl (AES/GCM/NoPadding, 256-bit key, 96-bit nonce, 128-bit tag)Domain model
UserExternalCredentialentity +CredentialStatusenum (VALID / INVALIDATED)UserExternalCredentialRepositoryport + Spring Data JPA implValidation layer (composite dispatcher)
ExternalCredentialValidatorport (application layer)CredentialValidatorAdapterinterface (infrastructure, per-provider)InvenioRdmCredentialValidatorAdapter— validates viaGET /api/usersper InvenioRDM OpenAPI spec (operationId:getAUserById)SourceTypeDispatchingCredentialValidator— routes bySourceType, extensible for future providers by adding one bean entryCredentialValidationExceptionfor transient failuresClient enhancements
InvenioRdmClient.getAuthenticatedUser()— new endpointInvenioRdmClient.search()/getRecord()— auth-header overloads (backward-compatible)SourceInstanceDescriptorextended withsourceTypefieldApplication service
ExternalCredentialServiceinterface +DefaultExternalCredentialServiceAddCredentialResult:Success/InvalidToken/ServiceError/UnknownInstancefinallyblock after every callSpring wiring
InvenioRdmConfigurationregisters all new beansINVENIO_RDMtoday; adding a future provider = implement adapter + register one entryCommit 2 — UI: Add credentials (Checkpoint 10)
/external-providersroute (ExternalProvidersMain)ExternalCredentialService.listCredentialStatuses()— iterates all configured instancesPasswordFieldwith no echo, validate-then-save flow, token field cleared on dialog closeCommit 3 — UI: Remove credentials (FEAT-DATSET-15)
Disconnect flow (
ExternalProvidersMain)VaadinIcon.TRASH, tertiary + error stylingAlertDialog) — warns the user that the token will be removed and reconnection will be required; requires explicit "Disconnect" confirmation or "Cancel" to abortperformDisconnect()— callscredentialService.removeCredential(userId, instanceId), shows a toast on success or when no token was found, then re-renders the pageNOT_CONFIGUREDstateApplication service (
DefaultExternalCredentialService.removeCredential())SourceInstanceRegistry, looks up the credential by user/source-type/instance, and delegates toUserExternalCredentialRepository.deleteByUserIdAndSourceTypeAndInstanceId()(physical row delete)trueif a credential was removed,falseif none existedInvenioRdmDatasetSource.resolveTokenForUser()returnsnullfor that user/instance — noAuthorizationheader is sent, so InvenioRDM will not return access-restricted recordsAcceptance Criteria status
FEAT-DATSET-14 — Add credentials
/external-providersunderUserMainLayoutSourceInstanceRegistry(admin-configured viaapplication.properties)GET /api/userswith Bearer auth; 200 = validFEAT-DATSET-15 — Remove credentials
removeCredential()→ JPA delete → downstream token resolution returnsnull(no auth header sent)Architecture decisions (from ADRs)
char[]exists only in infrastructure adapter methods, zeroed after useCredentialValidatorAdapter+ one bean entry inInvenioRdmConfigurationSecurity measures
char[], zeroed infinallyblockPasswordField, no echo, field cleared on dialog closeFiles changed
sql/migrations/create-user-external-credential.sql,sql/complete-schema.sqlapplication.propertiesExternalCredentialService,ExternalCredentialValidator,CredentialEncryptor,CredentialValidationException,DefaultExternalCredentialServiceUserExternalCredential,CredentialStatus,UserExternalCredentialRepositoryAesGcmCredentialEncryptor,SourceTypeDispatchingCredentialValidator,CredentialValidatorAdapter,InvenioRdmCredentialValidatorAdapter,UserExternalCredentialJpaRepository,UserExternalCredentialRepositoryImpl,InvenioRdmClient(extensions)ExternalProvidersMain,VerificationSidebar,TokenInputInvenioRdmConfiguration,PropertiesBackedSourceInstanceRegistry,SourceInstanceDescriptorAesGcmCredentialEncryptorSpec(10 tests)docs/implementation-plan/feat-datset-14-*Verification
AesGcmCredentialEncryptorSpectests passDeferred
Checklist
See
docs/implementation-plan/feat-datset-14-checklist.mdfor the per-checkpoint reviewer checklist.