anastasis: tag state properly

This commit is contained in:
Florian Dold 2022-04-13 08:44:37 +02:00
parent f3d8b44743
commit b28583ba7e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
15 changed files with 81 additions and 81 deletions

View File

@ -196,6 +196,7 @@ function getCountries(
export async function getBackupStartState(): Promise<ReducerStateBackup> {
return {
reducer_type: "backup",
backup_state: BackupStates.ContinentSelecting,
continents: getContinents({
requireProvider: true,
@ -205,6 +206,7 @@ export async function getBackupStartState(): Promise<ReducerStateBackup> {
export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
return {
reducer_type: "recovery",
recovery_state: RecoveryStates.ContinentSelecting,
continents: getContinents({
requireProvider: true,
@ -571,6 +573,7 @@ async function uploadSecret(
const talerPayUri = resp.headers.get("Taler");
if (!talerPayUri) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`,
};
@ -579,6 +582,7 @@ async function uploadSecret(
const parsedUri = parsePayUri(talerPayUri);
if (!parsedUri) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`,
};
@ -587,6 +591,7 @@ async function uploadSecret(
continue;
}
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: `could not upload truth (HTTP status ${resp.status})`,
};
@ -674,6 +679,7 @@ async function uploadSecret(
const talerPayUri = resp.headers.get("Taler");
if (!talerPayUri) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`,
};
@ -682,6 +688,7 @@ async function uploadSecret(
const parsedUri = parsePayUri(talerPayUri);
if (!parsedUri) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`,
};
@ -690,6 +697,7 @@ async function uploadSecret(
continue;
}
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: `could not upload policy (http status ${resp.status})`,
};
@ -734,13 +742,13 @@ async function downloadPolicy(
throw Error("invalid state");
}
for (const prov of state.selected_version.providers) {
const pi = state.authentication_providers?.[prov.provider_url];
const pi = state.authentication_providers?.[prov.url];
if (!pi || pi.status !== "ok") {
continue;
}
const userId = await userIdentifierDerive(userAttributes, pi.salt);
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.provider_url);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.url);
reqUrl.searchParams.set("version", `${prov.version}`);
const resp = await fetch(reqUrl.href);
if (resp.status !== 200) {
@ -759,7 +767,7 @@ async function downloadPolicy(
policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
} catch (e) {}
foundRecoveryInfo = {
provider_url: prov.provider_url,
provider_url: prov.url,
secret_name: rd.secret_name ?? "<unknown>",
version: policyVersion,
};
@ -768,6 +776,7 @@ async function downloadPolicy(
}
if (!foundRecoveryInfo || !recoveryDoc) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED,
hint: "No backups found at any provider for your identity information.",
};
@ -874,7 +883,7 @@ async function pollChallenges(
const s2 = await requestTruth(state, truth, {
pin: feedback.answer_code,
});
if (s2.recovery_state) {
if (s2.reducer_type === "recovery") {
state = s2;
}
}
@ -1001,6 +1010,7 @@ async function requestTruth(
}
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "got unexpected /truth/ response status",
http_status: resp.status,
@ -1110,6 +1120,7 @@ async function selectChallenge(
// FIXME: look at response, include in challenge_feedback!
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "got unexpected /truth/.../challenge response status",
http_status: resp.status,
@ -1125,6 +1136,7 @@ async function backupSelectContinent(
});
if (countries.length <= 0) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
hint: "continent not found",
};
@ -1423,10 +1435,14 @@ async function nextFromChallengeSelecting(
args: void,
): Promise<ReducerStateRecovery | ReducerStateError> {
const s2 = await tryRecoverSecret(state);
if (s2.recovery_state === RecoveryStates.RecoveryFinished) {
if (
s2.reducer_type === "recovery" &&
s2.recovery_state === RecoveryStates.RecoveryFinished
) {
return s2;
}
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "Not enough challenges solved",
};
@ -1462,7 +1478,7 @@ export function mergeDiscoveryAggregate(
const oldIndex = polHashToIndex[pol.policy_hash];
if (oldIndex != null) {
aggregatedPolicies[oldIndex].providers.push({
provider_url: pol.provider_url,
url: pol.provider_url,
version: pol.version,
});
} else {
@ -1471,7 +1487,7 @@ export function mergeDiscoveryAggregate(
policy_hash: pol.policy_hash,
providers: [
{
provider_url: pol.provider_url,
url: pol.provider_url,
version: pol.version,
},
],
@ -1592,7 +1608,7 @@ const recoveryTransitions: Record<
...transition("add_provider", codecForAny(), addProviderRecovery),
...transition("delete_provider", codecForAny(), deleteProviderRecovery),
...transition(
"change_version",
"select_version",
codecForActionArgsChangeVersion(),
changeVersion,
),
@ -1621,7 +1637,7 @@ export async function discoverPolicies(
state: ReducerState,
cursor?: DiscoveryCursor,
): Promise<DiscoveryResult> {
if (!state.recovery_state) {
if (state.reducer_type !== "recovery") {
throw Error("can only discover providers in recovery state");
}
@ -1685,12 +1701,14 @@ export async function reduceAction(
h = recoveryTransitions[state.recovery_state][action];
} else {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Invalid state (needs backup_state or recovery_state)`,
};
}
if (!h) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}' in state '${stateName}'`,
};
@ -1700,9 +1718,10 @@ export async function reduceAction(
parsedArgs = h.argCodec.decode(args);
} catch (e: any) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
hint: "argument validation failed",
message: e.toString(),
detail: e.toString(),
};
}
try {
@ -1710,7 +1729,10 @@ export async function reduceAction(
} catch (e) {
logger.error("action handler failed");
if (e instanceof ReducerError) {
return e.errorJson;
return {
reducer_type: "error",
...e.errorJson,
}
}
throw e;
}

View File

@ -73,19 +73,30 @@ export interface CoreSecret {
}
export interface ReducerStateBackup {
recovery_state?: undefined;
reducer_type: "backup";
backup_state: BackupStates;
code?: undefined;
currencies?: string[];
continents?: ContinentInfo[];
countries?: CountryInfo[];
identity_attributes?: { [n: string]: string };
authentication_providers?: { [url: string]: AuthenticationProviderStatus };
authentication_methods?: AuthMethod[];
required_attributes?: UserAttributeSpec[];
selected_continent?: string;
selected_country?: string;
secret_name?: string;
policies?: Policy[];
recovery_data?: {
@ -179,18 +190,10 @@ export interface RecoveryInformation {
}
export interface ReducerStateRecovery {
reducer_type: "recovery";
recovery_state: RecoveryStates;
/**
* Unused in the recovery states.
*/
backup_state?: undefined;
/**
* Unused in the recovery states.
*/
code?: undefined;
identity_attributes?: { [n: string]: string };
continents?: ContinentInfo[];
@ -267,11 +270,10 @@ export interface TruthMetaData {
}
export interface ReducerStateError {
backup_state?: undefined;
recovery_state?: undefined;
reducer_type: "error";
code: number;
hint?: string;
message?: string;
detail?: string;
}
export enum BackupStates {
@ -302,12 +304,13 @@ export interface MethodSpec {
usage_fee: string;
}
export type AuthenticationProviderStatusEmpty = {
export type AuthenticationProviderStatusNotContacted = {
status: "not-contacted";
};
export interface AuthenticationProviderStatusOk {
status: "ok";
annual_fee: string;
business_name: string;
currency: string;
@ -320,8 +323,13 @@ export interface AuthenticationProviderStatusOk {
// FIXME: add timestamp?
}
export interface AuthenticationProviderStatusDisabled {
status: "disabled";
}
export interface AuthenticationProviderStatusError {
status: "error";
http_status?: number;
code: number;
hint?: string;
@ -329,7 +337,8 @@ export interface AuthenticationProviderStatusError {
}
export type AuthenticationProviderStatus =
| AuthenticationProviderStatusEmpty
| AuthenticationProviderStatusNotContacted
| AuthenticationProviderStatusDisabled
| AuthenticationProviderStatusError
| AuthenticationProviderStatusOk;
@ -486,7 +495,6 @@ export interface PolicyMetaInfo {
secret_name?: string;
}
/**
* Aggregated / de-duplicated policy meta info.
*/
@ -495,7 +503,7 @@ export interface AggregatedPolicyMetaInfo {
policy_hash: string;
attribute_mask: number;
providers: {
provider_url: string;
url: string;
version: number;
}[];
}

View File

@ -19,7 +19,7 @@ export function AttributeEntryScreen(): VNode {
const [attrs, setAttrs] = useState<Record<string, string>>(
currentIdentityAttributes,
);
const isBackup = state && state.backup_state;
const isBackup = state?.reducer_type === "backup";
const [askUserIfSure, setAskUserIfSure] = useState(false);
if (!reducer) {

View File

@ -30,10 +30,7 @@ export function AuthenticationEditorScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}
const configuredAuthMethods: AuthMethod[] =
@ -62,7 +59,7 @@ export function AuthenticationEditorScreen(): VNode {
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
if ("http_status" in p && !("error_code" in p) && p.methods) {
if (p.status === "ok") {
for (const meth of p.methods) {
authAvailableSet.add(meth.type);
}

View File

@ -9,10 +9,7 @@ export function BackupFinishedScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}
const details = reducer.currentReducerState.success_details;

View File

@ -55,10 +55,7 @@ export function ChallengeOverviewScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "recovery") {
return <div>invalid state</div>;
}

View File

@ -7,10 +7,7 @@ export function ChallengePayingScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "recovery") {
return <div>invalid state</div>;
}
const payments = [""]; //reducer.currentReducerState.payments ??

View File

@ -38,10 +38,7 @@ export function EditPoliciesScreen({
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}

View File

@ -7,10 +7,7 @@ export function PoliciesPayingScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.policy_payment_requests ?? [];

View File

@ -18,10 +18,7 @@ export function RecoveryFinishedScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "recovery") {
return <div>invalid state</div>;
}
const secretName = reducer.currentReducerState.recovery_document?.secret_name;

View File

@ -15,10 +15,7 @@ export function ReviewPoliciesScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}

View File

@ -27,10 +27,7 @@ export function SecretEditorScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}

View File

@ -31,7 +31,7 @@ export function SecretSelectionScreen(): VNode {
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
reducer.currentReducerState.reducer_type !== "recovery"
) {
return <div>invalid state</div>;
}
@ -73,14 +73,17 @@ export function SecretSelectionScreen(): VNode {
}
return (
<AnastasisClientFrame title="Recovery: Select secret" hideNext="Please select version to recover">
<AnastasisClientFrame
title="Recovery: Select secret"
hideNext="Please select version to recover"
>
<p>Found versions:</p>
{policies.map((x) => (
<div>
{x.policy_hash} / {x.secret_name}
<button
onClick={async () => {
await reducer.transition("change_version", {
await reducer.transition("select_version", {
selection: x,
});
}}
@ -119,7 +122,7 @@ export function OldSecretSelectionScreen(): VNode {
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
reducer.currentReducerState.reducer_type !== "recovery"
) {
return <div>invalid state</div>;
}
@ -127,7 +130,7 @@ export function OldSecretSelectionScreen(): VNode {
async function doSelectVersion(p: string, n: number): Promise<void> {
if (!reducer) return Promise.resolve();
return reducer.runTransaction(async (tx) => {
await tx.transition("change_version", {
await tx.transition("select_version", {
version: n,
provider_url: p,
});

View File

@ -135,10 +135,7 @@ export function SolveScreen(): VNode {
</AnastasisClientFrame>
);
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "recovery") {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>

View File

@ -7,10 +7,7 @@ export function TruthsPayingScreen(): VNode {
if (!reducer) {
return <div>no reducer in context</div>;
}
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
if (reducer.currentReducerState?.reducer_type !== "backup") {
return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.payments ?? [];