Integrate file connectors with credential envelopes

This commit is contained in:
2026-07-28 19:33:01 +02:00
parent 0fd8f6e02a
commit b667e7ff0e
5 changed files with 366 additions and 15 deletions

View File

@@ -225,6 +225,14 @@ export default function FileConnectorSettingsPanel({
() => credentials.filter((credential) => connectorBelongsToEditableScope(credential, scopeType, activeScopeId)),
[activeScopeId, credentials, scopeType]
);
const selectableCredentials = useMemo(
() => credentials.filter((credential) =>
connectorBelongsToEditableScope(credential, scopeType, activeScopeId) ||
credential.source_kind === "credential_envelope" &&
reusableCredentialAvailableAtScope(credential, scopeType, activeScopeId)
),
[activeScopeId, credentials, scopeType]
);
useEffect(() => {
resetDeltaWatermark(deltaKey);
@@ -424,7 +432,7 @@ export default function FileConnectorSettingsPanel({
endpoint_url: cleanOrNull(draft.endpointUrl),
base_path: cleanOrNull(draft.basePath),
enabled: draft.enabled,
credential_profile_id: cleanOrNull(credentialProfileIdForDraft(draft, scopedCredentials)),
credential_profile_id: cleanOrNull(credentialProfileIdForDraft(draft, selectableCredentials)),
credential_mode: draft.credentialMode,
capabilities,
policy: policyFromDraft(draft),
@@ -694,7 +702,7 @@ export default function FileConnectorSettingsPanel({
const panelTitle = title ?? i18nMessage("i18n:govoplan-files.value_file_connections", { value0: scopeLabel(scopeType) });
const selectedCredentialIds = new Set(scopedProfiles.map((profile) => profile.credential_profile_id).filter((value): value is string => Boolean(value)));
const firstCompatibleProfileByCredential = new Map<string, string>();
for (const credential of scopedCredentials) {
for (const credential of selectableCredentials) {
const selectedProfile = scopedProfiles.find((profile) => profile.credential_profile_id === credential.id);
const fallbackProfile = selectedProfile ?? scopedProfiles.find((profile) => !selectedCredentialIds.has(credential.id) && credentialMatchesProfile(credential, profile));
if (fallbackProfile) firstCompatibleProfileByCredential.set(credential.id, fallbackProfile.id);
@@ -717,8 +725,8 @@ export default function FileConnectorSettingsPanel({
const credentialTestUnsupported = Boolean(credentialTestProfile && credentialTestProfile.provider !== "webdav" && credentialTestProfile.provider !== "nextcloud");
const credentialTestDisabled = !credentialDraft || saving || testingCredentialLogin || !credentialTestProfile || credentialTestUnsupported;
const credentialTestTooltip = credentialTestHelpText(credentialTestProfile, credentialTestUnsupported);
const profileCredentialOptions = draft ? credentialOptionsForProfileDraft(scopedCredentials, draft) : [];
const profileCredentialSelectValue = draft ? credentialProfileIdForDraft(draft, scopedCredentials) : "";
const profileCredentialOptions = draft ? credentialOptionsForProfileDraft(selectableCredentials, draft) : [];
const profileCredentialSelectValue = draft ? credentialProfileIdForDraft(draft, selectableCredentials) : "";
const connectorColumns: ConnectionTreeColumn<ConnectorTreeRow>[] = [
{
id: "connection",
@@ -755,7 +763,7 @@ export default function FileConnectorSettingsPanel({
function connectorChildren(row: ConnectorTreeRow): ConnectorTreeRow[] {
if (row.kind !== "profile") return [];
return scopedCredentials.
return selectableCredentials.
filter((credential) => firstCompatibleProfileByCredential.get(credential.id) === row.profile.id).
map((credential) => ({ kind: "credential" as const, id: `credential:${row.profile.id}:${credential.id}`, credential, profile: row.profile }));
}
@@ -1432,10 +1440,39 @@ function connectorCredentialDraftKey(draft: ConnectorCredentialDraft): string {
function credentialOptionsForProfileDraft(credentials: FileConnectorCredential[], draft: ConnectorProfileDraft): FileConnectorCredential[] {
return credentials.filter((credential) =>
credential.id === draft.credentialProfileId ||
credential.enabled && (!credential.provider || credential.provider === draft.provider)
credential.enabled &&
(!credential.provider || credential.provider === draft.provider) &&
reusableCredentialAllowsProfile(credential, draft.id)
);
}
function reusableCredentialAvailableAtScope(
credential: FileConnectorCredential,
scopeType: ConnectorScope,
scopeId: string | null
): boolean {
if (connectorBelongsToEditableScope(credential, scopeType, scopeId)) return true;
if (credential.source_kind !== "credential_envelope") return false;
const inherited = credential.metadata?.inherit_to_lower_scopes === true;
if (!inherited) return false;
if (credential.scope_type === "system") return scopeType !== "system";
if (credential.scope_type === "tenant") {
return scopeType === "group" || scopeType === "user";
}
return false;
}
function reusableCredentialAllowsProfile(
credential: FileConnectorCredential,
profileId: string
): boolean {
if (credential.source_kind !== "credential_envelope") return true;
const refs = Array.isArray(credential.metadata?.allowed_server_refs)
? credential.metadata.allowed_server_refs.filter((value): value is string => typeof value === "string")
: [];
return refs.length === 0 || refs.includes(`files:${profileId}`);
}
function credentialProfileIdForDraft(draft: ConnectorProfileDraft, credentials: FileConnectorCredential[]): string {
const options = credentialOptionsForProfileDraft(credentials, draft);
if (draft.credentialProfileId && options.some((credential) => credential.id === draft.credentialProfileId)) {