vaultkit 0.1.0
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 +7 -0
- data/README.md +961 -0
- data/bin/funl +0 -0
- data/bin/vkit +30 -0
- data/lib/vkit/cli/api/client.rb +115 -0
- data/lib/vkit/cli/base_cli.rb +173 -0
- data/lib/vkit/cli/commands/approval_command.rb +94 -0
- data/lib/vkit/cli/commands/base_command.rb +42 -0
- data/lib/vkit/cli/commands/datasource_command.rb +93 -0
- data/lib/vkit/cli/commands/fetch_command.rb +48 -0
- data/lib/vkit/cli/commands/login_command.rb +136 -0
- data/lib/vkit/cli/commands/logout_command.rb +12 -0
- data/lib/vkit/cli/commands/policy_bundle_command.rb +62 -0
- data/lib/vkit/cli/commands/policy_deploy_command.rb +32 -0
- data/lib/vkit/cli/commands/policy_validate_command.rb +31 -0
- data/lib/vkit/cli/commands/request_command.rb +102 -0
- data/lib/vkit/cli/commands/requests_list_command.rb +47 -0
- data/lib/vkit/cli/commands/scan_command.rb +47 -0
- data/lib/vkit/cli/commands/whoami_command.rb +14 -0
- data/lib/vkit/cli/commands.rb +5 -0
- data/lib/vkit/cli/errors.rb +6 -0
- data/lib/vkit/cli/policy_bundle_validator.rb +71 -0
- data/lib/vkit/cli/requests_cli.rb +23 -0
- data/lib/vkit/cli.rb +4 -0
- data/lib/vkit/core/auth_client.rb +104 -0
- data/lib/vkit/core/credential_resolver.rb +37 -0
- data/lib/vkit/core/credential_store.rb +186 -0
- data/lib/vkit/core/table_formatter.rb +36 -0
- data/lib/vkit/policy/bundle_compiler.rb +154 -0
- data/lib/vkit/policy/schema/policy_bundle.schema.json +296 -0
- data/lib/vkit/policy/validate_bundle.rb +37 -0
- data/lib/vkit/utils/banner.rb +0 -0
- data/lib/vkit/utils/config_loader.rb +0 -0
- data/lib/vkit/utils/logger.rb +0 -0
- data/lib/vkit.rb +3 -0
- metadata +94 -0
data/bin/funl
ADDED
|
File without changes
|
data/bin/vkit
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/vkit"
|
|
4
|
+
require_relative "../lib/vkit/cli/errors"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
Vkit::CLI::BaseCLI.start(ARGV)
|
|
8
|
+
|
|
9
|
+
rescue Vkit::CLI::Error => e
|
|
10
|
+
warn "❌ #{e.message}"
|
|
11
|
+
exit 1
|
|
12
|
+
|
|
13
|
+
rescue Thor::Error => e
|
|
14
|
+
warn "❌ #{e.message}"
|
|
15
|
+
exit 1
|
|
16
|
+
|
|
17
|
+
rescue Interrupt
|
|
18
|
+
warn "\n⏹️ Interrupted"
|
|
19
|
+
exit 130
|
|
20
|
+
|
|
21
|
+
rescue => e
|
|
22
|
+
warn e.message
|
|
23
|
+
|
|
24
|
+
# Only show stack trace when debugging
|
|
25
|
+
if ENV["VKIT_DEBUG"] == "true"
|
|
26
|
+
warn e.backtrace.join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Vkit
|
|
8
|
+
module CLI
|
|
9
|
+
module API
|
|
10
|
+
class Client
|
|
11
|
+
DEFAULT_TIMEOUT = 15
|
|
12
|
+
|
|
13
|
+
def initialize(base_url:, token:)
|
|
14
|
+
raise ArgumentError, "base_url required" if base_url.nil?
|
|
15
|
+
raise ArgumentError, "token required" if token.nil?
|
|
16
|
+
|
|
17
|
+
@base_url = base_url.chomp("/")
|
|
18
|
+
@token = token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get(path, params: {})
|
|
22
|
+
request(method: :get, path: path, body: params.empty? ? nil : params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def post(path, body:)
|
|
26
|
+
request(method: :post, path: path, body: body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def put(path, body:)
|
|
30
|
+
request(method: :put, path: path, body: body)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def patch(path, body:)
|
|
34
|
+
request(method: :patch, path: path, body: body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def request(method:, path:, body: nil)
|
|
40
|
+
uri = URI("#{@base_url}#{path}")
|
|
41
|
+
|
|
42
|
+
req = build_request(method, uri)
|
|
43
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
44
|
+
req["Content-Type"] = "application/json"
|
|
45
|
+
req.body = JSON.dump(body) if body
|
|
46
|
+
|
|
47
|
+
res = Net::HTTP.start(
|
|
48
|
+
uri.host,
|
|
49
|
+
uri.port,
|
|
50
|
+
use_ssl: uri.scheme == "https",
|
|
51
|
+
open_timeout: DEFAULT_TIMEOUT,
|
|
52
|
+
read_timeout: DEFAULT_TIMEOUT
|
|
53
|
+
) do |http|
|
|
54
|
+
http.request(req)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
handle_response(res)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_request(method, uri)
|
|
61
|
+
case method
|
|
62
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
63
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
64
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
65
|
+
when :patch then Net::HTTP::Patch.new(uri)
|
|
66
|
+
else
|
|
67
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def handle_response(res)
|
|
72
|
+
code = res.code.to_i
|
|
73
|
+
body =
|
|
74
|
+
if res.body.nil? || res.body.strip.empty?
|
|
75
|
+
{}
|
|
76
|
+
else
|
|
77
|
+
JSON.parse(res.body) rescue { "error" => res.body }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
case code
|
|
81
|
+
when 200..299
|
|
82
|
+
body
|
|
83
|
+
|
|
84
|
+
when 403
|
|
85
|
+
if body.is_a?(Hash) && body.key?("error")
|
|
86
|
+
raise APIError.new(code, body["error"])
|
|
87
|
+
else
|
|
88
|
+
# Valid domain response (e.g. policy denied)
|
|
89
|
+
body
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
when 202
|
|
93
|
+
# Queued for approval
|
|
94
|
+
body
|
|
95
|
+
|
|
96
|
+
when 422
|
|
97
|
+
raise APIError.new(code, body["error"] || body)
|
|
98
|
+
|
|
99
|
+
else
|
|
100
|
+
raise APIError.new(code, body["error"] || body)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class APIError < StandardError
|
|
106
|
+
attr_reader :status
|
|
107
|
+
|
|
108
|
+
def initialize(status, message)
|
|
109
|
+
@status = status
|
|
110
|
+
super("API #{status}: #{message}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "requests_cli"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module CLI
|
|
8
|
+
class BaseCLI < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# LOGIN
|
|
14
|
+
desc "login", "Authenticate with VaultKit control plane"
|
|
15
|
+
option :endpoint, type: :string
|
|
16
|
+
option :email, type: :string
|
|
17
|
+
def login
|
|
18
|
+
Commands::LoginCommand.new(
|
|
19
|
+
endpoint: options[:endpoint],
|
|
20
|
+
email: options[:email]
|
|
21
|
+
).call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "whoami", "Show current identity"
|
|
25
|
+
def whoami
|
|
26
|
+
Commands::WhoamiCommand.new.call
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "logout", "Clear stored credentials"
|
|
30
|
+
def logout
|
|
31
|
+
Commands::LogoutCommand.new.call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# REQUEST
|
|
35
|
+
desc "request", "Send an inline JSON AQL request (use --aql or pipe via STDIN)"
|
|
36
|
+
option :aql, type: :string, desc: "AQL JSON payload (inline)"
|
|
37
|
+
option :env, type: :string, default: "production"
|
|
38
|
+
option :requester_region, type: :string
|
|
39
|
+
option :dataset_region, type: :string
|
|
40
|
+
option :requester_clearance, type: :string, desc: "Clearance level (low/high/admin)"
|
|
41
|
+
option :format, type: :string, default: "table", enum: %w[json table]
|
|
42
|
+
def request
|
|
43
|
+
Commands::RequestCommand.new.call(options[:aql], options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "requests SUBCOMMAND ...ARGS", "Manage request history"
|
|
47
|
+
subcommand "requests", Vkit::CLI::RequestsCLI
|
|
48
|
+
|
|
49
|
+
# APPROVALS
|
|
50
|
+
desc "approval:list", "List all approval requests"
|
|
51
|
+
option :state, type: :string, default: "pending", enum: %w[pending approved denied all]
|
|
52
|
+
define_method("approval:list") do
|
|
53
|
+
Commands::ApprovalCommand.new.call_list(state: options[:state])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "approval:approve ID", "Approve a pending request"
|
|
57
|
+
option :ttl, type: :numeric, default: 3600
|
|
58
|
+
define_method("approval:approve") do |id|
|
|
59
|
+
Commands::ApprovalCommand.new.call_approve(
|
|
60
|
+
id: id,
|
|
61
|
+
ttl_seconds: options[:ttl]
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "approval:deny ID", "Deny a pending request"
|
|
66
|
+
option :reason, type: :string
|
|
67
|
+
define_method("approval:deny") do |id|
|
|
68
|
+
Commands::ApprovalCommand.new.call_deny(
|
|
69
|
+
id: id,
|
|
70
|
+
reason: options[:reason]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# FETCH
|
|
75
|
+
desc "fetch --grant ID", "Fetch data from Funl using a valid grant"
|
|
76
|
+
option :grant, type: :string, required: true
|
|
77
|
+
option :format, type: :string, default: "json", enum: %w[json table]
|
|
78
|
+
def fetch
|
|
79
|
+
Commands::FetchCommand.new.call(
|
|
80
|
+
grant_ref: options[:grant],
|
|
81
|
+
format: options[:format]
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# DATASOURCE
|
|
86
|
+
desc "datasource SUBCOMMAND ...ARGS", "Manage datasources (admin only)"
|
|
87
|
+
subcommand "datasource", Class.new(Thor) {
|
|
88
|
+
desc "add", "Add a new datasource (admin only)"
|
|
89
|
+
option :id, required: true
|
|
90
|
+
option :engine, required: true
|
|
91
|
+
option :username, required: true
|
|
92
|
+
option :password, required: true
|
|
93
|
+
option :config, required: true
|
|
94
|
+
|
|
95
|
+
def add
|
|
96
|
+
Commands::DatasourceCommand.new.add(
|
|
97
|
+
id: options[:id],
|
|
98
|
+
engine: options[:engine],
|
|
99
|
+
username: options[:username],
|
|
100
|
+
password: options[:password],
|
|
101
|
+
config: options[:config]
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc "list", "List all datasources (admin only)"
|
|
106
|
+
def list
|
|
107
|
+
Commands::DatasourceCommand.new.list
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
desc "get ID", "Fetch a datasource by ID (admin only)"
|
|
111
|
+
def get(id)
|
|
112
|
+
Commands::DatasourceCommand.new.get(id)
|
|
113
|
+
end
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# SCAN
|
|
117
|
+
desc "scan DATASOURCE", "Scan datasource and diff against registry"
|
|
118
|
+
option :mode, type: :string, default: "diff_only", enum: %w[diff_only apply]
|
|
119
|
+
def scan(datasource)
|
|
120
|
+
Commands::ScanCommand.new.call(
|
|
121
|
+
datasource,
|
|
122
|
+
mode: options[:mode]
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# POLICY
|
|
127
|
+
desc "policy SUBCOMMAND ...ARGS", "Manage policy bundles"
|
|
128
|
+
subcommand "policy", Class.new(Thor) {
|
|
129
|
+
|
|
130
|
+
desc "bundle", "Compile YAML policies into a JSON policy bundle"
|
|
131
|
+
option :policies_dir, type: :string, default: "config/policies"
|
|
132
|
+
option :registry_dir, type: :string, default: "config"
|
|
133
|
+
option :out, type: :string, default: "dist/policy_bundle.json"
|
|
134
|
+
option :org, type: :string
|
|
135
|
+
option :version, type: :string
|
|
136
|
+
|
|
137
|
+
def bundle
|
|
138
|
+
Commands::PolicyBundleCommand.new.call(
|
|
139
|
+
policies_dir: options[:policies_dir],
|
|
140
|
+
registry_dir: options[:registry_dir],
|
|
141
|
+
out: options[:out],
|
|
142
|
+
org: options[:org],
|
|
143
|
+
version: options[:version]
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
desc "validate", "Validate a compiled policy bundle"
|
|
148
|
+
option :bundle, type: :string, default: "dist/policy_bundle.json"
|
|
149
|
+
option :schema, type: :string
|
|
150
|
+
|
|
151
|
+
def validate
|
|
152
|
+
Commands::PolicyValidateCommand.new.call(
|
|
153
|
+
bundle_path: options[:bundle],
|
|
154
|
+
schema_path: options[:schema]
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
desc "deploy", "Deploy a policy bundle to VaultKit"
|
|
159
|
+
option :bundle, type: :string, default: "dist/policy_bundle.json"
|
|
160
|
+
option :org, type: :string, required: true
|
|
161
|
+
option :activate, type: :boolean, default: true
|
|
162
|
+
|
|
163
|
+
def deploy
|
|
164
|
+
Commands::PolicyDeployCommand.new.call(
|
|
165
|
+
bundle_path: options[:bundle],
|
|
166
|
+
org: options[:org],
|
|
167
|
+
activate: options[:activate]
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vkit
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class ApprovalCommand < BaseCommand
|
|
9
|
+
DEFAULT_TTL = 3600
|
|
10
|
+
|
|
11
|
+
def call_list(state: "pending")
|
|
12
|
+
with_auth do
|
|
13
|
+
user = credential_store.user
|
|
14
|
+
org = user["organization_slug"]
|
|
15
|
+
|
|
16
|
+
rows = authenticated_client.get(
|
|
17
|
+
"/api/v1/orgs/#{org}/approvals?state=#{state}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if rows.empty?
|
|
21
|
+
puts empty_message_for(state)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts "📋 Approvals (#{rows.size}, state=#{state})"
|
|
26
|
+
rows.each do |r|
|
|
27
|
+
puts "─" * 60
|
|
28
|
+
puts "🆔 #{r["id"]}"
|
|
29
|
+
puts "📂 Dataset: #{r["dataset"]}"
|
|
30
|
+
puts "📄 Fields: #{Array(r["fields"]).join(', ')}"
|
|
31
|
+
puts "👤 Requester: #{r["requester_email"]}"
|
|
32
|
+
puts "🔐 Required: #{r["approver_role"] || 'n/a'}"
|
|
33
|
+
puts "⚙️ Status: #{r["state"].capitalize}"
|
|
34
|
+
puts "🔑 Grant Ref: #{r["grant_ref"] || 'n/a'}" if r["state"] == "approved"
|
|
35
|
+
puts "💬 Reason: #{r["reason"]}"
|
|
36
|
+
puts "📅 Created: #{r["created_at"]}"
|
|
37
|
+
end
|
|
38
|
+
puts "─" * 60
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call_approve(id:, ttl_seconds: DEFAULT_TTL)
|
|
43
|
+
with_auth do
|
|
44
|
+
user = credential_store.user
|
|
45
|
+
org = user["organization_slug"]
|
|
46
|
+
|
|
47
|
+
res = authenticated_client.post(
|
|
48
|
+
"/api/v1/orgs/#{org}/approvals/#{id}/approve",
|
|
49
|
+
body: { ttl_seconds: ttl_seconds }
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
puts "✅ Approved request #{id}"
|
|
53
|
+
puts " → Grant Ref: #{res["grant_ref"] || res["grant_id"]}"
|
|
54
|
+
puts " → Expires: #{res["expires_at"]}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call_deny(id:, reason: nil)
|
|
59
|
+
with_auth do
|
|
60
|
+
user = credential_store.user
|
|
61
|
+
org = user["organization_slug"]
|
|
62
|
+
|
|
63
|
+
reason ||= prompt_reason
|
|
64
|
+
raise "Denial reason cannot be empty" if reason.to_s.empty?
|
|
65
|
+
|
|
66
|
+
authenticated_client.post(
|
|
67
|
+
"/api/v1/orgs/#{org}/approvals/#{id}/deny",
|
|
68
|
+
body: { reason: reason }
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
puts "🚫 Denied request #{id}"
|
|
72
|
+
puts " → Reason: #{reason}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def prompt_reason
|
|
79
|
+
print "Enter reason for denial: "
|
|
80
|
+
STDIN.gets&.strip
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def empty_message_for(state)
|
|
84
|
+
case state
|
|
85
|
+
when "pending" then "📭 No pending approvals."
|
|
86
|
+
when "approved" then "📭 No approved requests."
|
|
87
|
+
when "denied" then "📭 No denied requests."
|
|
88
|
+
else "📭 No approvals found."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Vkit
|
|
2
|
+
module CLI
|
|
3
|
+
module Commands
|
|
4
|
+
class BaseCommand
|
|
5
|
+
protected
|
|
6
|
+
|
|
7
|
+
def with_auth
|
|
8
|
+
return yield unless requires_auth?
|
|
9
|
+
|
|
10
|
+
store = credential_store
|
|
11
|
+
|
|
12
|
+
unless store.logged_in?
|
|
13
|
+
raise "Not logged in. Run `vkit login`."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
yield
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def authenticated_client
|
|
20
|
+
store = credential_store
|
|
21
|
+
|
|
22
|
+
if requires_auth? && !store.logged_in?
|
|
23
|
+
raise "Not logged in. Run `vkit login`."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Vkit::CLI::API::Client.new(
|
|
27
|
+
base_url: store.endpoint,
|
|
28
|
+
token: store.token
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def requires_auth?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def credential_store
|
|
37
|
+
@credential_store ||= Vkit::Core::CredentialStore.new
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vkit
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class DatasourceCommand < BaseCommand
|
|
9
|
+
REDACT = "[REDACTED]"
|
|
10
|
+
|
|
11
|
+
def add(id:, engine:, username:, password:, config:)
|
|
12
|
+
with_auth do
|
|
13
|
+
user = require_admin!
|
|
14
|
+
org = user["organization_slug"]
|
|
15
|
+
|
|
16
|
+
config_hash = config ? JSON.parse(config) : {}
|
|
17
|
+
|
|
18
|
+
response = authenticated_client.post(
|
|
19
|
+
"/api/v1/orgs/#{org}/datasources",
|
|
20
|
+
body: {
|
|
21
|
+
name: id,
|
|
22
|
+
engine: engine,
|
|
23
|
+
username: username,
|
|
24
|
+
password: password,
|
|
25
|
+
config: config_hash
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
puts "✅ Datasource created:"
|
|
30
|
+
print_datasource(response)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list
|
|
35
|
+
with_auth do
|
|
36
|
+
user = require_admin!
|
|
37
|
+
org = user["organization_slug"]
|
|
38
|
+
|
|
39
|
+
rows = authenticated_client.get(
|
|
40
|
+
"/api/v1/orgs/#{org}/datasources"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
puts "📦 Datasources (#{rows.size}):"
|
|
44
|
+
rows.each do |ds|
|
|
45
|
+
print_datasource(ds)
|
|
46
|
+
puts "-" * 40
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def get(name)
|
|
52
|
+
with_auth do
|
|
53
|
+
user = require_admin!
|
|
54
|
+
org = user["organization_slug"]
|
|
55
|
+
|
|
56
|
+
ds = authenticated_client.get(
|
|
57
|
+
"/api/v1/orgs/#{org}/datasources/#{name}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print_datasource(ds)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def require_admin!
|
|
67
|
+
user = credential_store.user
|
|
68
|
+
raise "Not logged in. Run: vkit login" unless user
|
|
69
|
+
|
|
70
|
+
unless user["role"] == "admin"
|
|
71
|
+
raise "⛔ Access denied: only admin users may manage datasources"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
user
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def print_datasource(ds)
|
|
78
|
+
puts JSON.pretty_generate(
|
|
79
|
+
{
|
|
80
|
+
name: ds["name"] || ds[:name],
|
|
81
|
+
engine: ds["engine"] || ds[:engine],
|
|
82
|
+
config: ds["config"] || ds[:config],
|
|
83
|
+
username: REDACT,
|
|
84
|
+
password: REDACT,
|
|
85
|
+
created_at: ds["created_at"] || ds[:created_at],
|
|
86
|
+
updated_at: ds["updated_at"] || ds[:updated_at]
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vkit
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class FetchCommand < BaseCommand
|
|
9
|
+
def call(grant_ref:, format: "json")
|
|
10
|
+
with_auth do
|
|
11
|
+
user = credential_store.user
|
|
12
|
+
org = user["organization_slug"]
|
|
13
|
+
|
|
14
|
+
response = authenticated_client.post(
|
|
15
|
+
"/api/v1/orgs/#{org}/grants/#{grant_ref}/fetch",
|
|
16
|
+
body: {}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
rows = response["rows"] || []
|
|
20
|
+
meta = response["meta"] || {}
|
|
21
|
+
|
|
22
|
+
print_result(rows, meta, format)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def print_result(rows, meta, format)
|
|
29
|
+
puts "✅ OK — #{rows.size} rows"
|
|
30
|
+
|
|
31
|
+
if meta.any?
|
|
32
|
+
puts "ℹ️ Query Metadata:"
|
|
33
|
+
puts JSON.pretty_generate(meta)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case format
|
|
37
|
+
when "json"
|
|
38
|
+
puts JSON.pretty_generate(rows)
|
|
39
|
+
when "table"
|
|
40
|
+
Vkit::Core::TableFormatter.render(rows)
|
|
41
|
+
else
|
|
42
|
+
raise "Unknown format: #{format}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|