Prepare supervised email-connect SMTP foundation and projection
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
parent
f7c01d4000
commit
e77f3989b4
8 changed files with 914 additions and 3 deletions
|
|
@ -222,7 +222,7 @@
|
|||
| task | RPF-WP-0067-T02 | done | — | workplans/RPF-WP-0067-netkingdom-identity-receipt-and-metadata-answers.md |
|
||||
| task | RPF-WP-0067-T03 | wait | — | workplans/RPF-WP-0067-netkingdom-identity-receipt-and-metadata-answers.md |
|
||||
| task | RPF-WP-0068-T01 | done | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md |
|
||||
| task | RPF-WP-0068-T02 | todo | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md |
|
||||
| task | RPF-WP-0068-T02 | done | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md |
|
||||
| task | RPF-WP-0068-T03 | wait | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md |
|
||||
| task | RPF-WP-0068-T04 | todo | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md |
|
||||
| intake | RPF-IN-0001 | closed | — | intakes/intakes.md |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ CCR-2026-0056 proposes one additional reader of the existing platform SMTP entry
|
|||
RPF-WP-0068 tracks preparation, foundation/binding proof, attended admission and
|
||||
native consumer delivery. The parent requirement is CSOC-WP-0015-T02.
|
||||
|
||||
**Source preparation is complete; execution is not ready.** The dedicated service
|
||||
**The complete controller is now prepared** in
|
||||
[the foundation packet](email-connect-smtp-foundation-review-2026-10-31.md).
|
||||
Its41 SMTP tests pass. **Execution still awaits review/approval.** The dedicated service
|
||||
account/store/projection have only passed server dry-run. Native binding is not
|
||||
confirmed, the CCR is proposed, and no new policy or role has been installed.
|
||||
|
||||
|
|
|
|||
128
docs/email-connect-smtp-foundation-review-2026-10-31.md
Normal file
128
docs/email-connect-smtp-foundation-review-2026-10-31.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Email-connect platform SMTP foundation review
|
||||
|
||||
Prepared for CCR-2026-0056 and RPF-WP-0068, supporting CSOC-WP-0015-T02.
|
||||
This is a concrete review packet, not evidence of live installation.
|
||||
|
||||
## Decision scope
|
||||
|
||||
Admit email-connect as a third reader of the existing platform SMTP mailbox,
|
||||
alongside telemetry and Authelia. Create exactly one service account, namespace
|
||||
SecretStore and ExternalSecret in email-connect. The scoped Kubernetes role reads
|
||||
the whole exact OpenBao KV record at
|
||||
`platform/data/workloads/railiance-telemetry/smtp`; ESO projects only SMTP_PASSWORD
|
||||
into the new Secret's `password` key. OpenBao does not enforce field-level access.
|
||||
The role has no default policy, a maximum 15-minute lifetime and only the exact
|
||||
service-account/namespace binding with audience openbao.
|
||||
|
||||
This does not change the email-connect deployment or sender, create a caller,
|
||||
send email, scan a mailbox, activate backup monitoring, rotate the shared password,
|
||||
or grant suppression authority. Those live obligations remain in CSOC-WP-0015.
|
||||
The user already selected platform-root, with Bernd as human operator, for replies
|
||||
and bounces. This new reader requires its own admission review.
|
||||
|
||||
The source-backed foundation proposal is:
|
||||
|
||||
`sha256:f8da6c831160e1c6354ce54f936809d2d4fb48eb45eb23484dc0bcdfe0bb52f0`
|
||||
|
||||
Reproduce it without credentials or network operations:
|
||||
|
||||
```bash
|
||||
python3 /home/worsch/railiance-platform/scripts/email_connect_smtp_foundation.py
|
||||
```
|
||||
|
||||
The digest binds the controller, all three resources including server defaults,
|
||||
cluster/namespace/application identities, fixed kubeconfig path and admission
|
||||
digest. The latter binds the CCR's functional scope, policy and seven dependency
|
||||
files. Changes to execution code or scope require a new proposal. Approval and
|
||||
native binding evidence do not change functional scope.
|
||||
|
||||
## Preconditions and approval boundary
|
||||
|
||||
CCR-2026-0056 remains **proposed**, with native binding unconfirmed. The
|
||||
[credential workflow](credential-change-approval.md) states: “Only `approved`
|
||||
requests may be applied.” Its shared-reader lane requires platform review;
|
||||
automatic single-reader approval does not apply. Its client workflow also states:
|
||||
“The **founder** resolves the State Hub decision” and “Clients do not run
|
||||
`sync-decision` on their own CCRs.” No review, approval or sync is impersonated here.
|
||||
|
||||
The completed packet is ready for that platform/consumer review and founder
|
||||
decision. After the owner workflow has made the exact CCR approved, the founder
|
||||
can run the attended command below. The controller creates and verifies the
|
||||
dedicated service account, then uses canonical confirm-binding itself before
|
||||
admitting the OpenBao role. The operator need not falsely confirm an absent SA.
|
||||
|
||||
## Attended command after approval
|
||||
|
||||
Run in the founder's terminal; this is not an agent execution instruction.
|
||||
The OpenBao relay must be available at port18200 and the fixed primary kubeconfig
|
||||
must be present. `warden plan` resolves this operation to
|
||||
openbao-platform-admin-login with one interactive OIDC/MFA login.
|
||||
|
||||
```bash
|
||||
mkdir -m 700 /home/worsch/email-connect-smtp-admission-20261031
|
||||
BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 \
|
||||
warden access openbao-platform-admin-login --exec -- \
|
||||
python3 /home/worsch/railiance-platform/scripts/email_connect_smtp_foundation.py \
|
||||
--execute \
|
||||
--expect-digest sha256:f8da6c831160e1c6354ce54f936809d2d4fb48eb45eb23484dc0bcdfe0bb52f0 \
|
||||
--receipt /home/worsch/email-connect-smtp-admission-20261031/receipt.jsonl
|
||||
```
|
||||
|
||||
The directory creation is intentionally exclusive. If it already exists, inspect
|
||||
its contents and prior receipts before proceeding; do not remove or overwrite
|
||||
evidence. Do not run the second command if the first failed. The helper separately
|
||||
requires a private caller-owned directory and a new receipt path.
|
||||
|
||||
The child must be silent because Warden rejects child output. It records a private
|
||||
mode0600 append-only JSON Lines journal, fsynced before changes. Share only the
|
||||
last complete receipt and Warden outcome; never login tokens or provider values.
|
||||
The expected final child status is `foundation_admitted_projection_ready`.
|
||||
Warden must also report successful session revocation and cleanup.
|
||||
|
||||
## What execution verifies
|
||||
|
||||
Before mutation, pinned OpenBao and Kubernetes cluster/namespace identities,
|
||||
platform-admin without root, current deployment identity/generation and absence
|
||||
of all three new objects and the target Secret are required. Objects are created
|
||||
exclusively; none is applied over an existing object. No automatic rollback or
|
||||
retry follows a partial result.
|
||||
|
||||
The new service account disables automount. Its new short-lived JWT passes native
|
||||
TokenReview for its UID, full service-account name and openbao audience. Only then
|
||||
does canonical confirm-binding run. Guarded admission preserves the shared mailbox,
|
||||
telemetry/Authelia roles and policies, and existing agent denial. It authenticates
|
||||
to SMTP using certificate-verified STARTTLS without submitting mail.
|
||||
|
||||
Actual workload OpenBao login must have only the dedicated policy and bounded TTL,
|
||||
read its exact record, and deny metadata, siblings and mutation capabilities.
|
||||
Wrong service account and wrong audience must fail. The workload token is revoked
|
||||
and lookup must fail afterward. This does not claim an independently exercised
|
||||
same-name/wrong-namespace fixture; exact namespace binding is checked in role
|
||||
metadata and the positive namespace identity is pinned.
|
||||
|
||||
Both store and projection must report Ready with exact reviewed specs; the new
|
||||
Secret must name the new ExternalSecret as controller owner. Secret checks use
|
||||
PartialObjectMetadata content negotiation through a private Unix-socket proxy,
|
||||
with no full-object fallback. Secret values are neither requested nor journaled.
|
||||
Existing application/configuration/old projection fingerprints must remain equal.
|
||||
|
||||
If execution fails, retain the journal and inspect its last phase and native state.
|
||||
There may be a created SA, admitted role or projection even if no final success
|
||||
was written. Do not rerun blindly, revoke unrelated readers, rotate the mailbox or
|
||||
roll out the application while outcome is uncertain.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
41 synthetic SMTP tests pass, including12 controller cases and15 admission cases.
|
||||
They exercise ordering, drift refusal, target collisions, binding refusal, partial
|
||||
creation, privacy/exclusive journals, interruption, native token capability and
|
||||
revocation checks, and metadata-only response enforcement/proxy cleanup.
|
||||
All three resources passed a fresh native server dry-run. A read-only native
|
||||
metadata query confirmed the target Secret absent. No credential was fetched by
|
||||
the agent and no production resource was installed.
|
||||
|
||||
Evidence: `docs/evidence/email-connect-smtp-foundation-preparation-2026-10-31.json`.
|
||||
After the attended return, verify receipt and native state, record CCR verification
|
||||
and rotation inventory, and hand the custody result to CSOC-WP-0015-T02. The sender
|
||||
rollout, actual delivered-message authentication, inbox access, authoritative return
|
||||
handling, suppression admission and retention remain separate unfinished work.
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"schema": "platform.email-connect-smtp-foundation-preparation.v1",
|
||||
"prepared_at": "2026-10-31T10:59:04.381919+00:00",
|
||||
"proposal": {
|
||||
"proposal_digest": "sha256:f8da6c831160e1c6354ce54f936809d2d4fb48eb45eb23484dc0bcdfe0bb52f0",
|
||||
"proposal": {
|
||||
"schema": "platform.email-connect-smtp-foundation-proposal.v1",
|
||||
"foundation": {
|
||||
"schema": "platform.email-connect-smtp-foundation.v1",
|
||||
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
|
||||
"namespace_uid": "5678c5d7-e4f5-4a62-8f24-1c6e1d906319",
|
||||
"deployment_uid": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72",
|
||||
"deployment_generation": 74,
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"automountServiceAccountToken": false,
|
||||
"kind": "ServiceAccount",
|
||||
"metadata": {
|
||||
"name": "email-connect-platform-smtp-eso",
|
||||
"namespace": "email-connect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "SecretStore",
|
||||
"metadata": {
|
||||
"name": "openbao-email-connect-platform-smtp",
|
||||
"namespace": "email-connect"
|
||||
},
|
||||
"spec": {
|
||||
"provider": {
|
||||
"vault": {
|
||||
"auth": {
|
||||
"kubernetes": {
|
||||
"mountPath": "kubernetes",
|
||||
"role": "email-connect-platform-smtp-eso",
|
||||
"serviceAccountRef": {
|
||||
"audiences": [
|
||||
"openbao"
|
||||
],
|
||||
"name": "email-connect-platform-smtp-eso"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path": "platform",
|
||||
"server": "http://openbao.openbao.svc:8200",
|
||||
"version": "v2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "ExternalSecret",
|
||||
"metadata": {
|
||||
"name": "email-connect-platform-smtp",
|
||||
"namespace": "email-connect"
|
||||
},
|
||||
"spec": {
|
||||
"data": [
|
||||
{
|
||||
"remoteRef": {
|
||||
"conversionStrategy": "Default",
|
||||
"decodingStrategy": "None",
|
||||
"key": "workloads/railiance-telemetry/smtp",
|
||||
"metadataPolicy": "None",
|
||||
"property": "SMTP_PASSWORD"
|
||||
},
|
||||
"secretKey": "password"
|
||||
}
|
||||
],
|
||||
"refreshInterval": "1h",
|
||||
"secretStoreRef": {
|
||||
"kind": "SecretStore",
|
||||
"name": "openbao-email-connect-platform-smtp"
|
||||
},
|
||||
"target": {
|
||||
"creationPolicy": "Owner",
|
||||
"deletionPolicy": "Retain",
|
||||
"name": "email-connect-platform-smtp",
|
||||
"template": {
|
||||
"engineVersion": "v2",
|
||||
"mergePolicy": "Replace",
|
||||
"type": "Opaque"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"admission_digest": "sha256:14a051abc35d94b9fbfbc8b47f19f9ba7bc95c779ea98fbb9083072386e72160",
|
||||
"kubeconfig": "/home/worsch/.kube/config-railiance01",
|
||||
"controller_sha256": "4f1355c0cd48368fa1ae4dcb0138296b3880359329a4ace35dee51783b0ff13f"
|
||||
},
|
||||
"ccr_status": "proposed"
|
||||
},
|
||||
"validation": {
|
||||
"smtp_tests_passed": 41,
|
||||
"smtp_tests_failed": 0,
|
||||
"server_dry_run_resources": 3,
|
||||
"native_target_secret_absent": true,
|
||||
"target_metadata_only_api": true
|
||||
},
|
||||
"test_source_sha256": "afdb7877637ced8a1d755bea1b609378b9b74603f3561762ed1dcb38ddd8d82c",
|
||||
"approved": false,
|
||||
"executed": false,
|
||||
"resources_installed": false,
|
||||
"email_sent": false,
|
||||
"secrets_read_by_agent": false
|
||||
}
|
||||
84
openbao/email-connect-platform-smtp/foundation.json
Normal file
84
openbao/email-connect-platform-smtp/foundation.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"schema": "platform.email-connect-smtp-foundation.v1",
|
||||
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
|
||||
"namespace_uid": "5678c5d7-e4f5-4a62-8f24-1c6e1d906319",
|
||||
"deployment_uid": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72",
|
||||
"deployment_generation": 74,
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"automountServiceAccountToken": false,
|
||||
"kind": "ServiceAccount",
|
||||
"metadata": {
|
||||
"name": "email-connect-platform-smtp-eso",
|
||||
"namespace": "email-connect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "SecretStore",
|
||||
"metadata": {
|
||||
"name": "openbao-email-connect-platform-smtp",
|
||||
"namespace": "email-connect"
|
||||
},
|
||||
"spec": {
|
||||
"provider": {
|
||||
"vault": {
|
||||
"auth": {
|
||||
"kubernetes": {
|
||||
"mountPath": "kubernetes",
|
||||
"role": "email-connect-platform-smtp-eso",
|
||||
"serviceAccountRef": {
|
||||
"audiences": [
|
||||
"openbao"
|
||||
],
|
||||
"name": "email-connect-platform-smtp-eso"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path": "platform",
|
||||
"server": "http://openbao.openbao.svc:8200",
|
||||
"version": "v2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "ExternalSecret",
|
||||
"metadata": {
|
||||
"name": "email-connect-platform-smtp",
|
||||
"namespace": "email-connect"
|
||||
},
|
||||
"spec": {
|
||||
"data": [
|
||||
{
|
||||
"remoteRef": {
|
||||
"conversionStrategy": "Default",
|
||||
"decodingStrategy": "None",
|
||||
"key": "workloads/railiance-telemetry/smtp",
|
||||
"metadataPolicy": "None",
|
||||
"property": "SMTP_PASSWORD"
|
||||
},
|
||||
"secretKey": "password"
|
||||
}
|
||||
],
|
||||
"refreshInterval": "1h",
|
||||
"secretStoreRef": {
|
||||
"kind": "SecretStore",
|
||||
"name": "openbao-email-connect-platform-smtp"
|
||||
},
|
||||
"target": {
|
||||
"creationPolicy": "Owner",
|
||||
"deletionPolicy": "Retain",
|
||||
"name": "email-connect-platform-smtp",
|
||||
"template": {
|
||||
"engineVersion": "v2",
|
||||
"mergePolicy": "Replace",
|
||||
"type": "Opaque"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
314
scripts/email_connect_smtp_foundation.py
Normal file
314
scripts/email_connect_smtp_foundation.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent attended CCR-2026-0056 foundation, admission and projection.
|
||||
|
||||
Default output is an offline proposal. Never changes the application sender.
|
||||
An incomplete attempt requires inspection, not an automatic retry or rollback.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import email_connect_smtp_delivery as admission
|
||||
from state_hub_preflight_lane import command, data, bao, capabilities
|
||||
|
||||
ROOT = admission.ROOT
|
||||
BUNDLE = ROOT / 'openbao/email-connect-platform-smtp/foundation.json'
|
||||
NS = 'email-connect'
|
||||
SA = admission.LANE['service_account']
|
||||
KUBECONFIG = '/home/worsch/.kube/config-railiance01'
|
||||
require = admission.require
|
||||
|
||||
|
||||
def digest(value):
|
||||
return 'sha256:' + hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
|
||||
def prepare():
|
||||
ccr, owner, owner_digest = admission.prepare()
|
||||
bundle = json.loads(BUNDLE.read_text())
|
||||
proposal = dict(schema='platform.email-connect-smtp-foundation-proposal.v1',
|
||||
foundation=bundle, admission_digest=owner_digest,
|
||||
kubeconfig=KUBECONFIG,
|
||||
controller_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
|
||||
return ccr, proposal, digest(proposal)
|
||||
|
||||
|
||||
class Native:
|
||||
def __init__(self):
|
||||
self.kube = ['kubectl', '--kubeconfig', KUBECONFIG]
|
||||
|
||||
def get(self, kind, name, namespace=NS):
|
||||
args = self.kube + ['get', kind, name, '--ignore-not-found', '-o', 'json']
|
||||
if namespace:
|
||||
args += ['-n', namespace]
|
||||
result = command(args)
|
||||
return data(result) if result.stdout.strip() else None
|
||||
|
||||
def target_metadata(self, name):
|
||||
# Kubernetes content negotiation excludes Secret values at the server.
|
||||
# Use a private local socket, one fixed GET path and no full-object fallback.
|
||||
require(name == 'email-connect-platform-smtp', 'target_secret_scope_drift')
|
||||
path = '/api/v1/namespaces/email-connect/secrets/'+name
|
||||
with tempfile.TemporaryDirectory(prefix='email-connect-metadata-') as directory:
|
||||
sockpath = str(Path(directory)/'api.sock')
|
||||
proc = subprocess.Popen(self.kube + ['proxy', '--unix-socket='+sockpath,
|
||||
'--accept-paths=^'+path+'$', '--reject-methods=^(POST|PUT|PATCH|DELETE)$'],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
conn = None
|
||||
try:
|
||||
for _ in range(50):
|
||||
if Path(sockpath).exists():
|
||||
break
|
||||
require(proc.poll() is None, 'metadata_proxy_failed')
|
||||
time.sleep(0.1)
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
try:
|
||||
sock.connect(sockpath)
|
||||
except BaseException:
|
||||
sock.close()
|
||||
raise
|
||||
conn = http.client.HTTPConnection('localhost', timeout=10)
|
||||
conn.sock = sock
|
||||
conn.request('GET', path, headers={'Accept': 'application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1'})
|
||||
response = conn.getresponse()
|
||||
if response.status == 404:
|
||||
return None
|
||||
require(response.status == 200, 'metadata_query_failed')
|
||||
value = json.load(response)
|
||||
require(value.get('kind') == 'PartialObjectMetadata' and
|
||||
value.get('apiVersion') == 'meta.k8s.io/v1' and 'data' not in value,
|
||||
'metadata_only_response_required')
|
||||
return value['metadata']
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill(); proc.wait(timeout=5)
|
||||
|
||||
def create(self, resource):
|
||||
return data(command(self.kube + ['create', '-f', '-', '-o', 'json'], payload=resource))
|
||||
|
||||
def jwt(self, service_account=SA, audience='openbao'):
|
||||
return command(self.kube + ['-n', NS, 'create', 'token', service_account,
|
||||
'--audience='+audience, '--duration=10m']).stdout.decode().strip()
|
||||
|
||||
def token_review(self, jwt):
|
||||
return data(command(self.kube + ['create', '-f', '-', '-o', 'json'], payload={
|
||||
'apiVersion': 'authentication.k8s.io/v1', 'kind': 'TokenReview',
|
||||
'spec': {'token': jwt, 'audiences': ['openbao']}}))['status']
|
||||
|
||||
def wait(self, resource):
|
||||
command(self.kube + ['-n', NS, 'wait', '--for=condition=Ready',
|
||||
resource['kind']+'/'+resource['metadata']['name'], '--timeout=45s'])
|
||||
|
||||
|
||||
def exact_resource(actual, wanted):
|
||||
require(actual is not None, 'resource_missing')
|
||||
require(actual['apiVersion'] == wanted['apiVersion'] and actual['kind'] == wanted['kind'], 'resource_drift')
|
||||
require(all(actual['metadata'].get(k) == v for k, v in wanted['metadata'].items()), 'resource_identity_drift')
|
||||
if wanted['kind'] == 'ServiceAccount':
|
||||
require(actual.get('automountServiceAccountToken') is False
|
||||
and not actual.get('secrets') and not actual.get('imagePullSecrets'), 'service_account_drift')
|
||||
else:
|
||||
require(actual['spec'] == wanted['spec'], 'resource_spec_drift')
|
||||
require(not actual['metadata'].get('deletionTimestamp'), 'resource_deleting')
|
||||
|
||||
|
||||
def protected_application(native, bundle):
|
||||
deployment = native.get('deployment', 'email-connect')
|
||||
require(deployment['metadata']['uid'] == bundle['deployment_uid']
|
||||
and deployment['metadata']['generation'] == bundle['deployment_generation'], 'application_changed')
|
||||
values = []
|
||||
for kind, name in [('deployment', 'email-connect'), ('configmap', 'email-connect-config'),
|
||||
('externalsecret', 'email-connect-runtime')]:
|
||||
obj = deployment if kind == 'deployment' else native.get(kind, name)
|
||||
values.append({'uid': obj['metadata']['uid'], 'spec': obj.get('spec'), 'data': obj.get('data')})
|
||||
return digest(values)
|
||||
|
||||
|
||||
def verify_login(native):
|
||||
def login(sa=SA, audience='openbao', negative=False):
|
||||
result = bao(['write', '-format=json', 'auth/kubernetes/login', '-'],
|
||||
payload={'role': SA, 'jwt': native.jwt(sa, audience)}, allow_failure=negative)
|
||||
if negative:
|
||||
if result.returncode == 0:
|
||||
revoke(data(result)['auth']['client_token'])
|
||||
raise admission.Refused('negative_login_succeeded')
|
||||
require(b'400' in result.stderr or b'403' in result.stderr, 'negative_login_inconclusive')
|
||||
return None
|
||||
return data(result)['auth']
|
||||
|
||||
def revoke(token):
|
||||
bao(['write', '-f', 'auth/token/revoke-self'], token=token)
|
||||
check = bao(['token', 'lookup', '-format=json'], token=token, allow_failure=True)
|
||||
require(check.returncode != 0 and b'403' in check.stderr, 'revocation_unconfirmed')
|
||||
|
||||
auth = login()
|
||||
token = auth['client_token']
|
||||
try:
|
||||
require(auth['token_policies'] == [SA] and auth.get('policies', [SA]) == [SA]
|
||||
and 0 < auth['lease_duration'] <= 900, 'effective_policy_or_ttl_drift')
|
||||
path = admission.LANE['kv_path']
|
||||
paths = [path, path.replace('/data/', '/metadata/', 1), path+'-unrelated',
|
||||
'auth/token/create', 'auth/token/lookup-self', 'auth/token/revoke-self']
|
||||
expected = [['read'], ['deny'], ['deny'], ['deny'], ['read'], ['update']]
|
||||
require(capabilities(token, paths) == dict(zip(paths, expected)), 'capability_drift')
|
||||
mailbox = data(bao(['read', '-format=json', path], token=token))['data']
|
||||
admission.lane_module().validate_entry(mailbox)
|
||||
finally:
|
||||
revoke(token)
|
||||
login('default', negative=True)
|
||||
login(audience='wrong-audience', negative=True)
|
||||
return dict(native_consumer_auth_verified=True, exact_read_verified=True,
|
||||
mutation_sibling_metadata_denied=True, bounded_ttl=True,
|
||||
other_service_account_denied=True, wrong_audience_denied=True,
|
||||
workload_token_revocation_verified=True)
|
||||
|
||||
|
||||
def run(proposal, receipt, checkpoint, native=None):
|
||||
native = native or Native()
|
||||
bundle = proposal['foundation']
|
||||
status = data(bao(['status', '-format=json']))
|
||||
require(status.get('cluster_id') == admission.OPENBAO_CLUSTER_ID and
|
||||
status.get('initialized') is True and status.get('sealed') is False, 'openbao_instance_mismatch')
|
||||
policies = data(bao(['token', 'lookup', '-format=json']))['data']['policies']
|
||||
require('platform-admin' in policies and 'root' not in policies, 'attended_platform_admin_required')
|
||||
for name, uid in [('kube-system', bundle['cluster_uid']), (NS, bundle['namespace_uid'])]:
|
||||
obj = native.get('namespace', name, namespace=None)
|
||||
require(obj and obj['metadata']['uid'] == uid and not obj['metadata'].get('deletionTimestamp'),
|
||||
'cluster_or_namespace_identity_mismatch')
|
||||
before = protected_application(native, bundle)
|
||||
resources = bundle['resources']
|
||||
require([x['kind'] for x in resources] == ['ServiceAccount', 'SecretStore', 'ExternalSecret'], 'foundation_contract_drift')
|
||||
for x in resources:
|
||||
require(native.get(x['kind'], x['metadata']['name']) is None, 'target_exists_requires_inspection')
|
||||
target = resources[-1]['spec']['target']['name']
|
||||
require(native.target_metadata(target) is None, 'target_secret_exists_requires_inspection')
|
||||
receipt.update(phase='creating_service_account', mutation_outcome='unconfirmed')
|
||||
checkpoint(receipt)
|
||||
account = native.create(resources[0])
|
||||
exact_resource(account, resources[0])
|
||||
review = native.token_review(native.jwt())
|
||||
require(review.get('authenticated') is True and review.get('audiences') == ['openbao']
|
||||
and review['user']['username'] == 'system:serviceaccount:'+NS+':'+SA
|
||||
and review['user']['uid'] == account['metadata']['uid'], 'native_binding_unproven')
|
||||
receipt.update(phase='binding_verified', service_account_uid=account['metadata']['uid'],
|
||||
native_binding_verified=True)
|
||||
checkpoint(receipt)
|
||||
validator = admission.module('email_connect_foundation_ccr', ROOT/'scripts/credential-change.py')
|
||||
validator.confirm_binding(admission.CCR, reviewer='attended-platform-operator',
|
||||
comment='CCR-2026-0056 foundation: pinned cluster and namespace, exact new service account and native audience TokenReview verified; receipt '+receipt['proposal_digest'])
|
||||
ccr, _, owner_digest = admission.prepare()
|
||||
admission.execution_guard(ccr, proposal['admission_digest'], owner_digest)
|
||||
owner_receipt = {}
|
||||
def owner_checkpoint(value):
|
||||
receipt.update(phase='owner_admission', owner_admission=dict(value))
|
||||
checkpoint(receipt)
|
||||
admission.admit(owner_receipt, owner_checkpoint)
|
||||
receipt.update(phase='native_login_verification')
|
||||
checkpoint(receipt)
|
||||
receipt.update(verify_login(native))
|
||||
for wanted in resources[1:]:
|
||||
receipt.update(phase='creating_'+wanted['kind'].lower())
|
||||
checkpoint(receipt)
|
||||
created = native.create(wanted)
|
||||
exact_resource(created, wanted)
|
||||
native.wait(wanted)
|
||||
live = native.get(wanted['kind'], wanted['metadata']['name'])
|
||||
exact_resource(live, wanted)
|
||||
require(live['metadata']['uid'] == created['metadata']['uid'] and
|
||||
any(c.get('type') == 'Ready' and c.get('status') == 'True'
|
||||
for c in live.get('status', {}).get('conditions', [])), 'projection_not_ready')
|
||||
if wanted['kind'] == 'ExternalSecret':
|
||||
require(bool(live['status'].get('refreshTime')), 'projection_refresh_unproven')
|
||||
metadata = native.target_metadata(target)
|
||||
require(metadata and any(o.get('uid') == live['metadata']['uid']
|
||||
and o.get('controller') is True for o in metadata.get('ownerReferences', [])),
|
||||
'projection_ownership_unproven')
|
||||
receipt['projection_refresh_time'] = live['status']['refreshTime']
|
||||
require(protected_application(native, bundle) == before, 'application_changed')
|
||||
receipt.update(status='foundation_admitted_projection_ready', phase='completed',
|
||||
mutation_outcome='verified', projection_ready=True, application_sender_changed=False)
|
||||
checkpoint(receipt)
|
||||
|
||||
|
||||
def execute(ccr, proposal, actual, expected, path, runner=run):
|
||||
require(actual == expected, 'reviewed_digest_required')
|
||||
require(ccr['status'] == 'approved', 'approved_ccr_required')
|
||||
info = path.parent.lstat()
|
||||
require(path.is_absolute() and path.parent.is_dir() and not path.parent.is_symlink()
|
||||
and info.st_uid == os.getuid() and not info.st_mode & 0o077, 'private_receipt_parent_required')
|
||||
fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
receipt = dict(schema='platform.email-connect-smtp-foundation.v1', status='started',
|
||||
phase='preflight', proposal_digest=actual, started_at=datetime.now(timezone.utc).isoformat(),
|
||||
mutation_outcome='not_started', email_sent=False, application_sender_changed=False)
|
||||
def checkpoint(value):
|
||||
raw = (json.dumps(value, sort_keys=True)+'\n').encode()
|
||||
while raw:
|
||||
n = os.write(fd, raw)
|
||||
if n <= 0:
|
||||
raise OSError('receipt_write_failed')
|
||||
raw = raw[n:]
|
||||
os.fsync(fd)
|
||||
try:
|
||||
checkpoint(receipt)
|
||||
parent = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(parent)
|
||||
finally:
|
||||
os.close(parent)
|
||||
runner(proposal, receipt, checkpoint)
|
||||
require(receipt['status'] == 'foundation_admitted_projection_ready', 'success_receipt_required')
|
||||
result = 0
|
||||
except Exception:
|
||||
receipt.update(status='failed_requires_inspection', reason='contained_foundation_failure')
|
||||
result = 1
|
||||
finally:
|
||||
key = 'interrupted_at' if receipt['status'] == 'started' else 'completed_at'
|
||||
receipt[key] = datetime.now(timezone.utc).isoformat()
|
||||
try:
|
||||
checkpoint(receipt)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--execute', action='store_true')
|
||||
parser.add_argument('--expect-digest')
|
||||
parser.add_argument('--receipt', type=Path)
|
||||
args = parser.parse_args()
|
||||
ccr, proposal, value = prepare()
|
||||
if not args.execute:
|
||||
print(json.dumps(dict(proposal_digest=value, proposal=proposal, ccr_status=ccr['status']), indent=2))
|
||||
return 0
|
||||
require(args.receipt is not None, 'private_receipt_required')
|
||||
return execute(ccr, proposal, value, args.expect_digest, args.receipt)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--execute' in sys.argv:
|
||||
with open(os.devnull, 'w') as sink:
|
||||
os.dup2(sink.fileno(), 1); os.dup2(sink.fileno(), 2)
|
||||
try:
|
||||
code = main()
|
||||
except Exception:
|
||||
code = 1
|
||||
sys.exit(code)
|
||||
sys.exit(main())
|
||||
262
tests/test_email_connect_smtp_foundation.py
Normal file
262
tests/test_email_connect_smtp_foundation.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"""Exercise the attended controller against synthetic Kubernetes/Bao boundaries."""
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
import email_connect_smtp_foundation as foundation
|
||||
|
||||
|
||||
class Native:
|
||||
def __init__(self, proposal):
|
||||
self.bundle = proposal['foundation']
|
||||
self.created = []
|
||||
self.objects = {}
|
||||
self.bad_cluster = False
|
||||
self.bad_binding = False
|
||||
self.target_exists = False
|
||||
self.fail_kind = None
|
||||
self.not_ready = False
|
||||
self.drift = False
|
||||
self.changed_application = False
|
||||
self.bad_ownership = False
|
||||
|
||||
def get(self, kind, name, namespace=foundation.NS):
|
||||
if kind == 'namespace':
|
||||
key = 'cluster_uid' if name == 'kube-system' else 'namespace_uid'
|
||||
return {'metadata': {'uid': 'wrong' if self.bad_cluster else self.bundle[key]}}
|
||||
if kind == 'deployment':
|
||||
return {'metadata': {'uid': self.bundle['deployment_uid'], 'generation': 74},
|
||||
'spec': {'unchanged': not (self.created and self.changed_application)}}
|
||||
if name in ('email-connect-config', 'email-connect-runtime'):
|
||||
return {'metadata': {'uid': name}, 'spec': {}, 'data': {}}
|
||||
return copy.deepcopy(self.objects.get((kind, name)))
|
||||
|
||||
def target_metadata(self, name):
|
||||
if self.target_exists:
|
||||
return {'uid': 'foreign'}
|
||||
if 'ExternalSecret' in self.created:
|
||||
return {'ownerReferences': [{'uid': 'wrong' if self.bad_ownership else 'uid-ExternalSecret', 'controller': True}]}
|
||||
return None
|
||||
|
||||
def create(self, wanted):
|
||||
kind = wanted['kind']
|
||||
self.created.append(kind)
|
||||
if self.fail_kind == kind:
|
||||
raise RuntimeError('synthetic provider secret text')
|
||||
obj = copy.deepcopy(wanted)
|
||||
obj['metadata']['uid'] = 'uid-'+kind
|
||||
obj['status'] = {'conditions': [{'type': 'Ready', 'status': 'False' if self.not_ready else 'True'}],
|
||||
'refreshTime': '2026-10-31T12:00:00Z'}
|
||||
if self.drift and kind == 'SecretStore':
|
||||
obj['spec']['provider']['vault']['path'] = 'wrong'
|
||||
self.objects[(kind, wanted['metadata']['name'])] = obj
|
||||
return copy.deepcopy(obj)
|
||||
|
||||
def jwt(self, *args):
|
||||
return 'fixture-jwt'
|
||||
|
||||
def token_review(self, jwt):
|
||||
return {'authenticated': not self.bad_binding, 'audiences': ['openbao'],
|
||||
'user': {'username': 'system:serviceaccount:email-connect:'+foundation.SA, 'uid': 'uid-ServiceAccount'}}
|
||||
|
||||
def wait(self, resource):
|
||||
pass
|
||||
|
||||
|
||||
def completed(value, code=0):
|
||||
return subprocess.CompletedProcess([], code, json.dumps(value).encode(), b'403 permission denied' if code else b'')
|
||||
|
||||
|
||||
class FoundationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ccr, self.proposal, self.digest = foundation.prepare()
|
||||
self.native = Native(self.proposal)
|
||||
self.validator = Mock()
|
||||
self.records = []
|
||||
self.owner_calls = []
|
||||
|
||||
def owner(self, args, **kwargs):
|
||||
if args[0] == 'status':
|
||||
return completed({'cluster_id': foundation.admission.OPENBAO_CLUSTER_ID, 'initialized': True, 'sealed': False})
|
||||
return completed({'data': {'policies': ['platform-admin']}})
|
||||
|
||||
def run_controller(self):
|
||||
approved = copy.deepcopy(self.ccr)
|
||||
approved['status'] = 'approved'
|
||||
approved['openbao']['auth']['bound_claims_confirmed'] = True
|
||||
def admit(row, checkpoint):
|
||||
self.owner_calls.append('admit')
|
||||
row.update(status='reader_admitted_verified'); checkpoint(row)
|
||||
receipt = {'proposal_digest': self.digest, 'status': 'started'}
|
||||
with patch.object(foundation, 'bao', side_effect=self.owner), \
|
||||
patch.object(foundation.admission, 'module', return_value=self.validator), \
|
||||
patch.object(foundation.admission, 'prepare', return_value=(approved, {}, self.proposal['admission_digest'])), \
|
||||
patch.object(foundation.admission, 'admit', side_effect=admit), \
|
||||
patch.object(foundation, 'verify_login', return_value={'native_consumer_auth_verified': True}):
|
||||
foundation.run(self.proposal, receipt, lambda r: self.records.append(copy.deepcopy(r)), self.native)
|
||||
return receipt
|
||||
|
||||
def test_offline_proposal_and_live_default_refusal(self):
|
||||
self.assertEqual(self.ccr['status'], 'proposed')
|
||||
self.assertEqual(len(self.proposal['foundation']['resources']), 3)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'receipt'
|
||||
with self.assertRaises(foundation.admission.Refused):
|
||||
foundation.execute(self.ccr, self.proposal, self.digest, self.digest, path)
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
def test_complete_controller_requires_binding_before_admission_and_ready_after(self):
|
||||
result = self.run_controller()
|
||||
self.assertEqual(self.native.created, ['ServiceAccount', 'SecretStore', 'ExternalSecret'])
|
||||
self.validator.confirm_binding.assert_called_once()
|
||||
self.assertEqual(result['status'], 'foundation_admitted_projection_ready')
|
||||
self.assertFalse(result['application_sender_changed'])
|
||||
phases = [r['phase'] for r in self.records]
|
||||
self.assertLess(phases.index('binding_verified'), phases.index('owner_admission'))
|
||||
self.assertLess(phases.index('native_login_verification'), phases.index('creating_secretstore'))
|
||||
|
||||
def test_wrong_cluster_and_existing_targets_precede_mutation(self):
|
||||
for mode in ('cluster', 'target', 'existing'):
|
||||
with self.subTest(mode=mode):
|
||||
self.native = Native(self.proposal)
|
||||
self.native.bad_cluster = mode == 'cluster'
|
||||
self.native.target_exists = mode == 'target'
|
||||
if mode == 'existing':
|
||||
self.native.objects[('SecretStore', 'openbao-email-connect-platform-smtp')] = {'foreign': True}
|
||||
with self.assertRaises(foundation.admission.Refused):
|
||||
self.run_controller()
|
||||
self.assertFalse(self.native.created)
|
||||
self.assertFalse(self.owner_calls)
|
||||
|
||||
def test_bad_tokenreview_never_confirms_or_admits(self):
|
||||
self.native.bad_binding = True
|
||||
with self.assertRaises(foundation.admission.Refused): self.run_controller()
|
||||
self.assertEqual(self.native.created, ['ServiceAccount'])
|
||||
self.validator.confirm_binding.assert_not_called()
|
||||
self.assertFalse(self.owner_calls)
|
||||
|
||||
def test_projection_drift_not_ready_foreign_owner_and_application_changes_refuse(self):
|
||||
for field in ('drift', 'not_ready', 'bad_ownership', 'changed_application'):
|
||||
with self.subTest(field=field):
|
||||
self.native = Native(self.proposal); setattr(self.native, field, True)
|
||||
with self.assertRaises(foundation.admission.Refused): self.run_controller()
|
||||
self.assertFalse(any(r.get('status') == 'foundation_admitted_projection_ready' for r in self.records))
|
||||
|
||||
def test_creation_failure_keeps_checkpoint_and_does_not_rollback_or_retry(self):
|
||||
self.native.fail_kind = 'SecretStore'
|
||||
with self.assertRaises(RuntimeError): self.run_controller()
|
||||
self.assertEqual(self.native.created, ['ServiceAccount', 'SecretStore'])
|
||||
self.assertEqual(self.records[-1]['phase'], 'creating_secretstore')
|
||||
self.assertEqual(self.records[-1]['mutation_outcome'], 'unconfirmed')
|
||||
|
||||
def test_journal_failure_silent_private_and_exclusive(self):
|
||||
self.ccr['status'] = 'approved'
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'receipt'
|
||||
def fail(proposal, receipt, checkpoint):
|
||||
receipt.update(phase='creating_service_account', mutation_outcome='unconfirmed')
|
||||
checkpoint(receipt)
|
||||
raise RuntimeError('synthetic private provider text')
|
||||
self.assertEqual(foundation.execute(self.ccr, self.proposal, self.digest, self.digest, path, fail), 1)
|
||||
raw = path.read_bytes()
|
||||
rows = [json.loads(line) for line in raw.splitlines()]
|
||||
self.assertEqual(rows[-1]['status'], 'failed_requires_inspection')
|
||||
self.assertEqual(rows[-1]['phase'], 'creating_service_account')
|
||||
self.assertNotIn(b'private provider', raw)
|
||||
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||
with self.assertRaises(FileExistsError):
|
||||
foundation.execute(self.ccr, self.proposal, self.digest, self.digest, path, fail)
|
||||
self.assertEqual(path.read_bytes(), raw)
|
||||
|
||||
def test_interruption_cannot_be_success(self):
|
||||
self.ccr['status'] = 'approved'
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'receipt'
|
||||
def interrupt(*args): raise SystemExit(9)
|
||||
with self.assertRaises(SystemExit):
|
||||
foundation.execute(self.ccr, self.proposal, self.digest, self.digest, path, interrupt)
|
||||
last = json.loads(path.read_text().splitlines()[-1])
|
||||
self.assertEqual(last['status'], 'started')
|
||||
self.assertIn('interrupted_at', last)
|
||||
|
||||
def test_native_login_scope_denials_and_all_tokens_revoked(self):
|
||||
calls = []
|
||||
def bao(args, **kw):
|
||||
calls.append((args, kw))
|
||||
if args[:2] == ['write', '-format=json']:
|
||||
# The fake JWT identifies the requested service account/audience.
|
||||
if kw['payload']['jwt'] != foundation.SA+':openbao': return completed({}, 1)
|
||||
return completed({'auth': {'client_token': 'fixture', 'token_policies': [foundation.SA], 'lease_duration': 900}})
|
||||
if args[0] == 'read':
|
||||
return completed({'data': {'data': dict(foundation.admission.lane_module().FIELDS, SMTP_PASSWORD='fixture'), 'metadata': {'version': 2}}})
|
||||
if args[0] == 'write': return completed({})
|
||||
return completed({}, 1)
|
||||
def caps(token, paths):
|
||||
return dict(zip(paths, [['read'], ['deny'], ['deny'], ['deny'], ['read'], ['update']]))
|
||||
native = Mock()
|
||||
native.jwt.side_effect = lambda sa=foundation.SA, audience='openbao': sa+':'+audience
|
||||
with patch.object(foundation, 'bao', side_effect=bao), patch.object(foundation, 'capabilities', side_effect=caps):
|
||||
result = foundation.verify_login(native)
|
||||
self.assertTrue(result['other_service_account_denied'])
|
||||
self.assertTrue(result['workload_token_revocation_verified'])
|
||||
self.assertEqual(sum(a == ['write', '-f', 'auth/token/revoke-self'] for a, _ in calls), 1)
|
||||
|
||||
def test_wrong_digest_and_cli_refusal_are_before_receipt_or_owner_calls(self):
|
||||
self.ccr['status'] = 'approved'
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'receipt'
|
||||
with self.assertRaises(foundation.admission.Refused):
|
||||
foundation.execute(self.ccr, self.proposal, self.digest, 'wrong', path)
|
||||
result = subprocess.run([sys.executable, str(Path(foundation.__file__)), '--execute',
|
||||
'--expect-digest', 'wrong', '--receipt', str(path)], capture_output=True, timeout=10)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual((result.stdout, result.stderr), (b'', b''))
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
def test_metadata_query_negotiates_partial_object_and_cleans_proxy(self):
|
||||
for status, payload, refuses in [
|
||||
(404, {}, False),
|
||||
(200, {'kind': 'PartialObjectMetadata', 'apiVersion': 'meta.k8s.io/v1', 'metadata': {'uid': 'fixture'}}, False),
|
||||
(200, {'kind': 'Secret', 'apiVersion': 'v1', 'data': {'fixture': 'must-not-be-accepted'}}, True),
|
||||
(403, {}, True),
|
||||
]:
|
||||
with self.subTest(status=status, refuses=refuses):
|
||||
response = io.BytesIO(json.dumps(payload).encode()); response.status = status
|
||||
process = Mock(); process.poll.return_value = None
|
||||
conn = Mock(); conn.getresponse.return_value = response
|
||||
with patch.object(foundation.subprocess, 'Popen', return_value=process), \
|
||||
patch.object(foundation.socket, 'socket'), \
|
||||
patch.object(foundation.http.client, 'HTTPConnection', return_value=conn), \
|
||||
patch.object(foundation.time, 'sleep'):
|
||||
if refuses:
|
||||
with self.assertRaises(foundation.admission.Refused):
|
||||
foundation.Native().target_metadata('email-connect-platform-smtp')
|
||||
else:
|
||||
result = foundation.Native().target_metadata('email-connect-platform-smtp')
|
||||
self.assertEqual(result, None if status == 404 else {'uid': 'fixture'})
|
||||
self.assertEqual(conn.request.call_args.kwargs['headers']['Accept'],
|
||||
'application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1')
|
||||
process.terminate.assert_called_once(); process.wait.assert_called_once()
|
||||
conn.close.assert_called_once()
|
||||
|
||||
def test_native_scope_failure_still_revokes_workload_token(self):
|
||||
calls = []
|
||||
def bao(args, **kwargs):
|
||||
calls.append(args)
|
||||
if args[:2] == ['write', '-format=json']:
|
||||
return completed({'auth': {'client_token': 'fixture', 'token_policies': ['too-broad'], 'lease_duration': 900}})
|
||||
if args[:2] == ['write', '-f']: return completed({})
|
||||
return completed({}, 1)
|
||||
with patch.object(foundation, 'bao', side_effect=bao):
|
||||
with self.assertRaises(foundation.admission.Refused): foundation.verify_login(Mock())
|
||||
self.assertIn(['write', '-f', 'auth/token/revoke-self'], calls)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
|
|
@ -39,7 +39,7 @@ Evidence: docs/evidence/email-connect-smtp-admission-preparation-2026-10-31.json
|
|||
|
||||
```task
|
||||
id: RPF-WP-0068-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "de0ec1f9-cc11-593f-88c5-5a3972969f01"
|
||||
```
|
||||
|
|
@ -54,6 +54,16 @@ readback after admission without printing values or reading Kubernetes Secrets.
|
|||
Use coulomb-social/docs/release/platform-mail-sender-2026-10-31/candidate.json.
|
||||
Do not deploy the application sender here or reuse an existing consumer's grant.
|
||||
|
||||
Preparation completed 2026-10-31: email_connect_smtp_foundation.py binds the
|
||||
server-defaulted three-object bundle and pinned primary/namespace/application
|
||||
identities. It performs native TokenReview before canonical binding confirmation,
|
||||
guarded owner admission, positive/negative workload login with token revocation,
|
||||
exclusive projection creation and Ready/ownership checks. PartialObjectMetadata
|
||||
queries avoid Secret values; fresh native query confirms target absent. All41
|
||||
SMTP tests pass (12 foundation cases). No live creation/admission executed.
|
||||
Native execution results belong to T03/T04, not this preparation completion.
|
||||
Exact packet: docs/email-connect-smtp-foundation-review-2026-10-31.md.
|
||||
|
||||
## T03 — Obtain exact admission and execute through the attended owner lane
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue