152 lines
5.5 KiB
Zig
152 lines
5.5 KiB
Zig
//! Adds Alexa skill-specific Lambda permission.
|
|
//! Alexa requires the Lambda policy to include the skill ID as an event source token condition.
|
|
|
|
const std = @import("std");
|
|
const aws = @import("aws");
|
|
const json = std.json;
|
|
|
|
pub fn main() !u8 {
|
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
var stdout_buf: [4096]u8 = undefined;
|
|
var stderr_buf: [4096]u8 = undefined;
|
|
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
|
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
|
|
const stdout = &stdout_writer.interface;
|
|
const stderr = &stderr_writer.interface;
|
|
|
|
const args = try std.process.argsAlloc(allocator);
|
|
defer std.process.argsFree(allocator, args);
|
|
|
|
if (args.len < 3) {
|
|
try stderr.print("Usage: {s} <deploy-output.json> <ask-states.json> [--profile <profile>] [--region <region>]\n", .{args[0]});
|
|
try stderr.flush();
|
|
return 1;
|
|
}
|
|
|
|
// Parse optional arguments
|
|
var profile: ?[]const u8 = null;
|
|
var region_override: ?[]const u8 = null;
|
|
var i: usize = 3;
|
|
while (i < args.len) : (i += 1) {
|
|
if (std.mem.eql(u8, args[i], "--profile") and i + 1 < args.len) {
|
|
profile = args[i + 1];
|
|
i += 1;
|
|
} else if (std.mem.eql(u8, args[i], "--region") and i + 1 < args.len) {
|
|
region_override = args[i + 1];
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
// Read deploy output to get function name and region
|
|
const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| {
|
|
try stderr.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err });
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
defer allocator.free(deploy_output);
|
|
|
|
const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| {
|
|
try stderr.print("Failed to parse deploy output: {}\n", .{err});
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
defer deploy_parsed.deinit();
|
|
|
|
const function_name = deploy_parsed.value.object.get("function_name").?.string;
|
|
const region = region_override orelse deploy_parsed.value.object.get("region").?.string;
|
|
|
|
// Read ask-states.json to get skill ID
|
|
const ask_states = std.fs.cwd().readFileAlloc(allocator, args[2], 1024 * 1024) catch |err| {
|
|
try stderr.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err });
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
defer allocator.free(ask_states);
|
|
|
|
const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| {
|
|
try stderr.print("Failed to parse ask-states.json: {}\n", .{err});
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
defer ask_parsed.deinit();
|
|
|
|
// Use profile name to look up skill ID, defaulting to "default"
|
|
const profile_name = profile orelse "default";
|
|
const profiles_obj = ask_parsed.value.object.get("profiles") orelse {
|
|
try stderr.print("No 'profiles' field in ask-states.json\n", .{});
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
const profile_obj = profiles_obj.object.get(profile_name) orelse {
|
|
try stderr.print("Profile '{s}' not found in ask-states.json\n", .{profile_name});
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
const skill_id = profile_obj.object.get("skillId").?.string;
|
|
|
|
try stdout.print("Adding Alexa permission for skill {s} to function {s} in {s}", .{ skill_id, function_name, region });
|
|
if (profile) |p| try stdout.print(" (profile: {s})", .{p});
|
|
try stdout.print("\n", .{});
|
|
try stdout.flush();
|
|
|
|
// Build statement ID from skill ID (use last 12 chars to keep it short but unique)
|
|
var statement_id_buf: [64]u8 = undefined;
|
|
const statement_id = std.fmt.bufPrint(&statement_id_buf, "alexa-skill-{s}", .{skill_id[skill_id.len - 12 ..]}) catch {
|
|
try stderr.print("Failed to build statement ID\n", .{});
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
|
|
// Create AWS client and options
|
|
var client = aws.Client.init(allocator, .{});
|
|
defer client.deinit();
|
|
|
|
var diagnostics: aws.Diagnostics = .{
|
|
.response_status = undefined,
|
|
.response_body = undefined,
|
|
.allocator = allocator,
|
|
};
|
|
|
|
const opts = aws.Options{
|
|
.client = client,
|
|
.region = region,
|
|
.diagnostics = &diagnostics,
|
|
.credential_options = .{
|
|
.profile = .{
|
|
.profile_name = profile,
|
|
.prefer_profile_from_file = profile != null,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Add permission with skill ID as event source token
|
|
const services = aws.Services(.{.lambda}){};
|
|
|
|
_ = aws.Request(services.lambda.add_permission).call(.{
|
|
.function_name = function_name,
|
|
.statement_id = statement_id,
|
|
.action = "lambda:InvokeFunction",
|
|
.principal = "alexa-appkit.amazon.com",
|
|
.event_source_token = skill_id,
|
|
}, opts) catch |err| {
|
|
defer diagnostics.deinit();
|
|
|
|
// 409 Conflict means permission already exists - that's fine
|
|
if (diagnostics.response_status == .conflict) {
|
|
try stdout.print("Permission already exists for skill: {s}\n", .{skill_id});
|
|
try stdout.flush();
|
|
return 0;
|
|
}
|
|
|
|
try stderr.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status });
|
|
try stderr.flush();
|
|
return 1;
|
|
};
|
|
|
|
try stdout.print("Added Alexa permission for skill: {s}\n", .{skill_id});
|
|
try stdout.flush();
|
|
return 0;
|
|
}
|