-
Notifications
You must be signed in to change notification settings - Fork 1
Redact bnk_config from the instance-wide global cluster list #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8d065ba
0f8d98f
74a70c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -347,30 +347,43 @@ def create_cluster(self, project_id: int, cluster_data) -> dict[str, Any]: | |
| } | ||
|
|
||
| def list_all_clusters(self) -> dict[str, Any]: | ||
| """List all Kubernetes clusters (global).""" | ||
| """List all Kubernetes clusters (global). | ||
|
|
||
| This endpoint is instance-wide (require_viewer, not project-scoped), so | ||
| it must not expose the ADR-424 bnk_config -- host/DPU membership, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is accurate about this endpoint, but the conclusion it implies — that
Worth either narrowing this docstring's claim or fixing the project route's authorization — see the review body. |
||
| control-plane host, tmfifo pool CIDR -- cross-project to any viewer | ||
| (#116). bnk_config is redacted here; the project-scoped list and the | ||
| per-cluster detail keep it. No frontend consumer of this global list | ||
| reads bnk_config (only the project-scoped K8sClusterList does), so this | ||
| removes the disclosure without losing a feature -- and it also drops the | ||
| now-unnecessary membership bulk-fetch those fields required. | ||
| """ | ||
| from sqlalchemy.orm import selectinload | ||
|
|
||
| from routes.k8s._shared import serialize_cluster | ||
| from services.bnk_cluster_service import BnkClusterService | ||
|
|
||
| clusters = ( | ||
| self.db.query(KubernetesCluster) | ||
| .options(selectinload(KubernetesCluster.bnk_config)) | ||
| .all() | ||
| ) | ||
| # Bulk-fetch membership for all BNK clusters in 2 queries (not 2N). | ||
| # selectinload(bnk_config) already avoids the config N+1; this bulk | ||
| # call eliminates the host+DPU membership N+1 in _serialize_bnk_config. | ||
| bnk_ids = [c.id for c in clusters if getattr(c, "bnk_config", None)] | ||
| membership_map = BnkClusterService(self.db).bulk_cluster_membership(bnk_ids) | ||
| result = [ | ||
| serialize_cluster(c, membership=membership_map.get(c.id)) | ||
| for c in clusters | ||
| ] | ||
| result = [serialize_cluster(c, include_bnk_config=False) for c in clusters] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is what breaks |
||
| return {"clusters": result, "count": len(result)} | ||
|
|
||
| def list_project_clusters(self, project_id: int) -> dict[str, Any]: | ||
| """List all Kubernetes clusters for a project.""" | ||
| """List all Kubernetes clusters for a project. | ||
|
|
||
| NOTE (#116): this list still renders bnk_config, and its route is | ||
| require_viewer (any authenticated user) with NO membership/ownership | ||
| check -- project_id is a path param anyone may supply. Combined with the | ||
| global list (which still returns each cluster's project_id), any viewer | ||
| can read any project's bnk_config in two requests. Redacting bnk_config | ||
| on the global list (this change) is a strict improvement but does NOT | ||
| fully close #116: the project list must enforce per-project membership | ||
| first, and that is a pre-existing tenancy-model decision (require_viewer | ||
| is role-based across the app) larger than this change. Until that lands, | ||
| bnk_config here is readable by any authenticated user. | ||
| """ | ||
| from sqlalchemy.orm import selectinload | ||
|
|
||
| from routes.k8s._shared import serialize_cluster | ||
|
|
@@ -392,7 +405,17 @@ def list_project_clusters(self, project_id: int) -> dict[str, Any]: | |
| return {"clusters": result, "count": len(result)} | ||
|
|
||
| def get_cluster_details(self, cluster_id: int) -> dict[str, Any]: | ||
| """Get cluster details.""" | ||
| """Get cluster details. | ||
|
|
||
| NOTE (#116): GET /k8s/clusters/{cluster_id} is require_viewer with NO | ||
| project scope (unlike the PUT/DELETE on the same path, which use | ||
| require_cluster_owner). This handler hand-builds its dict and must NOT | ||
| gain a bnk_config key -- reusing serialize_cluster here (or adding | ||
| bnk_config by hand) would reintroduce the cross-project disclosure #116 | ||
| closes, one request further along, and the id needed comes straight from | ||
| the global list. If bnk_config is ever needed on detail, scope this route | ||
| to the project first. | ||
| """ | ||
| cluster = self._get_cluster(cluster_id) | ||
| context = PlatformContextService.serialize_cluster_context(cluster) | ||
| return { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth noting here that
get_cluster_detailsdoesn't route through this function at all — it hand-builds its dict and has never carriedbnk_config, so the PR description's "the per-cluster detail keeps it" isn't accurate.More usefully:
GET /k8s/clusters/{cluster_id}isDepends(require_viewer)with no project scoping, while PUT/DELETE on that path userequire_cluster_owner. If that handler is ever refactored to reuseserialize_cluster, it inheritsinclude_bnk_config=Trueand reopens #116 — with the cluster ids available from the very list this PR is redacting. A sentence in this docstring saying so would stop that.