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,
ChallengeInfo,
AggregatedPolicyMetaInfo,
AuthenticationProviderStatusMap,
} from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill";
import {
@ -329,15 +330,9 @@ async function backupEnterUserAttributes(
args: ActionArgsEnterUserAttributes,
): Promise<ReducerStateBackup> {
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<ReducerStateRecovery | ReducerStateError> {
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<ReducerStateRecovery | ReducerStateError> {
// 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<ReducerStateRecovery | ReducerStateError> {
@ -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<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 {
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 {

View File

@ -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<void> {

View File

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

View File

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