Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c46ff0d8 |
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
|
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
|
the key has to be long and random. Anyone who reaches the routes can replace credentials, as the
|
||||||
applications have no authentication.
|
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
|
# 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
|
## 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.
|
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.
|
again.
|
||||||
|
|
||||||
`CredentialStore` describes a store: the path of `config.json`, the key, the kind, the reserved
|
`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>`,
|
- `credentialProblems(value, store)`: the problems of the key's value by `<key>.<index>.<field>`,
|
||||||
for the application's own check of `config.json`.
|
for the application's own check of `config.json`.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"name": "@koszewscy/sk-react-toolkit",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"name": "@koszewscy/sk-react-toolkit",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@koszewscy/sk-react-toolkit",
|
"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.",
|
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"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,
|
* 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.
|
* first write creates it.
|
||||||
*/
|
*/
|
||||||
export type CredentialStore = {
|
export type CredentialStore = {
|
||||||
@@ -114,9 +115,30 @@ export function credentialProblems(entries: unknown, store: Pick<CredentialStore
|
|||||||
return problems
|
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. */
|
/** 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>[] {
|
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') : []
|
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 {
|
export function credentialValues(store: CredentialStore, name: string): CredentialValues {
|
||||||
const { data } = read(store)
|
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) {
|
if (problem !== undefined) {
|
||||||
throw new HttpError(500, `${problem[0]} in ${store.configPath} ${problem[1]}.`)
|
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> {
|
function problemsOf(value: CredentialProfile[], data: Record<string, unknown>): Record<string, string> {
|
||||||
const problems: Record<string, string> = {}
|
const problems: Record<string, string> = {}
|
||||||
for (const [path, message] of Object.entries(credentialProblems(toEntries(value, data, store, () => 'new'), store))) {
|
for (const [path, message] of Object.entries(credentialProblems(toEntries(value, data, store, () => 'new'), store))) {
|
||||||
const [, index, field] = path.split('.')
|
const [index, field] = path.slice(store.key.length + 1).split('.')
|
||||||
problems[field === undefined ? index ?? store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
|
problems[field === undefined ? index || store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
|
||||||
}
|
}
|
||||||
if (key() === null) {
|
if (key() === null) {
|
||||||
value.forEach((profile, index) => {
|
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)
|
throw new HttpError(422, 'The credentials are not valid.', problems)
|
||||||
}
|
}
|
||||||
const sealing = key()!
|
const sealing = key()!
|
||||||
const others = { ...file.data }
|
|
||||||
delete others[store.key]
|
|
||||||
const entries = toEntries(request.body.value, file.data, store, (secret) => encrypt(secret, sealing))
|
const entries = toEntries(request.body.value, file.data, store, (secret) => encrypt(secret, sealing))
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
throw new HttpError(500, `${store.configPath} cannot be written: ${(error as Error).message}`)
|
throw new HttpError(500, `${store.configPath} cannot be written: ${(error as Error).message}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user