refactor: Clean up internal storage

Each IDP class using storage now has a different storage.
This way those classes don't have to worry about clashing keys anymore.

All internal storage is now in the /.internal/ container,
thereby making it easier to take the location of the internal data into account:
only 1 path needs to be blocked and a regex router handling internal data
differently only has to match 1 path as well.
This commit is contained in:
Joachim Van Herwegen 2021-08-31 09:49:22 +02:00
parent 60fc273ea5
commit 1e1edd5c67
14 changed files with 90 additions and 78 deletions

View File

@ -5,10 +5,9 @@
"comment": "The storage adapter that persists usernames, passwords, etc.", "comment": "The storage adapter that persists usernames, passwords, etc.",
"@id": "urn:solid-server:auth:password:AccountStore", "@id": "urn:solid-server:auth:password:AccountStore",
"@type": "BaseAccountStore", "@type": "BaseAccountStore",
"args_storageName": "/idp/email-password-db", "saltRounds": 10,
"args_saltRounds": 10, "storage": {
"args_storage": { "@id": "urn:solid-server:default:AccountStorage"
"@id": "urn:solid-server:default:IdpStorage"
} }
} }
] ]

View File

@ -7,8 +7,7 @@
"@type": "WebIdAdapterFactory", "@type": "WebIdAdapterFactory",
"source": { "source": {
"@type": "ExpiringAdapterFactory", "@type": "ExpiringAdapterFactory",
"args_storageName": "/idp/oidc", "storage": { "@id": "urn:solid-server:default:IdpAdapterStorage" }
"args_storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
}, },
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" } "converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
} }

View File

@ -4,7 +4,6 @@
"files-scs:config/identity/handler/account-store/default.json", "files-scs:config/identity/handler/account-store/default.json",
"files-scs:config/identity/handler/adapter-factory/webid.json", "files-scs:config/identity/handler/adapter-factory/webid.json",
"files-scs:config/identity/handler/interaction/routes.json", "files-scs:config/identity/handler/interaction/routes.json",
"files-scs:config/identity/handler/key-value/storage.json",
"files-scs:config/identity/handler/provider-factory/identity.json" "files-scs:config/identity/handler/provider-factory/identity.json"
], ],
"@graph": [ "@graph": [

View File

@ -1,16 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Stores expiring data. This class has a `finalize` function that needs to be called after stopping the server.",
"@id": "urn:solid-server:default:ExpiringIdpStorage",
"@type": "WrappedExpiringStorage",
"source": { "@id": "urn:solid-server:default:IdpStorage" }
},
{
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
"@id": "urn:solid-server:default:Finalizer",
"ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:ExpiringIdpStorage" } ]
}
]
}

View File

@ -11,7 +11,7 @@
"args_adapterFactory": { "@id": "urn:solid-server:default:IdpAdapterFactory" }, "args_adapterFactory": { "@id": "urn:solid-server:default:IdpAdapterFactory" },
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_idpPath": "/idp", "args_idpPath": "/idp",
"args_storage": { "@id": "urn:solid-server:default:IdpStorage" }, "args_storage": { "@id": "urn:solid-server:default:IdpKeyStorage" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"config": { "config": {

View File

@ -6,7 +6,19 @@
"@id": "urn:solid-server:auth:password:OwnershipValidator", "@id": "urn:solid-server:auth:password:OwnershipValidator",
"@type": "TokenOwnershipValidator", "@type": "TokenOwnershipValidator",
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, "converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" } "storage": { "@id": "urn:solid-server:default:ExpiringTokenStorage" }
},
{
"comment": "Stores expiring data. This class has a `finalize` function that needs to be called after stopping the server.",
"@id": "urn:solid-server:default:ExpiringTokenStorage",
"@type": "WrappedExpiringStorage",
"source": { "@id": "urn:solid-server:default:IdpTokenStorage" }
},
{
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
"@id": "urn:solid-server:default:Finalizer",
"ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:ExpiringTokenStorage" } ]
} }
] ]
} }

View File

@ -10,8 +10,23 @@
"@type": "MemoryMapStorage" "@type": "MemoryMapStorage"
}, },
{ {
"comment": "Storage used by the IDP component.", "comment": "Storage used by the IDP adapter.",
"@id": "urn:solid-server:default:IdpStorage", "@id": "urn:solid-server:default:IdpAdapterStorage",
"@type": "MemoryMapStorage"
},
{
"comment": "Storage used for the IDP keys.",
"@id": "urn:solid-server:default:IdpKeyStorage",
"@type": "MemoryMapStorage"
},
{
"comment": "Storage used for IDP ownership tokens.",
"@id": "urn:solid-server:default:IdpTokenStorage",
"@type": "MemoryMapStorage"
},
{
"comment": "Storage used for account management.",
"@id": "urn:solid-server:default:AccountStorage",
"@type": "MemoryMapStorage" "@type": "MemoryMapStorage"
} }
] ]

View File

@ -13,26 +13,46 @@
"@type": "JsonResourceStorage", "@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore_Backend" }, "source": { "@id": "urn:solid-server:default:ResourceStore_Backend" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/locks/" "container": "/.internal/locks/"
}, },
{ {
"comment": "Storage used by the IDP component.", "comment": "Storage used by the IDP adapter.",
"@id": "urn:solid-server:default:IdpStorage", "@id": "urn:solid-server:default:IdpAdapterStorage",
"@type": "JsonResourceStorage", "@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" }, "source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/idp/data/" "container": "/.internal/idp/adapter/"
},
{
"comment": "Storage used for the IDP keys.",
"@id": "urn:solid-server:default:IdpKeyStorage",
"@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/idp/keys/"
},
{
"comment": "Storage used for IDP ownership tokens.",
"@id": "urn:solid-server:default:IdpTokenStorage",
"@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/idp/tokens/"
},
{
"comment": "Storage used for account management.",
"@id": "urn:solid-server:default:AccountStorage",
"@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/accounts/"
}, },
{ {
"comment": "Block external access to the storage containers to avoid exposing internal data.", "comment": "Block external access to the storage containers to avoid exposing internal data.",
"@id": "urn:solid-server:default:PathBasedAuthorizer", "@id": "urn:solid-server:default:PathBasedAuthorizer",
"PathBasedAuthorizer:_paths": [ "PathBasedAuthorizer:_paths": [
{ {
"PathBasedAuthorizer:_paths_key": "^/locks(/.*)?$", "PathBasedAuthorizer:_paths_key": "^/.internal(/.*)?$",
"PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" }
},
{
"PathBasedAuthorizer:_paths_key": "^/idp/data(/.*)?$",
"PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" } "PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" }
} }
] ]

View File

@ -47,6 +47,9 @@ export interface IdentityProviderFactoryArgs {
responseWriter: ResponseWriter; responseWriter: ResponseWriter;
} }
const JWKS_KEY = 'jwks';
const COOKIES_KEY = 'cookie-secret';
/** /**
* Creates an OIDC Provider based on the provided configuration and parameters. * Creates an OIDC Provider based on the provided configuration and parameters.
* The provider will be cached and returned on subsequent calls. * The provider will be cached and returned on subsequent calls.
@ -138,8 +141,7 @@ export class IdentityProviderFactory implements ProviderFactory {
*/ */
private async generateJwks(): Promise<{ keys: JWK[] }> { private async generateJwks(): Promise<{ keys: JWK[] }> {
// Check to see if the keys are already saved // Check to see if the keys are already saved
const key = `${this.idpPath}/jwks`; const jwks = await this.storage.get(JWKS_KEY) as { keys: JWK[] } | undefined;
const jwks = await this.storage.get(key) as { keys: JWK[] } | undefined;
if (jwks) { if (jwks) {
return jwks; return jwks;
} }
@ -152,7 +154,7 @@ export class IdentityProviderFactory implements ProviderFactory {
// which is why we convert it into a plain object here. // which is why we convert it into a plain object here.
// Potentially this can be changed at a later point in time to `{ keys: [ jwk ]}`. // Potentially this can be changed at a later point in time to `{ keys: [ jwk ]}`.
const newJwks = { keys: [{ ...jwk }]}; const newJwks = { keys: [{ ...jwk }]};
await this.storage.set(key, newJwks); await this.storage.set(JWKS_KEY, newJwks);
return newJwks; return newJwks;
} }
@ -162,14 +164,13 @@ export class IdentityProviderFactory implements ProviderFactory {
*/ */
private async generateCookieKeys(): Promise<string[]> { private async generateCookieKeys(): Promise<string[]> {
// Check to see if the keys are already saved // Check to see if the keys are already saved
const key = `${this.idpPath}/cookie-secret`; const cookieSecret = await this.storage.get(COOKIES_KEY);
const cookieSecret = await this.storage.get(key);
if (Array.isArray(cookieSecret)) { if (Array.isArray(cookieSecret)) {
return cookieSecret; return cookieSecret;
} }
// If they are not, generate and save them // If they are not, generate and save them
const newCookieSecret = [ randomBytes(64).toString('hex') ]; const newCookieSecret = [ randomBytes(64).toString('hex') ];
await this.storage.set(key, newCookieSecret); await this.storage.set(COOKIES_KEY, newCookieSecret);
return newCookieSecret; return newCookieSecret;
} }

View File

@ -25,39 +25,31 @@ export interface ForgotPasswordPayload {
export type EmailPasswordData = AccountPayload | ForgotPasswordPayload; export type EmailPasswordData = AccountPayload | ForgotPasswordPayload;
export interface BaseAccountStoreArgs {
storageName: string;
storage: KeyValueStorage<string, EmailPasswordData>;
saltRounds: number;
}
/** /**
* A EmailPasswordStore that uses a KeyValueStorage * A EmailPasswordStore that uses a KeyValueStorage
* to persist its information. * to persist its information.
*/ */
export class BaseAccountStore implements AccountStore { export class BaseAccountStore implements AccountStore {
private readonly storageName: string;
private readonly storage: KeyValueStorage<string, EmailPasswordData>; private readonly storage: KeyValueStorage<string, EmailPasswordData>;
private readonly saltRounds: number; private readonly saltRounds: number;
public constructor(args: BaseAccountStoreArgs) { public constructor(storage: KeyValueStorage<string, EmailPasswordData>, saltRounds: number) {
this.storageName = args.storageName; this.storage = storage;
this.storage = args.storage; this.saltRounds = saltRounds;
this.saltRounds = args.saltRounds;
} }
/** /**
* Generates a ResourceIdentifier to store data for the given email. * Generates a ResourceIdentifier to store data for the given email.
*/ */
private getAccountResourceIdentifier(email: string): string { private getAccountResourceIdentifier(email: string): string {
return `${this.storageName}/account/${encodeURIComponent(email)}`; return `account/${encodeURIComponent(email)}`;
} }
/** /**
* Generates a ResourceIdentifier to store data for the given recordId. * Generates a ResourceIdentifier to store data for the given recordId.
*/ */
private getForgotPasswordRecordResourceIdentifier(recordId: string): string { private getForgotPasswordRecordResourceIdentifier(recordId: string): string {
return `${this.storageName}/forgot-password-resource-identifier/${encodeURIComponent(recordId)}`; return `forgot-password-resource-identifier/${encodeURIComponent(recordId)}`;
} }
/** /**

View File

@ -3,11 +3,6 @@ import { getLoggerFor } from '../../logging/LogUtil';
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage'; import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
import type { AdapterFactory } from './AdapterFactory'; import type { AdapterFactory } from './AdapterFactory';
export interface ExpiringAdapterArgs {
storageName: string;
storage: ExpiringStorage<string, unknown>;
}
/** /**
* An IDP storage adapter that uses an ExpiringStorage * An IDP storage adapter that uses an ExpiringStorage
* to persist data. * to persist data.
@ -15,30 +10,28 @@ export interface ExpiringAdapterArgs {
export class ExpiringAdapter implements Adapter { export class ExpiringAdapter implements Adapter {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly storageName: string;
private readonly name: string; private readonly name: string;
private readonly storage: ExpiringStorage<string, unknown>; private readonly storage: ExpiringStorage<string, unknown>;
public constructor(name: string, args: ExpiringAdapterArgs) { public constructor(name: string, storage: ExpiringStorage<string, unknown>) {
this.name = name; this.name = name;
this.storageName = args.storageName; this.storage = storage;
this.storage = args.storage;
} }
private grantKeyFor(id: string): string { private grantKeyFor(id: string): string {
return `${this.storageName}/grant/${encodeURIComponent(id)}`; return `grant/${encodeURIComponent(id)}`;
} }
private userCodeKeyFor(userCode: string): string { private userCodeKeyFor(userCode: string): string {
return `${this.storageName}/user_code/${encodeURIComponent(userCode)}`; return `user_code/${encodeURIComponent(userCode)}`;
} }
private uidKeyFor(uid: string): string { private uidKeyFor(uid: string): string {
return `${this.storageName}/uid/${encodeURIComponent(uid)}`; return `uid/${encodeURIComponent(uid)}`;
} }
private keyFor(id: string): string { private keyFor(id: string): string {
return `${this.storageName}/${this.name}/${encodeURIComponent(id)}`; return `${this.name}/${encodeURIComponent(id)}`;
} }
public async upsert(id: string, payload: AdapterPayload, expiresIn?: number): Promise<void> { public async upsert(id: string, payload: AdapterPayload, expiresIn?: number): Promise<void> {
@ -117,13 +110,13 @@ export class ExpiringAdapter implements Adapter {
* The factory for a ExpiringStorageAdapter * The factory for a ExpiringStorageAdapter
*/ */
export class ExpiringAdapterFactory implements AdapterFactory { export class ExpiringAdapterFactory implements AdapterFactory {
private readonly args: ExpiringAdapterArgs; private readonly storage: ExpiringStorage<string, unknown>;
public constructor(args: ExpiringAdapterArgs) { public constructor(storage: ExpiringStorage<string, unknown>) {
this.args = args; this.storage = storage;
} }
public createStorageAdapter(name: string): ExpiringAdapter { public createStorageAdapter(name: string): ExpiringAdapter {
return new ExpiringAdapter(name, this.args); return new ExpiringAdapter(name, this.storage);
} }
} }

View File

@ -158,7 +158,7 @@ describe('An IdentityProviderFactory', (): void => {
expect(result1.config.jwks).toEqual(result2.config.jwks); expect(result1.config.jwks).toEqual(result2.config.jwks);
expect(storage.get).toHaveBeenCalledTimes(4); expect(storage.get).toHaveBeenCalledTimes(4);
expect(storage.set).toHaveBeenCalledTimes(2); expect(storage.set).toHaveBeenCalledTimes(2);
expect(storage.set).toHaveBeenCalledWith('/idp/jwks', result1.config.jwks); expect(storage.set).toHaveBeenCalledWith('jwks', result1.config.jwks);
expect(storage.set).toHaveBeenCalledWith('/idp/cookie-secret', result1.config.cookies?.keys); expect(storage.set).toHaveBeenCalledWith('cookie-secret', result1.config.cookies?.keys);
}); });
}); });

View File

@ -5,7 +5,6 @@ import { BaseAccountStore } from '../../../../../../src/identity/interaction/ema
import type { KeyValueStorage } from '../../../../../../src/storage/keyvalue/KeyValueStorage'; import type { KeyValueStorage } from '../../../../../../src/storage/keyvalue/KeyValueStorage';
describe('A BaseAccountStore', (): void => { describe('A BaseAccountStore', (): void => {
const storageName = '/mail/storage';
let storage: KeyValueStorage<string, EmailPasswordData>; let storage: KeyValueStorage<string, EmailPasswordData>;
const saltRounds = 11; const saltRounds = 11;
let store: BaseAccountStore; let store: BaseAccountStore;
@ -21,7 +20,7 @@ describe('A BaseAccountStore', (): void => {
delete: jest.fn((id: string): any => map.delete(id)), delete: jest.fn((id: string): any => map.delete(id)),
} as any; } as any;
store = new BaseAccountStore({ storageName, storage, saltRounds }); store = new BaseAccountStore(storage, saltRounds);
}); });
it('can create accounts.', async(): Promise<void> => { it('can create accounts.', async(): Promise<void> => {

View File

@ -7,7 +7,6 @@ import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringS
jest.useFakeTimers(); jest.useFakeTimers();
describe('An ExpiringAdapterFactory', (): void => { describe('An ExpiringAdapterFactory', (): void => {
const storageName = '/storage';
const name = 'nnaammee'; const name = 'nnaammee';
const id = 'http://alice.test.com/card#me'; const id = 'http://alice.test.com/card#me';
const grantId = 'grant123456'; const grantId = 'grant123456';
@ -27,7 +26,7 @@ describe('An ExpiringAdapterFactory', (): void => {
delete: jest.fn().mockImplementation((key: string): any => map.delete(key)), delete: jest.fn().mockImplementation((key: string): any => map.delete(key)),
} as any; } as any;
factory = new ExpiringAdapterFactory({ storageName, storage }); factory = new ExpiringAdapterFactory(storage);
adapter = factory.createStorageAdapter(name); adapter = factory.createStorageAdapter(name);
}); });