From 24926fe4b0810405a459ec37be9c410b1b015c8c Mon Sep 17 00:00:00 2001 From: mh Date: Tue, 26 Nov 2024 23:43:33 +0100 Subject: [PATCH] add support for hooks --- README.md | 31 +++++++++++++++++++++++ lib/trocla.rb | 9 +++++++ lib/trocla/hooks.rb | 32 +++++++++++++++++++++++ spec/fixtures/delete_test_hook.rb | 12 +++++++++ spec/fixtures/set_test_hook.rb | 12 +++++++++ spec/spec_helper.rb | 17 +++++++++++-- spec/trocla/hooks_spec.rb | 42 +++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 lib/trocla/hooks.rb create mode 100644 spec/fixtures/delete_test_hook.rb create mode 100644 spec/fixtures/set_test_hook.rb create mode 100644 spec/trocla/hooks_spec.rb diff --git a/README.md b/README.md index 45f5f73..39bb25f 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,37 @@ encryption_options: :public_key: '/var/lib/puppet/ssl/public_keys/trocla.pem' ``` +## Hooks + +You can specify hooks to be called whenever trocla sets or deletes a password. The idea is that this allows you to run custom code that can trigger further actions based on deleting or setting a password. + +Enabling hooks is done through the following configuration: + +```YAML +hooks: + set: + my_hook: /path/to/my_hook_file.rb + delete: + other_hook: /path/to/my_other_hook_file.rb +``` + +A hook must have the following implementation based on the above config: + +```Ruby +class Trocla + module Hooks + def self.my_hook(trocla, key, format, options) + # [... your code ...] + end + end +end +``` +You can specify only either one or both kinds of hooks. + +Hooks must not raise any exceptions or interrupt the flow itself. They can also not change the value that was set or revert a deletion. + +However, they have Trocla itself available (through `trocla`) and you must ensure to not create infinite loops. + ## Update & Changes See [Changelog](CHANGELOG.md) diff --git a/lib/trocla.rb b/lib/trocla.rb index 3dd37a8..bfb92e9 100644 --- a/lib/trocla.rb +++ b/lib/trocla.rb @@ -3,6 +3,7 @@ require 'trocla/formats' require 'trocla/encryptions' require 'trocla/stores' +require 'trocla/hooks' # Trocla class class Trocla @@ -67,6 +68,7 @@ def reset_password(key, format, options = {}) def delete_password(key, format = nil, options = {}) v = store.delete(key, format) + hooks_runner.run('delete', key, format, options) if v.is_a?(Hash) Hash[*v.map do |f, encrypted_value| [f, render(format, decrypt(encrypted_value), options)] @@ -78,6 +80,7 @@ def delete_password(key, format = nil, options = {}) def set_password(key, format, password, options = {}) store.set(key, format, encrypt(password), options) + hooks_runner.run('set', key, format, options) render(format, password, options) end @@ -122,6 +125,12 @@ def build_store clazz.new(config['store_options'], self) end + def hooks_runner + @hooks_runner ||= begin + Trocla::Hooks::Runner.new(self) + end + end + def read_config if @config_file.nil? default_config diff --git a/lib/trocla/hooks.rb b/lib/trocla/hooks.rb new file mode 100644 index 0000000..09ab88e --- /dev/null +++ b/lib/trocla/hooks.rb @@ -0,0 +1,32 @@ +class Trocla + module Hooks + class Runner + attr_reader :trocla + def initialize(trocla) + @trocla = trocla + end + + def run(action, key, format, options) + hooks[action].each do |cmd| + Trocla::Hooks.send(cmd, trocla, key, format, options) + end + end + + private + + def hooks + @hooks ||= begin + res = {} + (trocla.config['hooks'] || {}).each do |action,action_hooks| + res[action] ||= [] + action_hooks.each do |cmd,file| + require File.join(file) + res[action] << cmd + end + end + res + end + end + end + end +end diff --git a/spec/fixtures/delete_test_hook.rb b/spec/fixtures/delete_test_hook.rb new file mode 100644 index 0000000..be90a46 --- /dev/null +++ b/spec/fixtures/delete_test_hook.rb @@ -0,0 +1,12 @@ +class Trocla + module Hooks + + def self.delete_test_hook(trocla, key, format, options) + self.delete_messages << "#{key}_#{format}" + end + + def self.delete_messages + @delete_messages ||= [] + end + end +end diff --git a/spec/fixtures/set_test_hook.rb b/spec/fixtures/set_test_hook.rb new file mode 100644 index 0000000..aa35807 --- /dev/null +++ b/spec/fixtures/set_test_hook.rb @@ -0,0 +1,12 @@ +class Trocla + module Hooks + + def self.set_test_hook(trocla, key, format, options) + self.set_messages << "#{key}_#{format}" + end + + def self.set_messages + @set_messages ||= [] + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a1f0b99..e6bc1b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -241,13 +241,13 @@ def default_config end def test_config - @config ||= default_config.merge({ + @test_config ||= default_config.merge({ 'store' => :memory, }) end def test_config_persistent - @config ||= default_config.merge({ + @test_config_persistent ||= default_config.merge({ 'store_options' => { 'adapter' => :YAML, 'adapter_options' => { @@ -267,6 +267,19 @@ def ssl_test_config }) end +def hooks_config + @hooks_config ||= test_config.merge({ + 'hooks' => { + 'set' => { + 'set_test_hook' => File.expand_path(File.join(base_dir,'spec/fixtures/set_test_hook.rb')) + }, + 'delete' => { + 'delete_test_hook' => File.expand_path(File.join(base_dir,'spec/fixtures/delete_test_hook.rb')) + } + } + }) +end + def base_dir File.dirname(__FILE__)+'/../' end diff --git a/spec/trocla/hooks_spec.rb b/spec/trocla/hooks_spec.rb new file mode 100644 index 0000000..6c4b556 --- /dev/null +++ b/spec/trocla/hooks_spec.rb @@ -0,0 +1,42 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe "Trocla::Hooks::Runner" do + + before(:each) do + expect_any_instance_of(Trocla).to receive(:read_config).and_return(hooks_config) + @trocla = Trocla.new + end + + after(:each) do + Trocla::Hooks.set_messages.clear + Trocla::Hooks.delete_messages.clear + end + + describe 'running hooks' do + describe 'setting password' do + it "calls the set hook" do + @trocla.password('random1', 'plain') + expect(Trocla::Hooks.set_messages.length).to eql(1) + expect(Trocla::Hooks.delete_messages.length).to eql(0) + expect(Trocla::Hooks.set_messages.first).to eql("random1_plain") + end + end + describe 'deleting password' do + it "calls the delete hook" do + @trocla.delete_password('random1', 'plain') + expect(Trocla::Hooks.delete_messages.length).to eql(1) + expect(Trocla::Hooks.set_messages.length).to eql(0) + expect(Trocla::Hooks.delete_messages.first).to eql("random1_plain") + end + end + describe 'reset password' do + it "calls the delete and set hook" do + @trocla.reset_password('random1', 'plain') + expect(Trocla::Hooks.set_messages.length).to eql(1) + expect(Trocla::Hooks.set_messages.first).to eql("random1_plain") + expect(Trocla::Hooks.delete_messages.length).to eql(1) + expect(Trocla::Hooks.delete_messages.first).to eql("random1_plain") + end + end + end +end