mellon 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3d709112b62b9ed79d5d650602042faf2a3dfc2f
4
+ data.tar.gz: deda5ba7ba3a00ea77b3f914fd23c8d14cd7c04a
5
+ SHA512:
6
+ metadata.gz: ede7a998d84a0504c55bcf1f203c832a8c800e647f43ff4c17a7edfb1801ee2e37292f5a771e04d97798985af4c2e0c32d9507dc7c5428d73acc6ed6ea38a45d
7
+ data.tar.gz: 19fef434f6cdd5a71599f9774a4d56e284ae82d51d819b2a001de0626da9d237ada8eaef8e89e4569d3fb91877d732169c9cefe0429eea11ae3524794bdacb70
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ spec/.*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require ./spec/spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Kim Burgestrand
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Mellon
2
+
3
+ > Speak, Friend, and enter.
4
+
5
+ Mellon is four things:
6
+
7
+ - A simple library for reading and writing notes in Mac OSX keychains. (see [Mellon::Keychain][])
8
+ - A simple library for using Mac OSX keychain notes as hash storage. (see [Mellon::Store][])
9
+ - A tiny CLI interface for [Mellon::Keychain][]. (see [Command-line interface](#command-line-interface))
10
+
11
+ I built Mellon because I wanted Yet Another Way of Managing Application Secrets™. Frankly, I am fed up
12
+ with passing around some secret file not included in our source code repository that runs out of sync
13
+ every time we add or change values during development.
14
+
15
+ Mellon, together with Econfig, solves this problem.
16
+
17
+ Mellon is sponsored by [Elabs][].
18
+
19
+ [![elabs logo][]][Elabs]
20
+
21
+ ## Usage
22
+
23
+ Mellon is intended for usage during development only. The recommended workflow is:
24
+
25
+ 1. Create an OSX keychain using Keychain Access, name it something hip and clever, like `thanks-a-latte`.
26
+ 2. Share your keychain with your colleagues, through iCloud sync, Dropbox or similar.
27
+ 3. Hook up Mellon with your application according to instructions below.
28
+
29
+ ### Using with Rails and Econfig (recommended)
30
+
31
+ See instructions over at [econfig-keychain](https://github.com/elabs/econfig-keychain).
32
+
33
+ # Documentation
34
+
35
+ An API reference can be found at [rdoc.info/github/elabs/mellon](http://rdoc.info/github/elabs/mellon/master/frames).
36
+
37
+ ## Mellon::Keychain
38
+
39
+ Mellon::Keychain allows you to read and write notes in OSX keychains.
40
+
41
+ ```ruby
42
+ keychain = Mellon::Keychain.default
43
+ keychain["ruby note"] # => nil
44
+ keychain["ruby note"] = "Hello from Ruby!" # creates keychain note `ruby note`
45
+ keychain["ruby note"] # => "Hello from Ruby!"
46
+ keychain["ruby note"] = nil # deletes keychain note `ruby note`
47
+ ```
48
+
49
+ More documentation can be found at the [API reference for Mellon::Keychain](http://rdoc.info/github/elabs/mellon/master/Mellon/Keychain).
50
+
51
+ ## Mellon::Store
52
+
53
+ Mellon::Store is a layer above Mellon::Keychain, allowing you to use a single keychain note as hash storage. Notes are serialized as YAML by default.
54
+
55
+ ```ruby
56
+ project_name = "ruby note"
57
+ store = Mellon::Store.new(project_name, keychain: Mellon::Keychain.default)
58
+ store["some key"] # => nil
59
+ store["some key"] = "Hello from Ruby!" # creates keychain note "ruby note", and puts value for "some key" in it
60
+ store["some key"] # => "Hello from Ruby!"
61
+
62
+ # Have a peek at the data, which is serialized as YAML
63
+ store.keychain[store.project_name] # => "---\nsome key: Hello from Ruby!\n"
64
+ ```
65
+
66
+ More documentation can be found at the [API reference for Mellon::Store](http://rdoc.info/github/elabs/mellon/master/Mellon/Store).
67
+
68
+ ## Command-line interface
69
+
70
+ When you install the Mellon gem you also get an executable called `mellon`. See `mellon -h` for usage information.
71
+
72
+ # License
73
+
74
+ [See LICENSE](./LICENSE).
75
+
76
+ [Elabs]: http://www.elabs.se/
77
+ [elabs logo]: ./elabs-logo.png?raw=true
78
+ [Mellon::Keychain]: #mellon-keychain
79
+ [Mellon::Store]: #mellon-store
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :console do
7
+ exec *%w[bundle exec pry -r mellon]
8
+ end
9
+
10
+ task :default => :spec
data/bin/mellon ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "mellon"
5
+ require "slop"
6
+
7
+ # Options
8
+ $keychain = nil
9
+
10
+ def find_closest(*filenames)
11
+ require "pathname"
12
+ Pathname.pwd.ascend do |parent|
13
+ filenames.each do |filename|
14
+ file = parent + filename
15
+ return parent.to_s if file.exist?
16
+ end
17
+ end
18
+
19
+ yield
20
+ end
21
+
22
+ $default_name = File.basename(find_closest("Gemfile", "Rakefile") { Dir.pwd })
23
+
24
+ # Common procs
25
+ define_common = lambda do |dsl|
26
+ dsl.on :k, :keychain=, "Specify keychain to use" do |keychain_name|
27
+ keychain_path = File.expand_path(keychain_name)
28
+
29
+ $keychain = if File.exists?(keychain_path)
30
+ Mellon::Keychain.new(keychain_path)
31
+ else
32
+ Mellon::Keychain.find(keychain_name)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Convenience
38
+ def name_and_keychain(argv)
39
+ name = argv.join(" ")
40
+ name = $default_name if name.empty?
41
+ $keychain ||= Mellon::Keychain.search(name)
42
+
43
+ unless $keychain
44
+ $stderr.puts "key not found: #{name}"
45
+ yield if block_given?
46
+ exit false
47
+ end
48
+
49
+ [name, $keychain]
50
+ end
51
+
52
+ Slop.parse(strict: true, help: true) do
53
+ on :v, :version, "Show Mellon version." do
54
+ puts "Mellon v#{Mellon::VERSION}"
55
+ exit
56
+ end
57
+
58
+ description "list globally known keychains."
59
+ command "list" do
60
+ run do
61
+ puts "Available keychains:"
62
+ Mellon::Keychain.list.each do |keychain|
63
+ puts " #{keychain.name}"
64
+ end
65
+ end
66
+ end
67
+
68
+ description "edit or create a keychain entry."
69
+ command "edit" do
70
+ banner "Usage: mellon edit [options] [name (default: #{$default_name})]"
71
+ define_common[self]
72
+
73
+ run do |opts, argv|
74
+ name, keychain = name_and_keychain(argv) do
75
+ $stderr.puts "If you want to create it, you need to specify keychain with -k."
76
+ end
77
+
78
+ editor = ENV.fetch("EDITOR") do
79
+ $stderr.puts "$EDITOR is not set. Please set it to your preferred editor."
80
+ exit false
81
+ end
82
+
83
+ require "tempfile"
84
+ Tempfile.open([name, ".txt"]) do |io|
85
+ File.write io.path, keychain[name]
86
+ system editor, io.path
87
+ keychain[name] = File.read(io.path)
88
+ end
89
+ end
90
+ end
91
+
92
+ description "show a keychain entry."
93
+ command "show" do
94
+ banner "Usage: mellon show [options] [name (default: #{$default_name})]"
95
+ define_common[self]
96
+
97
+ run do |opts, argv|
98
+ name, keychain = name_and_keychain(argv)
99
+ print keychain[name]
100
+ end
101
+ end
102
+
103
+ run do |opts, argv|
104
+ puts opts
105
+ end
106
+ end
data/elabs-logo.png ADDED
Binary file
@@ -0,0 +1,239 @@
1
+ require "plist"
2
+
3
+ module Mellon
4
+ # Keychain provides simple methods for reading and storing keychain entries.
5
+ class Keychain
6
+ DEFAULT_OPTIONS = { type: :note }
7
+ TYPES = {
8
+ "note" => {
9
+ kind: "secure note",
10
+ type: "note"
11
+ }
12
+ }
13
+
14
+ class << self
15
+ # Find the first keychain that contains the key.
16
+ #
17
+ # @param [String] key
18
+ # @return [Keychain, nil]
19
+ def search(key)
20
+ output = ShellUtils.security("find-generic-password", "-l", key)
21
+ new(output[/keychain: "(.+)"/i, 1], ensure_exists: false)
22
+ rescue CommandError
23
+ nil
24
+ end
25
+
26
+ # Find a keychain matching the given name.
27
+ #
28
+ # @param [String] name
29
+ # @return [Keychain]
30
+ # @raise [KeyError] if no matching keychain was found
31
+ def find(name)
32
+ quoted = Regexp.quote(name)
33
+ regexp = Regexp.new(quoted, Regexp::IGNORECASE)
34
+
35
+ keychain = list.find do |keychain|
36
+ keychain.path =~ regexp
37
+ end
38
+
39
+ if keychain.nil?
40
+ raise KeyError, "Could not find keychain “#{name}” in #{list.map(&:name).join(", ")}"
41
+ end
42
+
43
+ keychain
44
+ end
45
+
46
+ # @return [Keychain] default keychain
47
+ def default
48
+ keychain_path = ShellUtils.security("default-keychain")[KEYCHAIN_REGEXP, 1]
49
+ Keychain.new(keychain_path, ensure_exists: false)
50
+ end
51
+
52
+ # @return [Array<Keychain>] all available keychains
53
+ def list
54
+ ShellUtils.security("list-keychains").scan(KEYCHAIN_REGEXP).map do |(keychain_path)|
55
+ Keychain.new(keychain_path, ensure_exists: false)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Initialize a keychain on the given path.
61
+ #
62
+ # @param [String] path
63
+ # @param [Boolean] ensure_exists check if keychain exists or not
64
+ def initialize(path, ensure_exists: true)
65
+ @path = path
66
+ @name = File.basename(path, ".keychain")
67
+ command "show-keychain-info" if ensure_exists
68
+ end
69
+
70
+ # @return [String] path to keychain
71
+ attr_reader :path
72
+
73
+ # @return [String] keychain name (without extension)
74
+ attr_reader :name
75
+
76
+ # Retrieve a value, but if it does not exist return the default value,
77
+ # or call the provided block, or raise an error. See Hash#fetch.
78
+ #
79
+ # @param [String] key
80
+ # @param default
81
+ # @return [String] value for key, default, or value from block
82
+ # @yield if key does not exist, and block is given
83
+ # @raise [KeyError] if key does not exist, and no default is given
84
+ def fetch(key, *args, &block)
85
+ self[key] or {}.fetch(key, *args, &block)
86
+ end
87
+
88
+ # @param [String] key
89
+ # @return [String, nil] contents of entry at key, or nil if not set
90
+ def [](key)
91
+ _, data = read(key)
92
+ data
93
+ end
94
+
95
+ # Write data to entry key, or updating existing one if it exists.
96
+ #
97
+ # @param [String] key
98
+ # @param [String] data
99
+ def []=(key, data)
100
+ info, _ = read(key)
101
+ info ||= {}
102
+
103
+ if data
104
+ write(key, data, info)
105
+ else
106
+ delete(key, info)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ # Read a key from the keychain.
113
+ #
114
+ # @param [String] key
115
+ # @return [Array<Hash, String>, nil] tuple of entry info, and text contents, or nil if key does not exist
116
+ def read(key)
117
+ command "find-generic-password", "-g", "-l", key do |info, password_info|
118
+ [parse_info(info), parse_contents(password_info)]
119
+ end
120
+ rescue CommandError => e
121
+ nil
122
+ end
123
+
124
+ # Write data with given key to the keychain, or update existing key if it exists.
125
+ #
126
+ # @note keychain entries are not unique by key, but also by the information
127
+ # provided through options; two entries with same key but different
128
+ # account name (for example), will become two different entries when
129
+ # writing.
130
+ #
131
+ # @param [String] key
132
+ # @param [String] data
133
+ # @param [Hash] options
134
+ # @option options [#to_s] :type (:note) one of Keychain::TYPES
135
+ # @option options [String] :account_name ("")
136
+ # @option options [String] :service_name (key)
137
+ # @option options [String] :label (service_name)
138
+ # @raise [CommandError] if writing fails
139
+ def write(key, data, options = {})
140
+ info = build_info(key, options)
141
+
142
+ command "add-generic-password",
143
+ "-a", info[:account_name],
144
+ "-s", info[:service_name],
145
+ "-l", info[:label],
146
+ "-D", info[:kind],
147
+ "-C", info[:type],
148
+ "-T", "", # which applications have access (none)
149
+ "-U", # upsert
150
+ "-w", data
151
+ end
152
+
153
+ # Delete the entry matching key and options.
154
+ #
155
+ # @param [String] key
156
+ # @param [Hash] options
157
+ # @option (see #write)
158
+ def delete(key, options = {})
159
+ info = build_info(key, options)
160
+
161
+ command "delete-generic-password",
162
+ "-a", info[:account_name],
163
+ "-s", info[:service_name],
164
+ "-l", info[:label],
165
+ "-D", info[:kind],
166
+ "-C", info[:type]
167
+ end
168
+
169
+ # Execute a command with the context of this keychain.
170
+ #
171
+ # @param [Array<String>] command
172
+ def command(*command, &block)
173
+ command += [path]
174
+ ShellUtils.security *command, &block
175
+ end
176
+
177
+ private
178
+
179
+ # Build an info hash used for #write and #delete.
180
+ #
181
+ # @param [String] key
182
+ # @param [Hash] options
183
+ # @return [Hash]
184
+ def build_info(key, options = {})
185
+ options = DEFAULT_OPTIONS.merge(options)
186
+
187
+ note_type = TYPES.fetch(options.fetch(:type, :note).to_s)
188
+ account_name = options.fetch(:account_name, "")
189
+ service_name = options.fetch(:service_name, key)
190
+ label = options.fetch(:label, service_name)
191
+
192
+ {
193
+ account_name: account_name,
194
+ service_name: service_name,
195
+ label: label,
196
+ kind: note_type.fetch(:kind),
197
+ type: note_type.fetch(:type),
198
+ }
199
+ end
200
+
201
+ # Parse entry information.
202
+ #
203
+ # @param [String] info
204
+ # @return [Hash]
205
+ def parse_info(info)
206
+ extract = lambda { |key| info[/#{key}.+=(?:<NULL>|[^"]*"(.+)")/, 1].to_s }
207
+ {
208
+ account_name: extract["acct"],
209
+ kind: extract["desc"],
210
+ type: extract["type"],
211
+ label: extract["0x00000007"],
212
+ service_name: extract["svce"],
213
+ }
214
+ end
215
+
216
+ # Parse entry contents.
217
+ #
218
+ # @param [String]
219
+ # @return [String]
220
+ def parse_contents(password_info)
221
+ unpacked = password_info[/password: 0x([a-f0-9]+)/i, 1]
222
+
223
+ password = if unpacked
224
+ [unpacked].pack("H*")
225
+ else
226
+ password_info[/password: "(.+)"/m, 1]
227
+ end
228
+
229
+ password ||= ""
230
+
231
+ parsed = Plist.parse_xml(password.force_encoding("".encoding))
232
+ if parsed and parsed["NOTE"]
233
+ parsed["NOTE"]
234
+ else
235
+ password
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,36 @@
1
+ require "open3"
2
+ require "shellwords"
3
+
4
+ module Mellon
5
+ module ShellUtils
6
+ module_function
7
+
8
+ def security(*command, &block)
9
+ sh("security", *command, &block)
10
+ end
11
+
12
+ def sh(*command)
13
+ $stderr.puts command.join(" ") if $VERBOSE
14
+ stdout, stderr, status = Open3.capture3(*command)
15
+
16
+ stdout.chomp!
17
+ stderr.chomp!
18
+
19
+ unless status.success?
20
+ error_string = Shellwords.join(command)
21
+ error_string << "\n"
22
+
23
+ stderr = "<no output>" if stderr.empty?
24
+ error_string << " " << stderr.chomp
25
+
26
+ raise CommandError, "[ERROR] #{error_string}"
27
+ end
28
+
29
+ if block_given?
30
+ yield [stdout, stderr]
31
+ else
32
+ stdout
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ require "yaml"
2
+
3
+ module Mellon
4
+ # Used for storing multiple values in a single Keychain entry.
5
+ #
6
+ # This is useful for configuring applications, e.g. having one entry per application,
7
+ # where each entry contains all configuration for said application.
8
+ class Store
9
+ attr_reader :project_name
10
+ attr_reader :keychain
11
+ attr_reader :serializer
12
+
13
+ # @example use keychain where entry exists, or default keychain
14
+ # Store.new("myapp")
15
+ #
16
+ # @example automatically find keychain
17
+ # Store.new("myapp", "shared_keychain")
18
+ #
19
+ # @example explicitly use keychain
20
+ # Store.new("myapp", Mellon::Keychain.new("/path/to/keychain.keychain"))
21
+ #
22
+ # @overload initialize(project_name)
23
+ # @overload initialize(project_name, keychain_name)
24
+ # @overload initialize(project_name, keychain)
25
+ #
26
+ # @param [String] project_name
27
+ # @param [String, Keychain, nil] keychain
28
+ # @param [#dump, #load] serializer
29
+ def initialize(project_name, keychain: Keychain.search(project_name), serializer: YAML)
30
+ @project_name = project_name.to_s
31
+ @keychain = if keychain.is_a?(Keychain)
32
+ keychain
33
+ elsif keychain.nil?
34
+ Keychain.default
35
+ else
36
+ Keychain.find(keychain.to_s)
37
+ end
38
+ @serializer = serializer
39
+ end
40
+
41
+ # @see Hash#fetch
42
+ def fetch(*args, &block)
43
+ data.fetch(*args, &block)
44
+ end
45
+
46
+ # Retrieve a key from the store.
47
+ #
48
+ # @param [String] key
49
+ # @return [String, nil] value stored, or nil
50
+ def [](key)
51
+ data[key]
52
+ end
53
+
54
+ # Set a key in the store.
55
+ #
56
+ # @param [String] key
57
+ # @param [String] value
58
+ def []=(key, value)
59
+ dump data.merge(key => value)
60
+ end
61
+
62
+ private
63
+
64
+ def data
65
+ config = @keychain[@project_name]
66
+
67
+ if config
68
+ @serializer.load(config)
69
+ else
70
+ {}
71
+ end
72
+ end
73
+
74
+ def dump(hash)
75
+ @keychain[@project_name] = @serializer.dump(hash)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module Mellon
2
+ VERSION = "1.0.0"
3
+ end
data/lib/mellon.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "mellon/version"
2
+ require "mellon/shell_utils"
3
+ require "mellon/keychain"
4
+ require "mellon/store"
5
+
6
+ module Mellon
7
+ KEYCHAIN_REGEXP = /"(.+)"/
8
+
9
+ class Error < StandardError; end
10
+ class CommandError < Error; end
11
+ end
data/mellon.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "mellon/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mellon"
8
+ spec.version = Mellon::VERSION
9
+ spec.authors = ["Kim Burgestrand"]
10
+ spec.email = ["kim@burgestrand.se"]
11
+ spec.summary = %q{A command-line utility for managing secret application credentials via OSX keychain.}
12
+ spec.homepage = "https://github.com/elabs/mellon"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "plist"
21
+ spec.add_dependency "slop"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "pry"
27
+ end
Binary file
@@ -0,0 +1,88 @@
1
+ describe Mellon::Keychain do
2
+ subject(:keychain) do
3
+ Mellon::Keychain.new(keychain_path)
4
+ end
5
+
6
+ specify "#name" do
7
+ keychain.name.should eq "temporary_keychain"
8
+ end
9
+
10
+ specify "#path" do
11
+ keychain.path.should eq keychain_path
12
+ end
13
+
14
+ describe "#initialize" do
15
+ it "raises an error if keychain does not exist" do
16
+ expect { Mellon::Keychain.new("missing.keychain") }.to raise_error(Mellon::Error, /missing.keychain/)
17
+ end
18
+ end
19
+
20
+ describe "fetch" do
21
+ it "delegates (and as such, behaves equally) to #[]" do
22
+ keychain.should_receive(:[]).with("simple").and_call_original
23
+ keychain.fetch("simple").should eq "Simple note"
24
+ end
25
+
26
+ describe "behaves like Hash#fetch" do
27
+ specify "when key exists" do
28
+ keychain.fetch("simple", nil).should eq "Simple note"
29
+ keychain.fetch("simple", "default value").should eq "Simple note"
30
+ keychain.fetch("simple", "default value") { "block value" }.should eq "Simple note"
31
+ keychain.fetch("simple") { "block value" }.should eq "Simple note"
32
+
33
+ keychain.fetch("simple").should eq "Simple note"
34
+ end
35
+
36
+ specify "when key does not exist" do
37
+ keychain.fetch("missing", nil).should eq nil
38
+ keychain.fetch("missing", "default value").should eq "default value"
39
+ keychain.fetch("missing", "default value") { "block value" }.should eq "block value"
40
+ keychain.fetch("missing") { "block value" }.should eq "block value"
41
+
42
+ expect { keychain.fetch("missing") }.to raise_error(KeyError)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "#[key]" do
48
+ it "reads simple entries" do
49
+ keychain["simple"].should eq "Simple note"
50
+ end
51
+
52
+ it "reads encoded entries" do
53
+ keychain["encoded"].should eq "Encoded\nnote"
54
+ end
55
+
56
+ it "reads plist entries" do
57
+ keychain["plist"].should eq "Plist note."
58
+ end
59
+
60
+ it "reads empty entries" do
61
+ keychain["empty"].should eq ""
62
+ end
63
+
64
+ it "returns nil when there is no entry with the given name" do
65
+ keychain["nonexisting note"].should be_nil
66
+ end
67
+ end
68
+
69
+ describe "#[]=" do
70
+ it "can create a new note" do
71
+ keychain["new note"].should be_nil
72
+ keychain["new note"] = "This is new data"
73
+ keychain["new note"].should eq "This is new data"
74
+ end
75
+
76
+ it "can write data to an existing note" do
77
+ keychain["existing"].should eq "Existing note."
78
+ keychain["existing"] = "This is new"
79
+ keychain["existing"].should eq "This is new"
80
+ end
81
+
82
+ it "can delete an existing note" do
83
+ keychain["doomed"].should_not be_nil
84
+ keychain["doomed"] = nil
85
+ keychain["doomed"].should be_nil
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,77 @@
1
+ describe Mellon::Store do
2
+ subject(:store) { Mellon::Store.new(project_name, keychain: keychain) }
3
+ let(:project_name) { "yaml store" }
4
+ let(:keychain) { Mellon::Keychain.new(keychain_path) }
5
+
6
+ describe "#initialize" do
7
+ it "requires a project name" do
8
+ expect { Mellon::Store.new }.to raise_error(ArgumentError)
9
+ end
10
+
11
+ it "finds and uses the keychain containing the project name by default" do
12
+ Mellon::Keychain.should_receive(:search).with(project_name).and_return(keychain)
13
+ Mellon::Store.new(project_name).keychain.should eq keychain
14
+ end
15
+
16
+ it "uses the default keychain is no keychain contains the project name" do
17
+ keychain = double
18
+ Mellon::Keychain.should_receive(:search).with(project_name).and_return(nil)
19
+ Mellon::Keychain.should_receive(:default).and_return(keychain)
20
+ Mellon::Store.new(project_name).keychain.should eq keychain
21
+ end
22
+
23
+ it "accepts specifying the keychain by name" do
24
+ keychain = double
25
+ Mellon::Keychain.should_receive(:find).with("projects").and_return(keychain)
26
+ store = Mellon::Store.new(project_name, keychain: "projects")
27
+ store.keychain.should eq keychain
28
+ end
29
+
30
+ it "accepts specifying the keychain object" do
31
+ store = Mellon::Store.new(project_name, keychain: keychain)
32
+ store.keychain.should eq keychain
33
+ end
34
+
35
+ it "allows setting the serializer" do
36
+ require "json"
37
+ store = Mellon::Store.new("json store", keychain: keychain, serializer: JSON)
38
+ store["some value"].should eq "This is some json value"
39
+ store["some value"] = "New value"
40
+ store["some value"].should eq "New value"
41
+ end
42
+ end
43
+
44
+ describe "#[]" do
45
+ it "returns the value for key inside the store" do
46
+ store["some value"].should eq "This is some yaml value"
47
+ end
48
+
49
+ it "returns nil if store entry does not exist" do
50
+ store = Mellon::Store.new("missing project", keychain: keychain)
51
+ store["some value"].should be_nil
52
+ end
53
+ end
54
+
55
+ describe "#[]=" do
56
+ it "assigns an existing value for key inside the store" do
57
+ store["some value"].should eq "This is some yaml value"
58
+ store["some value"] = "This is a new value"
59
+ store["some value"].should eq "This is a new value"
60
+ end
61
+
62
+ it "creates the store entry if it does not exist" do
63
+ store = Mellon::Store.new("missing project", keychain: keychain)
64
+ store["some value"].should be_nil
65
+ store["some value"] = "That value"
66
+ store["some value"].should eq "That value"
67
+ end
68
+ end
69
+
70
+ specify "#fetch" do
71
+ store.fetch("some value").should eq "This is some yaml value"
72
+ store.fetch("missing value", "default").should eq "default"
73
+ store.fetch("missing value") { "default" }.should eq "default"
74
+
75
+ expect { store.fetch("missing value") }.to raise_error(KeyError)
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ describe Mellon do
2
+ specify "VERSION" do
3
+ defined?(Mellon::VERSION).should be_true
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ require "mellon"
2
+
3
+ $stderr.puts "If asked for a password, just press enter. There is no password."
4
+
5
+ keychain_path = File.expand_path("./temporary_keychain.keychain", __dir__)
6
+ original_keychain_path = File.expand_path("./keychain.keychain", __dir__)
7
+
8
+ RSpec.configure do |config|
9
+ config.around do |example|
10
+ FileUtils.cp(original_keychain_path, keychain_path)
11
+ example.run
12
+ FileUtils.rm(keychain_path)
13
+ end
14
+
15
+ define_method :keychain_path do
16
+ keychain_path
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mellon
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kim Burgestrand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: plist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - kim@burgestrand.se
100
+ executables:
101
+ - mellon
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - .gitignore
106
+ - .rspec
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - Rakefile
111
+ - bin/mellon
112
+ - elabs-logo.png
113
+ - lib/mellon.rb
114
+ - lib/mellon/keychain.rb
115
+ - lib/mellon/shell_utils.rb
116
+ - lib/mellon/store.rb
117
+ - lib/mellon/version.rb
118
+ - mellon.gemspec
119
+ - spec/keychain.keychain
120
+ - spec/mellon/keychain_spec.rb
121
+ - spec/mellon/store_spec.rb
122
+ - spec/mellon_spec.rb
123
+ - spec/spec_helper.rb
124
+ homepage: https://github.com/elabs/mellon
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.2.2
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: A command-line utility for managing secret application credentials via OSX
148
+ keychain.
149
+ test_files:
150
+ - spec/keychain.keychain
151
+ - spec/mellon/keychain_spec.rb
152
+ - spec/mellon/store_spec.rb
153
+ - spec/mellon_spec.rb
154
+ - spec/spec_helper.rb