Skip to content

[FEAT-DATSET-14] InvenioRDM Credential Management - #1491

Open
sven1103 wants to merge 17 commits into
developmentfrom
feature/feat-datset-14-invenio-rdm-credentials
Open

[FEAT-DATSET-14] InvenioRDM Credential Management#1491
sven1103 wants to merge 17 commits into
developmentfrom
feature/feat-datset-14-invenio-rdm-credentials

Conversation

@sven1103

@sven1103 sven1103 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements two user stories from FEAT-DATASET-CONNECTION:

  • FEAT-DATSET-14: Add credentials for an InvenioRDM instance

    As a researcher, I want to provide my access token for an available InvenioRDM instance, so that I can connect access-restricted resources in a project.

  • FEAT-DATSET-15: Remove credentials for a configured InvenioRDM instance

    As a researcher, I want to remove my access token for an available InvenioRDM instance, because the token is not valid anymore.

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

  • New user_external_credential table — source-agnostic (source_type + instance_id + user_id), supports future providers without schema changes
  • Vault config property qbic.security.vault.external-credential.key-alias for dedicated AES master key

Encryption (ADR-0002 S2)

  • CredentialEncryptor port (application layer) + AesGcmCredentialEncryptor impl (AES/GCM/NoPadding, 256-bit key, 96-bit nonce, 128-bit tag)
  • 10 unit tests: roundtrip, nonce uniqueness, wrong-key failure, corruption detection, key-size validation

Domain model

  • UserExternalCredential entity + CredentialStatus enum (VALID / INVALIDATED)
  • UserExternalCredentialRepository port + Spring Data JPA impl

Validation layer (composite dispatcher)

  • ExternalCredentialValidator port (application layer)
  • CredentialValidatorAdapter interface (infrastructure, per-provider)
  • InvenioRdmCredentialValidatorAdapter — validates via GET /api/users per InvenioRDM OpenAPI spec (operationId: getAUserById)
  • SourceTypeDispatchingCredentialValidator — routes by SourceType, extensible for future providers by adding one bean entry
  • CredentialValidationException for transient failures

Client enhancements

  • InvenioRdmClient.getAuthenticatedUser() — new endpoint
  • InvenioRdmClient.search() / getRecord() — auth-header overloads (backward-compatible)
  • SourceInstanceDescriptor extended with sourceType field

Application service

  • ExternalCredentialService interface + DefaultExternalCredentialService
  • Sealed AddCredentialResult: Success / InvalidToken / ServiceError / UnknownInstance
  • Token zeroed in finally block after every call

Spring wiring

  • InvenioRdmConfiguration registers all new beans
  • Dispatcher configured for INVENIO_RDM today; adding a future provider = implement adapter + register one entry

Commit 2 — UI: Add credentials (Checkpoint 10)

/external-providers route (ExternalProvidersMain)

  • Benefit text (AC-6): explains that tokens enable connection of access-restricted datasets
  • Instance list loaded from ExternalCredentialService.listCredentialStatuses() — iterates all configured instances
  • Status indicators: green check for connected, red X for not configured
  • Add Token dialog: PasswordField with no echo, validate-then-save flow, token field cleared on dialog close
  • Help text links to standard InvenioRDM token creation page
  • Generic toast notifications for success, invalid token, and transient errors (no upstream details leaked to the user)

Commit 3 — UI: Remove credentials (FEAT-DATSET-15)

Disconnect flow (ExternalProvidersMain)

  • Disconnect button on each connected/invalidated instance card — VaadinIcon.TRASH, tertiary + error styling
  • Confirmation dialog (AlertDialog) — warns the user that the token will be removed and reconnection will be required; requires explicit "Disconnect" confirmation or "Cancel" to abort
  • performDisconnect() — calls credentialService.removeCredential(userId, instanceId), shows a toast on success or when no token was found, then re-renders the page
  • Card re-renders as "Not connected" — grey tag, descriptive text ("Connect your account to link …"), and a "Connect" button replacing the former "Disconnect" button
  • VerificationSidebar refresh — if the sidebar is open during disconnect, it is refreshed to reflect the new NOT_CONFIGURED state

Application service (DefaultExternalCredentialService.removeCredential())

  • Resolves the instance from the SourceInstanceRegistry, looks up the credential by user/source-type/instance, and delegates to UserExternalCredentialRepository.deleteByUserIdAndSourceTypeAndInstanceId() (physical row delete)
  • Returns true if a credential was removed, false if none existed
  • After removal, InvenioRdmDatasetSource.resolveTokenForUser() returns null for that user/instance — no Authorization header is sent, so InvenioRDM will not return access-restricted records

Acceptance Criteria status

FEAT-DATSET-14 — Add credentials

AC Status Notes
AC-1: User navigates to "External Providers" ✅ Done Route: /external-providers under UserMainLayout
AC-2: System displays configured instances ✅ Done Loaded from SourceInstanceRegistry (admin-configured via application.properties)
AC-3: Token validated against InvenioRDM via API ✅ Done GET /api/users with Bearer auth; 200 = valid
AC-4: Invalid token not added, user informed ✅ Done Generic "Token validation failed" toast; no token persisted
AC-5: Valid token added, instance connected ✅ Done "Token validated successfully" toast; status shown as VALID
AC-6: Benefit description shown ✅ Done Paragraph at top of page explains access-restricted dataset connection

FEAT-DATSET-15 — Remove credentials

AC Status Notes
AC-1: Token deleted, restricted access prevented ✅ Done Full stack: UI → removeCredential() → JPA delete → downstream token resolution returns null (no auth header sent)
AC-2: User informed of successful removal ✅ Done Toast notification shown after disconnect
AC-3: Config section reflects removal ✅ Done Card re-renders as "Not connected" with grey tag, descriptive text, and "Connect" button; sidebar refreshed if open

Architecture decisions (from ADRs)

  • ADR-0002 S2: Master AES-256 key in PKCS12 vault; per-token AES-GCM blobs in DB (HA-safe)
  • ADR-0002 D1: Decryption boundary — plaintext token char[] exists only in infrastructure adapter methods, zeroed after use
  • ADR-0002 I2: Instances admin-configured (no UI entry)
  • ADR-0002 P2: Stateless DatasetSource port, instance-parameterised
  • ADR-0002 T1: Per-user-per-instance tokens, never-borrow-credentials
  • Composite dispatcher: Adding a second provider = implement CredentialValidatorAdapter + one bean entry in InvenioRdmConfiguration

Security measures

Vector Mitigation
At-rest AES-256-GCM with per-token random nonce; dedicated master key in PKCS12 vault
In-flight HTTPS only (all target instances)
In-memory Plaintext token as char[], zeroed in finally block
Layer boundary Application service never holds plaintext beyond the add/validate call scope
Logging Token values never logged; only user ID + instance ID
Error messages Generic user-facing; raw upstream errors to server logs only
UI PasswordField, no echo, field cleared on dialog close
Removal Physical row delete; confirmation dialog prevents accidental disconnect

Files changed

Layer Files
Schema sql/migrations/create-user-external-credential.sql, sql/complete-schema.sql
Config application.properties
Application ports ExternalCredentialService, ExternalCredentialValidator, CredentialEncryptor, CredentialValidationException, DefaultExternalCredentialService
Domain UserExternalCredential, CredentialStatus, UserExternalCredentialRepository
Infrastructure AesGcmCredentialEncryptor, SourceTypeDispatchingCredentialValidator, CredentialValidatorAdapter, InvenioRdmCredentialValidatorAdapter, UserExternalCredentialJpaRepository, UserExternalCredentialRepositoryImpl, InvenioRdmClient (extensions)
Views ExternalProvidersMain, VerificationSidebar, TokenInput
Wiring InvenioRdmConfiguration, PropertiesBackedSourceInstanceRegistry, SourceInstanceDescriptor
Tests AesGcmCredentialEncryptorSpec (10 tests)
Docs docs/implementation-plan/feat-datset-14-*

Verification

  • Full compile: BUILD SUCCESS
  • 263/263 tests pass (0 failures, 0 errors)
  • 10 new AesGcmCredentialEncryptorSpec tests pass

Deferred

  • Checkpoint 11 integration smoke test (requires live DB + Spring context, planned for CI)
  • i18n message keys for toast notifications

Checklist

See docs/implementation-plan/feat-datset-14-checklist.md for the per-checkpoint reviewer checklist.

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).
@sven1103
sven1103 force-pushed the feature/feat-datset-14-invenio-rdm-credentials branch from 6394a35 to 7f2364a Compare July 29, 2026 09:53
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
@sven1103
sven1103 force-pushed the feature/feat-datset-14-invenio-rdm-credentials branch from 7f2364a to 34da266 Compare July 29, 2026 11:42
…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
@sven1103
sven1103 force-pushed the feature/feat-datset-14-invenio-rdm-credentials branch from 34da266 to c1255c9 Compare July 29, 2026 12:36
sven1103 and others added 2 commits July 29, 2026 15:23
- 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
@sven1103
sven1103 marked this pull request as ready for review July 30, 2026 14:47
@sven1103
sven1103 requested a review from a team as a code owner July 30, 2026 14:47
@sven1103 sven1103 linked an issue Jul 30, 2026 that may be closed by this pull request
@KochTobi
KochTobi self-requested a review July 30, 2026 14:52
String instanceBaseUrl,
boolean configured,
/** {@code "VALID"}, {@code "INVALIDATED"}, or {@code "NOT_CONFIGURED"} */
String status,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/migrations/NEXT.md Outdated
-alias external-credential-master-key \
-keystore /path/to/shared/keystore.p12 \
-storepass $DATAMANAGER_VAULT_KEY \
-keypass $DATAMANAGER_VAULT_ENTRY_PASSWORD \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@KochTobi KochTobi Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❯ 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the echo $AES_KEY command works

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs/migrations/NEXT.md Outdated
-alias external-credential-master-key \
-keystore /path/to/shared/keystore.p12 \
-storepass $DATAMANAGER_VAULT_KEY \
-keypass $DATAMANAGER_VAULT_ENTRY_PASSWORD \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread datamanager-app/frontend/themes/datamanager/components/all.css
Comment thread datamanager-app/frontend/themes/datamanager/components/all.css
Comment thread datamanager-app/frontend/themes/datamanager/components/custom.css
Comment thread datamanager-app/frontend/themes/datamanager/components/dialog.css

@KochTobi KochTobi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please refine the migration guide. The functionality provided by this PR is great!

Comment thread datamanager-app/src/main/java/life/qbic/datamanager/views/account/TokenInput.java Outdated

@KochTobi KochTobi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great improvements!
For me the command mentioned in the NEXT.md did not work. Did you verify that piping works in this case?

Comment thread datamanager-app/frontend/themes/datamanager/components/dialog.css
Comment thread docs/migrations/NEXT.md
@sven1103
sven1103 requested a review from KochTobi August 5, 2026 13:03

@KochTobi KochTobi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Story] FEAT-DATSET-15: Remove credentials for a configured InvenioRDM instance [Story] FEAT-DATSET-14: Add credentials for an InvenioRDM instance

3 participants