tundra-cli 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tundra/__init__.py +8 -0
- tundra/cli/__init__.py +10 -0
- tundra/cli/__main__.py +3 -0
- tundra/cli/cli.py +25 -0
- tundra/cli/permissions.py +206 -0
- tundra/entities.py +586 -0
- tundra/error.py +4 -0
- tundra/logger.py +9 -0
- tundra/snowflake_connector.py +575 -0
- tundra/snowflake_grants.py +2263 -0
- tundra/snowflake_permission.py +46 -0
- tundra/snowflake_role_grant_checker.py +121 -0
- tundra/snowflake_spec_loader.py +540 -0
- tundra/spec_file_loader.py +139 -0
- tundra/spec_schemas/__init__.py +0 -0
- tundra/spec_schemas/snowflake.py +238 -0
- tundra/types.py +103 -0
- tundra_cli-0.0.0.dist-info/METADATA +598 -0
- tundra_cli-0.0.0.dist-info/RECORD +23 -0
- tundra_cli-0.0.0.dist-info/WHEEL +5 -0
- tundra_cli-0.0.0.dist-info/entry_points.txt +2 -0
- tundra_cli-0.0.0.dist-info/licenses/LICENSE +7 -0
- tundra_cli-0.0.0.dist-info/top_level.txt +1 -0
tundra/__init__.py
ADDED
tundra/cli/__init__.py
ADDED
tundra/cli/__main__.py
ADDED
tundra/cli/cli.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
import tundra
|
|
6
|
+
from tundra.logger import GLOBAL_LOGGER as logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(invoke_without_command=True, no_args_is_help=True)
|
|
10
|
+
@click.option(
|
|
11
|
+
"-v", "--verbose", help="Increases log level with count, e.g -vv", count=True
|
|
12
|
+
)
|
|
13
|
+
@click.version_option(
|
|
14
|
+
version=tundra.__version__, prog_name="tundra",
|
|
15
|
+
message="%(prog)s %(version)s - Snowflake permissions with Iceberg table support"
|
|
16
|
+
)
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def cli(ctx, verbose):
|
|
19
|
+
logger.setLevel(logging.WARNING)
|
|
20
|
+
if verbose == 1:
|
|
21
|
+
logger.setLevel(logging.INFO)
|
|
22
|
+
if verbose >= 2:
|
|
23
|
+
logger.setLevel(logging.DEBUG)
|
|
24
|
+
|
|
25
|
+
ctx.ensure_object(dict)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from tundra import SpecLoadingError
|
|
6
|
+
from tundra.snowflake_connector import SnowflakeConnector
|
|
7
|
+
from tundra.snowflake_spec_loader import SnowflakeSpecLoader
|
|
8
|
+
|
|
9
|
+
from . import cli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def print_command(command, diff, dry=False):
|
|
13
|
+
"""Prints the queries to the command line with prefixes"""
|
|
14
|
+
diff_prefix = ""
|
|
15
|
+
if command["already_granted"]:
|
|
16
|
+
if diff:
|
|
17
|
+
diff_prefix = " "
|
|
18
|
+
else:
|
|
19
|
+
pass
|
|
20
|
+
else:
|
|
21
|
+
if diff:
|
|
22
|
+
diff_prefix = "+ "
|
|
23
|
+
|
|
24
|
+
if command.get("run_status"):
|
|
25
|
+
foreground_color = "green"
|
|
26
|
+
run_prefix = "[SUCCESS] "
|
|
27
|
+
elif command.get("run_status") is None and dry:
|
|
28
|
+
foreground_color = "cyan"
|
|
29
|
+
run_prefix = "[PENDING] "
|
|
30
|
+
elif command.get("run_status") is None:
|
|
31
|
+
foreground_color = "cyan"
|
|
32
|
+
run_prefix = "[SKIPPED] "
|
|
33
|
+
else:
|
|
34
|
+
foreground_color = "red"
|
|
35
|
+
run_prefix = "[ERROR] "
|
|
36
|
+
|
|
37
|
+
click.secho(f"{diff_prefix}{run_prefix}{command['sql']};", fg=foreground_color)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@cli.command() # type: ignore
|
|
41
|
+
@click.argument("spec")
|
|
42
|
+
@click.option("--dry", help="Do not actually run, just check.", is_flag=True)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--diff", help="Show full diff, both new and existing permissions.", is_flag=True
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--role",
|
|
48
|
+
multiple=True,
|
|
49
|
+
default=[],
|
|
50
|
+
help="Run grants for specific roles. Usage: --role testrole --role testrole2.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--user",
|
|
54
|
+
multiple=True,
|
|
55
|
+
default=[],
|
|
56
|
+
help="Run grants for specific users. Usage: --user testuser --user testuser2.",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--ignore-memberships",
|
|
60
|
+
help="Do not handle role membership grants/revokes",
|
|
61
|
+
is_flag=True,
|
|
62
|
+
)
|
|
63
|
+
@click.pass_context
|
|
64
|
+
def run(ctx, spec, dry, diff, role, user, ignore_memberships, print_skipped=False):
|
|
65
|
+
"""
|
|
66
|
+
Grant the permissions provided in the provided specification file for specific users and roles.
|
|
67
|
+
This fork includes support for Iceberg tables, external volumes, and catalog integrations.
|
|
68
|
+
"""
|
|
69
|
+
if role and user:
|
|
70
|
+
run_list = ["roles", "users"]
|
|
71
|
+
elif role:
|
|
72
|
+
run_list = ["roles"]
|
|
73
|
+
elif user:
|
|
74
|
+
run_list = ["users"]
|
|
75
|
+
else:
|
|
76
|
+
run_list = ["roles", "users"]
|
|
77
|
+
if ctx.parent.params.get("verbose", 0) >= 1:
|
|
78
|
+
print_skipped = True
|
|
79
|
+
tundra_grants(
|
|
80
|
+
spec=spec,
|
|
81
|
+
dry=dry,
|
|
82
|
+
diff=diff,
|
|
83
|
+
roles=role,
|
|
84
|
+
users=user,
|
|
85
|
+
run_list=run_list,
|
|
86
|
+
ignore_memberships=ignore_memberships,
|
|
87
|
+
print_skipped=print_skipped,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@click.command()
|
|
92
|
+
@click.argument("spec")
|
|
93
|
+
@click.option(
|
|
94
|
+
"--role",
|
|
95
|
+
multiple=True,
|
|
96
|
+
default=[],
|
|
97
|
+
help="Run grants for specific roles. Usage: --role testrole --role testrole2.",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--user",
|
|
101
|
+
multiple=True,
|
|
102
|
+
default=[],
|
|
103
|
+
help="Run grants for specific users. Usage: --user testuser --user testuser2.",
|
|
104
|
+
)
|
|
105
|
+
@click.option(
|
|
106
|
+
"--ignore-memberships",
|
|
107
|
+
help="Do not handle role membership grants/revokes",
|
|
108
|
+
is_flag=True,
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--run-list",
|
|
112
|
+
multiple=True,
|
|
113
|
+
default=["roles", "users"],
|
|
114
|
+
help="Run grants for specific users. Usage: --user testuser --user testuser2.",
|
|
115
|
+
)
|
|
116
|
+
def spec_test(spec, role, user, run_list, ignore_memberships):
|
|
117
|
+
"""
|
|
118
|
+
Load SnowFlake spec based on the roles.yml provided. CLI use only for confirming specifications are valid.
|
|
119
|
+
This fork includes validation for Iceberg tables, external volumes, and catalog integrations.
|
|
120
|
+
"""
|
|
121
|
+
load_specs(
|
|
122
|
+
spec,
|
|
123
|
+
role=role,
|
|
124
|
+
user=user,
|
|
125
|
+
run_list=run_list,
|
|
126
|
+
ignore_memberships=ignore_memberships,
|
|
127
|
+
do_spec_test=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_specs(spec, role, user, run_list, ignore_memberships, do_spec_test):
|
|
132
|
+
"""
|
|
133
|
+
Load specs separately.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
click.secho("Confirming spec loads successfully")
|
|
137
|
+
spec_loader = SnowflakeSpecLoader(
|
|
138
|
+
spec,
|
|
139
|
+
roles=role,
|
|
140
|
+
users=user,
|
|
141
|
+
run_list=run_list,
|
|
142
|
+
ignore_memberships=ignore_memberships,
|
|
143
|
+
spec_test=do_spec_test,
|
|
144
|
+
)
|
|
145
|
+
click.secho("Snowflake specs successfully loaded", fg="green")
|
|
146
|
+
except SpecLoadingError as exc:
|
|
147
|
+
for line in str(exc).splitlines():
|
|
148
|
+
click.secho(line, fg="red")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
return spec_loader
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def tundra_grants(
|
|
155
|
+
spec, dry, diff, roles, users, run_list, ignore_memberships, print_skipped
|
|
156
|
+
):
|
|
157
|
+
"""Grant the permissions provided in the provided specification file."""
|
|
158
|
+
spec_loader = load_specs(
|
|
159
|
+
spec,
|
|
160
|
+
role=roles,
|
|
161
|
+
user=users,
|
|
162
|
+
run_list=run_list,
|
|
163
|
+
ignore_memberships=ignore_memberships,
|
|
164
|
+
do_spec_test=False,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
sql_grant_queries = spec_loader.generate_permission_queries(
|
|
168
|
+
roles=roles,
|
|
169
|
+
users=users,
|
|
170
|
+
run_list=run_list,
|
|
171
|
+
ignore_memberships=ignore_memberships,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
click.secho()
|
|
175
|
+
if diff:
|
|
176
|
+
click.secho(
|
|
177
|
+
"SQL Commands generated for given spec file (Full diff with both new and already granted commands):"
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
click.secho("SQL Commands generated for given spec file:")
|
|
181
|
+
click.secho()
|
|
182
|
+
|
|
183
|
+
conn = SnowflakeConnector()
|
|
184
|
+
for query in sql_grant_queries:
|
|
185
|
+
if not dry:
|
|
186
|
+
status = None
|
|
187
|
+
if not query.get("already_granted"):
|
|
188
|
+
try:
|
|
189
|
+
conn.run_query(query.get("sql", ""))
|
|
190
|
+
status = True
|
|
191
|
+
except Exception:
|
|
192
|
+
status = False
|
|
193
|
+
|
|
194
|
+
ran_query = query
|
|
195
|
+
ran_query["run_status"] = status
|
|
196
|
+
print_command(ran_query, diff)
|
|
197
|
+
# If already granted, print command
|
|
198
|
+
elif print_skipped:
|
|
199
|
+
print_command(query, diff)
|
|
200
|
+
# If dry, print commands
|
|
201
|
+
else:
|
|
202
|
+
if not query.get("already_granted") or print_skipped:
|
|
203
|
+
print_command(query, diff, dry=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
cli.add_command(spec_test) # type: ignore
|