Skip to content
Merged
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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.011"
VERSION = "0.261.012"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
121 changes: 119 additions & 2 deletions application/single_app/functions_workspace_identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@
ACTION_IDENTITY_TABLEAU_AUTH_TYPES = {"api_key", "username_password"}
ACTION_IDENTITY_YAMCS_TYPES = {"yamcs"}
ACTION_IDENTITY_YAMCS_AUTH_TYPES = {"api_key", "bearer_token", "username_password"}
# Yamcs actions may sit behind a reverse proxy that enforces HTTP Basic authentication.
# That credential is separate from the Yamcs credential, so it gets its own identity

Check warning on line 77 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
# reference and is always a username/password pair.

Check warning on line 78 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 78 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
ACTION_PROXY_IDENTITY_FIELD = "basic_auth_identity_id"
ACTION_PROXY_IDENTITY_AUTH_TYPES = {"username_password"}

Check warning on line 80 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 80 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
ACTION_PROXY_IDENTITY_TYPES = {"yamcs"}


def _now_iso() -> str:
Expand Down Expand Up @@ -438,7 +444,13 @@
scope_type: str,
scope_id: str,
) -> Optional[Dict[str, Any]]:
"""Validate that an action references an action-capable identity in its own scope."""
"""Validate that an action references action-capable identities in its own scope.

Check warning on line 447 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Both the primary credential reference and the optional reverse-proxy credential

Check warning on line 449 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
reference are checked so callers do not need to know which ones an action uses.
"""
validate_action_proxy_identity_reference(action_data, scope_type, scope_id)

Check warning on line 452 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

identity_id = get_action_identity_reference_id(action_data)
if not identity_id:
return None
Expand All @@ -460,6 +472,50 @@
return identity


def get_action_proxy_identity_reference_id(action_data: Dict[str, Any]) -> str:
"""Return the reverse-proxy credential identity reference on an action, if present."""

Check warning on line 476 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
if not isinstance(action_data, dict):
return ""

additional_fields = action_data.get("additionalFields")
if not isinstance(additional_fields, dict):
return ""

return _normalize_text(additional_fields.get(ACTION_PROXY_IDENTITY_FIELD), 255)


def validate_action_proxy_identity_reference(

Check warning on line 487 in application/single_app/functions_workspace_identities.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
action_data: Dict[str, Any],
scope_type: str,
scope_id: str,
) -> Optional[Dict[str, Any]]:
"""Validate an action's reverse-proxy credential identity reference."""
identity_id = get_action_proxy_identity_reference_id(action_data)
if not identity_id:
return None

plugin_type = _normalize_text((action_data or {}).get("type"), 80).lower()
if plugin_type not in ACTION_PROXY_IDENTITY_TYPES:
raise ValueError("This action type does not support a proxy credential identity")

scope_type = _validate_scope(scope_type)
if scope_type == WORKSPACE_IDENTITY_SCOPE_PUBLIC:
raise ValueError("Public workspace identities cannot be used by actions")

identity = get_workspace_identity(scope_type, scope_id, identity_id)
if not identity_supports_usage(
identity,
"action",
source_type="action",
auth_types=ACTION_PROXY_IDENTITY_AUTH_TYPES,
):
raise ValueError(
"Selected workspace identity is not a username/password identity configured for action use"
)

return identity


def _get_action_identity_auth_types_for_plugin(action_data: Dict[str, Any]) -> Set[str]:
plugin_type = _normalize_text((action_data or {}).get("type"), 80).lower()
if plugin_type in ACTION_IDENTITY_SQL_TYPES:
Expand All @@ -483,10 +539,35 @@
scope_id: str,
return_type: SecretReturnType = SecretReturnType.TRIGGER,
) -> Dict[str, Any]:
"""Apply a referenced workspace identity to an action manifest for UI or runtime use."""
"""Apply referenced workspace identities to an action manifest for UI or runtime use.

Handles the primary credential reference and the optional reverse-proxy credential
reference independently, so an action may use either, both, or neither.
"""
if not isinstance(action_data, dict):
return action_data

hydrated_action = _hydrate_primary_action_identity(
action_data,
scope_type,
scope_id,
return_type,
)
return _hydrate_proxy_action_identity(
hydrated_action,
scope_type,
scope_id,
return_type,
)


def _hydrate_primary_action_identity(
action_data: Dict[str, Any],
scope_type: str,
scope_id: str,
return_type: SecretReturnType,
) -> Dict[str, Any]:
"""Apply the primary referenced workspace identity to an action manifest."""
identity_id = get_action_identity_reference_id(action_data)
if not identity_id:
return action_data
Expand All @@ -512,6 +593,42 @@
return _apply_action_identity_auth(hydrated_action, resolved_auth)


def _hydrate_proxy_action_identity(
action_data: Dict[str, Any],
scope_type: str,
scope_id: str,
return_type: SecretReturnType,
) -> Dict[str, Any]:
"""Apply a referenced reverse-proxy credential identity to an action manifest."""
identity_id = get_action_proxy_identity_reference_id(action_data)
if not identity_id:
return action_data

identity = validate_action_proxy_identity_reference(action_data, scope_type, scope_id)
hydrated_action = dict(action_data)
additional_fields = dict(hydrated_action.get("additionalFields") or {})
additional_fields[ACTION_PROXY_IDENTITY_FIELD] = identity_id
additional_fields["basic_auth_identity_auth_type"] = _normalize_text(
(identity.get("auth") or {}).get("auth_type"), 50
).lower()

if return_type == SecretReturnType.TRIGGER:
# The username is safe to echo back so the modal can show which account is used;
# the password stays out of any UI-bound payload.
additional_fields["basic_auth_username"] = _normalize_text(
(identity.get("auth") or {}).get("username"), 255
)
additional_fields["basic_auth_password"] = ""
hydrated_action["additionalFields"] = additional_fields
return hydrated_action

resolved_auth = get_workspace_identity_auth(scope_type, scope_id, identity_id)
additional_fields["basic_auth_username"] = str(resolved_auth.get("username") or "")
additional_fields["basic_auth_password"] = str(resolved_auth.get("password") or "")
hydrated_action["additionalFields"] = additional_fields
return hydrated_action


def _apply_action_identity_auth(action_data: Dict[str, Any], identity_auth: Dict[str, Any]) -> Dict[str, Any]:
"""Return a transient action manifest with identity credentials resolved for runtime use."""
action = dict(action_data)
Expand Down
64 changes: 63 additions & 1 deletion application/single_app/functions_yamcs_operations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# functions_yamcs_operations.py
"""Shared defaults and normalization helpers for Yamcs mission control action plugins."""

import base64
import re
from typing import Any, Dict, Optional

Expand All @@ -22,6 +23,28 @@
}
YAMCS_SUPPORTED_AUTH_TYPES = {"NoAuth", "key", "identity", "username_password"}

# Some ground segments front Yamcs with a reverse proxy (commonly Apache) that enforces
# HTTP Basic authentication before the request ever reaches Yamcs. That challenge is a
# separate layer from the Yamcs auth method, so it is configured independently.
YAMCS_BASIC_AUTH_ENABLED_FIELD = "enable_basic_auth"
YAMCS_BASIC_AUTH_USERNAME_FIELD = "basic_auth_username"
YAMCS_BASIC_AUTH_PASSWORD_FIELD = "basic_auth_password"
YAMCS_BASIC_AUTH_IDENTITY_FIELD = "basic_auth_identity_id"
YAMCS_BASIC_AUTH_IDENTITY_AUTH_TYPE_FIELD = "basic_auth_identity_auth_type"

# Proxy Basic auth occupies the Authorization header. Yamcs username/password and bearer
# token auth also send Authorization, so those cannot be combined. API key auth travels in
# the separate x-api-key header and unauthenticated Yamcs sends nothing, so both are safe.
YAMCS_BASIC_AUTH_COMPATIBLE_AUTH_METHODS = {
YAMCS_AUTH_METHOD_NONE,
YAMCS_AUTH_METHOD_API_KEY,
}
YAMCS_BASIC_AUTH_CONFLICT_MESSAGE = (
"Yamcs HTTP Basic authentication cannot be combined with username/password or bearer "
"token authentication because both send the HTTP Authorization header. Use 'No "
"Authentication' or 'API Key' for the Yamcs authentication method."
)

# Yamcs archive SQL is a full engine that also supports DDL/DML. Only these leading
# keywords are accepted, and only when archive SQL is explicitly enabled.
YAMCS_ALLOWED_READ_STATEMENTS = {
Expand All @@ -32,10 +55,12 @@
}

# Secrets always live in auth.key, but the constant keeps redaction plumbing symmetric
# with the other connector action types.
# with the other connector action types. The proxy Basic auth password is stored in
# additionalFields, so listing it here routes it through the same Key Vault handling.
YAMCS_SENSITIVE_ADDITIONAL_FIELDS = {
"api_key",
"access_token",
"basic_auth_password",
"password",
"token",
}
Expand Down Expand Up @@ -141,6 +166,7 @@ def normalize_yamcs_additional_fields(
# with the other connector action types and is always forced on.
fields["read_only"] = True
fields["enable_archive_sql"] = _as_bool(fields.get("enable_archive_sql"), default=False)
fields.update(normalize_yamcs_basic_auth_fields(fields))
fields["max_rows"] = _as_int(
fields.get("max_rows"),
YAMCS_DEFAULT_MAX_ROWS,
Expand All @@ -160,3 +186,39 @@ def normalize_yamcs_additional_fields(
YAMCS_MAX_BYTE_LIMIT,
)
return fields


def normalize_yamcs_basic_auth_fields(
additional_fields: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Normalize the reverse-proxy HTTP Basic authentication fields.

Stored values are preserved when the toggle is off so turning it back on does not
orphan the Key Vault secret that backs the password.
"""
fields = additional_fields if isinstance(additional_fields, dict) else {}
return {
YAMCS_BASIC_AUTH_ENABLED_FIELD: _as_bool(
fields.get(YAMCS_BASIC_AUTH_ENABLED_FIELD), default=False
),
YAMCS_BASIC_AUTH_USERNAME_FIELD: str(
fields.get(YAMCS_BASIC_AUTH_USERNAME_FIELD) or ""
).strip(),
YAMCS_BASIC_AUTH_PASSWORD_FIELD: str(fields.get(YAMCS_BASIC_AUTH_PASSWORD_FIELD) or ""),
YAMCS_BASIC_AUTH_IDENTITY_FIELD: str(
fields.get(YAMCS_BASIC_AUTH_IDENTITY_FIELD) or ""
).strip(),
}


def yamcs_basic_auth_conflicts_with_auth_method(auth_method: Any) -> bool:
"""Return True when proxy Basic auth cannot coexist with the Yamcs auth method."""
normalized_method = str(auth_method or "").strip().lower()
return normalized_method not in YAMCS_BASIC_AUTH_COMPATIBLE_AUTH_METHODS


def build_yamcs_basic_auth_header(username: Any, password: Any) -> str:
"""Build an HTTP Basic ``Authorization`` header value for the Yamcs reverse proxy."""
credential = f"{str(username or '')}:{str(password or '')}"
encoded = base64.b64encode(credential.encode("utf-8")).decode("ascii")
return f"Basic {encoded}"
Loading
Loading