commit b5d425e806a193c0389e8e90d9a6dbf9835631a3 Author: Jeffrey C. Ollie Date: Thu Feb 29 21:39:33 2024 -0600 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dc382c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/zig-cache +/zig-out + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a18b9d4 --- /dev/null +++ b/build.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("vault", .{ + .root_source_file = .{ .path = "src/root.zig" }, + }); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/root.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..b0dd9dc --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = "zig-vault", + .version = "0.0.0", + .dependencies = .{}, + + .paths = .{ + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c8a3f67 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn main() !void { + // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + + // stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try bw.flush(); // don't forget to flush! +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..b440787 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,231 @@ +const std = @import("std"); + +/// KeyValue secret engine, version 2 +pub const KV2 = struct { + base_url: std.Uri, + namespace: ?[]const u8 = null, + mount_path: []const u8, + token: []const u8, + + fn do( + self: @This(), + alloc: std.mem.Allocator, + comptime T: type, + method: std.http.Method, + path: []const u8, + payload: ?[]const u8, + ) !std.json.Parsed(T) { + var client = std.http.Client{ .allocator = alloc }; + defer client.deinit(); + + var authoriziation_buf: [128]u8 = undefined; + const authoriziation = try std.fmt.bufPrint(&authoriziation_buf, "Bearer {s}", .{self.token}); + + var namespace_buf: [128]u8 = undefined; + const extra_headers = h: { + if (self.namespace) |namespace| { + break :h &[_]std.http.Header{ + .{ + .name = "X-Vault-Namespace", + .value = try std.fmt.bufPrint(&namespace_buf, "{s}/", .{namespace}), + }, + .{ + .name = "Accept", + .value = "application/json", + }, + }; + } else { + break :h &[_]std.http.Header{ + .{ + .name = "Accept", + .value = "application/json", + }, + }; + } + }; + + var aux_buf: [1024]u8 = undefined; + const url = try self.base_url.resolve_inplace(path, &aux_buf); + + var server_header_buf: [2048]u8 = undefined; + var req = try client.open( + method, + url, + .{ + .server_header_buffer = &server_header_buf, + .headers = .{ + .authorization = .{ + .override = authoriziation, + }, + .content_type = .{ + .override = "application/json", + }, + }, + .extra_headers = extra_headers, + }, + ); + defer req.deinit(); + + if (payload) |p| req.transfer_encoding = .{ .content_length = p.len }; + + try req.send(.{}); + + if (payload) |p| try req.writeAll(p); + + try req.finish(); + try req.wait(); + + var result_buf = std.ArrayList(u8).init(alloc); + errdefer result_buf.deinit(); + + const reader = req.reader(); + + while (true) { + var buf: [4096]u8 = undefined; + const len = try reader.read(&buf); + if (len > 0) try result_buf.appendSlice(buf[0..len]); + if (len == 0) break; + } + + return try std.json.parseFromSlice( + T, + alloc, + try result_buf.toOwnedSlice(), + .{}, + ); + } + + pub fn ReadResult(comptime T: type) type { + return struct { + request_id: []const u8, + lease_id: []const u8, + renewable: bool, + lease_duration: u64, + data: struct { + data: T, + metadata: struct { + created_time: []const u8, + custom_metadata: ?std.json.Value = null, + deletion_time: []const u8, + destroyed: bool, + version: u64, + }, + }, + wrap_info: ?std.json.Value = null, + warnings: ?std.json.Value = null, + auth: ?std.json.Value = null, + }; + } + + pub fn read(self: @This(), alloc: std.mem.Allocator, comptime T: type, secret_path: []const u8, version: ?u64) !std.json.Parsed(ReadResult(T)) { + var query_buf: [1024]u8 = undefined; + + const query = q: { + if (version) |v| break :q try std.fmt.bufPrint(&query_buf, "?version={d}", .{v}); + break :q ""; + }; + + var path_buf: [1024]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "/v1/{s}/data/{s}{s}", .{ self.mount_path, secret_path, query }); + + return try self.do(alloc, ReadResult(T), .GET, path, null); + } + + pub fn ReadSubkeyResult(comptime T: type) type { + return struct { + subkeys: T, + metadata: struct { + created_time: []const u8, + custom_metadata: ?std.json.Value = null, + deletion_time: []const u8, + destroyed: bool, + version: u64, + }, + }; + } + + pub fn read_subkeys( + self: @This(), + alloc: std.mem.Allocator, + comptime T: type, + secret_path: []const u8, + version: ?u64, + depth: ?u64, + ) !std.json.Parsed(ReadSubkeyResult(T)) { + var query_buf: [1024]u8 = undefined; + var query_fbs = std.io.fixedBufferStream(&query_buf); + const query_writer = query_fbs.writer(); + + if (version) |v| try query_writer.print("?version={d}", .{v}); + if (version == null and depth != null) try query_writer.writeAll("?"); + if (version != null and depth != null) try query_writer.writeAll("&"); + if (depth) |d| try query_writer.print("depth={d}", .{d}); + + var path_buf: [1024]u8 = undefined; + const path = std.fmt.bufPrint(&path_buf, "/v1/{s}/subkeys/{s}{s}", .{ self.mount_path, secret_path, query_fbs.getWritten() }); + + return try self.do(alloc, ReadResult(T), .GET, path, null); + } + + pub const CreateOrUpdateResult = struct { + data: struct { + created_time: []const u8, + custom_metadata: ?std.json.Value = null, + deletion_time: []const u8, + destroyed: bool, + version: u64, + }, + }; + + pub fn create_or_update(self: @This(), alloc: std.mem.Allocator, comptime T: type, secret_path: []const u8, data: T, cas: ?u64) !std.json.Parsed(CreateOrUpdateResult) { + var path_buf: [1024]u8 = undefined; + const path = std.fmt.bufPrint(&path_buf, "/v1/{s}/data/{s}", .{ self.mount_path, secret_path }); + + var payload_buf: [1024]u8 = undefined; + var payload_fbs = std.io.fixedBufferStream(&payload_buf); + const payload_writer = payload_fbs.writer(); + + const payload = p: { + if (cas) |c| { + break :p struct { + options: struct { + cas: u64, + }, + data: T, + }{ + .options = .{ .cas = c }, + .data = data, + }; + } else { + break :p struct { + data: T, + }{ + .data = data, + }; + } + }; + + try std.json.stringify(payload, .{}, payload_writer); + + return try self.do(alloc, CreateOrUpdateResult, .POST, path, payload_fbs.getWritten()); + } + + // pub const DeleteResult = struct { + + // }; + // pub fn delete(self: @This(), alloc: std.mem.Allocator, mount_path: []const u8, secret_path: []const u8) !std.json.Parsed(DeleteResult) { + + // } + pub const ListResult = struct { + data: struct { + keys: [][]const u8, + }, + }; + + pub fn list(self: @This(), alloc: std.mem.Allocator, secret_path: []const u8) !std.json.Parsed(ListResult) { + var path_buf: [1024]u8 = undefined; + const path = std.fmt.bufPrint(&path_buf, "/v1/{s}/metadata/{s}", .{ self.mount_path, secret_path }); + + return try self.do(alloc, ListResult, .LIST, path, null); + } +};