strata-cli 0.1.11 → 0.1.13
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +12 -0
- data/lib/strata/cli/credentials.rb +2 -0
- data/lib/strata/cli/descriptions/datasource/add.txt +1 -1
- data/lib/strata/cli/generators/policy.rb +68 -0
- data/lib/strata/cli/generators/project.rb +15 -2
- data/lib/strata/cli/generators/templates/AGENTS.md +4 -2
- data/lib/strata/cli/generators/templates/security.yml +77 -0
- data/lib/strata/cli/helpers/prompts.rb +15 -0
- data/lib/strata/cli/sub_commands/audit.rb +1 -1
- data/lib/strata/cli/sub_commands/create.rb +103 -0
- data/lib/strata/cli/sub_commands/datasource.rb +14 -2
- data/lib/strata/cli/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61be99c9ce107eae809e67fa23dd4108a471dcc90b6adb039a298b5d59005106
|
|
4
|
+
data.tar.gz: '080076fa7d113e1e066caca27a3e33e3c438a0bb8d2e53a602ed75cf7a5b5ec5'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eaac835415776492c4858204340238a71fc9a8e45ad61183d46f532a22723774d39a16e9dfb30db5841bc897a12547a9a8fe93c1543e3b7e1975daecdf5b2d82
|
|
7
|
+
data.tar.gz: d443533139c4aea533e2bf0fd2d3f14e7de3bf7f1bb87e004e174a7232802b8c9086c7e470bb1640c697522fad07445db6bbfa60185e1a667fbe0f7c838ede58
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.13] - 2026-06-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Documentation**: ClickHouse adapter docs in README, gemspec description, `datasource add` help text, and CHANGELOG notes for the 0.1.12 feature set.
|
|
8
|
+
|
|
9
|
+
## [0.1.12] - 2026-06-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **ClickHouse adapter**: `strata datasource add clickhouse` with connection prompts and optional password credentials.
|
|
14
|
+
- **Security policy scaffolding**: `strata create policy` to add policies to `security.yml`; new projects include a `security.yml` template.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **dwh dependency**: Require `dwh` `~> 0.5.0` (ClickHouse adapter and dialect keyword/aggregate support).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Datasource introspection**: Pass only `catalog` and `schema` qualifiers to DWH for `tables` and `meta` (avoids leaking CLI flags like `--agent` or `--pattern` to adapters).
|
|
23
|
+
|
|
3
24
|
## [0.1.11] - 2026-05-22
|
|
4
25
|
|
|
5
26
|
### Changed
|
data/README.md
CHANGED
|
@@ -375,6 +375,17 @@ my_databricks:
|
|
|
375
375
|
catalog: main
|
|
376
376
|
schema: default
|
|
377
377
|
auth_mode: oauth_m2m
|
|
378
|
+
|
|
379
|
+
my_clickhouse:
|
|
380
|
+
adapter: clickhouse
|
|
381
|
+
name: ClickHouse
|
|
382
|
+
protocol: http
|
|
383
|
+
host: localhost
|
|
384
|
+
port: 8123
|
|
385
|
+
database: default
|
|
386
|
+
username: default
|
|
387
|
+
tier: warm
|
|
388
|
+
query_timeout: 3600
|
|
378
389
|
```
|
|
379
390
|
|
|
380
391
|
### Databricks authentication
|
|
@@ -403,6 +414,7 @@ Use service principal credentials and run `strata ds auth my_databricks` to save
|
|
|
403
414
|
- **Druid** - Apache Druid support
|
|
404
415
|
- **Redshift** - Amazon Redshift support
|
|
405
416
|
- **Databricks** - Databricks SQL warehouse support
|
|
417
|
+
- **ClickHouse** - ClickHouse HTTP/native SQL support
|
|
406
418
|
- **SQLite** - SQLite database support
|
|
407
419
|
|
|
408
420
|
## Security
|
|
@@ -48,6 +48,8 @@ module Strata
|
|
|
48
48
|
credentials["auth_mode"] = "oauth_m2m"
|
|
49
49
|
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
50
50
|
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
51
|
+
when "clickhouse"
|
|
52
|
+
credentials["password"] = @prompt.mask("Enter Password (leave blank if none):")
|
|
51
53
|
else
|
|
52
54
|
if required?
|
|
53
55
|
unless %w[postgres mysql trino sqlserver].include?(adapter)
|
|
@@ -18,4 +18,4 @@ Examples:
|
|
|
18
18
|
|
|
19
19
|
strata datasource add snowflake # Directly add Snowflake datasource
|
|
20
20
|
|
|
21
|
-
Supported adapters: postgres, mysql, sqlserver, snowflake, athena, trino, duckdb, druid
|
|
21
|
+
Supported adapters: postgres, mysql, sqlserver, snowflake, athena, trino, duckdb, druid, clickhouse, databricks, redshift, sqlite
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "group"
|
|
4
|
+
require_relative "../output"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module Generators
|
|
9
|
+
# Appends a new policy entry to the project's security.yml.
|
|
10
|
+
# Receives a fully-resolved attrs hash from the Create subcommand.
|
|
11
|
+
class Policy < Group
|
|
12
|
+
argument :attrs, type: :hash
|
|
13
|
+
|
|
14
|
+
def append_policy_to_security_yml
|
|
15
|
+
security_file = "security.yml"
|
|
16
|
+
|
|
17
|
+
unless File.exist?(security_file)
|
|
18
|
+
raise Strata::CommandError,
|
|
19
|
+
"security.yml not found in the current directory. Run 'strata init' first."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
content = File.read(security_file)
|
|
23
|
+
new_entry = format_policy_yaml(attrs)
|
|
24
|
+
|
|
25
|
+
updated = if content.match?(/^policies:\s*\[\]/)
|
|
26
|
+
# First policy — replace inline empty array with block sequence
|
|
27
|
+
content.sub(/^policies:\s*\[\]/, "policies:\n#{new_entry}")
|
|
28
|
+
else
|
|
29
|
+
# Subsequent policy — append after existing entries
|
|
30
|
+
content.rstrip + "\n#{new_entry}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
File.write(security_file, updated)
|
|
34
|
+
Output.print_status(:updated, security_file, type: :success, context: self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_policy_yaml(a)
|
|
40
|
+
lines = []
|
|
41
|
+
lines << " - name: #{a[:name]}"
|
|
42
|
+
lines << " mode: #{a[:mode]}"
|
|
43
|
+
lines << " mask_value: \"#{a[:mask_value]}\"" if a[:mode] == "mask_data"
|
|
44
|
+
lines << " triggers:"
|
|
45
|
+
|
|
46
|
+
tags = Array(a[:field_tags])
|
|
47
|
+
lines << " field_tags: [#{tags.join(", ")}]"
|
|
48
|
+
|
|
49
|
+
field_names = Array(a[:field_names]).reject(&:empty?)
|
|
50
|
+
unless field_names.empty?
|
|
51
|
+
lines << " field_names:"
|
|
52
|
+
field_names.each { |n| lines << " - #{n}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
lines << " context_dimension: #{a[:context_dimension]}"
|
|
56
|
+
lines << " permission_resolution:"
|
|
57
|
+
lines << " source: #{a[:permission_source]}"
|
|
58
|
+
lines << " value_from: #{a[:value_from]}"
|
|
59
|
+
lines << " tag_key: #{a[:tag_key]}" if a[:tag_key].to_s.strip.length > 0
|
|
60
|
+
lines << " bypass:"
|
|
61
|
+
lines << " system_admin: #{a[:bypass_system_admin]}"
|
|
62
|
+
lines << " project_admin: #{a[:bypass_project_admin]}"
|
|
63
|
+
lines.join("\n") + "\n"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -57,7 +57,7 @@ module Strata
|
|
|
57
57
|
def create_project_structure
|
|
58
58
|
return if cloned_from_git?
|
|
59
59
|
|
|
60
|
-
empty_directory uid
|
|
60
|
+
empty_directory uid unless existing_directory?
|
|
61
61
|
empty_directory File.join(uid, "models")
|
|
62
62
|
empty_directory File.join(uid, "tests")
|
|
63
63
|
end
|
|
@@ -102,6 +102,12 @@ module Strata
|
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def create_security_file
|
|
106
|
+
return if cloned_from_git?
|
|
107
|
+
|
|
108
|
+
copy_file "security.yml", "#{uid}/security.yml"
|
|
109
|
+
end
|
|
110
|
+
|
|
105
111
|
def initialize_git
|
|
106
112
|
return if cloned_from_git?
|
|
107
113
|
|
|
@@ -113,7 +119,8 @@ module Strata
|
|
|
113
119
|
|
|
114
120
|
def setup_datasource
|
|
115
121
|
return if cloned_from_git?
|
|
116
|
-
return if options.key?(:datasource)
|
|
122
|
+
return if options.key?(:datasource)
|
|
123
|
+
return if existing_directory?
|
|
117
124
|
|
|
118
125
|
print_status(:setup, "Let's configure your first datasource", type: :info)
|
|
119
126
|
print_info("\n")
|
|
@@ -140,6 +147,10 @@ module Strata
|
|
|
140
147
|
@cloned_from_git ||= false
|
|
141
148
|
end
|
|
142
149
|
|
|
150
|
+
def existing_directory?
|
|
151
|
+
File.directory?(name.to_s)
|
|
152
|
+
end
|
|
153
|
+
|
|
143
154
|
def fetch_project_info
|
|
144
155
|
conn = Faraday.new(url: options[:source]) do |f|
|
|
145
156
|
f.request :authorization, "Bearer", options[:api_key]
|
|
@@ -180,6 +191,8 @@ module Strata
|
|
|
180
191
|
def uid
|
|
181
192
|
@uid ||= if options.key?(:source) && @project_info
|
|
182
193
|
@project_info["uid"] || options[:source].split("/").last
|
|
194
|
+
elsif existing_directory?
|
|
195
|
+
name.to_s
|
|
183
196
|
else
|
|
184
197
|
Utils.url_safe_str(name)
|
|
185
198
|
end
|
|
@@ -28,11 +28,13 @@ Read this file end-to-end before creating or editing `models/**/*.yml`. Do not m
|
|
|
28
28
|
|
|
29
29
|
These are non-negotiable. Violations break deploy, query planning, or production reports.
|
|
30
30
|
|
|
31
|
-
1. **Every field `name` is unique project-wide** — one name = one dimension or measure entity across all `tbl.*.yml` files. Strata has no `table.field` namespaces; bracket references like `[Total Revenue]` resolve by name alone.
|
|
31
|
+
1. **Every field `name` is unique project-wide per dimension or measure** — one name = one dimension or measure entity across all `tbl.*.yml` files. Strata has no `table.field` namespaces; bracket references like `[Total Revenue]` resolve by name alone.
|
|
32
32
|
2. **`many_to_many` join cardinality is not supported.** Use a junction/bridge table with two relationships (`one_to_many` + `many_to_one`) instead.
|
|
33
33
|
3. **Measures on unrelated detail facts must have distinct names** — e.g. `Store Gross Sales`, `Catalog Gross Sales`, not one `Gross Sales` on three channel facts. See [Naming conventions](#naming-conventions).
|
|
34
|
-
4. **Dimensions may share names** when the business role is the same; Strata picks among tables at query time. **Measures do not combine that way** — duplicate measure names
|
|
34
|
+
4. **Dimensions may share names** when the business role is the same; Strata picks among tables at query time. **Measures do not combine that way** — duplicate measure names implies varying levels of aggregation of the same basic fact. The correct table and measure combination will be determined by context (i.e. what dimensions are present in the same query.)
|
|
35
35
|
5. **Cross-fact totals use compound measures or blending** — not the same measure name on multiple facts. See [Combined metrics across facts](#combined-metrics-across-facts).
|
|
36
|
+
6. Security policies can be implemented in the security.yml file. Use the guidelines documented therin.
|
|
37
|
+
7. Strata supports role playing or aliasing of dimension tables. For example, user wants date_dim table to also be used when query "Order Return Date". In that case, we simply point to the same physical_name "date_dim" but set the table name to "Order Return Date". If the user asks to alias an dimension table from an existing model we can simply copy the existing model, change the table name, and prefix all the dimensions in it with the appropriate prefix representing the role.
|
|
36
38
|
|
|
37
39
|
## Unsupported or impossible requirements
|
|
38
40
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Semantic Security Policies
|
|
2
|
+
#
|
|
3
|
+
# This file defines row-level security policies enforced at query planning time.
|
|
4
|
+
# Policies are branch-scoped and deployed alongside your semantic model.
|
|
5
|
+
#
|
|
6
|
+
# Two enforcement modes:
|
|
7
|
+
#
|
|
8
|
+
# mask_data — Sensitive column values are replaced with a mask string.
|
|
9
|
+
# Rows remain in the result set; users can see that restricted
|
|
10
|
+
# data exists but cannot read it.
|
|
11
|
+
#
|
|
12
|
+
# filter_data — Rows for context values the user cannot access are removed
|
|
13
|
+
# entirely. Restricted data is invisible to the user.
|
|
14
|
+
#
|
|
15
|
+
# How it works:
|
|
16
|
+
# 1. A policy is triggered when a query projects a field tagged with one of
|
|
17
|
+
# the trigger tags (e.g. "pii").
|
|
18
|
+
# 2. The context_dimension defines the security boundary (e.g. "Call Center ID").
|
|
19
|
+
# It must be reachable in every universe that contains a triggered field,
|
|
20
|
+
# otherwise the query is rejected with a hard error.
|
|
21
|
+
# 3. Permission resolution determines which context dimension values the current
|
|
22
|
+
# user is allowed to see, derived from their group memberships or user record.
|
|
23
|
+
#
|
|
24
|
+
# Tagging fields: add a tags key to any dimension in your table YAML files:
|
|
25
|
+
#
|
|
26
|
+
# fields:
|
|
27
|
+
# - type: dimension
|
|
28
|
+
# name: Agent Name
|
|
29
|
+
# tags: [pii]
|
|
30
|
+
# data_type: string
|
|
31
|
+
# expression:
|
|
32
|
+
# sql: agent_name
|
|
33
|
+
|
|
34
|
+
policies: []
|
|
35
|
+
|
|
36
|
+
# ── Example: mask PII dimensions scoped by call center ──────────────────────
|
|
37
|
+
#
|
|
38
|
+
# - name: PII Call Center Masking
|
|
39
|
+
# mode: mask_data # mask_data | filter_data
|
|
40
|
+
# mask_value: "#######" # Value shown to users without access (mask_data only)
|
|
41
|
+
#
|
|
42
|
+
# triggers:
|
|
43
|
+
# field_tags: [pii] # Triggers on any field tagged "pii"
|
|
44
|
+
# # field_names: # Optional: trigger on specific fields by display name
|
|
45
|
+
# # - Agent Name # Resolved to UIDs at deploy time
|
|
46
|
+
#
|
|
47
|
+
# # Dimension that scopes visibility. Resolved by name at deploy time.
|
|
48
|
+
# # Must be reachable in the universe for any query that includes a triggered field.
|
|
49
|
+
# context_dimension: Call Center ID
|
|
50
|
+
#
|
|
51
|
+
# permission_resolution:
|
|
52
|
+
# source: groups # groups | user
|
|
53
|
+
# value_from: tag # groups: name | tag / user: email | tag
|
|
54
|
+
# tag_key: call_center_id # Required when value_from is "tag".
|
|
55
|
+
# # Extracts the value from group tags formatted as "call_center_id:TMNT"
|
|
56
|
+
#
|
|
57
|
+
# bypass:
|
|
58
|
+
# system_admin: true # System admins bypass this policy entirely
|
|
59
|
+
# project_admin: false
|
|
60
|
+
#
|
|
61
|
+
# ── Example: filter rows so each user sees only their own employee record ────
|
|
62
|
+
#
|
|
63
|
+
# - name: Employee Self-Service
|
|
64
|
+
# mode: filter_data
|
|
65
|
+
#
|
|
66
|
+
# triggers:
|
|
67
|
+
# field_tags: [employee_pii]
|
|
68
|
+
#
|
|
69
|
+
# context_dimension: Employee Email
|
|
70
|
+
#
|
|
71
|
+
# permission_resolution:
|
|
72
|
+
# source: user # Resolve from the current user's own attributes
|
|
73
|
+
# value_from: email # Allowed value = the authenticated user's email address
|
|
74
|
+
#
|
|
75
|
+
# bypass:
|
|
76
|
+
# system_admin: true
|
|
77
|
+
# project_admin: false
|
|
@@ -28,6 +28,21 @@ module Strata
|
|
|
28
28
|
MSG_MODELS_LIST_HEADER = "\n Semantic Models:\n"
|
|
29
29
|
MSG_MODELS_COUNT = "\n Total: %d model(s)\n"
|
|
30
30
|
|
|
31
|
+
# Policy Command Prompts
|
|
32
|
+
MSG_POLICY_NAME = " Policy name:"
|
|
33
|
+
MSG_POLICY_MODE = " Enforcement mode:"
|
|
34
|
+
MSG_POLICY_MASK_VALUE = " Mask value (shown for restricted data):"
|
|
35
|
+
MSG_POLICY_FIELD_TAGS = " Field tags that trigger this policy (comma-separated, e.g. pii,hipaa):"
|
|
36
|
+
MSG_POLICY_FIELD_NAMES = " Specific field names (comma-separated, optional — leave blank to skip):"
|
|
37
|
+
MSG_POLICY_CONTEXT_DIM = " Context dimension name:"
|
|
38
|
+
MSG_POLICY_PERM_SOURCE = " Permission source:"
|
|
39
|
+
MSG_POLICY_VALUE_FROM = " Resolve allowed values from:"
|
|
40
|
+
MSG_POLICY_TAG_KEY = " Tag key (e.g. call_center_id):"
|
|
41
|
+
MSG_POLICY_BYPASS_ADMIN = " Bypass for system admins?"
|
|
42
|
+
MSG_POLICY_BYPASS_PROJECT = " Bypass for project admins?"
|
|
43
|
+
MSG_POLICY_CREATED = "\n✔ Policy '%s' added to security.yml"
|
|
44
|
+
MSG_POLICY_HINT = "\n💡 Run 'strata deploy' to activate this policy"
|
|
45
|
+
|
|
31
46
|
# Migration hook options
|
|
32
47
|
MIGRATION_HOOK_OPTIONS = {
|
|
33
48
|
"pre (before deployment)" => "pre",
|
|
@@ -30,7 +30,7 @@ module Strata
|
|
|
30
30
|
ALLOWED_FIELD_KEYS = %w[
|
|
31
31
|
type name description hidden grains data_type display_type format
|
|
32
32
|
secure disable_listing value_list_size snapshot exclusion_type
|
|
33
|
-
exclusions inclusions extended_blend_group synonyms expression
|
|
33
|
+
exclusions inclusions extended_blend_group synonyms expression tags
|
|
34
34
|
].freeze
|
|
35
35
|
ALLOWED_EXPRESSION_KEYS = %w[sql array lookup primary_key].freeze
|
|
36
36
|
EXPRESSION_BOOLEAN_KEYS = %w[array lookup primary_key].freeze
|
|
@@ -51,6 +51,21 @@ module Strata
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
desc "policy [NAME]", "Scaffold a new security policy in security.yml"
|
|
55
|
+
method_option :interactive, aliases: "-i", type: :boolean, default: false,
|
|
56
|
+
desc: "Walk through all options interactively"
|
|
57
|
+
method_option :mode, type: :string, desc: "Enforcement mode: mask_data or filter_data"
|
|
58
|
+
method_option :tags, type: :array, desc: "Field tags that trigger this policy"
|
|
59
|
+
method_option :context, type: :string, desc: "Context dimension name"
|
|
60
|
+
|
|
61
|
+
def policy(name = nil)
|
|
62
|
+
attrs = policy_interactive_mode? ? collect_policy_interactive(name) : collect_policy_fast(name)
|
|
63
|
+
require_relative "../generators/policy"
|
|
64
|
+
Generators::Policy.new([attrs]).invoke_all
|
|
65
|
+
say MSG_POLICY_CREATED % attrs[:name], :green
|
|
66
|
+
say MSG_POLICY_HINT, :yellow
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
desc "relation RELATION_PATH", "Create a relation (join) definition file"
|
|
55
70
|
long_desc_from_file("create/relation")
|
|
56
71
|
|
|
@@ -122,6 +137,94 @@ module Strata
|
|
|
122
137
|
|
|
123
138
|
private
|
|
124
139
|
|
|
140
|
+
# ── Policy helpers ────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def policy_interactive_mode?
|
|
143
|
+
options[:interactive] ||
|
|
144
|
+
(options[:mode].nil? && options[:tags].nil? && options[:context].nil?)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def collect_policy_fast(name)
|
|
148
|
+
{
|
|
149
|
+
name: name || "New Policy",
|
|
150
|
+
mode: options[:mode] || "mask_data",
|
|
151
|
+
mask_value: "#######",
|
|
152
|
+
field_tags: Array(options[:tags]),
|
|
153
|
+
field_names: [],
|
|
154
|
+
context_dimension: options[:context] || "<context_dimension>",
|
|
155
|
+
permission_source: "groups",
|
|
156
|
+
value_from: "tag",
|
|
157
|
+
tag_key: "<tag_key>",
|
|
158
|
+
bypass_system_admin: true,
|
|
159
|
+
bypass_project_admin: false
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def collect_policy_interactive(name)
|
|
164
|
+
say "\n Policy\n", :bold
|
|
165
|
+
policy_name = name || prompt.ask(MSG_POLICY_NAME) { |q| q.required true }
|
|
166
|
+
|
|
167
|
+
mode = prompt.select(MSG_POLICY_MODE, [
|
|
168
|
+
{name: "mask_data — replace restricted values with a mask", value: "mask_data"},
|
|
169
|
+
{name: "filter_data — remove restricted rows entirely", value: "filter_data"}
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
mask_value = if mode == "mask_data"
|
|
173
|
+
prompt.ask(MSG_POLICY_MASK_VALUE, default: "#######")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
say "\n Triggers\n", :bold
|
|
177
|
+
tags_raw = prompt.ask(MSG_POLICY_FIELD_TAGS, default: "")
|
|
178
|
+
field_tags = tags_raw.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
179
|
+
|
|
180
|
+
names_raw = prompt.ask(MSG_POLICY_FIELD_NAMES, default: "")
|
|
181
|
+
field_names = names_raw.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
182
|
+
|
|
183
|
+
say "\n Context\n", :bold
|
|
184
|
+
context_dimension = prompt.ask(MSG_POLICY_CONTEXT_DIM) { |q| q.required true }
|
|
185
|
+
|
|
186
|
+
say "\n Permission Resolution\n", :bold
|
|
187
|
+
permission_source = prompt.select(MSG_POLICY_PERM_SOURCE, [
|
|
188
|
+
{name: "groups — derive from the user's group memberships", value: "groups"},
|
|
189
|
+
{name: "user — derive from the user's own attributes", value: "user"}
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
value_from_choices = if permission_source == "groups"
|
|
193
|
+
[
|
|
194
|
+
{name: "tag — extract value from a group tag (e.g. call_center_id:TMNT)", value: "tag"},
|
|
195
|
+
{name: "name — use the group's display name as the allowed value", value: "name"}
|
|
196
|
+
]
|
|
197
|
+
else
|
|
198
|
+
[
|
|
199
|
+
{name: "email — the user's email address", value: "email"},
|
|
200
|
+
{name: "tag — extract value from a user tag", value: "tag"}
|
|
201
|
+
]
|
|
202
|
+
end
|
|
203
|
+
value_from = prompt.select(MSG_POLICY_VALUE_FROM, value_from_choices)
|
|
204
|
+
|
|
205
|
+
tag_key = if value_from == "tag"
|
|
206
|
+
prompt.ask(MSG_POLICY_TAG_KEY) { |q| q.required true }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
say "\n Bypass\n", :bold
|
|
210
|
+
bypass_system_admin = prompt.yes?(MSG_POLICY_BYPASS_ADMIN, default: true)
|
|
211
|
+
bypass_project_admin = prompt.yes?(MSG_POLICY_BYPASS_PROJECT, default: false)
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
name: policy_name,
|
|
215
|
+
mode: mode,
|
|
216
|
+
mask_value: mask_value,
|
|
217
|
+
field_tags: field_tags,
|
|
218
|
+
field_names: field_names,
|
|
219
|
+
context_dimension: context_dimension,
|
|
220
|
+
permission_source: permission_source,
|
|
221
|
+
value_from: value_from,
|
|
222
|
+
tag_key: tag_key,
|
|
223
|
+
bypass_system_admin: bypass_system_admin,
|
|
224
|
+
bypass_project_admin: bypass_project_admin
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
125
228
|
def handle_table_creation_with_path(table_path)
|
|
126
229
|
# Parse the path to extract schema, physical name, and model path
|
|
127
230
|
parsed = parse_table_path(table_path)
|
|
@@ -187,7 +187,7 @@ module Strata
|
|
|
187
187
|
return unless ds_key
|
|
188
188
|
|
|
189
189
|
adapter = create_adapter(ds_key)
|
|
190
|
-
tables = adapter.tables(**
|
|
190
|
+
tables = adapter.tables(**adapter_qualifiers)
|
|
191
191
|
tables = tables.select { it =~ /#{options[:pattern]}/ } if options[:pattern]
|
|
192
192
|
|
|
193
193
|
if agent_mode?
|
|
@@ -214,7 +214,7 @@ module Strata
|
|
|
214
214
|
method_option :schema, aliases: "s", type: :string, desc: "Change the schema from the configured one."
|
|
215
215
|
def meta(ds_key, table_name)
|
|
216
216
|
adapter = create_adapter(ds_key)
|
|
217
|
-
md = adapter.metadata(table_name, **
|
|
217
|
+
md = adapter.metadata(table_name, **adapter_qualifiers)
|
|
218
218
|
|
|
219
219
|
if agent_mode?
|
|
220
220
|
columns = md.columns.map do |column|
|
|
@@ -275,6 +275,10 @@ module Strata
|
|
|
275
275
|
false
|
|
276
276
|
end
|
|
277
277
|
|
|
278
|
+
def adapter_qualifiers
|
|
279
|
+
options.transform_keys(&:to_sym).slice(:catalog, :schema)
|
|
280
|
+
end
|
|
281
|
+
|
|
278
282
|
def validate_file_path(file_path, project_path = Dir.pwd)
|
|
279
283
|
expanded = File.expand_path(file_path, project_path)
|
|
280
284
|
project_root = File.expand_path(project_path)
|
|
@@ -370,6 +374,14 @@ module Strata
|
|
|
370
374
|
"schema" => "default",
|
|
371
375
|
"auth_mode" => "oauth_m2m"
|
|
372
376
|
}
|
|
377
|
+
when "clickhouse"
|
|
378
|
+
{
|
|
379
|
+
"protocol" => "http",
|
|
380
|
+
"host" => "localhost",
|
|
381
|
+
"port" => 8123,
|
|
382
|
+
"database" => "test_db",
|
|
383
|
+
"username" => "default"
|
|
384
|
+
}
|
|
373
385
|
else
|
|
374
386
|
{}
|
|
375
387
|
end
|
data/lib/strata/cli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: strata-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajo Abraham
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.5.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
26
|
+
version: 0.5.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: aws-sdk-athena
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -224,7 +224,7 @@ description: |
|
|
|
224
224
|
Comprehensive CLI tool for managing Strata Semantic Analytics projects. Create and initialize
|
|
225
225
|
projects, manage datasource connections, build semantic table models with AI assistance, run
|
|
226
226
|
audits and validations, and deploy projects to Strata servers. Supports multiple data warehouse
|
|
227
|
-
adapters including PostgreSQL, MySQL, SQL Server, Snowflake, Athena, Trino, and more.
|
|
227
|
+
adapters including PostgreSQL, MySQL, SQL Server, Snowflake, Athena, Trino, ClickHouse, and more.
|
|
228
228
|
email:
|
|
229
229
|
- ajo@strata.site
|
|
230
230
|
- allen@strata.site
|
|
@@ -268,6 +268,7 @@ files:
|
|
|
268
268
|
- lib/strata/cli/generators/datasource.rb
|
|
269
269
|
- lib/strata/cli/generators/group.rb
|
|
270
270
|
- lib/strata/cli/generators/migration.rb
|
|
271
|
+
- lib/strata/cli/generators/policy.rb
|
|
271
272
|
- lib/strata/cli/generators/project.rb
|
|
272
273
|
- lib/strata/cli/generators/relation.rb
|
|
273
274
|
- lib/strata/cli/generators/table.rb
|
|
@@ -286,6 +287,7 @@ files:
|
|
|
286
287
|
- lib/strata/cli/generators/templates/migration.swap.yml
|
|
287
288
|
- lib/strata/cli/generators/templates/project.yml
|
|
288
289
|
- lib/strata/cli/generators/templates/rel.domain.yml
|
|
290
|
+
- lib/strata/cli/generators/templates/security.yml
|
|
289
291
|
- lib/strata/cli/generators/templates/strata.yml
|
|
290
292
|
- lib/strata/cli/generators/templates/table.table_name.yml
|
|
291
293
|
- lib/strata/cli/generators/templates/test.yml
|