easy_creds 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 +7 -0
- data/CHANGELOG.md +51 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/exe/easy_creds +6 -0
- data/lib/easy_creds/actions/edit.rb +156 -0
- data/lib/easy_creds/actions/editor_edit.rb +70 -0
- data/lib/easy_creds/actions/init.rb +93 -0
- data/lib/easy_creds/actions/local/delete.rb +44 -0
- data/lib/easy_creds/actions/local/edit.rb +13 -0
- data/lib/easy_creds/actions/local/editor_edit.rb +13 -0
- data/lib/easy_creds/actions/local/init.rb +32 -0
- data/lib/easy_creds/actions/local/status.rb +49 -0
- data/lib/easy_creds/actions/local/sync_key.rb +63 -0
- data/lib/easy_creds/actions/local.rb +31 -0
- data/lib/easy_creds/actions/pull.rb +142 -0
- data/lib/easy_creds/actions/push.rb +149 -0
- data/lib/easy_creds/actions/status.rb +82 -0
- data/lib/easy_creds/cli.rb +147 -0
- data/lib/easy_creds/config.rb +52 -0
- data/lib/easy_creds/configuration.rb +91 -0
- data/lib/easy_creds/credentials_io.rb +128 -0
- data/lib/easy_creds/differ.rb +41 -0
- data/lib/easy_creds/doctor.rb +100 -0
- data/lib/easy_creds/env_picker.rb +24 -0
- data/lib/easy_creds/flatten.rb +25 -0
- data/lib/easy_creds/generators.rb +113 -0
- data/lib/easy_creds/init_state.rb +65 -0
- data/lib/easy_creds/installer.rb +55 -0
- data/lib/easy_creds/onboarding/gem_setup.rb +98 -0
- data/lib/easy_creds/onboarding/project_wizard.rb +106 -0
- data/lib/easy_creds/onboarding/register_prompt.rb +47 -0
- data/lib/easy_creds/onboarding/runner.rb +17 -0
- data/lib/easy_creds/onboarding/template_picker.rb +36 -0
- data/lib/easy_creds/overlay.rb +71 -0
- data/lib/easy_creds/project.rb +74 -0
- data/lib/easy_creds/projects/registry.rb +135 -0
- data/lib/easy_creds/provider.rb +30 -0
- data/lib/easy_creds/providers/base.rb +25 -0
- data/lib/easy_creds/providers/one_password.rb +187 -0
- data/lib/easy_creds/railtie.rb +10 -0
- data/lib/easy_creds/templates/files/default-beastmode.yml +33 -0
- data/lib/easy_creds/templates/files/microservice-minimal.yml +7 -0
- data/lib/easy_creds/templates/files/rails-api.yml +20 -0
- data/lib/easy_creds/templates/files/rails-fullstack.yml +37 -0
- data/lib/easy_creds/templates/registry.rb +62 -0
- data/lib/easy_creds/templates/renderer.rb +37 -0
- data/lib/easy_creds/theme.rb +129 -0
- data/lib/easy_creds/thor_cli.rb +299 -0
- data/lib/easy_creds/vault_picker.rb +56 -0
- data/lib/easy_creds/version.rb +5 -0
- data/lib/easy_creds/views/diff_table.rb +36 -0
- data/lib/easy_creds/views/header.rb +40 -0
- data/lib/easy_creds/views/init_dispatch.rb +132 -0
- data/lib/easy_creds/views/init_tree.rb +250 -0
- data/lib/easy_creds/views/local_menu.rb +38 -0
- data/lib/easy_creds/views/menu.rb +55 -0
- data/lib/easy_creds/views/project_picker.rb +56 -0
- data/lib/easy_creds/views/settings_menu.rb +108 -0
- data/lib/easy_creds/views/templates_menu.rb +142 -0
- data/lib/easy_creds/views/welcome_screen.rb +131 -0
- data/lib/easy_creds.rb +54 -0
- data/lib/tasks/easy_creds.rake +23 -0
- metadata +292 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Onboarding
|
|
5
|
+
class GemSetup
|
|
6
|
+
GLOBAL_DIRS = %w[log templates projects].freeze
|
|
7
|
+
|
|
8
|
+
def self.run(prompt:)
|
|
9
|
+
new(prompt: prompt).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(prompt:)
|
|
13
|
+
@prompt = prompt
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
Theme.section('easy_creds — first time setup')
|
|
18
|
+
|
|
19
|
+
global_dir = ask_global_dir
|
|
20
|
+
EasyCreds.configure { |c| c.global_dir = global_dir }
|
|
21
|
+
create_dirs(global_dir)
|
|
22
|
+
seed_default_template(global_dir)
|
|
23
|
+
seed_projects_registry(global_dir)
|
|
24
|
+
check_op_cli!
|
|
25
|
+
ensure_signed_in
|
|
26
|
+
vault = pick_vault
|
|
27
|
+
EasyCreds.configure { |c| c.default_vault = vault }
|
|
28
|
+
write_config(global_dir)
|
|
29
|
+
|
|
30
|
+
Theme.success("Setup complete. Config written to #{global_dir}/config.yml")
|
|
31
|
+
Theme.notice('Run `easy_creds onboard` inside a project to scaffold credentials.')
|
|
32
|
+
global_dir
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ask_global_dir
|
|
38
|
+
default = File.expand_path('~/.easy_creds')
|
|
39
|
+
raw = @prompt.ask('Global config directory:', default: default)
|
|
40
|
+
File.expand_path(raw)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_dirs(global_dir)
|
|
44
|
+
GLOBAL_DIRS.each { |sub| FileUtils.mkdir_p(File.join(global_dir, sub)) }
|
|
45
|
+
Theme.notice("Created #{global_dir}/")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def seed_default_template(global_dir)
|
|
49
|
+
dest = File.join(global_dir, 'templates', 'default.yml')
|
|
50
|
+
return if File.exist?(dest)
|
|
51
|
+
|
|
52
|
+
src = File.join(Templates::Registry::BUNDLED_DIR, 'default-beastmode.yml')
|
|
53
|
+
return unless File.exist?(src)
|
|
54
|
+
|
|
55
|
+
FileUtils.cp(src, dest)
|
|
56
|
+
Theme.notice("Seeded default template at #{dest}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def seed_projects_registry(global_dir)
|
|
60
|
+
projects_path = File.join(global_dir, 'projects.yml')
|
|
61
|
+
return if File.exist?(projects_path)
|
|
62
|
+
|
|
63
|
+
data = { 'schema_version' => 1, 'projects' => [] }
|
|
64
|
+
File.write(projects_path, data.to_yaml)
|
|
65
|
+
FileUtils.chmod(0o600, projects_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_op_cli!
|
|
69
|
+
return if system('which op > /dev/null 2>&1')
|
|
70
|
+
|
|
71
|
+
Theme.failure('op CLI not found. Install with: brew install 1password-cli')
|
|
72
|
+
exit 2
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ensure_signed_in
|
|
76
|
+
return if op_signed_in?
|
|
77
|
+
|
|
78
|
+
Theme.notice('Not signed in to 1Password.')
|
|
79
|
+
puts " Run #{Theme.bold('op signin')} in another terminal, then press Enter..."
|
|
80
|
+
$stdin.gets
|
|
81
|
+
ensure_signed_in
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def op_signed_in?
|
|
85
|
+
system('op whoami > /dev/null 2>&1')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pick_vault
|
|
89
|
+
VaultPicker.pick(@prompt)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def write_config(global_dir)
|
|
93
|
+
config_path = File.join(global_dir, 'config.yml')
|
|
94
|
+
Configuration.write_file!(EasyCreds.config, config_path)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Onboarding
|
|
5
|
+
class ProjectWizard
|
|
6
|
+
def self.run(prompt:, root: nil, force: false, non_interactive: false)
|
|
7
|
+
new(prompt: prompt, root: root, force: force,
|
|
8
|
+
non_interactive: non_interactive).run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(prompt:, root: nil, force: false, non_interactive: false)
|
|
12
|
+
@prompt = prompt
|
|
13
|
+
@root = root ? Pathname.new(root) : detect_root
|
|
14
|
+
@force = force
|
|
15
|
+
@non_interactive = non_interactive
|
|
16
|
+
@rails = @root.join('config/application.rb').exist?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
show_detected_mode
|
|
21
|
+
template = pick_template
|
|
22
|
+
dest = pick_destination_path
|
|
23
|
+
customised = customise(template)
|
|
24
|
+
write_example(customised, dest)
|
|
25
|
+
offer_bootstrap if @rails && !@non_interactive
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def show_detected_mode
|
|
31
|
+
mode = @rails ? Theme.ok("Rails (#{@root})") : Theme.info("Standalone (#{slug})")
|
|
32
|
+
puts ''
|
|
33
|
+
puts " #{Theme.dim('Project:')} #{mode}"
|
|
34
|
+
puts ''
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pick_template
|
|
38
|
+
global_dir = EasyCreds.config.global_dir
|
|
39
|
+
if @non_interactive
|
|
40
|
+
Templates::Registry.new(global_dir: global_dir).load(:default)
|
|
41
|
+
else
|
|
42
|
+
TemplatePicker.choose(prompt: @prompt, global_dir: global_dir)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def pick_destination_path
|
|
47
|
+
choices = destination_choices
|
|
48
|
+
dest_key = @non_interactive ? :project : @prompt.select('Save example to:', choices, cycle: true)
|
|
49
|
+
resolve_dest(dest_key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def destination_choices
|
|
53
|
+
global_dir = EasyCreds.config.global_dir
|
|
54
|
+
choices = []
|
|
55
|
+
choices << { name: "project — #{@root}/config/credentials/example.yml", value: :project } if @rails
|
|
56
|
+
choices << { name: "global — #{global_dir}/projects/#{slug}/example.yml", value: :global }
|
|
57
|
+
choices
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resolve_dest(dest_key)
|
|
61
|
+
global_dir = EasyCreds.config.global_dir
|
|
62
|
+
if dest_key == :project
|
|
63
|
+
@root.join('config/credentials/example.yml')
|
|
64
|
+
else
|
|
65
|
+
Pathname.new(File.join(global_dir, 'projects', slug, 'example.yml'))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def customise(template)
|
|
70
|
+
return template if @non_interactive
|
|
71
|
+
|
|
72
|
+
renderer = Templates::Renderer.new(template)
|
|
73
|
+
renderer.customise(@prompt)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_example(template, dest)
|
|
77
|
+
if dest.exist? && !@force
|
|
78
|
+
Theme.failure("#{dest} already exists. Use --force to overwrite.")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
dest.parent.mkpath
|
|
83
|
+
dest.write(template.to_yaml)
|
|
84
|
+
Theme.success("Wrote #{dest}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def offer_bootstrap
|
|
88
|
+
return unless @prompt.yes?('Bootstrap config/credentials/development.yml.enc now?', default: false)
|
|
89
|
+
|
|
90
|
+
io = CredentialsIO.new(@root)
|
|
91
|
+
io.ensure_key!('development') unless io.key_exists?('development')
|
|
92
|
+
Theme.success('Key generated at config/credentials/development.key')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def slug
|
|
96
|
+
File.basename(@root)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def detect_root
|
|
100
|
+
dir = Pathname.new(Dir.pwd)
|
|
101
|
+
dir = dir.parent until dir.root? || dir.join('config/application.rb').exist?
|
|
102
|
+
dir.root? ? Pathname.new(Dir.pwd) : dir
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Onboarding
|
|
5
|
+
module RegisterPrompt
|
|
6
|
+
# Returns :registered, :continue, or :quit
|
|
7
|
+
def self.run(prompt:, project:, registry:)
|
|
8
|
+
return :continue if registry.find_by_path(project.root)
|
|
9
|
+
|
|
10
|
+
Theme.section("New project detected at #{project.root}")
|
|
11
|
+
choice = prompt.select(
|
|
12
|
+
'Register this project with easy_creds?',
|
|
13
|
+
[
|
|
14
|
+
{ name: 'register and continue', value: :register },
|
|
15
|
+
{ name: 'continue without registering', value: :skip },
|
|
16
|
+
{ name: 'quit', value: :quit }
|
|
17
|
+
],
|
|
18
|
+
cycle: true
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
case choice
|
|
22
|
+
when :register
|
|
23
|
+
registry.register(
|
|
24
|
+
name: project_name(project),
|
|
25
|
+
path: project.root.to_s,
|
|
26
|
+
kind: project.rails? ? :rails : :standalone,
|
|
27
|
+
github_url: detect_github_url(project.root)
|
|
28
|
+
)
|
|
29
|
+
:registered
|
|
30
|
+
when :skip then :continue
|
|
31
|
+
else :quit
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.project_name(project)
|
|
36
|
+
File.basename(project.root.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.detect_github_url(root)
|
|
40
|
+
url = `git -C #{Shellwords.escape(root.to_s)} remote get-url origin 2>/dev/null`.strip
|
|
41
|
+
url.empty? ? nil : url
|
|
42
|
+
rescue StandardError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Onboarding
|
|
5
|
+
class Runner
|
|
6
|
+
def self.start(project: nil, **)
|
|
7
|
+
prompt = TTY::Prompt.new(interrupt: :exit)
|
|
8
|
+
config_path = File.join(EasyCreds.config.global_dir, 'config.yml')
|
|
9
|
+
|
|
10
|
+
GemSetup.run(prompt: prompt) unless File.exist?(config_path)
|
|
11
|
+
|
|
12
|
+
root = project&.root || Dir.pwd
|
|
13
|
+
ProjectWizard.run(prompt: prompt, root: root, **)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Onboarding
|
|
5
|
+
class TemplatePicker
|
|
6
|
+
def self.choose(prompt:, global_dir: nil)
|
|
7
|
+
new(prompt: prompt, global_dir: global_dir).choose
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(prompt:, global_dir: nil)
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
@global_dir = global_dir
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def choose
|
|
16
|
+
registry = Templates::Registry.new(global_dir: @global_dir)
|
|
17
|
+
choices = build_choices(registry)
|
|
18
|
+
name = @prompt.select('Select a template:', choices, cycle: true)
|
|
19
|
+
name == :blank ? {} : registry.load(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_choices(registry)
|
|
25
|
+
choices = registry.list.map { |name| { name: display_name(name, registry), value: name } }
|
|
26
|
+
choices << { name: 'custom (blank)', value: :blank }
|
|
27
|
+
choices
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_name(name, registry)
|
|
31
|
+
source = registry.user_defined?(name) ? Theme.ok('user') : Theme.dim('built-in')
|
|
32
|
+
"#{name.to_s.ljust(30)} #{source}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/encrypted_configuration'
|
|
4
|
+
require 'active_support/ordered_options'
|
|
5
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
6
|
+
require 'active_support/core_ext/hash/keys'
|
|
7
|
+
|
|
8
|
+
module EasyCreds
|
|
9
|
+
module Overlay
|
|
10
|
+
# Merge config/credentials/<env>_local.yml.enc on top of the base credentials.
|
|
11
|
+
#
|
|
12
|
+
# Accepts either a Rails application (the form the installer-generated
|
|
13
|
+
# initializer uses) or the explicit (creds, root, env) triple:
|
|
14
|
+
#
|
|
15
|
+
# EasyCreds::Overlay.apply!(Rails.application)
|
|
16
|
+
# EasyCreds::Overlay.apply!(Rails.application.credentials, Rails.root, Rails.env)
|
|
17
|
+
#
|
|
18
|
+
# @param app_or_creds [#credentials, #root, #env] a Rails application, or the
|
|
19
|
+
# credentials object when root/env are supplied explicitly
|
|
20
|
+
# @param root [String, Pathname, nil] project root (omit when passing an app)
|
|
21
|
+
# @param env [String, Symbol, nil] environment name (omit when passing an app)
|
|
22
|
+
# @return [void]
|
|
23
|
+
def self.apply!(app_or_creds, root = nil, env = nil)
|
|
24
|
+
if root.nil? && env.nil?
|
|
25
|
+
app = app_or_creds
|
|
26
|
+
creds = app.credentials
|
|
27
|
+
root = app.root
|
|
28
|
+
env = app.env.to_s
|
|
29
|
+
else
|
|
30
|
+
creds = app_or_creds
|
|
31
|
+
env = env.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
enc = Pathname.new(root).join("config/credentials/#{env}_local.yml.enc")
|
|
35
|
+
key = Pathname.new(root).join("config/credentials/#{env}_local.key")
|
|
36
|
+
|
|
37
|
+
return unless enc.exist? && key.exist?
|
|
38
|
+
|
|
39
|
+
overlay = ActiveSupport::EncryptedConfiguration.new(
|
|
40
|
+
config_path: enc.to_s,
|
|
41
|
+
key_path: key.to_s,
|
|
42
|
+
env_key: 'RAILS_MASTER_KEY_LOCAL_UNUSED',
|
|
43
|
+
raise_if_missing_key: false
|
|
44
|
+
).config.deep_symbolize_keys
|
|
45
|
+
|
|
46
|
+
overlay.each do |top_key, value|
|
|
47
|
+
merge_key(creds, top_key, value)
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
warn "[easy_creds_overlay] skipped: #{e.class}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.merge_key(creds, top_key, value)
|
|
54
|
+
if value.is_a?(Hash)
|
|
55
|
+
existing = creds.public_send(top_key)
|
|
56
|
+
base = existing.respond_to?(:to_h) ? existing.to_h.deep_symbolize_keys : {}
|
|
57
|
+
creds.public_send(:"#{top_key}=", to_ordered_options(base.deep_merge(value)))
|
|
58
|
+
else
|
|
59
|
+
creds.public_send(:"#{top_key}=", value)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
private_class_method :merge_key
|
|
63
|
+
|
|
64
|
+
def self.to_ordered_options(hash)
|
|
65
|
+
hash.each_with_object(ActiveSupport::OrderedOptions.new) do |(k, v), opts|
|
|
66
|
+
opts[k] = v.is_a?(Hash) ? to_ordered_options(v) : v
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
private_class_method :to_ordered_options
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
# Detects whether the current working directory is a Rails project or a
|
|
5
|
+
# standalone directory, and exposes project-scoped paths (credentials dir,
|
|
6
|
+
# log dir, etc.) to the rest of the gem without coupling to Rails constants.
|
|
7
|
+
module Project
|
|
8
|
+
# Returns a Rails or Standalone project object for the given path.
|
|
9
|
+
def self.detect(cwd = Dir.pwd)
|
|
10
|
+
root = find_rails_root(Pathname.new(cwd.to_s))
|
|
11
|
+
root ? Project::Rails.new(root: root) : build_standalone(cwd)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.build_standalone(cwd)
|
|
15
|
+
slug = Pathname.new(cwd.to_s).basename.to_s.gsub(/[^a-z0-9_-]/i, '-').downcase
|
|
16
|
+
global_dir = Pathname.new(EasyCreds.config.global_dir).expand_path
|
|
17
|
+
Standalone.new(root: Pathname.new(cwd.to_s), slug: slug, global_dir: global_dir)
|
|
18
|
+
end
|
|
19
|
+
private_class_method :build_standalone
|
|
20
|
+
|
|
21
|
+
def self.find_rails_root(dir)
|
|
22
|
+
until dir.root?
|
|
23
|
+
return dir if dir.join('config/application.rb').exist?
|
|
24
|
+
|
|
25
|
+
dir = dir.parent
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
private_class_method :find_rails_root
|
|
30
|
+
|
|
31
|
+
# Represents a Rails application project.
|
|
32
|
+
class Rails
|
|
33
|
+
attr_reader :root
|
|
34
|
+
|
|
35
|
+
def initialize(root:)
|
|
36
|
+
@root = Pathname.new(root.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def rails? = true
|
|
40
|
+
def standalone? = false
|
|
41
|
+
|
|
42
|
+
def log_dir = @root.join('log')
|
|
43
|
+
def credentials_dir = @root.join('config/credentials')
|
|
44
|
+
def environments_dir = @root.join('config/environments')
|
|
45
|
+
def gitignore_path = @root.join('.gitignore')
|
|
46
|
+
def git_repo? = @root.join('.git').exist?
|
|
47
|
+
def example_template_path = credentials_dir.join('example.yml')
|
|
48
|
+
def config_file_path = @root.join('config/easy_creds.yml')
|
|
49
|
+
def slug = @root.basename.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Represents a non-Rails directory (standalone usage).
|
|
53
|
+
class Standalone
|
|
54
|
+
attr_reader :root, :slug, :global_dir
|
|
55
|
+
|
|
56
|
+
def initialize(root:, slug:, global_dir:)
|
|
57
|
+
@root = Pathname.new(root.to_s)
|
|
58
|
+
@slug = slug
|
|
59
|
+
@global_dir = Pathname.new(global_dir.to_s)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rails? = false
|
|
63
|
+
def standalone? = true
|
|
64
|
+
|
|
65
|
+
def log_dir = @global_dir.join('log')
|
|
66
|
+
def credentials_dir = @global_dir.join('projects', slug)
|
|
67
|
+
def environments_dir = nil
|
|
68
|
+
def gitignore_path = @root.join('.gitignore')
|
|
69
|
+
def git_repo? = @root.join('.git').exist?
|
|
70
|
+
def example_template_path = credentials_dir.join('example.yml')
|
|
71
|
+
def config_file_path = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Projects
|
|
5
|
+
Project = Data.define(
|
|
6
|
+
:name, :path, :kind, :github_url, :template, :last_seen_at, :registered_at
|
|
7
|
+
) do
|
|
8
|
+
def exists_on_disk? = File.directory?(path)
|
|
9
|
+
def display_path = path.sub(Dir.home, '~')
|
|
10
|
+
def rails? = kind == :rails
|
|
11
|
+
|
|
12
|
+
def to_h_yaml
|
|
13
|
+
{
|
|
14
|
+
'name' => name,
|
|
15
|
+
'path' => path,
|
|
16
|
+
'kind' => kind.to_s,
|
|
17
|
+
'github_url' => github_url,
|
|
18
|
+
'template' => template&.to_s,
|
|
19
|
+
'last_seen_at' => last_seen_at&.iso8601,
|
|
20
|
+
'registered_at' => registered_at&.iso8601
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Registry
|
|
26
|
+
def self.registry_path
|
|
27
|
+
File.join(EasyCreds.config.global_dir, 'projects.yml')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.load
|
|
31
|
+
path = registry_path
|
|
32
|
+
unless File.exist?(path)
|
|
33
|
+
return new({ 'schema_version' => 1, 'projects' => [] })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
data = YAML.safe_load_file(path) ||
|
|
37
|
+
{ 'schema_version' => 1, 'projects' => [] }
|
|
38
|
+
new(data)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(data)
|
|
42
|
+
@data = data
|
|
43
|
+
@projects = (data['projects'] || []).map { |h| build(h) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader :projects
|
|
47
|
+
|
|
48
|
+
def find_by_path(path)
|
|
49
|
+
norm = File.expand_path(path.to_s)
|
|
50
|
+
@projects.find { |p| File.expand_path(p.path) == norm }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_containing(cwd)
|
|
54
|
+
norm = File.expand_path(cwd.to_s)
|
|
55
|
+
@projects
|
|
56
|
+
.select { |p| norm == File.expand_path(p.path) || norm.start_with?(File.expand_path(p.path) + '/') }
|
|
57
|
+
.max_by { |p| p.path.length }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register(name:, path:, kind:, github_url: nil, template: nil)
|
|
61
|
+
norm = File.expand_path(path.to_s)
|
|
62
|
+
existing = find_by_path(norm)
|
|
63
|
+
if existing
|
|
64
|
+
touch(existing)
|
|
65
|
+
return existing
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
now = Time.now.utc
|
|
69
|
+
proj = Project.new(
|
|
70
|
+
name: name, path: norm, kind: kind.to_sym,
|
|
71
|
+
github_url: github_url.presence, template: template&.to_sym,
|
|
72
|
+
last_seen_at: now, registered_at: now
|
|
73
|
+
)
|
|
74
|
+
@projects << proj
|
|
75
|
+
save!
|
|
76
|
+
proj
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def touch(project)
|
|
80
|
+
idx = @projects.index { |p| p.path == project.path }
|
|
81
|
+
return unless idx
|
|
82
|
+
|
|
83
|
+
@projects[idx] = project.with(last_seen_at: Time.now.utc)
|
|
84
|
+
save!
|
|
85
|
+
@projects[idx]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def remove(project)
|
|
89
|
+
@projects.reject! { |p| p.path == project.path }
|
|
90
|
+
save!
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def prune_missing!
|
|
94
|
+
gone = @projects.reject(&:exists_on_disk?)
|
|
95
|
+
return [] if gone.empty?
|
|
96
|
+
|
|
97
|
+
gone_paths = gone.map(&:path).to_set
|
|
98
|
+
@projects.reject! { |p| gone_paths.include?(p.path) }
|
|
99
|
+
save!
|
|
100
|
+
gone
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def save!
|
|
104
|
+
path = self.class.registry_path
|
|
105
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
106
|
+
out = { 'schema_version' => 1, 'projects' => @projects.map(&:to_h_yaml) }
|
|
107
|
+
File.write(path, out.to_yaml)
|
|
108
|
+
FileUtils.chmod(0o600, path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def build(h)
|
|
114
|
+
Project.new(
|
|
115
|
+
name: h['name'] || File.basename(h['path'].to_s),
|
|
116
|
+
path: h['path'].to_s,
|
|
117
|
+
kind: (h['kind'] || 'standalone').to_sym,
|
|
118
|
+
github_url: h['github_url'],
|
|
119
|
+
template: h['template']&.to_sym,
|
|
120
|
+
last_seen_at: parse_time(h['last_seen_at']),
|
|
121
|
+
registered_at: parse_time(h['registered_at'])
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_time(val)
|
|
126
|
+
return nil unless val
|
|
127
|
+
return val if val.is_a?(Time)
|
|
128
|
+
|
|
129
|
+
Time.parse(val.to_s)
|
|
130
|
+
rescue ArgumentError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
# Registry of available secret-storage providers.
|
|
5
|
+
# v1 ships only :onepassword. Future providers (env-var, AWS Secrets Manager,
|
|
6
|
+
# HashiCorp Vault) register here without changing call-sites.
|
|
7
|
+
module Provider
|
|
8
|
+
@registry = {}
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def register(name, klass)
|
|
12
|
+
@registry[name.to_sym] = klass
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(name)
|
|
16
|
+
@registry.fetch(name.to_sym) do
|
|
17
|
+
raise ArgumentError, "Unknown provider '#{name}'. Registered: #{@registry.keys.join(', ')}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(name, **opts)
|
|
22
|
+
fetch(name).new(**opts)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def registered
|
|
26
|
+
@registry.keys
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Providers
|
|
5
|
+
# Abstract interface all EasyCreds providers must implement.
|
|
6
|
+
# Subclasses raise NotImplementedError for any unimplemented method.
|
|
7
|
+
class Base
|
|
8
|
+
def initialize(config: nil, logger: nil)
|
|
9
|
+
@config = config
|
|
10
|
+
@logger = logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def signed_in? = raise(NotImplementedError, "#{self.class}#signed_in?")
|
|
14
|
+
def account_email = raise(NotImplementedError, "#{self.class}#account_email")
|
|
15
|
+
def vault_exists? = raise(NotImplementedError, "#{self.class}#vault_exists?")
|
|
16
|
+
def create_vault(name) = raise(NotImplementedError, "#{self.class}#create_vault")
|
|
17
|
+
def item(env) = raise(NotImplementedError, "#{self.class}#item")
|
|
18
|
+
def item_exists?(env) = raise(NotImplementedError, "#{self.class}#item_exists?")
|
|
19
|
+
def create_item(env, fields) = raise(NotImplementedError, "#{self.class}#create_item")
|
|
20
|
+
def update_item(env, all_fields) = raise(NotImplementedError, "#{self.class}#update_item")
|
|
21
|
+
def read_credentials_key(env, item) = raise(NotImplementedError, "#{self.class}#read_credentials_key")
|
|
22
|
+
def write_credentials_key(env, value, item) = raise(NotImplementedError, "#{self.class}#write_credentials_key")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|