anastasis-core: maximize diversity in provider selection
This commit is contained in:
parent
f4ec05c33a
commit
082bef3346
@ -90,6 +90,7 @@ import {
|
|||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import { unzlibSync, zlibSync } from "fflate";
|
import { unzlibSync, zlibSync } from "fflate";
|
||||||
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
||||||
|
import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
|
||||||
|
|
||||||
const { fetch } = fetchPonyfill({});
|
const { fetch } = fetchPonyfill({});
|
||||||
|
|
||||||
@ -290,109 +291,6 @@ async function backupEnterUserAttributes(
|
|||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PolicySelectionResult {
|
|
||||||
policies: Policy[];
|
|
||||||
policy_providers: PolicyProvider[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type MethodSelection = number[];
|
|
||||||
|
|
||||||
function enumerateSelections(n: number, m: number): MethodSelection[] {
|
|
||||||
const selections: MethodSelection[] = [];
|
|
||||||
const a = new Array(n);
|
|
||||||
const sel = (i: number) => {
|
|
||||||
if (i === n) {
|
|
||||||
selections.push([...a]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const start = i == 0 ? 0 : a[i - 1] + 1;
|
|
||||||
for (let j = start; j < m; j++) {
|
|
||||||
a[i] = j;
|
|
||||||
sel(i + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sel(0);
|
|
||||||
return selections;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provider information used during provider/method mapping.
|
|
||||||
*/
|
|
||||||
interface ProviderInfo {
|
|
||||||
url: string;
|
|
||||||
methodCost: Record<string, AmountString>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign providers to a method selection.
|
|
||||||
*/
|
|
||||||
function assignProviders(
|
|
||||||
methods: AuthMethod[],
|
|
||||||
providers: ProviderInfo[],
|
|
||||||
methodSelection: number[],
|
|
||||||
): Policy | undefined {
|
|
||||||
const selectedProviders: string[] = [];
|
|
||||||
for (const mi of methodSelection) {
|
|
||||||
const m = methods[mi];
|
|
||||||
let found = false;
|
|
||||||
for (const prov of providers) {
|
|
||||||
if (prov.methodCost[m.type]) {
|
|
||||||
selectedProviders.push(prov.url);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
/* No provider found for this method */
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
methods: methodSelection.map((x, i) => {
|
|
||||||
return {
|
|
||||||
authentication_method: x,
|
|
||||||
provider: selectedProviders[i],
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function suggestPolicies(
|
|
||||||
methods: AuthMethod[],
|
|
||||||
providers: ProviderInfo[],
|
|
||||||
): PolicySelectionResult {
|
|
||||||
const numMethods = methods.length;
|
|
||||||
if (numMethods === 0) {
|
|
||||||
throw Error("no methods");
|
|
||||||
}
|
|
||||||
let numSel: number;
|
|
||||||
if (numMethods <= 2) {
|
|
||||||
numSel = numMethods;
|
|
||||||
} else if (numMethods <= 4) {
|
|
||||||
numSel = numMethods - 1;
|
|
||||||
} else if (numMethods <= 6) {
|
|
||||||
numSel = numMethods - 2;
|
|
||||||
} else if (numMethods == 7) {
|
|
||||||
numSel = numMethods - 3;
|
|
||||||
} else {
|
|
||||||
numSel = 4;
|
|
||||||
}
|
|
||||||
const policies: Policy[] = [];
|
|
||||||
const selections = enumerateSelections(numSel, numMethods);
|
|
||||||
logger.info(`selections: ${j2s(selections)}`);
|
|
||||||
for (const sel of selections) {
|
|
||||||
const p = assignProviders(methods, providers, sel);
|
|
||||||
if (p) {
|
|
||||||
policies.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
policies,
|
|
||||||
policy_providers: providers.map((x) => ({
|
|
||||||
provider_url: x.url,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truth data as stored in the reducer.
|
* Truth data as stored in the reducer.
|
||||||
|
169
packages/anastasis-core/src/policy-suggestion.ts
Normal file
169
packages/anastasis-core/src/policy-suggestion.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { AmountString, j2s, Logger } from "@gnu-taler/taler-util";
|
||||||
|
import { AuthMethod, Policy, PolicyProvider } from "./reducer-types.js";
|
||||||
|
|
||||||
|
const logger = new Logger("anastasis-core:policy-suggestion.ts");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider information used during provider/method mapping.
|
||||||
|
*/
|
||||||
|
export interface ProviderInfo {
|
||||||
|
url: string;
|
||||||
|
methodCost: Record<string, AmountString>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestPolicies(
|
||||||
|
methods: AuthMethod[],
|
||||||
|
providers: ProviderInfo[],
|
||||||
|
): PolicySelectionResult {
|
||||||
|
const numMethods = methods.length;
|
||||||
|
if (numMethods === 0) {
|
||||||
|
throw Error("no methods");
|
||||||
|
}
|
||||||
|
let numSel: number;
|
||||||
|
if (numMethods <= 2) {
|
||||||
|
numSel = numMethods;
|
||||||
|
} else if (numMethods <= 4) {
|
||||||
|
numSel = numMethods - 1;
|
||||||
|
} else if (numMethods <= 6) {
|
||||||
|
numSel = numMethods - 2;
|
||||||
|
} else if (numMethods == 7) {
|
||||||
|
numSel = numMethods - 3;
|
||||||
|
} else {
|
||||||
|
numSel = 4;
|
||||||
|
}
|
||||||
|
const policies: Policy[] = [];
|
||||||
|
const selections = enumerateMethodSelections(numSel, numMethods);
|
||||||
|
logger.info(`selections: ${j2s(selections)}`);
|
||||||
|
for (const sel of selections) {
|
||||||
|
const p = assignProviders(policies, methods, providers, sel);
|
||||||
|
if (p) {
|
||||||
|
policies.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
policies,
|
||||||
|
policy_providers: providers.map((x) => ({
|
||||||
|
provider_url: x.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign providers to a method selection.
|
||||||
|
*
|
||||||
|
* The evaluation of the assignment is made with respect to
|
||||||
|
* previously generated policies.
|
||||||
|
*/
|
||||||
|
function assignProviders(
|
||||||
|
existingPolicies: Policy[],
|
||||||
|
methods: AuthMethod[],
|
||||||
|
providers: ProviderInfo[],
|
||||||
|
methodSelection: number[],
|
||||||
|
): Policy | undefined {
|
||||||
|
const providerSelections = enumerateProviderMappings(
|
||||||
|
methodSelection.length,
|
||||||
|
providers.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
let bestProvSel: ProviderSelection | undefined;
|
||||||
|
let bestDiversity = 0;
|
||||||
|
|
||||||
|
for (const provSel of providerSelections) {
|
||||||
|
// First, check if selection is even possible with the methods offered
|
||||||
|
let possible = true;
|
||||||
|
for (const methIndex in provSel) {
|
||||||
|
const provIndex = provSel[methIndex];
|
||||||
|
const meth = methods[methIndex];
|
||||||
|
const prov = providers[provIndex];
|
||||||
|
if (!prov.methodCost[meth.type]) {
|
||||||
|
possible = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!possible) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate diversity, always prefer policies
|
||||||
|
// that increase diversity.
|
||||||
|
const providerSet = new Set<string>();
|
||||||
|
for (const pol of existingPolicies) {
|
||||||
|
for (const m of pol.methods) {
|
||||||
|
providerSet.add(m.provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const provIndex of provSel) {
|
||||||
|
const prov = providers[provIndex];
|
||||||
|
providerSet.add(prov.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diversity = providerSet.size;
|
||||||
|
if (!bestProvSel || diversity > bestDiversity) {
|
||||||
|
bestProvSel = provSel;
|
||||||
|
bestDiversity = diversity;
|
||||||
|
}
|
||||||
|
// TODO: also evaluate costs and duplicates (same challenge at same provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestProvSel) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
methods: bestProvSel.map((x, i) => ({
|
||||||
|
authentication_method: methodSelection[i],
|
||||||
|
provider: providers[x].url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderSelection = number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute provider mappings.
|
||||||
|
* Enumerates all n-combinations with repetition of m providers.
|
||||||
|
*/
|
||||||
|
function enumerateProviderMappings(n: number, m: number): ProviderSelection[] {
|
||||||
|
const selections: ProviderSelection[] = [];
|
||||||
|
const a = new Array(n);
|
||||||
|
const sel = (i: number, start: number = 0) => {
|
||||||
|
if (i === n) {
|
||||||
|
selections.push([...a]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let j = start; j < m; j++) {
|
||||||
|
a[i] = j;
|
||||||
|
sel(i + 1, j);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sel(0);
|
||||||
|
return selections;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PolicySelectionResult {
|
||||||
|
policies: Policy[];
|
||||||
|
policy_providers: PolicyProvider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type MethodSelection = number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute method selections.
|
||||||
|
* Enumerates all n-combinations without repetition of m methods.
|
||||||
|
*/
|
||||||
|
function enumerateMethodSelections(n: number, m: number): MethodSelection[] {
|
||||||
|
const selections: MethodSelection[] = [];
|
||||||
|
const a = new Array(n);
|
||||||
|
const sel = (i: number, start: number = 0) => {
|
||||||
|
if (i === n) {
|
||||||
|
selections.push([...a]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let j = start; j < m; j++) {
|
||||||
|
a[i] = j;
|
||||||
|
sel(i + 1, j + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sel(0);
|
||||||
|
return selections;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user