anastasis-core: async provider synchronization

This commit is contained in:
Florian Dold 2022-04-15 12:56:16 +02:00
parent 098d1eb7eb
commit d1b4cc994b
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 101 additions and 19 deletions

View File

@ -94,6 +94,7 @@ import {
PolicyMetaInfo, PolicyMetaInfo,
ChallengeInfo, ChallengeInfo,
AggregatedPolicyMetaInfo, AggregatedPolicyMetaInfo,
AuthenticationProviderStatusMap,
} from "./reducer-types.js"; } from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill"; import fetchPonyfill from "fetch-ponyfill";
import { import {
@ -329,15 +330,9 @@ async function backupEnterUserAttributes(
args: ActionArgsEnterUserAttributes, args: ActionArgsEnterUserAttributes,
): Promise<ReducerStateBackup> { ): Promise<ReducerStateBackup> {
const attributes = args.identity_attributes; 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 = { const newState = {
...state, ...state,
backup_state: BackupStates.AuthenticationsEditing, backup_state: BackupStates.AuthenticationsEditing,
authentication_providers: newProviders,
identity_attributes: attributes, identity_attributes: attributes,
}; };
return newState; return newState;
@ -733,15 +728,23 @@ async function uploadSecret(
async function downloadPolicy( async function downloadPolicy(
state: ReducerStateRecovery, state: ReducerStateRecovery,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
logger.info("downloading policy");
let foundRecoveryInfo: RecoveryInternalData | undefined = undefined; let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
let recoveryDoc: RecoveryDocument | undefined = undefined; let recoveryDoc: RecoveryDocument | undefined = undefined;
const userAttributes = state.identity_attributes!; const userAttributes = state.identity_attributes!;
if (!state.selected_version) { if (!state.selected_version) {
throw Error("invalid state"); 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) { 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") { 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; continue;
} }
const userId = await userIdentifierDerive(userAttributes, pi.provider_salt); const userId = await userIdentifierDerive(userAttributes, pi.provider_salt);
@ -750,6 +753,9 @@ async function downloadPolicy(
reqUrl.searchParams.set("version", `${prov.version}`); reqUrl.searchParams.set("version", `${prov.version}`);
const resp = await fetch(reqUrl.href); const resp = await fetch(reqUrl.href);
if (resp.status !== 200) { if (resp.status !== 200) {
logger.info(
`Could not download policy from provider ${prov.url}, status ${resp.status}`,
);
continue; continue;
} }
const body = await resp.arrayBuffer(); const body = await resp.arrayBuffer();
@ -1058,16 +1064,10 @@ async function recoveryEnterUserAttributes(
args: ActionArgsEnterUserAttributes, args: ActionArgsEnterUserAttributes,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
// FIXME: validate attributes // 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 = { const st: ReducerStateRecovery = {
...state, ...state,
recovery_state: RecoveryStates.SecretSelecting, recovery_state: RecoveryStates.SecretSelecting,
identity_attributes: args.identity_attributes, identity_attributes: args.identity_attributes,
authentication_providers: newProviders,
}; };
return st; return st;
} }
@ -1514,7 +1514,7 @@ async function nextFromChallengeSelecting(
}; };
} }
async function syncProviders( async function syncAllProvidersTransition(
state: ReducerStateRecovery, state: ReducerStateRecovery,
args: void, args: void,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
@ -1722,7 +1722,7 @@ const recoveryTransitions: Record<
), ),
...transition("poll", codecForAny(), pollChallenges), ...transition("poll", codecForAny(), pollChallenges),
...transition("next", codecForAny(), nextFromChallengeSelecting), ...transition("next", codecForAny(), nextFromChallengeSelecting),
...transition("sync_providers", codecForAny(), syncProviders), ...transition("sync_providers", codecForAny(), syncAllProvidersTransition),
}, },
[RecoveryStates.ChallengeSolving]: { [RecoveryStates.ChallengeSolving]: {
...transitionRecoveryJump("back", RecoveryStates.ChallengeSelecting), ...transitionRecoveryJump("back", RecoveryStates.ChallengeSelecting),
@ -1746,6 +1746,7 @@ export async function discoverPolicies(
const providerUrls = Object.keys(state.authentication_providers || {}); const providerUrls = Object.keys(state.authentication_providers || {});
// FIXME: Do we need to re-contact providers here / check if they're disabled? // 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) { for (const providerUrl of providerUrls) {
const providerInfo = await getProviderInfo(providerUrl); const providerInfo = await getProviderInfo(providerUrl);
@ -1839,3 +1840,43 @@ export async function reduceAction(
throw e; 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<AuthenticationProviderStatusMap> {
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,
};
}

View File

@ -186,6 +186,10 @@ export interface RecoveryInformation {
}[][]; }[][];
} }
export interface AuthenticationProviderStatusMap {
[url: string]: AuthenticationProviderStatus;
}
export interface ReducerStateRecovery { export interface ReducerStateRecovery {
reducer_type: "recovery"; reducer_type: "recovery";
@ -231,7 +235,7 @@ export interface ReducerStateRecovery {
value: string; value: string;
}; };
authentication_providers?: { [url: string]: AuthenticationProviderStatus }; authentication_providers?: AuthenticationProviderStatusMap;
} }
/** /**
@ -342,7 +346,7 @@ export interface ReducerStateBackupUserAttributesCollecting
selected_country: string; selected_country: string;
currencies: string[]; currencies: string[];
required_attributes: UserAttributeSpec[]; required_attributes: UserAttributeSpec[];
authentication_providers: { [url: string]: AuthenticationProviderStatus }; authentication_providers: AuthenticationProviderStatusMap;
} }
export interface ActionArgsEnterUserAttributes { export interface ActionArgsEnterUserAttributes {

View File

@ -21,6 +21,7 @@ import { TalerErrorCode } from "@gnu-taler/taler-util";
import { import {
AggregatedPolicyMetaInfo, AggregatedPolicyMetaInfo,
BackupStates, BackupStates,
completeProviderStatus,
discoverPolicies, discoverPolicies,
DiscoveryCursor, DiscoveryCursor,
getBackupStartState, getBackupStartState,
@ -206,6 +207,44 @@ export function useAnastasisReducer(): AnastasisReducerApi {
console.log(e); console.log(e);
} }
setAnastasisStateInternal(newState); 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<void> { async function doTransition(action: string, args: any): Promise<void> {

View File

@ -37,7 +37,6 @@ export function ContinentSelectionScreen(): VNode {
if (!theCountry) return; if (!theCountry) return;
reducer.transition("select_country", { reducer.transition("select_country", {
country_code: countryCode, country_code: countryCode,
currencies: [theCountry.currency],
}); });
}; };

View File

@ -61,7 +61,6 @@ import {
getRetryDuration, getRetryDuration,
resetRetryInfo, resetRetryInfo,
RetryInfo, RetryInfo,
updateRetryInfoTimeout,
} from "../util/retries.js"; } from "../util/retries.js";
import { import {
getExchangeDetails, getExchangeDetails,