vaultkit 0.1.3 → 1.0.1
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/lib/vkit/cli/base_cli.rb +62 -0
- data/lib/vkit/cli/commands/agent_tokens_create_command.rb +74 -0
- data/lib/vkit/cli/commands/agent_tokens_list_command.rb +68 -0
- data/lib/vkit/cli/commands/agent_tokens_revoke_command.rb +72 -0
- data/lib/vkit/cli/commands/approval_watch_command.rb +129 -0
- data/lib/vkit/cli/commands/fetch_command.rb +2 -2
- data/lib/vkit/cli/commands/policy_bundle_command.rb +4 -0
- data/lib/vkit/cli/commands/reset_command.rb +20 -0
- data/lib/vkit/policy/bundle_compiler.rb +4 -0
- data/lib/vkit/policy/policy_validator.rb +129 -0
- data/lib/vkit/policy/validation_error.rb +5 -0
- data/lib/vkit/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4b12d7696d6e2ec91bdf7843bd8648a1c9f0fdce6c59a6c4cbc902b3e4739c8
|
|
4
|
+
data.tar.gz: 7ae893269eb4075c797294585af9f28ad5da121f892b11e7d2e12ea8f625e956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48fc209dc487ee6649012b5d59da82946b37339cf4d3e1875d34c98146e07c79b93147ea510b98674736b6baf81c422a317f699c287103d0fe354e9191aadfb0
|
|
7
|
+
data.tar.gz: 62970b31c4cc95ab323e8f2868e41c4214905e680b427a9fe2475621d3e705b0d690c1ad44c113abd5d0b4186398322485b617cca332376c66abdeaacf0d4f6d
|
data/lib/vkit/cli/base_cli.rb
CHANGED
|
@@ -31,6 +31,11 @@ module Vkit
|
|
|
31
31
|
Commands::LogoutCommand.new.call
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
desc "reset", "Clear all stored credentials and configuration"
|
|
35
|
+
def reset
|
|
36
|
+
Commands::ResetCommand.new.call
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
# REQUEST
|
|
35
40
|
desc "request", "Send an inline JSON AQL request (use --aql or pipe via STDIN)"
|
|
36
41
|
option :aql, type: :string, desc: "AQL JSON payload (inline)"
|
|
@@ -71,6 +76,20 @@ module Vkit
|
|
|
71
76
|
)
|
|
72
77
|
end
|
|
73
78
|
|
|
79
|
+
desc "approvals:watch", "Watch pending approval requests"
|
|
80
|
+
option :interval, type: :numeric, default: 3, desc: "Polling interval in seconds"
|
|
81
|
+
option :format, type: :string, default: "table", enum: %w[table json], desc: "Output format (table for humans, json for automations)"
|
|
82
|
+
option :pretty, type: :boolean, default: false, desc: "Pretty-print JSON output"
|
|
83
|
+
option :since, type: :string, desc: "Only show approvals created after this time (ISO8601 or 10m, 2h)"
|
|
84
|
+
define_method("approvals:watch") do
|
|
85
|
+
Commands::ApprovalWatchCommand.new.call(
|
|
86
|
+
interval: options[:interval],
|
|
87
|
+
format: options[:format],
|
|
88
|
+
pretty: options[:pretty],
|
|
89
|
+
since: options[:since]
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
74
93
|
# FETCH
|
|
75
94
|
desc "fetch --grant ID", "Fetch data from Funl using a valid grant"
|
|
76
95
|
option :grant, type: :string, required: true
|
|
@@ -168,6 +187,49 @@ module Vkit
|
|
|
168
187
|
)
|
|
169
188
|
end
|
|
170
189
|
}
|
|
190
|
+
|
|
191
|
+
desc "agents SUBCOMMAND ...ARGS", "Manage agents and automation identities"
|
|
192
|
+
subcommand "agents", Class.new(Thor) {
|
|
193
|
+
|
|
194
|
+
# agents tokens SUBCOMMAND
|
|
195
|
+
desc "tokens SUBCOMMAND ...ARGS", "Manage agent tokens"
|
|
196
|
+
subcommand "tokens", Class.new(Thor) {
|
|
197
|
+
|
|
198
|
+
# agents tokens list
|
|
199
|
+
desc "list", "List tokens for an agent"
|
|
200
|
+
option :format, type: :string, default: "table", enum: %w[table json]
|
|
201
|
+
def list
|
|
202
|
+
Commands::AgentTokensListCommand.new.call(
|
|
203
|
+
agent: options[:agent],
|
|
204
|
+
format: options[:format]
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# agents tokens create
|
|
209
|
+
desc "create", "Create a new agent token (automation identity)"
|
|
210
|
+
option :name, required: true, desc: "Human-readable name (e.g. billing-bot)"
|
|
211
|
+
option :expires_in, type: :string, desc: "Token lifetime (e.g. 1h, 24h, 30d)"
|
|
212
|
+
option :role, type: :string, default: "agent", desc: "Role assigned to this token"
|
|
213
|
+
def create
|
|
214
|
+
Commands::AgentTokensCreateCommand.new.call(
|
|
215
|
+
name: options[:name],
|
|
216
|
+
expires_in: options[:expires_in],
|
|
217
|
+
role: options[:role]
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# agents tokens revoke
|
|
222
|
+
desc "revoke", "Revoke an agent token"
|
|
223
|
+
option :token, required: true, desc: "Token ID or prefix"
|
|
224
|
+
option :force, type: :boolean, default: false, desc: "Skip confirmation"
|
|
225
|
+
def revoke
|
|
226
|
+
Commands::AgentTokensRevokeCommand.new.call(
|
|
227
|
+
token: options[:token],
|
|
228
|
+
force: options[:force]
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
}
|
|
232
|
+
}
|
|
171
233
|
end
|
|
172
234
|
end
|
|
173
235
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class AgentTokensCreateCommand < BaseCommand
|
|
10
|
+
def call(options)
|
|
11
|
+
with_auth do
|
|
12
|
+
user = credential_store.user
|
|
13
|
+
org = user["organization_slug"]
|
|
14
|
+
|
|
15
|
+
response =
|
|
16
|
+
authenticated_client.post(
|
|
17
|
+
"/api/v1/orgs/#{org}/agent_tokens",
|
|
18
|
+
body: build_body(options)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
token = response.fetch("token")
|
|
22
|
+
secret = response.fetch("secret")
|
|
23
|
+
|
|
24
|
+
puts "🔐 AGENT TOKEN ISSUED (shown once)"
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
puts "🧠 Name: #{token["name"]}"
|
|
28
|
+
puts "🎭 Role: #{token["role"]}"
|
|
29
|
+
puts "🆔 Token ID: #{token["id"]}"
|
|
30
|
+
puts "🏷 Prefix: #{token["prefix"]}"
|
|
31
|
+
puts "⏳ Expires At: #{token["expires_at"] || "never"}"
|
|
32
|
+
puts
|
|
33
|
+
|
|
34
|
+
puts "🔑 TOKEN"
|
|
35
|
+
puts "────────────────────────────────────────"
|
|
36
|
+
puts secret
|
|
37
|
+
puts "────────────────────────────────────────"
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
puts "⚠️ Store this token securely. It cannot be retrieved again."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_body(options)
|
|
47
|
+
body = {
|
|
48
|
+
name: options.fetch(:name),
|
|
49
|
+
role: options[:role],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if options[:expires_in]
|
|
53
|
+
body[:expires_at] = parse_expires_in(options[:expires_in]).iso8601
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
body
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_expires_in(value)
|
|
60
|
+
number = value.to_i
|
|
61
|
+
|
|
62
|
+
case value
|
|
63
|
+
when /\A\d+h\z/
|
|
64
|
+
Time.now.utc + (number * 60 * 60)
|
|
65
|
+
when /\A\d+d\z/
|
|
66
|
+
Time.now.utc + (number * 24 * 60 * 60)
|
|
67
|
+
else
|
|
68
|
+
raise "Invalid expires_in format (use 1h, 24h, 30d)"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../../core/table_formatter"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class AgentTokensListCommand < BaseCommand
|
|
10
|
+
def call(options)
|
|
11
|
+
format = options[:format] || "table"
|
|
12
|
+
|
|
13
|
+
with_auth do
|
|
14
|
+
user = credential_store.user
|
|
15
|
+
org = user["organization_slug"]
|
|
16
|
+
|
|
17
|
+
response =
|
|
18
|
+
authenticated_client.get(
|
|
19
|
+
"/api/v1/orgs/#{org}/agent_tokens",
|
|
20
|
+
params: build_query(options)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
tokens = response["tokens"] || []
|
|
24
|
+
|
|
25
|
+
print_result(tokens, format)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_query(options)
|
|
32
|
+
{}.tap do |q|
|
|
33
|
+
q[:name] = options[:agent] if options[:agent]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def print_result(tokens, format)
|
|
38
|
+
if tokens.empty?
|
|
39
|
+
puts "No tokens found."
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
case format
|
|
44
|
+
when "json"
|
|
45
|
+
puts JSON.pretty_generate(tokens)
|
|
46
|
+
|
|
47
|
+
when "table"
|
|
48
|
+
Vkit::Core::TableFormatter.render(
|
|
49
|
+
tokens.map do |t|
|
|
50
|
+
{
|
|
51
|
+
"ID" => t["id"],
|
|
52
|
+
"Agent" => t["name"],
|
|
53
|
+
"Prefix" => t["prefix"],
|
|
54
|
+
"Expires At" => t["expires_at"] || "never",
|
|
55
|
+
"Revoked At" => t["revoked_at"] || "-",
|
|
56
|
+
"Created At" => t["created_at"]
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
else
|
|
62
|
+
raise "Unknown format: #{format}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vkit
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class AgentTokensRevokeCommand < BaseCommand
|
|
9
|
+
def call(options)
|
|
10
|
+
token_ref = options.fetch(:token)
|
|
11
|
+
|
|
12
|
+
with_auth do
|
|
13
|
+
user = credential_store.user
|
|
14
|
+
org = user["organization_slug"]
|
|
15
|
+
|
|
16
|
+
token = resolve_token!(org, token_ref)
|
|
17
|
+
|
|
18
|
+
unless options[:force]
|
|
19
|
+
confirm!(token)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
authenticated_client.post(
|
|
23
|
+
"/api/v1/orgs/#{org}/agent_tokens/#{token["id"]}/revoke",
|
|
24
|
+
body: {}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
puts "🛑 AGENT TOKEN REVOKED"
|
|
28
|
+
puts
|
|
29
|
+
puts "🧠 Name: #{token["name"]}"
|
|
30
|
+
puts "🏷 Prefix: #{token["prefix"]}"
|
|
31
|
+
puts "🆔 ID: #{token["id"]}"
|
|
32
|
+
puts
|
|
33
|
+
puts "✅ The token is no longer valid."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def resolve_token!(org, token_ref)
|
|
40
|
+
response =
|
|
41
|
+
authenticated_client.get(
|
|
42
|
+
"/api/v1/orgs/#{org}/agent_tokens"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
tokens = response["tokens"] || []
|
|
46
|
+
|
|
47
|
+
token =
|
|
48
|
+
tokens.find do |t|
|
|
49
|
+
t["id"] == token_ref || t["prefix"] == token_ref
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
raise "Agent token not found: #{token_ref}" unless token
|
|
53
|
+
|
|
54
|
+
token
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def confirm!(token)
|
|
58
|
+
puts "⚠️ You are about to revoke this agent token:"
|
|
59
|
+
puts
|
|
60
|
+
puts "🧠 Name: #{token["name"]}"
|
|
61
|
+
puts "🏷 Prefix: #{token["prefix"]}"
|
|
62
|
+
puts "🆔 ID: #{token["id"]}"
|
|
63
|
+
puts
|
|
64
|
+
print "\nType 'revoke' to confirm: "
|
|
65
|
+
|
|
66
|
+
input = STDIN.gets&.strip
|
|
67
|
+
abort "Aborted." unless input == "revoke"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Vkit
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class ApprovalWatchCommand < BaseCommand
|
|
10
|
+
DEFAULT_INTERVAL = 3
|
|
11
|
+
|
|
12
|
+
def call(interval: DEFAULT_INTERVAL, format: "table", pretty: false, since: nil)
|
|
13
|
+
with_auth do
|
|
14
|
+
user = credential_store.user
|
|
15
|
+
org = user["organization_slug"]
|
|
16
|
+
|
|
17
|
+
since_time = parse_since(since)
|
|
18
|
+
seen = {}
|
|
19
|
+
|
|
20
|
+
if format == "table"
|
|
21
|
+
puts "⏳ Watching approval queue (Ctrl+C to stop)…"
|
|
22
|
+
puts "Since: #{since_time.iso8601}" if since_time
|
|
23
|
+
puts
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
approvals =
|
|
28
|
+
authenticated_client.get(
|
|
29
|
+
"/api/v1/orgs/#{org}/approvals",
|
|
30
|
+
params: build_query(since_time)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
approvals.each do |approval|
|
|
34
|
+
next if seen[approval["id"]]
|
|
35
|
+
|
|
36
|
+
emit(approval, format, pretty)
|
|
37
|
+
seen[approval["id"]] = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sleep interval
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
rescue Interrupt
|
|
44
|
+
puts "\n👋 Watch stopped." if format == "table"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_query(since_time)
|
|
50
|
+
{}.tap do |q|
|
|
51
|
+
q[:state] = "pending"
|
|
52
|
+
q[:since] = since_time.iso8601 if since_time
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_since(value)
|
|
57
|
+
return nil if value.nil?
|
|
58
|
+
|
|
59
|
+
now = Time.now.utc
|
|
60
|
+
|
|
61
|
+
case value
|
|
62
|
+
when /\A(\d+)m\z/
|
|
63
|
+
now - Regexp.last_match(1).to_i * 60
|
|
64
|
+
when /\A(\d+)h\z/
|
|
65
|
+
now - Regexp.last_match(1).to_i * 60 * 60
|
|
66
|
+
when /\A(\d+)d\z/
|
|
67
|
+
now - Regexp.last_match(1).to_i * 60 * 60 * 24
|
|
68
|
+
else
|
|
69
|
+
Time.iso8601(value)
|
|
70
|
+
end
|
|
71
|
+
rescue ArgumentError
|
|
72
|
+
raise "Invalid --since value (use ISO8601 or 10m, 2h, 1d)"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit(approval, format, pretty)
|
|
76
|
+
case format
|
|
77
|
+
when "json"
|
|
78
|
+
print_json(normalize(approval), pretty: pretty)
|
|
79
|
+
when "table"
|
|
80
|
+
render_human(approval)
|
|
81
|
+
else
|
|
82
|
+
raise "Unknown format: #{format}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_json(obj, pretty:)
|
|
87
|
+
if pretty
|
|
88
|
+
puts JSON.pretty_generate(obj)
|
|
89
|
+
else
|
|
90
|
+
puts JSON.generate(obj)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalize(a)
|
|
95
|
+
{
|
|
96
|
+
type: "approval.pending",
|
|
97
|
+
id: a["id"],
|
|
98
|
+
dataset: a["dataset"],
|
|
99
|
+
fields: a["fields"],
|
|
100
|
+
requester: a["requester"],
|
|
101
|
+
approver_role: a["approver_role"],
|
|
102
|
+
created_at: a["created_at"]
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_human(a)
|
|
107
|
+
puts "🔔 NEW APPROVAL"
|
|
108
|
+
puts "────────────────────────────"
|
|
109
|
+
puts "ID: #{a["id"]}"
|
|
110
|
+
puts "Dataset: #{a["dataset"]}"
|
|
111
|
+
puts "Fields: #{Array(a["fields"]).join(", ")}"
|
|
112
|
+
puts "Requester: #{a["requester"]}"
|
|
113
|
+
puts "Role Req: #{a["approver_role"] || "any"}"
|
|
114
|
+
puts "Created: #{format_time(a["created_at"])}"
|
|
115
|
+
puts
|
|
116
|
+
puts "▶ Approve: vkit approval approve #{a["id"]}"
|
|
117
|
+
puts "▶ Deny: vkit approval deny #{a["id"]} --reason \"…\""
|
|
118
|
+
puts
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_time(value)
|
|
122
|
+
Time.parse(value).getlocal.strftime("%Y-%m-%d %H:%M:%S")
|
|
123
|
+
rescue
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../../policy/bundle_compiler"
|
|
6
|
+
require_relative "../../policy/validation_error"
|
|
6
7
|
|
|
7
8
|
module Vkit
|
|
8
9
|
module CLI
|
|
@@ -58,6 +59,9 @@ module Vkit
|
|
|
58
59
|
puts " Checksum: #{bundle.dig("bundle", "checksum")}"
|
|
59
60
|
puts " Output: #{out}"
|
|
60
61
|
end
|
|
62
|
+
rescue Vkit::Policy::ValidationError => e
|
|
63
|
+
puts e.message
|
|
64
|
+
exit 1
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
private
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Vkit
|
|
2
|
+
module CLI
|
|
3
|
+
module Commands
|
|
4
|
+
class ResetCommand < BaseCommand
|
|
5
|
+
def call
|
|
6
|
+
print "⚠️ This will clear all stored credentials. Continue? (y/N): "
|
|
7
|
+
response = $stdin.gets.chomp
|
|
8
|
+
|
|
9
|
+
unless response.downcase == 'y'
|
|
10
|
+
puts "Cancelled"
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
credential_store.clear!
|
|
15
|
+
puts "🧹 All credentials cleared"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -2,6 +2,7 @@ require "json"
|
|
|
2
2
|
require "yaml"
|
|
3
3
|
require "digest"
|
|
4
4
|
require "time"
|
|
5
|
+
require_relative "policy_validator"
|
|
5
6
|
|
|
6
7
|
module Vkit
|
|
7
8
|
module Policy
|
|
@@ -40,6 +41,9 @@ module Vkit
|
|
|
40
41
|
files.map do |f|
|
|
41
42
|
data = YAML.load_file(f)
|
|
42
43
|
raise "Policy file #{f} must be a Hash" unless data.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
PolicyValidator.validate!(data, file: File.basename(f))
|
|
46
|
+
|
|
43
47
|
data.merge("__file" => File.basename(f))
|
|
44
48
|
end
|
|
45
49
|
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Vkit
|
|
2
|
+
module Policy
|
|
3
|
+
class PolicyValidator
|
|
4
|
+
ACTION_KEYS = %w[deny require_approval mask allow].freeze
|
|
5
|
+
|
|
6
|
+
def self.validate!(policy, file: nil)
|
|
7
|
+
prefix = file ? "In #{file}:\n\n" : ""
|
|
8
|
+
|
|
9
|
+
require_string!(policy, "id", prefix)
|
|
10
|
+
require_string!(policy, "description", prefix)
|
|
11
|
+
|
|
12
|
+
action = policy["action"]
|
|
13
|
+
unless action.is_a?(Hash)
|
|
14
|
+
raise ValidationError, <<~MSG
|
|
15
|
+
#{prefix}Invalid action format.
|
|
16
|
+
|
|
17
|
+
Expected:
|
|
18
|
+
|
|
19
|
+
action:
|
|
20
|
+
deny: true
|
|
21
|
+
|
|
22
|
+
Got:
|
|
23
|
+
|
|
24
|
+
action: #{action.inspect}
|
|
25
|
+
|
|
26
|
+
VaultKit requires `action` to be a mapping so it can attach
|
|
27
|
+
metadata like reason, ttl, approvals, and masking rules.
|
|
28
|
+
MSG
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
validate_action!(action, prefix)
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.require_string!(hash, key, prefix)
|
|
36
|
+
value = hash[key]
|
|
37
|
+
return if value.is_a?(String) && !value.strip.empty?
|
|
38
|
+
|
|
39
|
+
raise ValidationError, <<~MSG
|
|
40
|
+
#{prefix}#{key} is required and must be a non-empty string.
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.validate_action!(action, prefix)
|
|
45
|
+
intents = ACTION_KEYS.select { |k| action[k] == true }
|
|
46
|
+
|
|
47
|
+
if intents.empty?
|
|
48
|
+
raise ValidationError, <<~MSG
|
|
49
|
+
#{prefix}Action must specify exactly one intent.
|
|
50
|
+
|
|
51
|
+
Choose one of:
|
|
52
|
+
|
|
53
|
+
- deny
|
|
54
|
+
- require_approval
|
|
55
|
+
- mask
|
|
56
|
+
- allow
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
action:
|
|
61
|
+
deny: true
|
|
62
|
+
MSG
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if intents.size > 1
|
|
66
|
+
raise ValidationError, <<~MSG
|
|
67
|
+
#{prefix}Action specifies multiple intents: #{intents.join(', ')}.
|
|
68
|
+
|
|
69
|
+
Only one intent may be set to true.
|
|
70
|
+
MSG
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
intent = intents.first
|
|
74
|
+
|
|
75
|
+
case intent
|
|
76
|
+
when "deny"
|
|
77
|
+
require_reason!(action, prefix)
|
|
78
|
+
|
|
79
|
+
when "require_approval"
|
|
80
|
+
require_reason!(action, prefix)
|
|
81
|
+
require_string!(action, "approver_role", prefix)
|
|
82
|
+
|
|
83
|
+
when "mask"
|
|
84
|
+
# masking config can be validated later
|
|
85
|
+
|
|
86
|
+
when "allow"
|
|
87
|
+
# nothing required
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
validate_ttl!(action["ttl"], prefix) if action.key?("ttl")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.require_reason!(action, prefix)
|
|
94
|
+
reason = action["reason"]
|
|
95
|
+
return if reason.is_a?(String) && !reason.strip.empty?
|
|
96
|
+
|
|
97
|
+
raise ValidationError, <<~MSG
|
|
98
|
+
#{prefix}action.reason is required and must be a non-empty string
|
|
99
|
+
when using deny or require_approval.
|
|
100
|
+
MSG
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.validate_ttl!(ttl, prefix)
|
|
104
|
+
case ttl
|
|
105
|
+
when Integer
|
|
106
|
+
return
|
|
107
|
+
when String
|
|
108
|
+
return if ttl.match?(/\A\d+[smhd]\z/)
|
|
109
|
+
|
|
110
|
+
raise ValidationError, <<~MSG
|
|
111
|
+
#{prefix}Invalid ttl format.
|
|
112
|
+
|
|
113
|
+
Expected examples:
|
|
114
|
+
- "30m"
|
|
115
|
+
- "1h"
|
|
116
|
+
- "2d"
|
|
117
|
+
|
|
118
|
+
Got:
|
|
119
|
+
ttl: #{ttl.inspect}
|
|
120
|
+
MSG
|
|
121
|
+
else
|
|
122
|
+
raise ValidationError, <<~MSG
|
|
123
|
+
#{prefix}ttl must be a string duration (e.g. "1h") or an integer (seconds).
|
|
124
|
+
MSG
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/vkit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vaultkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nnamdi Ogundu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -40,7 +40,11 @@ files:
|
|
|
40
40
|
- lib/vkit/cli/api/client.rb
|
|
41
41
|
- lib/vkit/cli/base_cli.rb
|
|
42
42
|
- lib/vkit/cli/commands.rb
|
|
43
|
+
- lib/vkit/cli/commands/agent_tokens_create_command.rb
|
|
44
|
+
- lib/vkit/cli/commands/agent_tokens_list_command.rb
|
|
45
|
+
- lib/vkit/cli/commands/agent_tokens_revoke_command.rb
|
|
43
46
|
- lib/vkit/cli/commands/approval_command.rb
|
|
47
|
+
- lib/vkit/cli/commands/approval_watch_command.rb
|
|
44
48
|
- lib/vkit/cli/commands/base_command.rb
|
|
45
49
|
- lib/vkit/cli/commands/datasource_command.rb
|
|
46
50
|
- lib/vkit/cli/commands/fetch_command.rb
|
|
@@ -51,6 +55,7 @@ files:
|
|
|
51
55
|
- lib/vkit/cli/commands/policy_validate_command.rb
|
|
52
56
|
- lib/vkit/cli/commands/request_command.rb
|
|
53
57
|
- lib/vkit/cli/commands/requests_list_command.rb
|
|
58
|
+
- lib/vkit/cli/commands/reset_command.rb
|
|
54
59
|
- lib/vkit/cli/commands/scan_command.rb
|
|
55
60
|
- lib/vkit/cli/commands/whoami_command.rb
|
|
56
61
|
- lib/vkit/cli/errors.rb
|
|
@@ -61,8 +66,10 @@ files:
|
|
|
61
66
|
- lib/vkit/core/credential_store.rb
|
|
62
67
|
- lib/vkit/core/table_formatter.rb
|
|
63
68
|
- lib/vkit/policy/bundle_compiler.rb
|
|
69
|
+
- lib/vkit/policy/policy_validator.rb
|
|
64
70
|
- lib/vkit/policy/schema/policy_bundle.schema.json
|
|
65
71
|
- lib/vkit/policy/validate_bundle.rb
|
|
72
|
+
- lib/vkit/policy/validation_error.rb
|
|
66
73
|
- lib/vkit/utils/banner.rb
|
|
67
74
|
- lib/vkit/utils/config_loader.rb
|
|
68
75
|
- lib/vkit/utils/logger.rb
|