anastasis-core: maximize diversity in provider selection
This commit is contained in:
parent
f4ec05c33a
commit
082bef3346
@ -90,6 +90,7 @@ import {
|
||||
} from "./crypto.js";
|
||||
import { unzlibSync, zlibSync } from "fflate";
|
||||
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
||||
import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
|
||||
|
||||
const { fetch } = fetchPonyfill({});
|
||||
|
||||
@ -290,109 +291,6 @@ async function backupEnterUserAttributes(
|
||||
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.
|
||||
|
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