refactor(sites): bundle the site licence instead of minting it
The published sites get the same key pair on every deploy, committed as sas/mocks/mock-licence.json (site_id 123, the mock estate's SYSSITE) rather than generated in the pipeline. The pair sits outside the deployed sync folder, so the CI estate stays unlicensed and the Cypress specs keep exercising the free tier. The Sites job copies the pair into the drive before the snapshot and refuses a pair whose two values are identical, which is how the app spots an HTTP-format key it cannot decrypt in a secure context. The pair trips the generic-api-key scanner, so .gitleaksignore carries the two fingerprints.
This commit is contained in:
1 parent
7d9df71fdb
commit
a07728be2b
5 files changed
+20
-97
No files matched your search
@@ -114,10 +114,16 @@ jobs:
|
||||
- name: Build the browser-hosted site
|
||||
run: |
|
||||
# The published sites ship a working licence so they are usable beyond
|
||||
# the free tier. It is minted per deploy rather than committed: the
|
||||
# pair is self-contained, and the CI estate stays unlicensed so the
|
||||
# specs can exercise the free tier.
|
||||
node sas/mocks/mint-licence.js "sasjs_root/drive/files$APP_LOC/mock-storage/licence.json"
|
||||
# the free tier. The pair is committed in sas/mocks/mock-licence.json
|
||||
# rather than minted here, so every deploy of both sites carries the
|
||||
# same key; the CI estate stays unlicensed because that file is outside
|
||||
# the deployed sync folder, so the specs still exercise the free tier.
|
||||
# Identical values in the two fields is the HTTP-format marker the app
|
||||
# refuses in a secure context, so refuse to publish one.
|
||||
node -e "const l=require('./sas/mocks/mock-licence.json'); if (!l.licenceKey || !l.activationKey || l.licenceKey === l.activationKey) { console.error('sas/mocks/mock-licence.json must hold an HTTPS-format key pair'); process.exit(1) }"
|
||||
LICENCE_DIR="sasjs_root/drive/files$APP_LOC/mock-storage"
|
||||
mkdir -p "$LICENCE_DIR"
|
||||
cp sas/mocks/mock-licence.json "$LICENCE_DIR/licence.json"
|
||||
|
||||
# DC's data files exist only after makedata has run, so the drive is
|
||||
# captured first and shipped as the site's starting drive. services/web9
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# sas/mocks/mock-licence.json holds a licence key pair for the mock estate: the
|
||||
# activation key is RSA key material the browser demo decrypts with, not a
|
||||
# credential for any service.
|
||||
sas/mocks/mock-licence.json:generic-api-key:2
|
||||
sas/mocks/mock-licence.json:generic-api-key:3
|
||||
+1
-1
@@ -135,7 +135,7 @@ After changing mock services, re-run `sasjs cbd -t server-ci` from `sas/mocks/sa
|
||||
|
||||
### Demo and staging sites
|
||||
|
||||
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The estate carries no licence key, so CI runs on the free tier until a key is registered through the licensing screen (`admin/registerkey`) - the Cypress setup does that for the specs that need one. The Sites job mints a licence for the published sites, so demo and staging are fully usable.
|
||||
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The estate carries no licence key, so CI runs on the free tier until a key is registered through the licensing screen (`admin/registerkey`) - the Cypress setup does that for the specs that need one. The Sites job writes a licence into the published drive, so demo and staging are fully usable.
|
||||
|
||||
## GUI Elements
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
// Mints a Data Controller licence key pair for a published mock site.
|
||||
//
|
||||
// The app decrypts `licenceKey` with `activationKey`:
|
||||
// activationKey = base64(pkcs8 RSA-OAEP private key)
|
||||
// licenceKey = base64(RSA-OAEP-SHA256(JSON licence data, public key))
|
||||
//
|
||||
// The two values must differ: that is how the app tells an HTTPS-format key
|
||||
// from an HTTP-format one (see detectLicenceKeyProtocolMismatch.ts). The
|
||||
// payload's site_id must match the estate's SYSSITE, which the mock reports
|
||||
// as 123.
|
||||
//
|
||||
// The Sites workflow runs this against the drive before the snapshot is taken,
|
||||
// so demo.datacontroller.io and staging.datacontroller.io are licensed. The
|
||||
// pair is minted per deploy rather than committed: it is self-contained, and
|
||||
// the CI estate stays unlicensed so the specs can exercise the free tier.
|
||||
//
|
||||
// Usage: node sas/mocks/mint-licence.js <out-file>
|
||||
const fs = require('fs')
|
||||
|
||||
const SITE_ID = '123' // the mock estate's SYSSITE
|
||||
const OUT = process.argv[2]
|
||||
|
||||
const b64 = (buf) => Buffer.from(buf).toString('base64')
|
||||
|
||||
async function main() {
|
||||
if (!OUT) {
|
||||
console.error('Usage: node sas/mocks/mint-licence.js <out-file>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const licenceData = {
|
||||
valid_until: '2099-12-31',
|
||||
users_allowed: 9999,
|
||||
hot_license_key: 'non-commercial-and-evaluation',
|
||||
site_id: SITE_ID,
|
||||
demo: false
|
||||
}
|
||||
|
||||
const plaintext = new TextEncoder().encode(JSON.stringify(licenceData))
|
||||
const cipher = await crypto.subtle.encrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
keyPair.publicKey,
|
||||
plaintext
|
||||
)
|
||||
const privateKeyBytes = await crypto.subtle.exportKey(
|
||||
'pkcs8',
|
||||
keyPair.privateKey
|
||||
)
|
||||
|
||||
const licenceKey = b64(cipher)
|
||||
const activationKey = b64(privateKeyBytes)
|
||||
|
||||
// Round-trip check with the same WebCrypto the app uses, so a minted pair is
|
||||
// known to decrypt before it is published.
|
||||
const imported = await crypto.subtle.importKey(
|
||||
'pkcs8',
|
||||
privateKeyBytes,
|
||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||
true,
|
||||
['decrypt']
|
||||
)
|
||||
const back = await crypto.subtle.decrypt(
|
||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||
imported,
|
||||
cipher
|
||||
)
|
||||
const decoded = JSON.parse(new TextDecoder().decode(back))
|
||||
|
||||
if (JSON.stringify(decoded) !== JSON.stringify(licenceData))
|
||||
throw new Error('round trip mismatch')
|
||||
if (licenceKey === activationKey)
|
||||
throw new Error('keys must differ to be an HTTPS-format key')
|
||||
|
||||
fs.writeFileSync(OUT, JSON.stringify({ licenceKey, activationKey }, null, 2))
|
||||
console.log(`minted licence for site ${SITE_ID} -> ${OUT}`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('FAILED', err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"licenceKey": "G4H/DNTdEMkmCIxOrYkRYTQ1P4sWg0gs8Zk9rakxFNYDZcLyEPMJJo82YJQ/S24P2E/YPxaWsDZXgOrWKfeCDS7NrLcwsoFNa8KRF2WTlRgSMiG4sjBRX8zNEIvszj5DAq4LEgOUUAdfctNfpUcygbxT1dhqy6gU3hBu/xRuXGd36rhxQwfahaKkfaCg/rFplkrvqnjoJ6622BxWP/HG1dxZUE7HNJyT4tNaXzlC9U6u/aaJJ66+V+9511BwjM9ss4nyHuHoogVE1yrzLMlL94P3QoM1hwkGHnEHmFzg3zHbQG1f3pUs9YzK0ytsFKU7AK948gJrGbf37Zqa+Mz7EZf6XzqqXx0zQ4y9zAhLV9s4QG8JdhrByhfG5kefrs4L/BpgKq3ElgYL5EMkrLJUrlqWYI2ouxJuIGCQz41VCWXJ2S7pd39ljifc8Zj+t2Mn5EEkRVsrbdxjCkyoZOQ1ALofa1+auUI26Rm5rwEgrfhEocDmphJpO2F8lZgwDxbepSE6v/vEV/JQWBQTyrX10W+qOEu8OhvZrYJo+eIJMaO7q1pjfdrn3XhoH+BeB3Sy1sevGLXQZC+CzOtcvajetGGCDAabtTLc49ouWPUiUOAWPE3uYymsMoRZgpLIMxaUM86hoK0r9T6wSaP0bddK754sXN1jWwIDAHSGz5WO1ko=",
|
||||
"activationKey": "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"
|
||||
}
|
||||
Reference in new issue
Block a user