diff --git a/catalog/surfaces.json b/catalog/surfaces.json index 898531e..59ff1c0 100644 --- a/catalog/surfaces.json +++ b/catalog/surfaces.json @@ -574,6 +574,7 @@ "enable": { "risk": "write", "required_selectors": ["zone", "name"], + "allowed_lanes": ["global"], "public_example": "cfctl apply sender_domain enable --zone example.com --name example.com --plan" } } diff --git a/commands/cfctl.sh b/commands/cfctl.sh index 25f9de8..809ea18 100644 --- a/commands/cfctl.sh +++ b/commands/cfctl.sh @@ -706,6 +706,26 @@ cfctl_recommended_lane_from_comparison() { jq -r '.summary.allowed_lanes[0] // empty' <<< "${comparison_json}" } +cfctl_recommended_lane_from_policy_or_comparison() { + local comparison_json="$1" + local policy_json="$2" + local recommended_lane + + recommended_lane="$(cfctl_recommended_lane_from_comparison "${comparison_json}")" + if [[ -n "${recommended_lane}" ]]; then + printf '%s\n' "${recommended_lane}" + return + fi + + jq -r ' + if ((.allowed_lanes // []) | length) == 1 then + .allowed_lanes[0] + else + "" + end + ' <<< "${policy_json}" +} + cfctl_select_requested_lane_if_available() { local requested_lane="${CF_TOKEN_LANE:-${CF_TOKEN_LANE_DEFAULT}}" @@ -2070,7 +2090,7 @@ cfctl_handle_classify() { selector_requirements_json="$(cfctl_requirement_check_json "${surface}" "${target_action}" "${operation}")" comparison_json="$(cfctl_compare_permission_all_lanes "${surface}" "${target_action}" "${operation}")" permission_json="$(jq -c --arg lane "${CF_ACTIVE_TOKEN_LANE:-}" '(.lanes | map(select(.lane == $lane)) | .[0].permission) // {state:"unknown", basis:"lane_unavailable", errors: [], request: null, status_code: null, permission_family: "Cloudflare API"}' <<< "${comparison_json}")" - recommended_lane="$(cfctl_recommended_lane_from_comparison "${comparison_json}")" + recommended_lane="$(cfctl_recommended_lane_from_policy_or_comparison "${comparison_json}" "${policy_json}")" public_example="$(jq -r '.public_example // empty' <<< "${policy_json}")" if [[ -z "${public_example}" || "${public_example}" == "null" ]]; then if [[ "${target_action}" == "apply" ]]; then @@ -2146,7 +2166,7 @@ cfctl_handle_classify() { "true" \ "${permission_json}" \ '{"state":"not_applicable"}' \ - "$(jq '{risk: .policy.risk, preview_required: .policy.preview_required, confirmation: .policy.confirmation, lock_strategy: .policy.lock_strategy, secret_policy: .policy.secret_policy, allowed_lanes: (.lane_comparison.summary.allowed_lanes // []), selector_ready: .selector_readiness.ready, recommended_lane: .lane_hint.recommended_lane, likely_failure_class: .likely_failure_class}' <<< "${result_json}")" \ + "$(jq '{risk: .policy.risk, preview_required: .policy.preview_required, confirmation: .policy.confirmation, lock_strategy: .policy.lock_strategy, secret_policy: .policy.secret_policy, allowed_lanes: (if ((.lane_comparison.summary.allowed_lanes // []) | length) > 0 then (.lane_comparison.summary.allowed_lanes // []) else (.policy.allowed_lanes // []) end), selector_ready: .selector_readiness.ready, recommended_lane: .lane_hint.recommended_lane, likely_failure_class: .likely_failure_class}' <<< "${result_json}")" \ "${result_json}" \ "" \ "" \ @@ -2361,11 +2381,11 @@ cfctl_handle_guide() { fi lane_comparison="$(cfctl_compare_permission_all_lanes "${surface}" "apply" "${requested_operation}")" current_lane="${CF_ACTIVE_TOKEN_LANE:-}" - recommended_lane="$(jq -r '.summary.allowed_lanes[0] // empty' <<< "${lane_comparison}")" + policy_json="$(cfctl_operation_policy_json "${surface}" "apply" "${requested_operation}")" + recommended_lane="$(cfctl_recommended_lane_from_policy_or_comparison "${lane_comparison}" "${policy_json}")" if [[ -n "${recommended_lane}" && "${recommended_lane}" != "${current_lane}" ]]; then command_prefix="CF_TOKEN_LANE=${recommended_lane} " fi - policy_json="$(cfctl_operation_policy_json "${surface}" "apply" "${requested_operation}")" preview_command="${command_prefix}$(cfctl_build_apply_command "${surface}" "${requested_operation}" " --plan")" apply_command="${command_prefix}$(cfctl_build_apply_command "${surface}" "${requested_operation}" " --ack-plan ")" diff --git a/docs/capabilities.md b/docs/capabilities.md index 7ce9f1d..c038bf3 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -79,7 +79,7 @@ This matrix is derived from the same catalogs used by `cfctl explain`, `cfctl cl | `security.txt` | `delete` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: zone | | `security.txt` | `upsert` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone | | `security.txt` | `sync` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | state match: zone | -| `sender_domain` | `enable` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: zone, name | +| `sender_domain` | `enable` | `write` | yes | `apply` | yes | `-` | `global` | required: zone, name | | `tunnel` | `cleanup-connections` | `destructive` | yes | `lease` | yes | `delete` | `dev`, `global` | required: id | | `tunnel` | `configure` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | required: id | | `tunnel` | `create` | `write` | yes | `apply` | yes | `-` | `dev`, `global` | - | diff --git a/scripts/verify_static_contract.sh b/scripts/verify_static_contract.sh index 84ac6d3..08d035f 100755 --- a/scripts/verify_static_contract.sh +++ b/scripts/verify_static_contract.sh @@ -290,6 +290,26 @@ jq -e ' and (.recommended_command | contains("--plan")) ' <<< "${email_routing_zone_guidance_json}" >/dev/null || die "email routing zone resolution lane guidance assertion failed" +sender_domain_guide_json="$( + env \ + -u CF_DEV_TOKEN \ + -u CF_GLOBAL_TOKEN \ + -u CLOUDFLARE_API_TOKEN \ + -u CLOUDFLARE_ACCOUNT_ID \ + CF_SHARED_ENV_FILE="/nonexistent/cfctl-empty-env" \ + CF_REPO_ENV_FILE="/nonexistent/cfctl-empty-env" \ + "${ROOT_DIR}/cfctl" guide sender_domain enable --zone example.com --name example.com +)" +jq -e ' + .ok == true + and .surface == "sender_domain" + and .operation == "enable" + and .result.lane_hint.recommended_lane == "global" + and (.result.commands.preview | startswith("CF_TOKEN_LANE=global cfctl apply sender_domain enable ")) + and (.result.commands.apply | startswith("CF_TOKEN_LANE=global cfctl apply sender_domain enable ")) + and (.result.commands.verify | startswith("CF_TOKEN_LANE=global cfctl verify sender_domain ")) +' <<< "${sender_domain_guide_json}" >/dev/null || die "sender-domain guide global lane assertion failed" + assert_jq_file "permission profile minimality policy" ' .profiles.read.allowed_surfaces != null and (.profiles.read.allowed_surfaces | index("audit.log")) != null @@ -731,6 +751,7 @@ assert_jq_file "surface module bindings" ' and .surfaces["sender_domain"].actions.apply.preview_required == true and .surfaces["sender_domain"].actions.apply.verification_required == true and .surfaces["sender_domain"].actions.apply.operations.enable.required_selectors == ["zone", "name"] + and .surfaces["sender_domain"].actions.apply.operations.enable.allowed_lanes == ["global"] and .surfaces["worker.route"].module == "worker_route" and .surfaces["worker.route"].standards_ref == "worker.route" and (.surfaces["worker.route"].docs_topics | index("workers-routes")) != null