mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
979 lines
31 KiB
JavaScript
979 lines
31 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createLocalIdentityCredentialCommandRunner,
|
|
runLocalIdentityCredentialCommandFile,
|
|
} = require('@qinglong/local-owner-cli/identity-credential-command');
|
|
const {
|
|
LocalIdentityCredentialAdministrationAuthorizationError,
|
|
createLocalIdentityCredentialAdministrationService,
|
|
} = require('@qinglong/local-admin/identity-credential-administration');
|
|
const {
|
|
establishAuthenticatedLocalCommand,
|
|
} = require('@qinglong/local-owner-console/authenticated-command');
|
|
const {
|
|
FileLocalCredentialAdministrationDelivery,
|
|
} = require('@qinglong/local-owner-console/credential-administration-delivery');
|
|
const {
|
|
provisionLocalOwnerPepperKey,
|
|
} = require('@qinglong/local-owner-console/pepper-custody');
|
|
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
openLocalSqliteIdentityCredentialAdministrationDatabase,
|
|
} = require('@qinglong/local-sqlite/identity-credential-administration');
|
|
const {
|
|
apiCredentialSecretDigest,
|
|
formatApiCredentialToken,
|
|
} = require('@qinglong/runtime-core/api-credential-token');
|
|
const {
|
|
LocalCredentialOwnerContinuityError,
|
|
LocalIdentityCredentialAuthorizationFenceConflictError,
|
|
LocalIdentityOwnerBindingConflictError,
|
|
} = require('@qinglong/runtime-core/local-identity-credential-administration');
|
|
|
|
const ISSUE_MUTATION_ID = '83000000-0000-4000-8000-000000000001';
|
|
const ACK_MUTATION_ID = '83000000-0000-4000-8000-000000000003';
|
|
const PEPPER = Buffer.alloc(32, 83).toString('base64url');
|
|
const MATERIAL_DIGEST = 'c'.repeat(64);
|
|
|
|
function fixture(t) {
|
|
const deploymentRoot = fs.mkdtempSync(
|
|
path.join(fs.realpathSync(os.tmpdir()), 'ql3-identity-command-'),
|
|
);
|
|
fs.chmodSync(deploymentRoot, 0o700);
|
|
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
|
const commands = path.join(deploymentRoot, 'commands');
|
|
const delivery = path.join(deploymentRoot, 'managed-credentials');
|
|
const keyring = path.join(deploymentRoot, 'owner-keys');
|
|
fs.mkdirSync(commands, { mode: 0o700 });
|
|
fs.mkdirSync(delivery, { mode: 0o700 });
|
|
fs.mkdirSync(keyring, { mode: 0o700 });
|
|
return {
|
|
deploymentRoot,
|
|
commands,
|
|
delivery,
|
|
keyring,
|
|
databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'),
|
|
credentialFilePath: path.join(deploymentRoot, 'owner-credential.json'),
|
|
};
|
|
}
|
|
|
|
function writeCommand(directory, name, value) {
|
|
const filePath = path.join(directory, name);
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
return filePath;
|
|
}
|
|
|
|
function baseOptions(state) {
|
|
return {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
};
|
|
}
|
|
|
|
function options(state) {
|
|
return {
|
|
...baseOptions(state),
|
|
credentialDeliveryDirectory: state.delivery,
|
|
};
|
|
}
|
|
|
|
test('issues, exactly replays and acknowledges a credential without returning secret material', async (t) => {
|
|
const state = fixture(t);
|
|
let nowMs = 1_000;
|
|
let credentialCalls = 0;
|
|
let acknowledgementCalls = 0;
|
|
let committed;
|
|
const audits = [];
|
|
const database = {
|
|
apiCredentials: {
|
|
async resolve() {
|
|
return null;
|
|
},
|
|
},
|
|
ownerPepper: {
|
|
async resolveActive() {
|
|
return {
|
|
generation: 1,
|
|
mutationId: 'pepper-active',
|
|
expectedGeneration: 0,
|
|
activePepperKeyId: 'owner-v1',
|
|
materialDigest: MATERIAL_DIGEST,
|
|
backupDigest: 'd'.repeat(64),
|
|
activatedAtMs: 0,
|
|
};
|
|
},
|
|
async resolveKey() {
|
|
return {
|
|
pepperKeyId: 'owner-v1',
|
|
materialDigest: MATERIAL_DIGEST,
|
|
state: 'active',
|
|
version: 2,
|
|
registeredAtMs: 0,
|
|
activatedAtMs: 0,
|
|
};
|
|
},
|
|
},
|
|
projectPolicy: {},
|
|
identityCredentialAdministration: {
|
|
async record(audit) {
|
|
audits.push(audit);
|
|
},
|
|
},
|
|
activateUserCredentialFence() {},
|
|
async close() {},
|
|
};
|
|
const service = {
|
|
async changeIdentity() {
|
|
throw new Error('not used');
|
|
},
|
|
async changeCredential(request) {
|
|
credentialCalls += 1;
|
|
if (!committed) {
|
|
committed = {
|
|
secretDigest: request.secretDigest,
|
|
deliveryDigest: request.deliveryDigest,
|
|
notBeforeAtMs: request.notBeforeAtMs,
|
|
expiresAtMs: request.expiresAtMs,
|
|
};
|
|
} else {
|
|
assert.equal(request.secretDigest, committed.secretDigest);
|
|
assert.equal(request.deliveryDigest, committed.deliveryDigest);
|
|
assert.equal(request.notBeforeAtMs, committed.notBeforeAtMs);
|
|
assert.equal(request.expiresAtMs, committed.expiresAtMs);
|
|
}
|
|
return {
|
|
status: credentialCalls === 1 ? 'inserted' : 'existing',
|
|
credential: {
|
|
credentialId: request.credentialId,
|
|
version: 1,
|
|
pepperKeyId: request.pepperKeyId,
|
|
state: 'active',
|
|
subject: request.target,
|
|
subjectStatus: 'active',
|
|
secretDigest: request.secretDigest,
|
|
createdAtMs: committed.notBeforeAtMs,
|
|
notBeforeAtMs: committed.notBeforeAtMs,
|
|
expiresAtMs: committed.expiresAtMs,
|
|
},
|
|
mutation: {
|
|
mutationId: request.mutationId,
|
|
operation: request.operation,
|
|
credentialId: request.credentialId,
|
|
credentialVersion: 1,
|
|
expectedPreviousVersion: 0,
|
|
changedBy: request.principal.subject,
|
|
createdAtMs: committed.notBeforeAtMs,
|
|
},
|
|
delivery: { digest: committed.deliveryDigest },
|
|
audit: {},
|
|
};
|
|
},
|
|
async acknowledgeCredentialDelivery(request) {
|
|
acknowledgementCalls += 1;
|
|
return {
|
|
status: acknowledgementCalls === 1 ? 'inserted' : 'existing',
|
|
acknowledgement: {
|
|
credentialMutationId: request.credentialMutationId,
|
|
acknowledgementMutationId: request.mutationId,
|
|
projectId: request.projectId,
|
|
deliveryDigest: request.expectedDeliveryDigest,
|
|
acknowledgedBy: request.principal.subject,
|
|
acknowledgedAtMs: nowMs,
|
|
},
|
|
audit: {},
|
|
};
|
|
},
|
|
};
|
|
const runner = createLocalIdentityCredentialCommandRunner({
|
|
async openDatabase() {
|
|
return database;
|
|
},
|
|
async authenticate() {
|
|
return {
|
|
principal: {
|
|
subject: { type: 'user', id: 'owner-user' },
|
|
authenticationId: 'local_identity_admin:test',
|
|
authenticatedAtMs: 0,
|
|
expiresAtMs: 120_000,
|
|
assurance: 'local_console',
|
|
},
|
|
databaseFence: {
|
|
credentialId: 'owner-primary',
|
|
credentialVersion: 1,
|
|
pepperKeyId: 'owner-v1',
|
|
materialDigest: MATERIAL_DIGEST,
|
|
subjectType: 'user',
|
|
subjectId: 'owner-user',
|
|
secretDigest: 'e'.repeat(64),
|
|
notBeforeAtMs: 0,
|
|
expiresAtMs: 120_000,
|
|
},
|
|
async confirm() {},
|
|
};
|
|
},
|
|
createService() {
|
|
return service;
|
|
},
|
|
createDelivery(directory) {
|
|
return new FileLocalCredentialAdministrationDelivery(directory);
|
|
},
|
|
createPepperProvider() {
|
|
return {
|
|
resolve() {
|
|
return {
|
|
pepperKeyId: 'owner-v1',
|
|
pepper: PEPPER,
|
|
summary: { digest: MATERIAL_DIGEST },
|
|
};
|
|
},
|
|
};
|
|
},
|
|
randomBytes() {
|
|
return Buffer.alloc(32, credentialCalls === 0 ? 84 : 85);
|
|
},
|
|
now() {
|
|
return nowMs;
|
|
},
|
|
});
|
|
const issueCommand = writeCommand(state.commands, 'issue.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.issue',
|
|
options: options(state),
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-planner' },
|
|
credentialId: 'agent-planner-primary',
|
|
expectedCurrentVersion: 0,
|
|
lifetimeMs: 60_000,
|
|
mutationId: ISSUE_MUTATION_ID,
|
|
requestId: 'managed-credential-issue',
|
|
failureAuditEventId: '83000000-0000-4000-8000-000000000002',
|
|
},
|
|
});
|
|
|
|
const first = await runner.run(issueCommand);
|
|
nowMs = 30_000;
|
|
const replay = await runner.run(issueCommand);
|
|
assert.equal(first.status, 'inserted');
|
|
assert.equal(replay.status, 'existing');
|
|
assert.deepEqual(replay.delivery, first.delivery);
|
|
const publicResult = JSON.stringify(replay);
|
|
assert.equal(publicResult.includes(PEPPER), false);
|
|
assert.equal(publicResult.includes('ql3c_'), false);
|
|
assert.equal(publicResult.includes(state.delivery), false);
|
|
assert.equal(publicResult.includes('secret'), false);
|
|
const readyPath = path.join(state.delivery, first.delivery.fileName);
|
|
assert.equal(fs.statSync(readyPath).mode & 0o777, 0o600);
|
|
|
|
const acknowledgeCommand = writeCommand(state.commands, 'ack.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.delivery.acknowledge',
|
|
options: options(state),
|
|
request: {
|
|
projectId: 'default',
|
|
credentialMutationId: ISSUE_MUTATION_ID,
|
|
expectedDeliveryDigest: first.delivery.digest,
|
|
mutationId: ACK_MUTATION_ID,
|
|
requestId: 'managed-credential-acknowledge',
|
|
failureAuditEventId: '83000000-0000-4000-8000-000000000004',
|
|
},
|
|
});
|
|
const acknowledged = await runner.run(acknowledgeCommand);
|
|
const acknowledgedReplay = await runner.run(acknowledgeCommand);
|
|
assert.equal(acknowledged.cleanup, 'removed');
|
|
assert.equal(acknowledgedReplay.cleanup, 'absent');
|
|
assert.equal(fs.existsSync(readyPath), false);
|
|
assert.deepEqual(audits, []);
|
|
});
|
|
|
|
test('commits the real SQLite Identity and credential lifecycle behind the Owner fence', async (t) => {
|
|
const state = fixture(t);
|
|
const ownerCredentialId = 'owner-primary';
|
|
const ownerSecret = Buffer.alloc(32, 86).toString('base64url');
|
|
const ownerToken = formatApiCredentialToken(ownerCredentialId, ownerSecret);
|
|
await migrateLocalSqlitePath({
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
});
|
|
const pepperSummary = provisionLocalOwnerPepperKey({
|
|
keyringDirectory: state.keyring,
|
|
pepperKeyId: 'owner-v1',
|
|
randomBytes: () => Buffer.alloc(32, 83),
|
|
});
|
|
const nowMs = Date.now();
|
|
const ownerDigest = apiCredentialSecretDigest(
|
|
PEPPER,
|
|
ownerCredentialId,
|
|
ownerSecret,
|
|
);
|
|
const client = new DatabaseSync(state.databasePath);
|
|
try {
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
|
"pepper_key_id", "material_digest", "backup_digest", "state",
|
|
"version", "register_mutation_id", "activate_mutation_id",
|
|
"registered_at_ms", "activated_at_ms"
|
|
) VALUES (?, ?, ?, 'active', 2, ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
'owner-v1',
|
|
pepperSummary.digest,
|
|
'd'.repeat(64),
|
|
'84000000-0000-4000-8000-000000000001',
|
|
'84000000-0000-4000-8000-000000000002',
|
|
nowMs - 2_000,
|
|
nowMs - 1_500,
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
|
"generation", "mutation_id", "expected_generation",
|
|
"previous_pepper_key_id", "active_pepper_key_id",
|
|
"material_digest", "backup_digest", "activated_at_ms"
|
|
) VALUES (1, ?, 0, NULL, ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
'84000000-0000-4000-8000-000000000002',
|
|
'owner-v1',
|
|
pepperSummary.digest,
|
|
'd'.repeat(64),
|
|
nowMs - 1_500,
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3IdentitySubjects" (
|
|
"subject_type", "subject_id", "status", "version",
|
|
"created_at_ms", "updated_at_ms"
|
|
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
|
|
)
|
|
.run(nowMs - 1_000, nowMs - 1_000);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ApiCredentials" (
|
|
"credential_id", "version", "state", "subject_type",
|
|
"subject_id", "secret_digest", "created_at_ms",
|
|
"not_before_at_ms", "expires_at_ms"
|
|
) VALUES (?, 1, 'active', 'user', 'owner-user', ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
ownerCredentialId,
|
|
ownerDigest,
|
|
nowMs - 1_000,
|
|
nowMs - 1_000,
|
|
nowMs + 10 * 60_000,
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
|
"credential_id", "credential_version", "pepper_key_id"
|
|
) VALUES (?, 1, 'owner-v1')`,
|
|
)
|
|
.run(ownerCredentialId);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ProjectRoleBindings" (
|
|
"project_id", "subject_type", "subject_id", "version", "state",
|
|
"role", "mutation_id", "changed_by_type", "changed_by_id",
|
|
"created_at_ms"
|
|
) VALUES (
|
|
'default', 'user', 'owner-user', 1, 'active', 'owner',
|
|
'owner-binding', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(nowMs - 500);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3Projects" (
|
|
"id", "name", "slug", "status", "version",
|
|
"created_at_ms", "updated_at_ms"
|
|
) VALUES ('secondary', 'Secondary', 'secondary', 'active', 1, ?, ?)`,
|
|
)
|
|
.run(nowMs - 500, nowMs - 500);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ProjectRoleBindings" (
|
|
"project_id", "subject_type", "subject_id", "version", "state",
|
|
"role", "mutation_id", "changed_by_type", "changed_by_id",
|
|
"created_at_ms"
|
|
) VALUES (
|
|
'secondary', 'user', 'owner-user', 1, 'active', 'owner',
|
|
'secondary-owner-binding', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(nowMs - 400);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
fs.chmodSync(state.databasePath, 0o600);
|
|
fs.writeFileSync(
|
|
state.credentialFilePath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
kind: 'qinglong3-local-identity-credential-presentation',
|
|
token: ownerToken,
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
|
|
const register = writeCommand(state.commands, 'register-agent.json', {
|
|
schemaVersion: 1,
|
|
operation: 'identity.register',
|
|
options: {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
},
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
expectedCurrentVersion: 0,
|
|
mutationId: '84000000-0000-4000-8000-000000000003',
|
|
requestId: 'identity-register-real',
|
|
failureAuditEventId: '84000000-0000-4000-8000-000000000004',
|
|
},
|
|
});
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(register)).identityStatus,
|
|
'active',
|
|
);
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(register)).status,
|
|
'existing',
|
|
);
|
|
|
|
const inspectIdentity = writeCommand(
|
|
state.commands,
|
|
'inspect-agent-identity.json',
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'identity.inspect',
|
|
options: baseOptions(state),
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
requestId: 'identity-inspect-real',
|
|
auditEventId: '86000000-0000-4000-8000-000000000001',
|
|
},
|
|
},
|
|
);
|
|
const inspectedIdentity = await runLocalIdentityCredentialCommandFile(
|
|
inspectIdentity,
|
|
);
|
|
assert.equal(inspectedIdentity.found, true);
|
|
assert.equal(inspectedIdentity.version, 1);
|
|
assert.equal(inspectedIdentity.identityStatus, 'active');
|
|
assert.deepEqual(inspectedIdentity.target, {
|
|
type: 'agent',
|
|
id: 'agent-real',
|
|
});
|
|
assert.equal(
|
|
Number.isSafeInteger(inspectedIdentity.createdAtMs) &&
|
|
inspectedIdentity.createdAtMs === inspectedIdentity.updatedAtMs,
|
|
true,
|
|
);
|
|
|
|
const issue = writeCommand(state.commands, 'issue-agent.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.issue',
|
|
options: options(state),
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
credentialId: 'agent-real-primary',
|
|
expectedCurrentVersion: 0,
|
|
lifetimeMs: 60_000,
|
|
mutationId: '84000000-0000-4000-8000-000000000005',
|
|
requestId: 'credential-issue-real',
|
|
failureAuditEventId: '84000000-0000-4000-8000-000000000006',
|
|
},
|
|
});
|
|
const issued = await runLocalIdentityCredentialCommandFile(issue);
|
|
assert.equal(issued.status, 'inserted');
|
|
assert.equal(issued.state, 'active');
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(issue)).status,
|
|
'existing',
|
|
);
|
|
assert.equal(JSON.stringify(issued).includes('ql3c_'), false);
|
|
|
|
const inspectCredential = writeCommand(
|
|
state.commands,
|
|
'inspect-agent-credential.json',
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'credential.inspect',
|
|
options: baseOptions(state),
|
|
request: {
|
|
projectId: 'default',
|
|
credentialId: 'agent-real-primary',
|
|
requestId: 'credential-inspect-real',
|
|
auditEventId: '86000000-0000-4000-8000-000000000002',
|
|
},
|
|
},
|
|
);
|
|
const inspectedCredential = await runLocalIdentityCredentialCommandFile(
|
|
inspectCredential,
|
|
);
|
|
assert.equal(inspectedCredential.found, true);
|
|
assert.equal(inspectedCredential.version, 1);
|
|
assert.equal(inspectedCredential.state, 'active');
|
|
assert.deepEqual(inspectedCredential.target, {
|
|
type: 'agent',
|
|
id: 'agent-real',
|
|
});
|
|
const inspectionOutput = JSON.stringify(inspectedCredential);
|
|
for (const forbidden of [
|
|
ownerDigest,
|
|
PEPPER,
|
|
'pepperKeyId',
|
|
'secretDigest',
|
|
'token',
|
|
state.deploymentRoot,
|
|
]) {
|
|
assert.equal(inspectionOutput.includes(forbidden), false);
|
|
}
|
|
|
|
const inspectFromSecondary = writeCommand(
|
|
state.commands,
|
|
'inspect-credential-from-secondary.json',
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'credential.inspect',
|
|
options: baseOptions(state),
|
|
request: {
|
|
projectId: 'secondary',
|
|
credentialId: 'agent-real-primary',
|
|
requestId: 'credential-inspect-secondary-owner',
|
|
auditEventId: '86000000-0000-4000-8000-000000000005',
|
|
},
|
|
},
|
|
);
|
|
await assert.rejects(
|
|
runLocalIdentityCredentialCommandFile(inspectFromSecondary),
|
|
LocalIdentityCredentialAdministrationAuthorizationError,
|
|
);
|
|
|
|
const inspectMissing = writeCommand(
|
|
state.commands,
|
|
'inspect-missing-credential.json',
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'credential.inspect',
|
|
options: baseOptions(state),
|
|
request: {
|
|
projectId: 'default',
|
|
credentialId: 'missing-primary',
|
|
requestId: 'credential-inspect-missing',
|
|
auditEventId: '86000000-0000-4000-8000-000000000003',
|
|
},
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalIdentityCredentialCommandFile(inspectMissing),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'credential.inspect',
|
|
projectId: 'default',
|
|
found: false,
|
|
},
|
|
);
|
|
|
|
const acknowledge = writeCommand(state.commands, 'ack-agent.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.delivery.acknowledge',
|
|
options: options(state),
|
|
request: {
|
|
projectId: 'default',
|
|
credentialMutationId: '84000000-0000-4000-8000-000000000005',
|
|
expectedDeliveryDigest: issued.delivery.digest,
|
|
mutationId: '84000000-0000-4000-8000-000000000007',
|
|
requestId: 'credential-ack-real',
|
|
failureAuditEventId: '84000000-0000-4000-8000-000000000008',
|
|
},
|
|
});
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(acknowledge)).cleanup,
|
|
'removed',
|
|
);
|
|
|
|
const revoke = writeCommand(state.commands, 'revoke-agent.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.revoke',
|
|
options: {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
},
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
credentialId: 'agent-real-primary',
|
|
expectedCurrentVersion: 1,
|
|
mutationId: '84000000-0000-4000-8000-000000000009',
|
|
requestId: 'credential-revoke-real',
|
|
failureAuditEventId: '84000000-0000-4000-8000-00000000000a',
|
|
},
|
|
});
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(revoke)).state,
|
|
'revoked',
|
|
);
|
|
|
|
const disable = writeCommand(state.commands, 'disable-agent.json', {
|
|
schemaVersion: 1,
|
|
operation: 'identity.disable',
|
|
options: {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
},
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
expectedCurrentVersion: 1,
|
|
mutationId: '84000000-0000-4000-8000-00000000000b',
|
|
requestId: 'identity-disable-real',
|
|
failureAuditEventId: '84000000-0000-4000-8000-00000000000c',
|
|
},
|
|
});
|
|
assert.equal(
|
|
(await runLocalIdentityCredentialCommandFile(disable)).identityStatus,
|
|
'disabled',
|
|
);
|
|
|
|
const disableOwner = writeCommand(state.commands, 'disable-owner.json', {
|
|
schemaVersion: 1,
|
|
operation: 'identity.disable',
|
|
options: {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
},
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'user', id: 'owner-user' },
|
|
expectedCurrentVersion: 1,
|
|
mutationId: '84000000-0000-4000-8000-00000000000d',
|
|
requestId: 'identity-disable-owner-rejected',
|
|
failureAuditEventId: '84000000-0000-4000-8000-00000000000e',
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
runLocalIdentityCredentialCommandFile(disableOwner),
|
|
LocalIdentityOwnerBindingConflictError,
|
|
);
|
|
|
|
const revokeOwner = writeCommand(state.commands, 'revoke-owner.json', {
|
|
schemaVersion: 1,
|
|
operation: 'credential.revoke',
|
|
options: {
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
},
|
|
request: {
|
|
projectId: 'default',
|
|
target: { type: 'user', id: 'owner-user' },
|
|
credentialId: ownerCredentialId,
|
|
expectedCurrentVersion: 1,
|
|
mutationId: '84000000-0000-4000-8000-00000000000f',
|
|
requestId: 'credential-revoke-owner-rejected',
|
|
failureAuditEventId: '85000000-0000-4000-8000-000000000001',
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
runLocalIdentityCredentialCommandFile(revokeOwner),
|
|
LocalCredentialOwnerContinuityError,
|
|
);
|
|
|
|
const fenceDatabase =
|
|
await openLocalSqliteIdentityCredentialAdministrationDatabase({
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
});
|
|
try {
|
|
const authenticated = await establishAuthenticatedLocalCommand(
|
|
fenceDatabase,
|
|
{
|
|
deploymentRoot: state.deploymentRoot,
|
|
databasePath: state.databasePath,
|
|
ownerPepperKeyringDirectory: state.keyring,
|
|
credentialFilePath: state.credentialFilePath,
|
|
authenticationNamespace: 'local_identity_admin',
|
|
},
|
|
);
|
|
await authenticated.confirm();
|
|
fenceDatabase.activateUserCredentialFence(authenticated.databaseFence);
|
|
await assert.rejects(
|
|
fenceDatabase.identityCredentialAdministration.inspectAuthorizedIdentity({
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
authorization: {
|
|
projectId: 'secondary',
|
|
actor: authenticated.principal.subject,
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
},
|
|
audit: {
|
|
eventId: '86000000-0000-4000-8000-000000000006',
|
|
requestId: 'identity-inspect-repository-scope-bypass',
|
|
operationId: 'identity.inspect',
|
|
projectId: 'secondary',
|
|
subject: authenticated.principal.subject,
|
|
authenticationId: authenticated.principal.authenticationId,
|
|
outcome: 'allowed',
|
|
reasons: ['owner_identity_inspect'],
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
occurredAtMs: Date.now(),
|
|
},
|
|
}),
|
|
LocalIdentityCredentialAuthorizationFenceConflictError,
|
|
);
|
|
let changedFence = false;
|
|
const repository = new Proxy(
|
|
fenceDatabase.identityCredentialAdministration,
|
|
{
|
|
get(target, property) {
|
|
if (property === 'inspectAuthorizedIdentity') {
|
|
return async (command) => {
|
|
if (!changedFence) {
|
|
changedFence = true;
|
|
const writer = new DatabaseSync(state.databasePath);
|
|
try {
|
|
writer
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ProjectRoleBindings" (
|
|
"project_id", "subject_type", "subject_id", "version",
|
|
"state", "role", "mutation_id", "changed_by_type",
|
|
"changed_by_id", "created_at_ms"
|
|
) VALUES (
|
|
'default', 'user', 'owner-user', 2, 'active', 'admin',
|
|
'owner-binding-demoted', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(Date.now());
|
|
} finally {
|
|
writer.close();
|
|
}
|
|
}
|
|
return target.inspectAuthorizedIdentity(command);
|
|
};
|
|
}
|
|
const value = Reflect.get(target, property, target);
|
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
},
|
|
},
|
|
);
|
|
const service = createLocalIdentityCredentialAdministrationService(
|
|
fenceDatabase.projectPolicy,
|
|
repository,
|
|
);
|
|
await assert.rejects(
|
|
service.inspectIdentity({
|
|
projectId: 'default',
|
|
target: { type: 'agent', id: 'agent-real' },
|
|
auditEventId: '86000000-0000-4000-8000-000000000004',
|
|
requestId: 'identity-inspect-fence-changed',
|
|
principal: authenticated.principal,
|
|
}),
|
|
LocalIdentityCredentialAuthorizationFenceConflictError,
|
|
);
|
|
} finally {
|
|
await fenceDatabase.close();
|
|
}
|
|
|
|
const anchorWriter = new DatabaseSync(state.databasePath);
|
|
try {
|
|
for (const audit of [
|
|
{
|
|
eventId: '87000000-0000-4000-8000-000000000001',
|
|
requestId: 'secondary-bootstrap-issue',
|
|
operationId: 'owner.bootstrap.issue',
|
|
occurredAtMs: nowMs - 900,
|
|
},
|
|
{
|
|
eventId: '87000000-0000-4000-8000-000000000002',
|
|
requestId: 'secondary-bootstrap-claim',
|
|
operationId: 'owner.bootstrap.claim',
|
|
occurredAtMs: nowMs - 800,
|
|
},
|
|
]) {
|
|
anchorWriter
|
|
.prepare(
|
|
`INSERT INTO "QingLong3SecurityAuditEvents" (
|
|
"event_id", "request_id", "operation_id", "project_id",
|
|
"subject_type", "subject_id", "authentication_id", "outcome",
|
|
"reasons_json", "fence_project_version",
|
|
"fence_binding_version", "occurred_at_ms"
|
|
) VALUES (?, ?, ?, 'secondary', 'user', 'owner-user',
|
|
'bootstrap-anchor-test', 'allowed', '["test_anchor"]',
|
|
1, NULL, ?)`,
|
|
)
|
|
.run(
|
|
audit.eventId,
|
|
audit.requestId,
|
|
audit.operationId,
|
|
audit.occurredAtMs,
|
|
);
|
|
}
|
|
anchorWriter
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerBootstrapChallenges" (
|
|
"project_id", "version", "issue_mutation_id", "issue_request_id",
|
|
"challenge_id", "token_digest", "issuer_authentication_id",
|
|
"issuer_authenticated_at_ms", "issuer_expires_at_ms",
|
|
"issued_at_ms", "expires_at_ms", "issue_audit_event_id",
|
|
"consumed_at_ms", "claim_mutation_id", "claim_request_id",
|
|
"claimed_subject_type", "claimed_subject_id", "credential_id",
|
|
"credential_version", "claim_authentication_id",
|
|
"claim_authenticated_at_ms", "claim_expires_at_ms",
|
|
"claim_assurance", "claim_audit_event_id"
|
|
) VALUES (
|
|
'secondary', 1, ?, 'secondary-bootstrap-issue',
|
|
'AAAAAAAAAAAAAAAAAAAAAA', ?, 'bootstrap-anchor-test',
|
|
?, ?, ?, ?, ?, ?, ?, 'secondary-bootstrap-claim',
|
|
'user', 'owner-user', ?, 1, 'bootstrap-anchor-test',
|
|
?, ?, 'single_factor', ?
|
|
)`,
|
|
)
|
|
.run(
|
|
'87000000-0000-4000-8000-000000000001',
|
|
'f'.repeat(64),
|
|
nowMs - 1_000,
|
|
nowMs + 60_000,
|
|
nowMs - 900,
|
|
nowMs + 60_000,
|
|
'87000000-0000-4000-8000-000000000001',
|
|
nowMs - 800,
|
|
'87000000-0000-4000-8000-000000000002',
|
|
ownerCredentialId,
|
|
nowMs - 1_000,
|
|
nowMs + 60_000,
|
|
'87000000-0000-4000-8000-000000000002',
|
|
);
|
|
} finally {
|
|
anchorWriter.close();
|
|
}
|
|
const anchoredDatabase =
|
|
await openLocalSqliteIdentityCredentialAdministrationDatabase({
|
|
databasePath: state.databasePath,
|
|
profile: 'edge',
|
|
});
|
|
try {
|
|
assert.equal(
|
|
await anchoredDatabase.identityCredentialAdministration.resolveAuthorityProjectId(),
|
|
'secondary',
|
|
);
|
|
} finally {
|
|
await anchoredDatabase.close();
|
|
}
|
|
|
|
const read = new DatabaseSync(state.databasePath, { readOnly: true });
|
|
try {
|
|
assert.deepEqual(
|
|
{
|
|
...read
|
|
.prepare(
|
|
`SELECT "status", "version"
|
|
FROM "QingLong3IdentitySubjects"
|
|
WHERE "subject_type" = 'agent' AND "subject_id" = 'agent-real'`,
|
|
)
|
|
.get(),
|
|
},
|
|
{ status: 'disabled', version: 2 },
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
...read
|
|
.prepare(
|
|
`SELECT "state", "version"
|
|
FROM "QingLong3ApiCredentials"
|
|
WHERE "credential_id" = 'agent-real-primary'
|
|
ORDER BY "version" DESC LIMIT 1`,
|
|
)
|
|
.get(),
|
|
},
|
|
{ state: 'revoked', version: 2 },
|
|
);
|
|
assert.equal(
|
|
read
|
|
.prepare(
|
|
`SELECT "state"
|
|
FROM "QingLong3ApiCredentials"
|
|
WHERE "credential_id" = ?
|
|
ORDER BY "version" DESC LIMIT 1`,
|
|
)
|
|
.get(ownerCredentialId).state,
|
|
'active',
|
|
);
|
|
assert.equal(
|
|
read
|
|
.prepare(
|
|
`SELECT count(*) AS "count"
|
|
FROM "QingLong3ApiCredentialAdministrationMutations"
|
|
WHERE "credential_id" = 'agent-real-primary'`,
|
|
)
|
|
.get().count,
|
|
2,
|
|
);
|
|
assert.equal(
|
|
read
|
|
.prepare(
|
|
`SELECT count(*) AS "count"
|
|
FROM "QingLong3ApiCredentialDeliveryAcknowledgements"
|
|
WHERE "credential_mutation_id" =
|
|
'84000000-0000-4000-8000-000000000005'`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
read
|
|
.prepare(
|
|
`SELECT count(*) AS "count"
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" IN ('identity.inspect', 'credential.inspect')
|
|
AND "outcome" = 'allowed'`,
|
|
)
|
|
.get().count,
|
|
3,
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
...read
|
|
.prepare(
|
|
`SELECT "outcome", "reasons_json" AS "reasonsJson"
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "event_id" = '86000000-0000-4000-8000-000000000005'`,
|
|
)
|
|
.get(),
|
|
},
|
|
{
|
|
outcome: 'denied',
|
|
reasonsJson: '["instance_authority_project_required"]',
|
|
},
|
|
);
|
|
assert.deepEqual(read.prepare('PRAGMA foreign_key_check').all(), []);
|
|
} finally {
|
|
read.close();
|
|
}
|
|
});
|