Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions dag/extdeps/bmc/http.dag
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ service redfish.Http {
}
}

operation GetSystemWithNetrcStdin {
input { bmc_host: NonEmptyStr, netrc_content: Secret }
output { body: String from "stdout", success: Bool from "exit_success" }
readonly
transport shell {
argv: ["curl", "--fail-with-body", "-sS", "-k", "--netrc-file", "/dev/stdin", "https://{bmc_host}/redfish/v1/Systems/system"]
stdin: netrc_content
}
exit {
0 => Unit
nonzero => String "redfish system GET (netrc stdin) failed"
}
}

operation GetManagerWithNetrc {
input { bmc_host: NonEmptyStr, manager_id: NonEmptyStr, netrc_file: NonEmptyStr }
output { body: String from "stdout", success: Bool from "exit_success" }
readonly
transport shell {
argv: ["curl", "--fail-with-body", "-sS", "-k", "--netrc-file", "{netrc_file}", "https://{bmc_host}/redfish/v1/Managers/{manager_id}"]
}
exit {
0 => Unit
nonzero => String "redfish manager GET (netrc) failed"
}
}

operation GetChassisSensors {
input { bmc_host: NonEmptyStr, chassis_id: NonEmptyStr, netrc_file: NonEmptyStr }
output { body: String from "stdout", success: Bool from "exit_success" }
Expand All @@ -75,6 +102,32 @@ service redfish.Http {
}
}

operation GetChassisThermalWithNetrc {
input { bmc_host: NonEmptyStr, chassis_id: NonEmptyStr, netrc_file: NonEmptyStr }
output { body: String from "stdout", success: Bool from "exit_success" }
readonly
transport shell {
argv: ["curl", "--fail-with-body", "-sS", "-k", "--netrc-file", "{netrc_file}", "https://{bmc_host}/redfish/v1/Chassis/{chassis_id}/Thermal"]
}
exit {
0 => Unit
nonzero => String "redfish chassis legacy thermal GET (netrc) failed"
}
}

operation GetChassisThermalSubsystemWithNetrc {
input { bmc_host: NonEmptyStr, chassis_id: NonEmptyStr, netrc_file: NonEmptyStr }
output { body: String from "stdout", success: Bool from "exit_success" }
readonly
transport shell {
argv: ["curl", "--fail-with-body", "-sS", "-k", "--netrc-file", "{netrc_file}", "https://{bmc_host}/redfish/v1/Chassis/{chassis_id}/ThermalSubsystem"]
}
exit {
0 => Unit
nonzero => String "redfish chassis thermal subsystem GET (netrc) failed"
}
}

operation GetPower {
input { bmc_host: NonEmptyStr, username: NonEmptyStr, password: Secret }
output { body: String from "stdout", success: Bool from "exit_success" }
Expand Down Expand Up @@ -137,3 +190,5 @@ service redfish.Http {
}
}
}

data redfish_stdin_credential_transport_note: String = "GetSystemWithNetrcStdin sends netrc content through inherited stdin and keeps the Secret out of argv and the environment by construction. New credential-probe consumers use this operation. The older Basic-Auth argv operations remain compatibility debt for callers outside this stack and must not be selected by BMC convergence."
32 changes: 32 additions & 0 deletions dag/extdeps/bmc/mock_corpus.dag
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,41 @@ data redfish_http_mock_case_get_system_with_netrc: PublishedMockCase = Published
case_id: 0
}

data redfish_http_mock_case_get_system_with_netrc_stdin: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetSystemWithNetrcStdin",
service: "redfish.Http",
operation: "GetSystemWithNetrcStdin",
case_id: 0
}

data redfish_http_mock_case_get_manager_with_netrc: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetManagerWithNetrc",
service: "redfish.Http",
operation: "GetManagerWithNetrc",
case_id: 0
}

data redfish_http_mock_case_get_chassis_sensors: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetChassisSensors",
service: "redfish.Http",
operation: "GetChassisSensors",
case_id: 0
}

data redfish_http_mock_case_get_chassis_thermal_with_netrc: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetChassisThermalWithNetrc",
service: "redfish.Http",
operation: "GetChassisThermalWithNetrc",
case_id: 0
}

data redfish_http_mock_case_get_chassis_thermal_subsystem_with_netrc: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetChassisThermalSubsystemWithNetrc",
service: "redfish.Http",
operation: "GetChassisThermalSubsystemWithNetrc",
case_id: 0
}

data redfish_http_mock_case_get_power: PublishedMockCase = PublishedMockCase {
operation_key: "redfish.Http.GetPower",
service: "redfish.Http",
Expand Down Expand Up @@ -61,7 +89,11 @@ data bmc_ipmi_tool_mock_case_sel_list: PublishedMockCase = PublishedMockCase {
data bmc_published_mock_corpus: List<PublishedMockCase> = [
redfish_http_mock_case_get_system,
redfish_http_mock_case_get_system_with_netrc,
redfish_http_mock_case_get_system_with_netrc_stdin,
redfish_http_mock_case_get_manager_with_netrc,
redfish_http_mock_case_get_chassis_sensors,
redfish_http_mock_case_get_chassis_thermal_with_netrc,
redfish_http_mock_case_get_chassis_thermal_subsystem_with_netrc,
redfish_http_mock_case_get_power,
redfish_http_mock_case_get_thermal,
bmc_ipmi_tool_mock_case_sensor_list,
Expand Down
5 changes: 4 additions & 1 deletion dag/extdeps/cloud/gcp/gcp.dag
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ service oauth2.Google {
service shell.GCloud {
operation AuthPrintAccessToken {
input {}
output { access_token: Secret from "stdout" }
output {
access_token: Secret from "stdout"
success: Bool from "exit_success"
}
readonly
transport shell { argv: ["gcloud", "auth", "print-access-token"] }
exit {
Expand Down
27 changes: 25 additions & 2 deletions dag/extdeps/cloud/gcp/secret_manager.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
module extdeps.cloud.gcp.secret_manager

import std.bytes { octets_bytes }
import std.encoding { Standard, base64_decode }
import std.types { Bytes, List, NonEmptyStr, Secret, String }
import extdeps.cloud.gcp.gcp { GcpProjectId }
import extdeps.cloud.gcp.errors { GcpErrorShape }
import extdeps.cloud.gcp.iam { GcpBinding, GcpPolicy }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri {
Expand Down Expand Up @@ -49,6 +55,23 @@ fn utf8_secret_from_access_payload(payload: Bytes) -> Secret {
utf8_decode_bytes(payload: payload) as Secret
}

type SecretAccessPayloadDecode
= SecretAccessPayloadDecoded { secret: Secret }
| SecretAccessPayloadDecodeRefused { reason: NonEmptyStr }

fn decode_secret_access_data_b64(data_b64: Secret) -> SecretAccessPayloadDecode {
match base64_decode(s: data_b64 as String, variant: Standard) {
Absent =>
SecretAccessPayloadDecodeRefused {
reason: "Secret Manager payload.data was not valid standard base64",
}
Present { value: octets } =>
SecretAccessPayloadDecoded {
secret: utf8_decode_bytes(payload: octets_bytes(o: octets)) as Secret,
}
}
}

service gcp.SecretManager {
config {
endpoint: "https://secretmanager.googleapis.com"
Expand All @@ -69,7 +92,7 @@ service gcp.SecretManager {
}
output {
payload: Bytes
data_b64: String from "payload/data"
data_b64: Secret from "payload/data"
name: String
}
readonly
Expand Down Expand Up @@ -104,7 +127,7 @@ service gcp.SecretManager {
}

operation AddVersion {
input { access_token: Secret, secret_name: String, payload_b64: String }
input { access_token: Secret, secret_name: String, payload_b64: Secret }
output { name: String }

idempotent
Expand Down
8 changes: 8 additions & 0 deletions dag/extdeps/diagnostic_mock_corpus.dag
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import extdeps.linux.mock_corpus { linux_edac_rasmcctl_mock_case_correctable_cou
import extdeps.bmc.mock_corpus {
redfish_http_mock_case_get_system,
redfish_http_mock_case_get_system_with_netrc,
redfish_http_mock_case_get_system_with_netrc_stdin,
redfish_http_mock_case_get_manager_with_netrc,
redfish_http_mock_case_get_chassis_sensors,
redfish_http_mock_case_get_chassis_thermal_with_netrc,
redfish_http_mock_case_get_chassis_thermal_subsystem_with_netrc,
redfish_http_mock_case_get_power,
redfish_http_mock_case_get_thermal,
bmc_ipmi_tool_mock_case_sensor_list,
Expand All @@ -26,7 +30,11 @@ data diagnostic_published_mock_corpus: List<PublishedMockCase> = [
ssh_session_mock_case_reachability,
redfish_http_mock_case_get_system,
redfish_http_mock_case_get_system_with_netrc,
redfish_http_mock_case_get_system_with_netrc_stdin,
redfish_http_mock_case_get_manager_with_netrc,
redfish_http_mock_case_get_chassis_sensors,
redfish_http_mock_case_get_chassis_thermal_with_netrc,
redfish_http_mock_case_get_chassis_thermal_subsystem_with_netrc,
redfish_http_mock_case_get_power,
redfish_http_mock_case_get_thermal,
linux_edac_rasmcctl_mock_case_correctable_counts,
Expand Down
87 changes: 83 additions & 4 deletions dag/gunbc/auth/access_token_source.dag
Original file line number Diff line number Diff line change
@@ -1,19 +1,98 @@
module gunbc.auth.access_token_source

import std.types { Secret }
import std.types { Secret, NonEmptyStr, Bool, String }
import std.resources { Network }
import std.upsert_decision {
Absent,
Apply,
Converged,
Noop,
Refuse,
UpsertClassification,
}
import extdeps.shell
import extdeps.cloud.gcp.gcp

type AccessTokenSource
= GcloudPrintToken
| OperatorSuppliedToken { token: Secret }

func resolve_access_token(source: AccessTokenSource) -> Secret
type AccessEnsurePlan
= HumanGcloudLogin {
authenticate_command: NonEmptyStr
retry_instruction: NonEmptyStr
}

type AccessTokenResolution
= AccessTokenReady { token: Secret }
| AccessTokenUpsertRequired { plan: AccessEnsurePlan }
| AccessTokenEnsureRefused { reason: NonEmptyStr }

data gcloud_human_authenticate_command: NonEmptyStr = "gcloud auth login"

data gcloud_human_auth_retry_instruction: NonEmptyStr = "complete gcloud authentication with an account authorized for the fleet secrets project, then re-run the same command"

data gcloud_human_auth_pause_note: String = "Human gcloud authentication is the current REALIZER of an access ENSURE upsert, never a fleet phase or unconditional preflight. A selected operation first has to require a Secret Manager read/write; that effect requires an access token; only then does GcloudPrintToken OBSERVE the access capability. A successful print classifies Converged/Noop and continues without exposing the token. A missing or expired session classifies Absent/Apply(HumanGcloudLogin), and re-driving after the upsert resumes the same operation. A path that never demands GCP access never evaluates this ensure and never asks the human to authenticate. OperatorSuppliedToken remains an explicit compatibility handler, but srv3/srv4 entrypoints default to gcloud rather than requiring a pasted token. A future workload-identity/OAuth handler can realize the same ensure without changing BMC convergence."

fn gcloud_access_ensure_classification(
success: Bool,
) -> UpsertClassification<AccessEnsurePlan> {
if success {
UpsertClassification {
verdict: Converged,
decision: Noop,
}
} else {
UpsertClassification {
verdict: Absent,
decision: Apply {
plan: HumanGcloudLogin {
authenticate_command: gcloud_human_authenticate_command,
retry_instruction: gcloud_human_auth_retry_instruction,
},
},
}
}
}

fn gcloud_access_token_resolution(success: Bool, token: Secret) -> AccessTokenResolution {
match gcloud_access_ensure_classification(success: success) {
UpsertClassification { verdict: _, decision: Noop } =>
AccessTokenReady { token: token }
UpsertClassification { verdict: _, decision: Apply { plan: plan } } =>
AccessTokenUpsertRequired { plan: plan }
UpsertClassification { verdict: _, decision: Refuse { reason: reason } } =>
AccessTokenEnsureRefused { reason: reason }
}
}

fn access_token_upsert_required_reason(plan: AccessEnsurePlan) -> String {
match plan {
HumanGcloudLogin {
authenticate_command: authenticate_command,
retry_instruction: retry_instruction,
} =>
concat(
"access ensure requires an upsert by the human gcloud handler; run `",
concat(
authenticate_command as String,
concat("`, then ", retry_instruction as String),
),
)
}
}

func ensure_access_token(source: AccessTokenSource) -> AccessTokenResolution
uses net: Network
{
match source {
GcloudPrintToken => shell.GCloud.AuthPrintAccessToken().access_token
OperatorSuppliedToken { token: t } => t
GcloudPrintToken => {
let printed = shell.GCloud.AuthPrintAccessToken()
gcloud_access_token_resolution(
success: printed.success,
token: printed.access_token,
)
}
OperatorSuppliedToken { token: t } => AccessTokenReady { token: t }
}
}
2 changes: 1 addition & 1 deletion dag/gunbc/auth/materialized_secret.dag
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,4 @@ data materialized_secret_binding_realization_scaffold: Disposition = Scaffold {
}
}

data materialized_secret_adc_scaffold_note: String = "A1 lands on top of a declared scaffold and says so: authenticating to Secret Manager at all currently depends on an access token the operator pastes by hand roughly hourly, because no modeled workflow identity can reach the store. That is A2 (reach-secrets ADC), already a DeclaredGap in host_standup P0:reach-secrets-network and in fleet_ssh_access. This lane does not smuggle a fix for it — the fetch leg simply inherits whatever credential is live — and A1's dependence on it is recorded here so that the day A2 lands, the thing to delete is a named row rather than an assumption nobody wrote down."
data materialized_secret_adc_scaffold_note: String = "A1 lands on top of a declared scaffold and says so: Secret Manager access currently uses gunbc.auth.access_token_source's demand-driven access ensure. The selected secret operation invokes `gcloud auth print-access-token` as its observation; a live session is Converged/Noop, while an absent or expired session derives Apply(HumanGcloudLogin), and re-driving resumes the same operation. The human interaction is therefore the current upsert realizer, not a prerequisite phase and never an operator-pasted token. A keyless OAuth/workload-identity realizer is still A2 (reach-secrets ADC), already a typed PhaseGap in host_standup P0:reach-secrets-network and in fleet_ssh_access. A1 inherits that handler at the fetch edge, so A2 can replace it without changing the materialization lifecycle."
Loading