Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c19a5d66f2 |
@@ -398,6 +398,65 @@ def _validate_change_request(payload):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _onboard_consumer(payload):
|
||||||
|
"""P18 (REQ-182): accept a self-service onboarding request.
|
||||||
|
|
||||||
|
Validates the payload against schemas/onboarding.schema.json, then
|
||||||
|
writes a 'pending' row to nova-contracts (D-119). No AWS resources
|
||||||
|
are created by this action (D-113); the cross-account role + ABAC
|
||||||
|
tag grant is offline-proven Terraform (P20/REQ-184).
|
||||||
|
"""
|
||||||
|
import jsonschema
|
||||||
|
schema_path = os.path.join(os.path.dirname(os.path.dirname(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
"schemas", "onboarding.schema.json")
|
||||||
|
try:
|
||||||
|
with open(schema_path) as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
# Strip the Lambda dispatch envelope (action) before validating
|
||||||
|
# against the onboarding schema (the schema is about the request,
|
||||||
|
# not the Lambda wrapper).
|
||||||
|
onboarding_payload = {k: v for k, v in payload.items() if k != "action"}
|
||||||
|
jsonschema.validate(instance=onboarding_payload, schema=schema)
|
||||||
|
except OSError:
|
||||||
|
raise ValueError("onboarding schema unavailable")
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
raise ValueError(f"onboarding payload invalid: {e.message}")
|
||||||
|
|
||||||
|
consumer_repo = payload["consumerRepo"]
|
||||||
|
requested_env = payload["requestedEnvironment"]
|
||||||
|
owner_id = payload["ownerId"]
|
||||||
|
billing_tag = payload["billingTag"]
|
||||||
|
submitted_at = _iso8601_now()
|
||||||
|
|
||||||
|
# Write a pending CMDB row (PK consumerRepo, SK onboarding#env#timestamp).
|
||||||
|
table = _get_dynamodb().Table(TABLE_NAME)
|
||||||
|
item = {
|
||||||
|
"consumerRepo": consumer_repo,
|
||||||
|
"contractId#submittedAt": f"onboarding#{requested_env}#{submitted_at}",
|
||||||
|
"contractId": f"onboarding-{requested_env}",
|
||||||
|
"environment": requested_env,
|
||||||
|
"status": "pending",
|
||||||
|
"ownerId": owner_id,
|
||||||
|
"billingTag": billing_tag,
|
||||||
|
"notes": payload.get("notes", ""),
|
||||||
|
"submittedAt": submitted_at,
|
||||||
|
}
|
||||||
|
table.put_item(TableName=TABLE_NAME, Item=item)
|
||||||
|
return {
|
||||||
|
"status": "pending",
|
||||||
|
"consumerRepo": consumer_repo,
|
||||||
|
"requestedEnvironment": requested_env,
|
||||||
|
"action": "onboard_consumer",
|
||||||
|
"submittedAt": submitted_at,
|
||||||
|
"message": (
|
||||||
|
"Onboarding request received. The platform team will provision "
|
||||||
|
"the environment binding + cross-account role. Track the status "
|
||||||
|
"via the nova-contracts table (status=pending → granted)."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
"""AWS Lambda handler entry point.
|
"""AWS Lambda handler entry point.
|
||||||
|
|
||||||
@@ -426,6 +485,8 @@ def lambda_handler(event, context):
|
|||||||
result = _report_error(payload)
|
result = _report_error(payload)
|
||||||
elif action == "validate_change_request":
|
elif action == "validate_change_request":
|
||||||
result = _validate_change_request(payload)
|
result = _validate_change_request(payload)
|
||||||
|
elif action == "onboard_consumer":
|
||||||
|
result = _onboard_consumer(payload)
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"statusCode": 400,
|
"statusCode": 400,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nova.cloudinit.dev/schemas/onboarding.schema.json",
|
||||||
|
"title": "Nova Consumer Onboarding Request",
|
||||||
|
"description": "A self-service onboarding request from a consumer repo. Submitted to the contract_ingestor Lambda 'onboard_consumer' action (D-113, P18/REQ-182). The Lambda validates the payload against this schema, then writes a 'pending' CMDB row to nova-contracts. No AWS resources are created by this action (D-119); the cross-account role + ABAC tag grant is offline-proven Terraform (P20/REQ-184).",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["consumerRepo", "requestedEnvironment", "ownerId", "billingTag"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"consumerRepo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The consumer repository in org/repo format.",
|
||||||
|
"pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$",
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"requestedEnvironment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The environment the consumer requests (must exist as a core/environments/<name>.json).",
|
||||||
|
"enum": ["dev", "qa", "prod", "dr"]
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The owning team or individual (for ABAC nova:owner tag + CMDB).",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 64
|
||||||
|
},
|
||||||
|
"billingTag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The cost-center / billing tag for the consumer's resources.",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 64
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional free-form notes for the platform team.",
|
||||||
|
"maxLength": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -593,4 +593,52 @@ class TestV14IdentityValidation:
|
|||||||
"""The _validate_caller_identity docstring documents the ABAC reliance."""
|
"""The _validate_caller_identity docstring documents the ABAC reliance."""
|
||||||
docstring = ingestor._validate_caller_identity.__doc__
|
docstring = ingestor._validate_caller_identity.__doc__
|
||||||
assert "ABAC" in docstring
|
assert "ABAC" in docstring
|
||||||
assert "PrincipalTag" in docstring
|
assert "PrincipalTag" in docstring
|
||||||
|
|
||||||
|
class TestOnboardConsumer:
|
||||||
|
"""P18 (REQ-182): the onboard_consumer action writes a pending CMDB row."""
|
||||||
|
|
||||||
|
_ARN = "arn:aws:sts::000:assumed-role/nova-deploy/test"
|
||||||
|
|
||||||
|
def test_valid_onboarding_writes_pending_row(self, moto_contracts_table):
|
||||||
|
payload = {
|
||||||
|
"action": "onboard_consumer",
|
||||||
|
"consumerRepo": "acdl/consumer-b",
|
||||||
|
"requestedEnvironment": "dev",
|
||||||
|
"ownerId": "team-b",
|
||||||
|
"billingTag": "cost-center-b",
|
||||||
|
}
|
||||||
|
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||||
|
resp = ingestor.lambda_handler(event, None)
|
||||||
|
assert resp["statusCode"] == 200
|
||||||
|
body = json.loads(resp["body"])
|
||||||
|
assert body["status"] == "pending"
|
||||||
|
assert body["action"] == "onboard_consumer"
|
||||||
|
assert body["requestedEnvironment"] == "dev"
|
||||||
|
|
||||||
|
def test_invalid_onboarding_rejected(self, moto_contracts_table):
|
||||||
|
# An invalid consumerRepo (no /) fails the identity format check
|
||||||
|
# (which runs for all actions) before the onboarding schema.
|
||||||
|
payload = {
|
||||||
|
"action": "onboard_consumer",
|
||||||
|
"consumerRepo": "not-a-repo-format",
|
||||||
|
"requestedEnvironment": "dev",
|
||||||
|
"ownerId": "team-b",
|
||||||
|
"billingTag": "cost-center-b",
|
||||||
|
}
|
||||||
|
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||||
|
resp = ingestor.lambda_handler(event, None)
|
||||||
|
assert resp["statusCode"] == 400
|
||||||
|
assert "invalid consumerRepo" in json.loads(resp["body"])["error"]
|
||||||
|
|
||||||
|
def test_missing_onboarding_field_rejected(self, moto_contracts_table):
|
||||||
|
payload = {
|
||||||
|
"action": "onboard_consumer",
|
||||||
|
"consumerRepo": "acdl/consumer-b",
|
||||||
|
"requestedEnvironment": "dev",
|
||||||
|
# ownerId + billingTag missing
|
||||||
|
}
|
||||||
|
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||||
|
resp = ingestor.lambda_handler(event, None)
|
||||||
|
assert resp["statusCode"] == 400
|
||||||
|
assert "onboarding payload invalid" in json.loads(resp["body"])["error"]
|
||||||
|
|||||||
Reference in New Issue
Block a user