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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +961 -0
  3. data/bin/funl +0 -0
  4. data/bin/vkit +30 -0
  5. data/lib/vkit/cli/api/client.rb +115 -0
  6. data/lib/vkit/cli/base_cli.rb +173 -0
  7. data/lib/vkit/cli/commands/approval_command.rb +94 -0
  8. data/lib/vkit/cli/commands/base_command.rb +42 -0
  9. data/lib/vkit/cli/commands/datasource_command.rb +93 -0
  10. data/lib/vkit/cli/commands/fetch_command.rb +48 -0
  11. data/lib/vkit/cli/commands/login_command.rb +136 -0
  12. data/lib/vkit/cli/commands/logout_command.rb +12 -0
  13. data/lib/vkit/cli/commands/policy_bundle_command.rb +62 -0
  14. data/lib/vkit/cli/commands/policy_deploy_command.rb +32 -0
  15. data/lib/vkit/cli/commands/policy_validate_command.rb +31 -0
  16. data/lib/vkit/cli/commands/request_command.rb +102 -0
  17. data/lib/vkit/cli/commands/requests_list_command.rb +47 -0
  18. data/lib/vkit/cli/commands/scan_command.rb +47 -0
  19. data/lib/vkit/cli/commands/whoami_command.rb +14 -0
  20. data/lib/vkit/cli/commands.rb +5 -0
  21. data/lib/vkit/cli/errors.rb +6 -0
  22. data/lib/vkit/cli/policy_bundle_validator.rb +71 -0
  23. data/lib/vkit/cli/requests_cli.rb +23 -0
  24. data/lib/vkit/cli.rb +4 -0
  25. data/lib/vkit/core/auth_client.rb +104 -0
  26. data/lib/vkit/core/credential_resolver.rb +37 -0
  27. data/lib/vkit/core/credential_store.rb +186 -0
  28. data/lib/vkit/core/table_formatter.rb +36 -0
  29. data/lib/vkit/policy/bundle_compiler.rb +154 -0
  30. data/lib/vkit/policy/schema/policy_bundle.schema.json +296 -0
  31. data/lib/vkit/policy/validate_bundle.rb +37 -0
  32. data/lib/vkit/utils/banner.rb +0 -0
  33. data/lib/vkit/utils/config_loader.rb +0 -0
  34. data/lib/vkit/utils/logger.rb +0 -0
  35. data/lib/vkit.rb +3 -0
  36. 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