keyman 1.0.0 → 1.1.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.
@@ -1,12 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- keyman (1.0.0)
4
+ keyman (1.1.0)
5
+ highline
5
6
  net-ssh (~> 2.6.3)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ highline (1.6.15)
10
12
  net-ssh (2.6.3)
11
13
 
12
14
  PLATFORMS
data/README.md CHANGED
@@ -4,10 +4,6 @@ This simple little utility allows you to manage the authorized_keys files for a
4
4
  number of servers & users. It is designed to provide easy access to ensure that
5
5
  you can revoke & grant access to appropriate people on multiple servers.
6
6
 
7
- **Please Note: this utility is somewhat un-tested and not currently used in any
8
- production environment. Your mileage may vary and we recommend testing in a
9
- non-production environment prior to use.**
10
-
11
7
  ## Installation
12
8
 
13
9
  To install, just install the Rubygem.
@@ -17,12 +13,19 @@ $ gem install keyman
17
13
  ```
18
14
 
19
15
  Once installed, you will need to create yourself a **manifest directory**. This
20
- directory will contain all your configuration for your key manager. You should
21
- create an empty directory and add two files, a `servers.rb` and a `users.rb` file.
16
+ directory will contain all your configuration for your key manager. You can easily
17
+ do this using the `init` command:
18
+
19
+ ```bash
20
+ $ keyman init path/to/manifest
21
+ ```
22
+
23
+ This will create a directory containing two files, a `users.km` and a `servers.km`.
24
+ These files contain examples and comments which should help you get started.
22
25
 
23
26
  ## Example Users/Groups Manifest File
24
27
 
25
- The below file is an example of a `users.rb` manifest file.
28
+ The below file is an example of a `users.km` manifest file.
26
29
 
27
30
  ```ruby
28
31
  group :admins do
@@ -39,7 +42,7 @@ end
39
42
 
40
43
  ## Example Server Manifest File
41
44
 
42
- The below file is an example of a `servers.rb` file.
45
+ The below file is an example of a `servers.km` file.
43
46
 
44
47
  ```ruby
45
48
  # An example configuration for a server where all admin users have
@@ -56,13 +59,30 @@ server do
56
59
  host 'database01.myapplication.com'
57
60
  user 'root', :admins, :dan
58
61
  end
62
+
63
+ # An example of a group of servers each with the same permissions. These
64
+ # will create servers with the same
65
+ server_group :load_balancers do
66
+ host 'lb01.myapplication.com'
67
+ host 'lb02.myapplication.com'
68
+ host 'lb03.myapplication.com'
69
+ user 'root', :admins
70
+ user 'app', :admins, :staff
71
+ end
59
72
  ```
60
73
 
74
+ You may add as many `.km` files as you wish to to your manifest directory and they
75
+ will be loaded. However, all **users** should be defined in `users.km` and nowhere
76
+ else.
77
+
61
78
  ## Pushing files to servers
62
79
 
63
- In order to push files to the server, you must already have YOUR key on the
64
- machine in order to authenticate. If you do not, you will not have access
65
- and will therefore be unable to push configuration.
80
+ In order to push your authorized_keys files to your servers, keyman must be able
81
+ to authenticate. In the first instance, we will attempt to use your local SSH keys
82
+ to do this. If we cannot authenticate with these, you will be prompted for the password
83
+ when you attempt to push. This password, if accepted, will then be cached for your
84
+ "session" and attempted for any subsequent servers which cannot be authenticated with
85
+ your SSH keys.
66
86
 
67
87
  ```bash
68
88
  $ cd path/to/manifest
@@ -70,6 +90,8 @@ $ cd path/to/manifest
70
90
  $ keyman push
71
91
  # to push configuration to a specific server
72
92
  $ keyman push database01.myapplication.com
93
+ # to push configuration to a server group
94
+ $ keyman push load_balancers
73
95
  ```
74
96
 
75
97
  There are other commands available within the app, you can view these by
@@ -78,3 +100,13 @@ viewing the inline help.
78
100
  ```bash
79
101
  $ keyman help
80
102
  ```
103
+
104
+ ## Storing your manifest directory
105
+
106
+ It is recommended to store your manifest directory in a Git repository. Once in a
107
+ repository, you will be required to ensure that your local branch is always the same
108
+ as your remote branch before you can push to a server. This ensures that you cannot
109
+ overwrite someone elses changes should you forget to pull before pushing.
110
+
111
+ This behaviour is automatic and currently non-optional when there is a .git directory
112
+ in your manifest.
data/bin/keyman CHANGED
@@ -22,17 +22,36 @@ begin
22
22
  puts "commands. In order to use this, you must be currently within your"
23
23
  puts "manifest directory."
24
24
  puts
25
- puts " keys {server} {user} - displays the authorized keys file for a server's user"
26
- puts " push - pushes the latest files to all servers"
27
- puts " push {server} - pushes the latest file to the specified server"
28
- puts " servers - displays a list of all servers"
29
- puts " permissions {server} - displays the permissions for given server"
30
- puts " users - displays a list of all users & groups"
25
+ puts " init {path} - creates a new manifest directory"
26
+ puts " keys {server} {user} - displays the authorized keys file for a server's user"
27
+ puts " push - pushes the latest files to all servers"
28
+ puts " push {server} - pushes the latest file to the specified server"
29
+ puts " servers - displays a list of all servers"
30
+ puts " permissions {server} - displays the permissions for given server"
31
+ puts " users - displays a list of all users & groups"
31
32
  puts
33
+ when 'init'
34
+ require 'fileutils'
35
+ if path = final_args[1]
36
+ if File.exist?(path)
37
+ raise Keyman::Error, "A file/directory already exists at #{path}"
38
+ else
39
+ FileUtils.mkdir(path)
40
+ template_root = File.expand_path(File.join('..', '..', 'templates'), __FILE__)
41
+ File.open(File.join(path, 'users.km'), 'w') { |f| f.write(File.read(File.join(template_root, 'users.km')))}
42
+ File.open(File.join(path, 'servers.km'), 'w') { |f| f.write(File.read(File.join(template_root, 'servers.km')))}
43
+ puts "\e[32mKeyman manifest directory created at #{path} successfully.\e[0m"
44
+ end
45
+ else
46
+ raise Keyman::Error, "You should pass a directory name to the init command to create a new manifest directory."
47
+ end
32
48
  else
49
+ puts "\e[37mUsing manifest from #{Keyman.manifest_dir}\e[0m"
33
50
  Keyman.run(final_args, options)
34
51
  end
35
52
 
53
+ rescue SystemExit, Interrupt
54
+ Process.exit(0)
36
55
  rescue Keyman::Error => e
37
56
  puts "\e[31m" + e.message + "\e[0m"
38
57
  Process.exit(1)
@@ -16,5 +16,6 @@ Gem::Specification.new do |s|
16
16
  s.email = "adam@atechmedia.com"
17
17
  s.homepage = "http://atechmedia.com"
18
18
  s.add_dependency('net-ssh', '~> 2.6.3')
19
+ s.add_dependency('highline')
19
20
  end
20
21
 
@@ -1,45 +1,55 @@
1
+ require 'yaml'
1
2
  require 'net/ssh'
3
+ require 'highline/import'
2
4
 
3
5
  require 'keyman/user'
4
6
  require 'keyman/group'
5
7
  require 'keyman/server'
6
- require 'keyman/keyfile'
7
-
8
+ require 'keyman/server_group'
9
+ require 'keyman/manifest'
8
10
 
9
11
  module Keyman
10
12
 
11
13
  # The current version of the keyman system
12
- VERSION = '1.0.0'
14
+ VERSION = '1.1.1'
13
15
 
14
16
  # An error which will be raised
15
17
  class Error < StandardError; end
16
18
 
17
19
  class << self
20
+ # Stores the actual manifest object
21
+ attr_accessor :manifest
22
+
23
+ # Storage for the manifest directory to work with
24
+ attr_accessor :manifest_dir
25
+
18
26
  # Storage for all the users, groups & servers which are loaded
19
27
  # from the manifest
20
28
  attr_accessor :users
21
29
  attr_accessor :groups
22
30
  attr_accessor :servers
23
-
24
- # Load a manifest from the given folder
25
- def load(directory)
26
- self.users = []
27
- self.groups = []
28
- self.servers = []
29
- if File.directory?(directory)
30
- ['groups.rb', 'servers.rb'].each do |file|
31
- path = File.join(directory, file)
32
- if File.exist?(path)
33
- Keyman::Keyfile.class_eval(File.read(path))
34
- else
35
- raise Error, "No '#{file}' was found in your manifest directory. Abandoning..."
36
- end
31
+ attr_accessor :server_groups
32
+
33
+ # Storage for a password cache to use within the current session
34
+ attr_accessor :password_cache
35
+
36
+ # Sets the default manifest_dir dir
37
+ def manifest_dir
38
+ @manifest_dir || self.config['manifest_dir'] || "./"
39
+ end
40
+
41
+ # Sets the configuration options
42
+ def config
43
+ @config ||= begin
44
+ config_dir = File.join(ENV['HOME'], '.keyman')
45
+ if File.exist?(config_dir)
46
+ YAML.load_file(config_dir)
47
+ else
48
+ {}
37
49
  end
38
- else
39
- raise Error, "No folder found at '#{directory}'"
40
50
  end
41
51
  end
42
-
52
+
43
53
  # Return a user or a group for the given name
44
54
  def user_or_group_for(name)
45
55
  self.users.select { |u| u.name == name.to_sym }.first || self.groups.select { |u| u.name == name.to_sym }.first
@@ -47,7 +57,7 @@ module Keyman
47
57
 
48
58
  # Execute a CLI command
49
59
  def run(args, options = {})
50
- load('./')
60
+ Manifest.load
51
61
  case args.first
52
62
  when 'keys'
53
63
  if server = self.servers.select { |s| s.host == args[1] }.first
@@ -73,10 +83,26 @@ module Keyman
73
83
  raise Error, "No server found with the hostname '#{args[1]}'"
74
84
  end
75
85
  when 'push'
86
+
87
+ if self.manifest.uses_git?
88
+ unless self.manifest.clean?
89
+ raise Error, "Your manifest is not clean. You should push to your repository before pushing."
90
+ end
91
+
92
+ unless self.manifest.latest_commit?
93
+ raise Error, "The remote server has a more up-to-date manifest. Pull first."
94
+ end
95
+
96
+ puts "\e[32mRepository check passed!\e[0m"
97
+ end
98
+
76
99
  if args[1]
77
- # push single server
78
- if server = self.servers.select { |s| s.host == args[1] }.first
100
+ server = self.servers.select { |s| s.host == args[1] }.first
101
+ server = self.server_groups.select { |s| s.name == args[1].to_sym }.first if server.nil?
102
+ if server.is_a?(Keyman::Server)
79
103
  server.push!
104
+ elsif server.is_a?(Keyman::ServerGroup)
105
+ server.servers.each(&:push!)
80
106
  else
81
107
  raise Error, "No server found with the hostname '#{args[1]}'"
82
108
  end
@@ -84,9 +110,21 @@ module Keyman
84
110
  self.servers.each(&:push!)
85
111
  end
86
112
  when 'servers'
87
- self.servers.each do |server|
88
- puts " * " + server.host
113
+ self.server_groups.sort_by(&:name).each do |group|
114
+ puts '-' * 80
115
+ puts group.name.to_s
116
+ puts '-' * 80
117
+ group.servers.each do |s|
118
+ puts " * #{s.host}"
119
+ end
120
+ end
121
+ puts '-' * 80
122
+ puts 'no group'
123
+ puts '-' * 80
124
+ self.servers.select { |s| s.group.nil?}.each do |s|
125
+ puts " * #{s.host}"
89
126
  end
127
+
90
128
  when 'users'
91
129
  self.groups.each do |group|
92
130
  puts "-" * 80
@@ -97,6 +135,27 @@ module Keyman
97
135
  puts "\e[37m#{u.key}\e[0m"
98
136
  end
99
137
  end
138
+
139
+ when 'status'
140
+ if Keyman.manifest.uses_git?
141
+ puts "Your manifest is using a remote git repository."
142
+ if Keyman.manifest.clean?
143
+ puts " * Your working copy is clean"
144
+ else
145
+ puts " * You have an un-clean working copy. You must commit before pushing."
146
+ end
147
+
148
+ if Keyman.manifest.latest_commit?
149
+ puts " * You have the latest commit fetched."
150
+ else
151
+ puts " * There is a newer version of this repo on the server."
152
+ end
153
+ else
154
+ puts "Your manifest does not use git. There is no status to display."
155
+ end
156
+
157
+ else
158
+ raise Error, "Invalid command '#{args.first}'"
100
159
  end
101
160
  end
102
161
  end
@@ -9,6 +9,9 @@ module Keyman
9
9
 
10
10
  # Add a new group with the given name
11
11
  def self.add(name, &block)
12
+ if existing = Keyman.user_or_group_for(name)
13
+ raise Error, "#{existing.class.to_s.split('::').last} already exists for '#{name}' - cannot define user with this name."
14
+ end
12
15
  g = Group.new
13
16
  g.name = name
14
17
  g.instance_eval(&block)
@@ -0,0 +1,77 @@
1
+ module Keyman
2
+ class Manifest
3
+
4
+ # Loads a manifest directory as live
5
+ def self.load(directory = Keyman.manifest_dir)
6
+ Keyman.users = []
7
+ Keyman.groups = []
8
+ Keyman.servers = []
9
+ Keyman.server_groups = []
10
+ manifest = self.new(directory)
11
+ if File.directory?(directory)
12
+ [File.join(directory, 'users.km'), Dir[File.join(directory, '*.km')]].flatten.uniq.compact.each do |file|
13
+ if File.exist?(file)
14
+ manifest.instance_eval(File.read(file))
15
+ else
16
+ raise Error, "No '#{file}' was found in your manifest directory. Abandoning..."
17
+ end
18
+ end
19
+ Keyman.manifest = manifest
20
+ else
21
+ raise Error, "No folder found at '#{directory}'"
22
+ end
23
+ end
24
+
25
+ # Initialize a new manifest with the current directory
26
+ def initialize(directory)
27
+ @directory = directory
28
+ end
29
+
30
+ # Adds a new group
31
+ def group(name, &users_block)
32
+ Group.add(name, &users_block)
33
+ end
34
+
35
+ # Adds a new server
36
+ def server(&block)
37
+ Server.add(&block)
38
+ end
39
+
40
+ # Adds a new user
41
+ def user(username, key)
42
+ User.add(username, key)
43
+ end
44
+
45
+ # Adds a new server group
46
+ def server_group(name, &block)
47
+ ServerGroup.add(name, &block)
48
+ end
49
+
50
+ # Does this manifest directory use git?
51
+ def uses_git?
52
+ File.directory?(File.join(@directory, '.git'))
53
+ end
54
+
55
+ # Is the current repo clean?
56
+ def clean?
57
+ `cd #{@directory} && git status`.chomp.include?('nothing to commit')
58
+ end
59
+
60
+ # Does the latest commit on the current branch match the remote brandh
61
+ def latest_commit?
62
+ local = `cd #{@directory} && git log --pretty=%H -n 1`.chomp
63
+ if `cd #{@directory} && git status`.chomp.match(/On branch (.*)\n/)
64
+ branch = $1
65
+ else
66
+ raise Error, "Unable to determine the local repository branch."
67
+ end
68
+ remote = `cd #{@directory} && git ls-remote 2> /dev/null | grep refs/heads/#{branch} `.chomp.split(/\s+/).first
69
+ if local.length == 40 && remote.length == 40
70
+ local == remote
71
+ else
72
+ raise Error, "Unable to determine local & remote commits"
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -1,7 +1,7 @@
1
1
  module Keyman
2
2
  class Server
3
3
 
4
- attr_accessor :host, :users, :location
4
+ attr_accessor :host, :users, :location, :group
5
5
 
6
6
  def initialize
7
7
  @users = {}
@@ -15,6 +15,17 @@ module Keyman
15
15
  s
16
16
  end
17
17
 
18
+ # Adds a new server based on it's name and location
19
+ def self.add_by_name(host, options = {})
20
+ s = self.new
21
+ s.host = host
22
+ s.location = options[:location]
23
+ s.users = options[:users]
24
+ s.group = options[:group]
25
+ Keyman.servers << s
26
+ s
27
+ end
28
+
18
29
  # Returns or sets the hostname of the server which should be used when connecting
19
30
  # and identifying this server
20
31
  def host(host = nil)
@@ -24,6 +35,7 @@ module Keyman
24
35
  # Sets a user on the server along with the access objects which should be
25
36
  # granted access
26
37
  def user(name, *access_objects)
38
+ access_objects.each { |ao| raise Error, "!! Invalid access object '#{ao}' on '#{self.host}'" unless Keyman.user_or_group_for(ao) }
27
39
  @users[name] = access_objects
28
40
  end
29
41
 
@@ -36,9 +48,12 @@ module Keyman
36
48
  # all objects from within the server
37
49
  def authorized_users(username)
38
50
  @users[username].map do |k|
39
- obj = Keyman.user_or_group_for(k)
40
- obj.is_a?(Group) ? obj.users : obj
41
- end.flatten.uniq
51
+ if obj = Keyman.user_or_group_for(k)
52
+ obj.is_a?(Group) ? obj.users : obj
53
+ else
54
+ nil
55
+ end
56
+ end.flatten.compact.uniq
42
57
  end
43
58
 
44
59
  # Returns a full string output for the authorized_keys file. Passes
@@ -59,20 +74,39 @@ module Keyman
59
74
  # configured here. This will not succeed if the current user does not
60
75
  # already have a key on the server.
61
76
  def push!
77
+ passwords_to_try = (Keyman.password_cache ||= [nil]).dup
62
78
  @users.each do |user, objects|
63
79
  begin
64
- Timeout.timeout(10) do |t|
65
- Net::SSH.start(self.host, user) do |ssh|
66
- ssh.exec!("mkdir -p ~/.ssh")
67
- file = authorized_keys(user).gsub("\n", "\\n").gsub("\t", "\\t")
68
- ssh.exec!("echo -e '#{file}' > ~/.ssh/authorized_keys")
80
+ passwords_to_try.each do |password|
81
+ Timeout.timeout(10) do |t|
82
+ Net::SSH.start(self.host, user, :password => password) do |ssh|
83
+ ssh.exec!("mkdir -p ~/.ssh")
84
+ file = authorized_keys(user).gsub("\n", "\\n").gsub("\t", "\\t")
85
+ ssh.exec!("echo -e '#{file}' > ~/.ssh/authorized_keys")
86
+ end
87
+ Keyman.password_cache << password if password
69
88
  end
70
89
  end
71
90
  puts "\e[32mPushed authorized_keys to #{user}@#{self.host}\e[0m"
91
+ rescue Net::SSH::AuthenticationFailed
92
+ passwords_to_try.shift
93
+ if passwords_to_try.empty?
94
+ puts "\e[35mAuthorization failed on to #{user}@#{self.host}.\e[0m"
95
+ password = HighLine.ask("Enter password: ") { |q| q.echo = "*" }
96
+ if password.length > 0
97
+ passwords_to_try << password
98
+ retry
99
+ else
100
+ puts "\e[37mSkipping #{user}@#{self.host}\e[0m"
101
+ end
102
+ else
103
+ retry
104
+ end
72
105
  rescue Timeout::Error
73
106
  puts "\e[31mTimed out while uploading authorized_keys to #{user}@#{self.host}\e[0m"
74
- rescue
75
- puts "\e[31mFailed to upload authorized_keys to #{user}@#{self.host}\e[0m"
107
+ rescue => e
108
+ puts "\e[31mFailed to upload authorized_keys to #{user}@#{self.host} (#{e.class})\e[0m"
109
+ puts e.message
76
110
  end
77
111
  end
78
112
  end
@@ -0,0 +1,29 @@
1
+ module Keyman
2
+ class ServerGroup
3
+
4
+ attr_accessor :servers, :users, :name
5
+
6
+ def initialize
7
+ @servers = []
8
+ @users = {}
9
+ end
10
+
11
+ def self.add(name, &block)
12
+ s = self.new
13
+ s.name = name
14
+ s.instance_eval(&block)
15
+ Keyman.server_groups << s
16
+ s
17
+ end
18
+
19
+ def server(host, location = nil)
20
+ @servers << Server.add_by_name(host, :users => @users, :group => self)
21
+ end
22
+
23
+ def user(name, *access_objects)
24
+ @users[name] = access_objects
25
+ @servers.each { |s| s.user(name, *access_objects) }
26
+ end
27
+
28
+ end
29
+ end
@@ -9,6 +9,10 @@ module Keyman
9
9
  if existing
10
10
  existing
11
11
  else
12
+ if existing = Keyman.user_or_group_for(name)
13
+ raise Error, "#{existing.class.to_s.split('::').last} already exists for '#{name}' - cannot define user with this name."
14
+ end
15
+
12
16
  u = self.new
13
17
  u.name = name.to_sym
14
18
  u.key = key
@@ -0,0 +1,22 @@
1
+ # Example Keyman server configuration file
2
+ # This file should contain all your server & server group definitions which should
3
+ # be used within this manifest.
4
+ #
5
+ # You may also create other files with the extension .km within your manifest directory.
6
+ # These must only contain server/server group definitions.
7
+ #
8
+ # server_group :app_servers do
9
+ # server 'app01.myapplication.com'
10
+ # server 'app02.myapplication.com'
11
+ # server 'app03.myapplication.com'
12
+ #
13
+ # user 'root', :admins
14
+ # user 'app', :admins, :staff
15
+ # end
16
+ #
17
+ # server do
18
+ # host 'db-a.myapplication.com'
19
+ # location :london
20
+ # user 'root', :admins
21
+ # user 'db', :admins, :database_admins
22
+ # end
@@ -0,0 +1,8 @@
1
+ # Example Keyman user/group configuration file
2
+ # This file should contain all your user & group definitions which should
3
+ # be used within this manifest.
4
+ #
5
+ # group :admins do
6
+ # user :adam, 'ssh-rsa AAAAA=='
7
+ # user :dave, 'ssh-rsa BBBBB=='
8
+ # end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keyman
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-29 00:00:00.000000000 Z
12
+ date: 2013-02-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: 2.6.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: highline
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  description: A simple library for managing distributed SSH keys
31
47
  email: adam@atechmedia.com
32
48
  executables:
@@ -39,11 +55,14 @@ files:
39
55
  - Gemfile.lock
40
56
  - keyman.gemspec
41
57
  - lib/keyman/group.rb
42
- - lib/keyman/keyfile.rb
58
+ - lib/keyman/manifest.rb
43
59
  - lib/keyman/server.rb
60
+ - lib/keyman/server_group.rb
44
61
  - lib/keyman/user.rb
45
62
  - lib/keyman.rb
46
63
  - README.md
64
+ - templates/servers.km
65
+ - templates/users.km
47
66
  homepage: http://atechmedia.com
48
67
  licenses: []
49
68
  post_install_message:
@@ -1,19 +0,0 @@
1
- module Keyman
2
- class Keyfile
3
-
4
- class << self
5
- def group(name, &users_block)
6
- Group.add(name, &users_block)
7
- end
8
-
9
- def server(&block)
10
- Server.add(&block)
11
- end
12
-
13
- def user(username, key)
14
- User.add(username, key)
15
- end
16
- end
17
-
18
- end
19
- end