add provider/ remove provider

This commit is contained in:
Sebastian 2021-11-09 00:19:50 -03:00
parent e369f26ec5
commit 7f6101a24d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
7 changed files with 175 additions and 50 deletions

View File

@ -65,6 +65,8 @@ import {
ActionArgsChangeVersion, ActionArgsChangeVersion,
TruthMetaData, TruthMetaData,
ActionArgsUpdatePolicy, ActionArgsUpdatePolicy,
ActionArgsAddProvider,
ActionArgsDeleteProvider,
} from "./reducer-types.js"; } from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill"; import fetchPonyfill from "fetch-ponyfill";
import { import {
@ -1060,9 +1062,15 @@ 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,
identity_attributes: args.identity_attributes, identity_attributes: args.identity_attributes,
authentication_providers: newProviders,
}; };
return downloadPolicy(st); return downloadPolicy(st);
} }
@ -1174,6 +1182,60 @@ function transitionRecoveryJump(
}; };
} }
//FIXME: doest the same that addProviderRecovery, but type are not generic enough
async function addProviderBackup(
state: ReducerStateBackup,
args: ActionArgsAddProvider,
): Promise<ReducerStateBackup> {
const info = await getProviderInfo(args.provider_url)
return {
...state,
authentication_providers: {
...(state.authentication_providers ?? {}),
[args.provider_url]: info,
},
};
}
//FIXME: doest the same that deleteProviderRecovery, but type are not generic enough
async function deleteProviderBackup(
state: ReducerStateBackup,
args: ActionArgsDeleteProvider,
): Promise<ReducerStateBackup> {
const authentication_providers = {... state.authentication_providers ?? {} }
delete authentication_providers[args.provider_url]
return {
...state,
authentication_providers,
};
}
async function addProviderRecovery(
state: ReducerStateRecovery,
args: ActionArgsAddProvider,
): Promise<ReducerStateRecovery> {
const info = await getProviderInfo(args.provider_url)
return {
...state,
authentication_providers: {
...(state.authentication_providers ?? {}),
[args.provider_url]: info,
},
};
}
async function deleteProviderRecovery(
state: ReducerStateRecovery,
args: ActionArgsDeleteProvider,
): Promise<ReducerStateRecovery> {
const authentication_providers = {... state.authentication_providers ?? {} }
delete authentication_providers[args.provider_url]
return {
...state,
authentication_providers,
};
}
async function addAuthentication( async function addAuthentication(
state: ReducerStateBackup, state: ReducerStateBackup,
args: ActionArgsAddAuthentication, args: ActionArgsAddAuthentication,
@ -1408,6 +1470,8 @@ const backupTransitions: Record<
...transitionBackupJump("back", BackupStates.UserAttributesCollecting), ...transitionBackupJump("back", BackupStates.UserAttributesCollecting),
...transition("add_authentication", codecForAny(), addAuthentication), ...transition("add_authentication", codecForAny(), addAuthentication),
...transition("delete_authentication", codecForAny(), deleteAuthentication), ...transition("delete_authentication", codecForAny(), deleteAuthentication),
...transition("add_provider", codecForAny(), addProviderBackup),
...transition("delete_provider", codecForAny(), deleteProviderBackup),
...transition("next", codecForAny(), nextFromAuthenticationsEditing), ...transition("next", codecForAny(), nextFromAuthenticationsEditing),
}, },
[BackupStates.PoliciesReviewing]: { [BackupStates.PoliciesReviewing]: {
@ -1476,6 +1540,8 @@ const recoveryTransitions: Record<
[RecoveryStates.SecretSelecting]: { [RecoveryStates.SecretSelecting]: {
...transitionRecoveryJump("back", RecoveryStates.UserAttributesCollecting), ...transitionRecoveryJump("back", RecoveryStates.UserAttributesCollecting),
...transitionRecoveryJump("next", RecoveryStates.ChallengeSelecting), ...transitionRecoveryJump("next", RecoveryStates.ChallengeSelecting),
...transition("add_provider", codecForAny(), addProviderRecovery),
...transition("delete_provider", codecForAny(), deleteProviderRecovery),
...transition( ...transition(
"change_version", "change_version",
codecForActionArgsChangeVersion(), codecForActionArgsChangeVersion(),

View File

@ -334,6 +334,14 @@ export const codecForActionArgsEnterUserAttributes = () =>
.property("identity_attributes", codecForAny()) .property("identity_attributes", codecForAny())
.build("ActionArgsEnterUserAttributes"); .build("ActionArgsEnterUserAttributes");
export interface ActionArgsAddProvider {
provider_url: string;
}
export interface ActionArgsDeleteProvider {
provider_url: string;
}
export interface ActionArgsAddAuthentication { export interface ActionArgsAddAuthentication {
authentication_method: { authentication_method: {
type: string; type: string;

View File

@ -40,6 +40,12 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState); } as ReducerState);
export const NewProviderWithoutProviderList = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
authentication_providers: {}
} as ReducerState);
export const NewVideoProvider = createExample(TestedComponent, { export const NewVideoProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing, ...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'video'}); } as ReducerState, { providerType: 'video'});

View File

@ -1,6 +1,6 @@
import { AuthenticationProviderStatusOk } from "anastasis-core"; import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { TextInput } from "../../components/fields/TextInput"; import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod"; import { authMethods, KnownAuthMethods } from "./authMethod";
@ -8,13 +8,13 @@ import { AnastasisClientFrame } from "./index";
interface Props { interface Props {
providerType?: KnownAuthMethods; providerType?: KnownAuthMethods;
cancel: () => void; onCancel: () => void;
} }
async function testProvider(url: string, expectedMethodType?: string): Promise<void> { async function testProvider(url: string, expectedMethodType?: string): Promise<void> {
try { try {
const response = await fetch(`${url}/config`) const response = await fetch(new URL("config", url).href)
const json = await (response.json().catch(d => ({}))) const json = await (response.json().catch(d => ({})))
if (!("methods" in json) || !Array.isArray(json.methods)) { if (!("methods" in json) || !Array.isArray(json.methods)) {
throw Error("This provider doesn't have authentication method. Check the provider URL") throw Error("This provider doesn't have authentication method. Check the provider URL")
@ -41,7 +41,7 @@ async function testProvider(url: string, expectedMethodType?: string): Promise<v
} }
export function AddingProviderScreen({ providerType, cancel }: Props): VNode { export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
const [providerURL, setProviderURL] = useState(""); const [providerURL, setProviderURL] = useState("");
@ -54,8 +54,8 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
useEffect(() => { useEffect(() => {
if (timeout) window.clearTimeout(timeout.current) if (timeout) window.clearTimeout(timeout.current)
timeout.current = window.setTimeout(async () => { timeout.current = window.setTimeout(async () => {
const url = providerURL.endsWith('/') ? providerURL.substring(0, providerURL.length - 1) : providerURL const url = providerURL.endsWith('/') ? providerURL : (providerURL + '/')
if (!url) return; if (!providerURL || authProviders.includes(url)) return;
try { try {
setTesting(true) setTesting(true)
await testProvider(url, providerType) await testProvider(url, providerType)
@ -67,40 +67,50 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
if (e instanceof Error) setError(e.message) if (e instanceof Error) setError(e.message)
} }
setTesting(false) setTesting(false)
}, 1000); }, 200);
}, [providerURL]) }, [providerURL, reducer])
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
function addProvider(): void { if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) {
// addAuthMethod({ return <div>invalid state</div>
// authentication_method: {
// type: "sms",
// instructions: `SMS to ${providerURL}`,
// challenge: encodeCrock(stringToBytes(providerURL)),
// },
// });
} }
async function addProvider(provider_url: string): Promise<void> {
await reducer?.transition("add_provider", { provider_url })
onCancel()
}
function deleteProvider(provider_url: string): void {
reducer?.transition("delete_provider", { provider_url })
}
const allAuthProviders = reducer.currentReducerState.authentication_providers || {}
const authProviders = Object.keys(allAuthProviders).filter(provUrl => {
const p = allAuthProviders[provUrl];
if (!providerLabel) {
return p && ("currency" in p)
} else {
return p && ("currency" in p) && p.methods.findIndex(m => m.type === providerType) !== -1
}
})
let errors = !providerURL ? 'Add provider URL' : undefined let errors = !providerURL ? 'Add provider URL' : undefined
let url: string | undefined;
try { try {
new URL(providerURL) url = new URL("",providerURL).href
} catch { } catch {
errors = 'Check the URL' errors = 'Check the URL'
} }
if (!!error && !errors) { if (!!error && !errors) {
errors = error errors = error
} }
if (!errors && authProviders.includes(url!)) {
if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) { errors = 'That provider is already known'
return <div>invalid state</div>
} }
const authProviders = reducer.currentReducerState.authentication_providers || {}
return ( return (
<AnastasisClientFrame hideNav <AnastasisClientFrame hideNav
title="Backup: Manage providers" title="Backup: Manage providers"
@ -119,40 +129,45 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
label="Provider URL" label="Provider URL"
placeholder="https://provider.com" placeholder="https://provider.com"
grabFocus grabFocus
error={errors}
bind={[providerURL, setProviderURL]} /> bind={[providerURL, setProviderURL]} />
</div> </div>
<p class="block"> <p class="block">
Example: https://kudos.demo.anastasis.lu Example: https://kudos.demo.anastasis.lu
</p> </p>
{testing && <p class="has-text-info">Testing</p>}
{testing && <p class="block has-text-info">Testing</p>}
{!!error && <p class="block has-text-danger">{error}</p>}
{error === "" && <p class="block has-text-success">This provider worked!</p>}
<div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={cancel}>Cancel</button> <button class="button" onClick={onCancel}>Cancel</button>
<span data-tooltip={errors}> <span data-tooltip={errors}>
<button class="button is-info" disabled={error !== "" || testing} onClick={addProvider}>Add</button> <button class="button is-info" disabled={error !== "" || testing} onClick={() => addProvider(url!)}>Add</button>
</span> </span>
</div> </div>
<p class="subtitle"> {authProviders.length > 0 ? (
Current providers !providerLabel ?
</p> <p class="subtitle">
{/* <table class="table"> */} Current providers
{Object.keys(authProviders).map(k => { </p> : <p class="subtitle">
const p = authProviders[k] Current providers for {providerLabel} service
if (("currency" in p)) { </p>
return <TableRow url={k} info={p} /> ) : (
} !providerLabel ? <p class="subtitle">
} No known providers, add one.
</p> : <p class="subtitle">
No known providers for {providerLabel} service
</p>
)} )}
{/* </table> */}
{authProviders.map(k => {
const p = allAuthProviders[k] as AuthenticationProviderStatusOk
return <TableRow url={k} info={p} onDelete={deleteProvider} />
})}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
function TableRow({ url, info }: { url: string, info: AuthenticationProviderStatusOk }) { function TableRow({ url, info, onDelete }: { onDelete: (s: string) => void, url: string, info: AuthenticationProviderStatusOk }) {
const [status, setStatus] = useState("checking") const [status, setStatus] = useState("checking")
useEffect(function () { useEffect(function () {
testProvider(url.endsWith('/') ? url.substring(0, url.length - 1) : url) testProvider(url.endsWith('/') ? url.substring(0, url.length - 1) : url)
@ -174,7 +189,7 @@ function TableRow({ url, info }: { url: string, info: AuthenticationProviderStat
</dl> </dl>
</div> </div>
<div class="block" style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}> <div class="block" style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
<button class="button is-danger" >Remove</button> <button class="button is-danger" onClick={() => onDelete(url)}>Remove</button>
</div> </div>
</div> </div>
} }

View File

@ -2,10 +2,12 @@ import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AddingProviderScreen } from "./AddingProviderScreen";
import { import {
authMethods, authMethods,
AuthMethodSetupProps, AuthMethodSetupProps,
AuthMethodWithRemove, AuthMethodWithRemove,
isKnownAuthMethods,
KnownAuthMethods, KnownAuthMethods,
} from "./authMethod"; } from "./authMethod";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
@ -18,6 +20,8 @@ export function AuthenticationEditorScreen(): VNode {
KnownAuthMethods | undefined KnownAuthMethods | undefined
>(undefined); >(undefined);
const [tooFewAuths, setTooFewAuths] = useState(false); const [tooFewAuths, setTooFewAuths] = useState(false);
const [manageProvider, setManageProvider] = useState<string | undefined>(undefined)
// const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined) // const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
@ -63,6 +67,14 @@ export function AuthenticationEditorScreen(): VNode {
} }
} }
if (manageProvider !== undefined) {
return <AddingProviderScreen
onCancel={() => setManageProvider(undefined)}
providerType={isKnownAuthMethods(manageProvider) ? manageProvider : undefined}
/>
}
if (selectedMethod) { if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined); const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => { const addMethod = (args: any): void => {
@ -86,9 +98,9 @@ export function AuthenticationEditorScreen(): VNode {
active active
onCancel={cancel} onCancel={cancel}
description="No providers founds" description="No providers founds"
label="Add a provider manually (not implemented!)" label="Add a provider manually"
onConfirm={() => { onConfirm={() => {
null; setManageProvider(selectedMethod)
}} }}
> >
<p> <p>
@ -179,9 +191,9 @@ export function AuthenticationEditorScreen(): VNode {
active={!noProvidersAck} active={!noProvidersAck}
onCancel={() => setNoProvidersAck(true)} onCancel={() => setNoProvidersAck(true)}
description="No providers founds" description="No providers founds"
label="Add a provider manually (not implemented!)" label="Add a provider manually"
onConfirm={() => { onConfirm={() => {
null; setManageProvider("")
}} }}
> >
<p> <p>
@ -201,11 +213,11 @@ export function AuthenticationEditorScreen(): VNode {
identity via the methods you configure here. The list of identity via the methods you configure here. The list of
authentication method is defined by the backup provider list. authentication method is defined by the backup provider list.
</p> </p>
{/* <p class="block"> <p class="block">
<button class="button is-info"> <button class="button is-info" onClick={() => setManageProvider("")}>
Manage backup providers Manage backup providers
</button> </button>
</p> */} </p>
{authAvailableSet.size > 0 && ( {authAvailableSet.size > 0 && (
<p class="block"> <p class="block">
We couldn't find provider for some of the authentication methods. We couldn't find provider for some of the authentication methods.

View File

@ -3,12 +3,14 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton"; import { AsyncButton } from "../../components/AsyncButton";
import { NumberInput } from "../../components/fields/NumberInput"; import { NumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
import { AddingProviderScreen } from "./AddingProviderScreen";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode { export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false); const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
const reducer = useAnastasisContext() const reducer = useAnastasisContext()
const [manageProvider, setManageProvider] = useState(false)
const currentVersion = (reducer?.currentReducerState const currentVersion = (reducer?.currentReducerState
&& ("recovery_document" in reducer.currentReducerState) && ("recovery_document" in reducer.currentReducerState)
&& reducer.currentReducerState.recovery_document?.version) || 0; && reducer.currentReducerState.recovery_document?.version) || 0;
@ -49,6 +51,10 @@ export function SecretSelectionScreen(): VNode {
/> />
} }
if (manageProvider) {
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />
}
return ( return (
<AnastasisClientFrame title="Recovery: Select secret"> <AnastasisClientFrame title="Recovery: Select secret">
<div class="columns"> <div class="columns">
@ -69,6 +75,12 @@ export function SecretSelectionScreen(): VNode {
</div> </div>
<div class="column"> <div class="column">
<p>Secret found, you can select another version or continue to the challenges solving</p> <p>Secret found, you can select another version or continue to the challenges solving</p>
<p class="block">
<button class="button is-info" onClick={() => setManageProvider(true)}>
Manage recovery providers
</button>
</p>
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>

View File

@ -41,7 +41,13 @@ interface AuthMethodConfiguration {
solve: (props: AuthMethodSolveProps) => VNode; solve: (props: AuthMethodSolveProps) => VNode;
skip?: boolean; skip?: boolean;
} }
export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban"; // export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
const ALL_METHODS = ['sms', 'email', 'post', 'question', 'video' , 'totp', 'iban'] as const;
export type KnownAuthMethods = (typeof ALL_METHODS)[number];
export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
return ALL_METHODS.includes(value as KnownAuthMethods)
}
type KnowMethodConfig = { type KnowMethodConfig = {
[name in KnownAuthMethods]: AuthMethodConfiguration; [name in KnownAuthMethods]: AuthMethodConfiguration;