Skip to content

Commit eb89dee

Browse files
authored
Improve GCP creds validation (dstackai#2322)
1 parent df5c7df commit eb89dee

3 files changed

Lines changed: 27 additions & 26 deletions

File tree

src/dstack/_internal/core/backends/gcp/auth.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import json
22
from typing import Optional, Tuple
33

4+
import google.api_core.exceptions
45
import google.auth
6+
import google.cloud.compute_v1 as compute_v1
57
from google.auth.credentials import Credentials
68
from google.auth.exceptions import DefaultCredentialsError
7-
from google.cloud import storage
89
from google.oauth2 import service_account
910

1011
from dstack._internal.core.errors import BackendAuthError
@@ -16,13 +17,16 @@
1617
from dstack._internal.core.models.common import is_core_model_instance
1718

1819

19-
def authenticate(creds: AnyGCPCreds) -> Tuple[Credentials, Optional[str]]:
20-
"""
21-
:raises BackendAuthError:
22-
:return: GCP credentials and project_id
23-
"""
24-
credentials, project_id = get_credentials(creds)
25-
validate_credentials(credentials)
20+
def authenticate(creds: AnyGCPCreds, project_id: Optional[str] = None) -> Tuple[Credentials, str]:
21+
credentials, credentials_project_id = get_credentials(creds)
22+
if project_id is None:
23+
# If project_id is not specified explicitly, try using credentials' project_id.
24+
# Explicit project_id takes precedence bacause credentials' project_id may be irrelevant.
25+
# For example, with Workload Identity Federation for GKE, it's cluster project_id.
26+
project_id = credentials_project_id
27+
if project_id is None:
28+
raise BackendAuthError("Credentials require project_id to be specified")
29+
validate_credentials(credentials, project_id)
2630
return credentials, project_id
2731

2832

@@ -40,17 +44,19 @@ def get_credentials(creds: AnyGCPCreds) -> Tuple[Credentials, Optional[str]]:
4044
try:
4145
default_credentials, project_id = google.auth.default()
4246
except DefaultCredentialsError:
43-
raise BackendAuthError()
47+
raise BackendAuthError("Failed to find default credentials")
4448

4549
return default_credentials, project_id
4650

4751

48-
def validate_credentials(credentials: Credentials):
52+
def validate_credentials(credentials: Credentials, project_id: str):
4953
try:
50-
storage_client = storage.Client(credentials=credentials)
51-
storage_client.list_buckets(max_results=1)
54+
regions_client = compute_v1.RegionsClient(credentials=credentials)
55+
regions_client.list(project=project_id)
56+
except google.api_core.exceptions.NotFound:
57+
raise BackendAuthError(f"project_id {project_id} not found")
5258
except Exception:
53-
raise BackendAuthError()
59+
raise BackendAuthError("Insufficient permissions")
5460

5561

5662
def default_creds_available() -> bool:

src/dstack/_internal/core/backends/gcp/compute.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class GCPCompute(Compute):
7070
def __init__(self, config: GCPConfig):
7171
super().__init__()
7272
self.config = config
73-
self.credentials, self.project_id = auth.authenticate(config.creds)
73+
self.credentials, _ = auth.authenticate(config.creds, self.config.project_id)
7474
self.instances_client = compute_v1.InstancesClient(credentials=self.credentials)
7575
self.firewalls_client = compute_v1.FirewallsClient(credentials=self.credentials)
7676
self.regions_client = compute_v1.RegionsClient(credentials=self.credentials)

src/dstack/_internal/server/services/backends/configurators/gcp.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,6 @@ def get_default_configs(self) -> List[GCPConfigInfoWithCreds]:
127127
_, project_id = auth.authenticate(GCPDefaultCreds())
128128
except BackendAuthError:
129129
return []
130-
131-
if project_id is None:
132-
return []
133-
134130
return [
135131
GCPConfigInfoWithCreds(
136132
project_id=project_id,
@@ -152,16 +148,15 @@ def get_config_values(self, config: GCPConfigInfoWithCredsPartial) -> GCPConfigV
152148
):
153149
raise_invalid_credentials_error(fields=[["creds"]])
154150
try:
155-
credentials, _ = auth.authenticate(creds=config.creds)
156-
# We ignore credentials' project_id since it may be irrelevant.
157-
# For example, with Workload Identity Federation for GKE, it's cluster project_id.
158-
# config.project_id is not validated directly since it would require org-level permissions.
159-
# config.project_id is validated indirectly when checking VPC.
160-
except BackendAuthError:
151+
credentials, _ = auth.authenticate(creds=config.creds, project_id=config.project_id)
152+
except BackendAuthError as e:
153+
details = None
154+
if len(e.args) > 0:
155+
details = e.args[0]
161156
if is_core_model_instance(config.creds, GCPServiceAccountCreds):
162-
raise_invalid_credentials_error(fields=[["creds", "data"]])
157+
raise_invalid_credentials_error(fields=[["creds", "data"]], details=details)
163158
else:
164-
raise_invalid_credentials_error(fields=[["creds"]])
159+
raise_invalid_credentials_error(fields=[["creds"]], details=details)
165160
config_values.regions = self._get_regions_element(
166161
selected=config.regions or DEFAULT_REGIONS
167162
)

0 commit comments

Comments
 (0)