Skip to content

Commit b11cc95

Browse files
committed
Implement workload_identity for USER object type
1 parent 97d1c44 commit b11cc95

File tree

11 files changed

+135
-2
lines changed

11 files changed

+135
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Changelog
22

3+
## [0.58.0] - 2025-10-06
4+
5+
- Introduced `workload_identity` authenticator and relevant CLI options (thanks to @jbylina).
6+
- Introduced `workload_identity` parameter for `USER` object type and `--refresh-workload-identity` CLI option.
7+
- Made `object_type` and `object_name` mandatory for `SNAPSHOT_SET`.
8+
39
## [0.57.1] - 2025-09-19
410

5-
- Fix typo in `SHARE` object type definition (thanks to @rex911).
11+
- Fixed typo in `SHARE` object type definition (thanks to @rex911).
612
- Update AbstractRoleResolver to not transfer ownership of notebooks and shares (thanks to @rex911).
713

814
## [0.57.0] - 2025-09-16

snowddl/app/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ def init_arguments_parser(self):
251251
parser.add_argument(
252252
"--refresh-user-passwords", help="Additionally refresh passwords of users", default=False, action="store_true"
253253
)
254+
parser.add_argument(
255+
"--refresh-workload-identity", help="Additionally refresh workload identites of users", default=False, action="store_true"
256+
)
254257
parser.add_argument(
255258
"--refresh-future-grants",
256259
help="Additionally refresh missing grants for existing objects derived from future grants",
@@ -504,6 +507,9 @@ def init_settings(self):
504507
if self.args.get("refresh_user_passwords"):
505508
settings.refresh_user_passwords = True
506509

510+
if self.args.get("refresh_workload_identity"):
511+
settings.refresh_workload_identity = True
512+
507513
if self.args.get("refresh_future_grants"):
508514
settings.refresh_future_grants = True
509515

snowddl/blueprint/blueprint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ class UserBlueprint(AbstractBlueprint):
477477
default_warehouse: Optional[AccountObjectIdent] = None
478478
default_namespace: Optional[Union[DatabaseIdent, SchemaIdent]] = None
479479
session_params: Dict[str, Union[bool, float, int, str]] = {}
480+
workload_identity: Optional[Dict[str, Union[bool, float, int, str, list]]] = None
480481

481482

482483
class ViewBlueprint(SchemaObjectBlueprint, DependsOnMixin):

snowddl/parser/user.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
"network_policy": {
8080
"type": "string",
8181
},
82+
"workload_identity": {
83+
"type": "object",
84+
"additionalProperties": {
85+
"type": ["array", "boolean", "number", "string"]
86+
}
87+
},
8288
},
8389
"additionalProperties": False
8490
}
@@ -132,6 +138,7 @@ def process_user(self, user_name, user_params, default_wh_map):
132138
default_warehouse=AccountObjectIdent(self.env_prefix, default_warehouse) if default_warehouse else None,
133139
default_namespace=build_default_namespace_ident(self.env_prefix, user_params.get("default_namespace")) if user_params.get("default_namespace") else None,
134140
session_params=self.normalise_params_dict(user_params.get("session_params", {})),
141+
workload_identity=self.normalise_params_dict(user_params.get("workload_identity")),
135142
business_roles=business_roles,
136143
comment=user_params.get("comment"),
137144
)

snowddl/resolver/user.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def get_existing_objects(self):
3737
"has_password": r["has_password"] == "true",
3838
"has_rsa_public_key": r["has_rsa_public_key"] == "true",
3939
"has_mfa": r["has_mfa"] == "true",
40+
"has_pat": r["has_pat"] == "true",
41+
"has_workload_identity": r["has_workload_identity"] == "true",
4042
"comment": r["comment"] if r["comment"] else None,
4143
}
4244

@@ -96,6 +98,12 @@ def create_object(self, bp: UserBlueprint):
9698
if bp.type:
9799
query.append_nl("TYPE = {type}", {"type": bp.type})
98100

101+
# Workload identity
102+
if bp.workload_identity:
103+
query.append_nl("WORKLOAD_IDENTITY = (")
104+
query.append(self._build_workload_identity_parameters(bp))
105+
query.append_nl(")")
106+
99107
# Object and session parameters
100108
query.append(self._build_common_parameters(bp))
101109

@@ -123,9 +131,15 @@ def compare_object(self, bp: UserBlueprint, row: dict):
123131
if self._compare_public_keys(bp, row):
124132
result = ResolveResult.ALTER
125133

134+
if self._compare_workload_identity_pre_type(bp, row):
135+
result = ResolveResult.ALTER
136+
126137
if self._compare_type(bp, row):
127138
result = ResolveResult.ALTER
128139

140+
if self._compare_workload_identity_post_type(bp, row):
141+
result = ResolveResult.ALTER
142+
129143
if self._compare_parameters(bp):
130144
result = ResolveResult.ALTER
131145

@@ -158,6 +172,23 @@ def _build_common_parameters(self, bp: UserBlueprint):
158172

159173
return query
160174

175+
def _build_workload_identity_parameters(self, bp: UserBlueprint):
176+
query = self.engine.query_builder()
177+
178+
for param_name, param_value in bp.workload_identity.items():
179+
query.append_nl(
180+
" {param_name:r} = {param_value:dp}",
181+
{
182+
"param_name": param_name,
183+
# ISSUER + SUBJECT is the unique key in Snowflake
184+
# SnowDDL has to append env_prefix in order to prevent duplicate key error
185+
# Feel free to adjust this logic for your own custom test environment
186+
"param_value": f"{param_value}:{self.config.env_prefix.rstrip('_$')}" if param_name == "SUBJECT" else param_value,
187+
},
188+
)
189+
190+
return query
191+
161192
def _compare_properties(self, bp: UserBlueprint, row: dict):
162193
query = self.engine.query_builder()
163194

@@ -336,6 +367,39 @@ def _compare_parameters(self, bp: UserBlueprint):
336367

337368
return False
338369

370+
def _compare_workload_identity_pre_type(self, bp: UserBlueprint, row: dict):
371+
if not bp.workload_identity and row["has_workload_identity"]:
372+
self.engine.execute_safe_ddl(
373+
"ALTER USER {name:i} UNSET WORKLOAD_IDENTITY",
374+
{
375+
"name": bp.full_name,
376+
},
377+
)
378+
379+
return True
380+
381+
return False
382+
383+
def _compare_workload_identity_post_type(self, bp: UserBlueprint, row: dict):
384+
if bp.workload_identity and (not row["has_workload_identity"] or self.engine.settings.refresh_workload_identity):
385+
query = self.engine.query_builder()
386+
387+
query.append(
388+
"ALTER USER {name:i} SET WORKLOAD_IDENTITY = (",
389+
{
390+
"name": bp.full_name,
391+
}
392+
)
393+
394+
query.append(self._build_workload_identity_parameters(bp))
395+
query.append_nl(")")
396+
397+
self.engine.execute_safe_ddl(query)
398+
399+
return True
400+
401+
return False
402+
339403
def _check_user_role_grant(self, bp: UserBlueprint):
340404
user_role = self._get_user_role_ident(bp)
341405

snowddl/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class SnowDDLSettings(BaseModelWithConfig):
2020
execute_resource_monitor: bool = False
2121
execute_outbound_share: bool = False
2222
refresh_user_passwords: bool = False
23+
refresh_workload_identity: bool = False
2324
refresh_future_grants: bool = False
2425
refresh_stage_encryption: bool = False
2526
refresh_secrets: bool = False

snowddl/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.57.1"
1+
__version__ = "0.58.0"

test/_config/step1/user.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ us002_us1:
3737
type: person
3838
password: YQYAtgqkbS5h
3939

40+
us003_us1:
41+
disabled: true
42+
type: service
43+
workload_identity:
44+
type: OIDC
45+
issuer: https://token.actions.githubusercontent.com
46+
subject: repo:littleK0i/SnowDDL:ref:refs/heads/master
47+
4048
np001_us1:
4149
disabled: true
4250
network_policy: np001_np1

test/_config/step2/user.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ us002_us1:
1818
type: legacy_service
1919
password: YQYAtgqkbS5h
2020

21+
us003_us1:
22+
disabled: true
23+
type: person
24+
2125
np001_us1:
2226
disabled: true
2327
network_policy: np001_np2

test/_config/step3/user.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ us002_us1:
1010
d4A3+Wp/pkTiYUh2GvjHTZrGViZXBPRjciP+6ktLMuXP4bW2DeS1xEYIUeYhxaNI
1111
IwIDAQAB
1212
13+
us003_us1:
14+
disabled: true
15+
type: service
16+
workload_identity:
17+
type: OIDC
18+
issuer: https://token.actions.githubusercontent.com
19+
subject: repo:littleK0i/SnowDDL:ref:refs/heads/master
20+
1321
np001_us1:
1422
disabled: true
1523

0 commit comments

Comments
 (0)