From d1b4cc994bd287af5c8a3114eab70ee01f92b4ec Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 15 Apr 2022 12:56:16 +0200 Subject: [PATCH] anastasis-core: async provider synchronization --- packages/anastasis-core/src/index.ts | 71 +++++++++++++++---- packages/anastasis-core/src/reducer-types.ts | 8 ++- .../src/hooks/use-anastasis-reducer.ts | 39 ++++++++++ .../pages/home/ContinentSelectionScreen.tsx | 1 - .../src/operations/reserves.ts | 1 - 5 files changed, 101 insertions(+), 19 deletions(-) diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index 5dd560a2c..9db152648 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -94,6 +94,7 @@ import { PolicyMetaInfo, ChallengeInfo, AggregatedPolicyMetaInfo, + AuthenticationProviderStatusMap, } from "./reducer-types.js"; import fetchPonyfill from "fetch-ponyfill"; import { @@ -329,15 +330,9 @@ async function backupEnterUserAttributes( args: ActionArgsEnterUserAttributes, ): Promise { const attributes = args.identity_attributes; - const providerUrls = Object.keys(state.authentication_providers ?? {}); - const newProviders = state.authentication_providers ?? {}; - for (const url of providerUrls) { - newProviders[url] = await getProviderInfo(url); - } const newState = { ...state, backup_state: BackupStates.AuthenticationsEditing, - authentication_providers: newProviders, identity_attributes: attributes, }; return newState; @@ -733,15 +728,23 @@ async function uploadSecret( async function downloadPolicy( state: ReducerStateRecovery, ): Promise { + logger.info("downloading policy"); let foundRecoveryInfo: RecoveryInternalData | undefined = undefined; let recoveryDoc: RecoveryDocument | undefined = undefined; const userAttributes = state.identity_attributes!; if (!state.selected_version) { throw Error("invalid state"); } + // FIXME: Do this concurrently/asynchronously so that one slow provider doens't block us. for (const prov of state.selected_version.providers) { - const pi = state.authentication_providers?.[prov.url]; + let pi = state.authentication_providers?.[prov.url]; if (!pi || pi.status !== "ok") { + // FIXME: this one blocks! + logger.info(`fetching provider info for ${prov.url}`); + pi = await getProviderInfo(prov.url); + } + logger.info(`new provider status is ${pi.status}`); + if (pi.status !== "ok") { continue; } const userId = await userIdentifierDerive(userAttributes, pi.provider_salt); @@ -750,6 +753,9 @@ async function downloadPolicy( reqUrl.searchParams.set("version", `${prov.version}`); const resp = await fetch(reqUrl.href); if (resp.status !== 200) { + logger.info( + `Could not download policy from provider ${prov.url}, status ${resp.status}`, + ); continue; } const body = await resp.arrayBuffer(); @@ -1058,16 +1064,10 @@ async function recoveryEnterUserAttributes( args: ActionArgsEnterUserAttributes, ): Promise { // FIXME: validate attributes - const providerUrls = Object.keys(state.authentication_providers ?? {}); - const newProviders = state.authentication_providers ?? {}; - for (const url of providerUrls) { - newProviders[url] = await getProviderInfo(url); - } const st: ReducerStateRecovery = { ...state, recovery_state: RecoveryStates.SecretSelecting, identity_attributes: args.identity_attributes, - authentication_providers: newProviders, }; return st; } @@ -1514,7 +1514,7 @@ async function nextFromChallengeSelecting( }; } -async function syncProviders( +async function syncAllProvidersTransition( state: ReducerStateRecovery, args: void, ): Promise { @@ -1722,7 +1722,7 @@ const recoveryTransitions: Record< ), ...transition("poll", codecForAny(), pollChallenges), ...transition("next", codecForAny(), nextFromChallengeSelecting), - ...transition("sync_providers", codecForAny(), syncProviders), + ...transition("sync_providers", codecForAny(), syncAllProvidersTransition), }, [RecoveryStates.ChallengeSolving]: { ...transitionRecoveryJump("back", RecoveryStates.ChallengeSelecting), @@ -1746,6 +1746,7 @@ export async function discoverPolicies( const providerUrls = Object.keys(state.authentication_providers || {}); // FIXME: Do we need to re-contact providers here / check if they're disabled? + // FIXME: Do this concurrently and take the first. Otherwise, one provider might block for a long time. for (const providerUrl of providerUrls) { const providerInfo = await getProviderInfo(providerUrl); @@ -1839,3 +1840,43 @@ export async function reduceAction( throw e; } } + +/** + * Update provider status of providers that we still need to contact. + * + * Returns updates as soon as new information about at least one provider + * is found. + * + * Returns an empty object if provider information is complete. + * + * FIXME: Also pass a cancelation token. + */ +export async function completeProviderStatus( + providerMap: AuthenticationProviderStatusMap, +): Promise { + const updateTasks: Promise<[string, AuthenticationProviderStatus]>[] = []; + for (const [provUrl, pi] of Object.entries(providerMap)) { + switch (pi.status) { + case "ok": + case "error": + case "disabled": + default: + continue; + case "not-contacted": + updateTasks.push( + (async () => { + return [provUrl, await getProviderInfo(provUrl)]; + })(), + ); + } + } + + if (updateTasks.length === 0) { + return {}; + } + + const [firstUrl, firstStatus] = await Promise.race(updateTasks); + return { + [firstUrl]: firstStatus, + }; +} diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index a0af2f86e..2e03a6ec4 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -186,6 +186,10 @@ export interface RecoveryInformation { }[][]; } +export interface AuthenticationProviderStatusMap { + [url: string]: AuthenticationProviderStatus; +} + export interface ReducerStateRecovery { reducer_type: "recovery"; @@ -231,7 +235,7 @@ export interface ReducerStateRecovery { value: string; }; - authentication_providers?: { [url: string]: AuthenticationProviderStatus }; + authentication_providers?: AuthenticationProviderStatusMap; } /** @@ -342,7 +346,7 @@ export interface ReducerStateBackupUserAttributesCollecting selected_country: string; currencies: string[]; required_attributes: UserAttributeSpec[]; - authentication_providers: { [url: string]: AuthenticationProviderStatus }; + authentication_providers: AuthenticationProviderStatusMap; } export interface ActionArgsEnterUserAttributes { diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index 434e5fb09..1b77db38f 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -21,6 +21,7 @@ import { TalerErrorCode } from "@gnu-taler/taler-util"; import { AggregatedPolicyMetaInfo, BackupStates, + completeProviderStatus, discoverPolicies, DiscoveryCursor, getBackupStartState, @@ -206,6 +207,44 @@ export function useAnastasisReducer(): AnastasisReducerApi { console.log(e); } setAnastasisStateInternal(newState); + + const tryUpdateProviders = () => { + const reducerState = newState.reducerState; + if ( + reducerState?.reducer_type !== "backup" && + reducerState?.reducer_type !== "recovery" + ) { + return; + } + const provMap = reducerState.authentication_providers; + if (!provMap) { + return; + } + const doUpdate = async () => { + const updates = await completeProviderStatus(provMap); + if (Object.keys(updates).length === 0) { + return; + } + console.log("got provider updates", updates); + const rs2 = reducerState; + if (rs2.reducer_type !== "backup" && rs2.reducer_type !== "recovery") { + return; + } + setAnastasisState({ + ...anastasisState, + reducerState: { + ...rs2, + authentication_providers: { + ...rs2.authentication_providers, + ...updates, + }, + }, + }); + }; + doUpdate().catch((e) => console.log(e)); + }; + + tryUpdateProviders(); }; async function doTransition(action: string, args: any): Promise { diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx index 2a480de48..e5dbcd8fc 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx @@ -37,7 +37,6 @@ export function ContinentSelectionScreen(): VNode { if (!theCountry) return; reducer.transition("select_country", { country_code: countryCode, - currencies: [theCountry.currency], }); }; diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 8d8912130..ff09d1a50 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -61,7 +61,6 @@ import { getRetryDuration, resetRetryInfo, RetryInfo, - updateRetryInfoTimeout, } from "../util/retries.js"; import { getExchangeDetails,