feature: the key of a credential store may be a path of keys, so stores of several kinds share one object of config.json.
Release / release (push) Failing after 16m30s
Release / release (push) Failing after 16m30s
This commit is contained in:
1 parent
7959c90012
commit
97c46ff0d8
6 files changed
+71
-12
No files matched your search
@@ -76,3 +76,22 @@ Consequences: Without the key no secret can be saved or used, and a changed key
|
||||
secrets unusable until they are entered again. SHA-256 does not slow down guessing a weak key, so
|
||||
the key has to be long and random. Anyone who reaches the routes can replace credentials, as the
|
||||
applications have no authentication.
|
||||
|
||||
## ADR-4: The key of a credential store is a path, so related stores share one object
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: sql-query-webui keeps connection profiles of PostgreSQL and of SQL Server. As stores of
|
||||
top-level keys, the keys have to carry the kind in their names, and a key such as `connections`
|
||||
does not say which kind it holds once there are several.
|
||||
|
||||
Decision: The key of a store is a key of `config.json` or a path of keys separated by dots, such
|
||||
as `connections.pg` and `connections.mssql`. The store reads the list at the path, reports its
|
||||
problems by `<path>.<index>.<field>`, and a write sets the list at the path, creating the objects
|
||||
of the path that are missing, or are not objects, and keeping their other keys, so stores of one
|
||||
object can be written one after the other. The kind stays the store's, not a field of an entry
|
||||
(ADR-3).
|
||||
|
||||
Consequences: A key of `config.json` holding a dot cannot be the key of a store. A single key
|
||||
reads and writes as before, so applications of one-key stores need no change. The application
|
||||
checks the object of a path, such as the keys it allows in it, itself.
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.0
|
||||
|
||||
feature: the key of a credential store may be a path of keys, so stores of several kinds share one object of config.json.
|
||||
|
||||
- `CredentialStore.key` is a key of `config.json` or a path of keys separated by dots, such as
|
||||
`connections.pg`; read, validate, write and `credentialValues` use the list at that path, and a
|
||||
write creates the objects of the path that are missing and keeps their other keys.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
fix: a credential store holds credentials of one kind, without a kind field in its entries, and the dialog edits several stores in tabs.
|
||||
|
||||
@@ -124,7 +124,19 @@ saved or used; a secret encrypted with another key cannot be decrypted and has t
|
||||
again.
|
||||
|
||||
`CredentialStore` describes a store: the path of `config.json`, the key, the kind, the reserved
|
||||
names and whether a missing `config.json` reads as `{}`.
|
||||
names and whether a missing `config.json` reads as `{}`. The key is a key of `config.json` or a
|
||||
path of keys separated by dots, such as `connections.pg`, for a list in nested objects; a write
|
||||
creates the objects of the path that are missing and keeps their other keys, so stores of several
|
||||
kinds can share one object:
|
||||
|
||||
```json
|
||||
{
|
||||
"connections": {
|
||||
"pg": [{ "name": "state", "host": "db.example.com", "port": 5432, "...": "..." }],
|
||||
"mssql": [{ "name": "warehouse", "host": "sql.example.com", "port": 1433, "...": "..." }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `credentialProblems(value, store)`: the problems of the key's value by `<key>.<index>.<field>`,
|
||||
for the application's own check of `config.json`.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@koszewscy/sk-react-toolkit",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@koszewscy/sk-react-toolkit",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"devDependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@koszewscy/sk-react-toolkit",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
+28
-8
@@ -8,7 +8,8 @@ import { decrypt, encrypt, encryptionKey } from './secrets.ts'
|
||||
|
||||
/**
|
||||
* A credential store: the list under key of config.json at configPath, every entry of the kind the application gives,
|
||||
* with the names reserved for its own credentials. With missingAsEmpty a missing config.json reads as {}, and the
|
||||
* with the names reserved for its own credentials. key is a key of config.json or a path of keys separated by dots,
|
||||
* such as connections.pg, for a list in nested objects. With missingAsEmpty a missing config.json reads as {}, and the
|
||||
* first write creates it.
|
||||
*/
|
||||
export type CredentialStore = {
|
||||
@@ -114,9 +115,30 @@ export function credentialProblems(entries: unknown, store: Pick<CredentialStore
|
||||
return problems
|
||||
}
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
|
||||
/** The value of data at key, a key or a path of keys separated by dots; undefined when it is not there. */
|
||||
function valueAt(data: Record<string, unknown>, key: string): unknown {
|
||||
return key.split('.').reduce<unknown>((value, part) => (isObject(value) ? value[part] : undefined), data)
|
||||
}
|
||||
|
||||
/**
|
||||
* data with value at key, a key or a path of keys separated by dots, after the other keys of its object; the objects
|
||||
* of the path that are missing, or are not objects, are created, and their other keys kept.
|
||||
*/
|
||||
function withValue(data: Record<string, unknown>, key: string, value: unknown): Record<string, unknown> {
|
||||
const [first, ...rest] = key.split('.')
|
||||
if (rest.length > 0) {
|
||||
return { ...data, [first]: withValue(isObject(data[first]) ? data[first] : {}, rest.join('.'), value) }
|
||||
}
|
||||
const others = { ...data }
|
||||
delete others[first]
|
||||
return { ...others, [first]: value }
|
||||
}
|
||||
|
||||
/** The entries of the store's key in config.json; a value that is not a list reads as none. */
|
||||
function entriesOf(data: Record<string, unknown>, store: CredentialStore): Record<string, unknown>[] {
|
||||
const entries = data[store.key]
|
||||
const entries = valueAt(data, store.key)
|
||||
return Array.isArray(entries) ? entries.filter((entry) => entry !== null && typeof entry === 'object') : []
|
||||
}
|
||||
|
||||
@@ -185,7 +207,7 @@ function toEntries(profiles: CredentialProfile[], data: Record<string, unknown>,
|
||||
*/
|
||||
export function credentialValues(store: CredentialStore, name: string): CredentialValues {
|
||||
const { data } = read(store)
|
||||
const [problem] = Object.entries(credentialProblems(data[store.key], store))
|
||||
const [problem] = Object.entries(credentialProblems(valueAt(data, store.key), store))
|
||||
if (problem !== undefined) {
|
||||
throw new HttpError(500, `${problem[0]} in ${store.configPath} ${problem[1]}.`)
|
||||
}
|
||||
@@ -234,8 +256,8 @@ export function registerCredentialRoutes(app: FastifyInstance, store: Credential
|
||||
function problemsOf(value: CredentialProfile[], data: Record<string, unknown>): Record<string, string> {
|
||||
const problems: Record<string, string> = {}
|
||||
for (const [path, message] of Object.entries(credentialProblems(toEntries(value, data, store, () => 'new'), store))) {
|
||||
const [, index, field] = path.split('.')
|
||||
problems[field === undefined ? index ?? store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
|
||||
const [index, field] = path.slice(store.key.length + 1).split('.')
|
||||
problems[field === undefined ? index || store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
|
||||
}
|
||||
if (key() === null) {
|
||||
value.forEach((profile, index) => {
|
||||
@@ -279,11 +301,9 @@ export function registerCredentialRoutes(app: FastifyInstance, store: Credential
|
||||
throw new HttpError(422, 'The credentials are not valid.', problems)
|
||||
}
|
||||
const sealing = key()!
|
||||
const others = { ...file.data }
|
||||
delete others[store.key]
|
||||
const entries = toEntries(request.body.value, file.data, store, (secret) => encrypt(secret, sealing))
|
||||
try {
|
||||
writeFileSync(store.configPath, `${JSON.stringify({ ...others, [store.key]: entries }, null, 2)}\n`)
|
||||
writeFileSync(store.configPath, `${JSON.stringify(withValue(file.data, store.key, entries), null, 2)}\n`)
|
||||
} catch (error) {
|
||||
throw new HttpError(500, `${store.configPath} cannot be written: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user