supso 0.10.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.
@@ -0,0 +1,95 @@
1
+ require 'json'
2
+
3
+ module Supso
4
+ class Organization
5
+ attr_accessor :name, :id
6
+
7
+ @@current_organization = nil
8
+
9
+ def initialize(name, id)
10
+ @name = name
11
+ @id = id
12
+ end
13
+
14
+ def save_to_file!
15
+ Util.ensure_path_exists!(Organization.current_organization_filename)
16
+ file = File.open(Organization.current_organization_filename, 'w')
17
+ file << self.saved_data.to_json
18
+ file.close
19
+ Project.save_project_directory_readme!
20
+ end
21
+
22
+ def saved_data
23
+ data = {}
24
+ data['name'] = self.name if self.name
25
+ data['id'] = self.id if self.id
26
+ data
27
+ end
28
+
29
+ def self.current_organization_filename
30
+ "#{ Supso.project_supso_config_root }/current_organization.json"
31
+ end
32
+
33
+ def self.current_organization_from_file
34
+ organization_data = {}
35
+ begin
36
+ organization_data = JSON.parse(File.read(Organization.current_organization_filename))
37
+ organization_data = {} if !organization_data.is_a?(Object)
38
+ rescue
39
+ organization_data = {}
40
+ end
41
+
42
+ if organization_data['id']
43
+ Organization.new(organization_data['name'], organization_data['id'])
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def self.current_organization
50
+ @@current_organization ||= Organization.current_organization_from_file
51
+ end
52
+
53
+ def self.current_organization_or_fetch
54
+ org = Organization.current_organization
55
+
56
+ if !org
57
+ Organization.fetch_current_organization!
58
+ org = Organization.current_organization
59
+ if !org
60
+ raise StandardError.new('Could not find current organization')
61
+ else
62
+ org
63
+ end
64
+ end
65
+ end
66
+
67
+ def self.set_current_organization!(name, id)
68
+ @@current_organization = Organization.new(name, id)
69
+ @@current_organization.save_to_file!
70
+ end
71
+
72
+ def self.delete_current_organization!
73
+ @@current_organization = nil
74
+ if File.exists?(Organization.current_organization_from_file)
75
+ File.delete(Organization.current_organization_from_file)
76
+ end
77
+ end
78
+
79
+ def self.fetch_current_organization!
80
+ user = User.current_user
81
+ data = {
82
+ auth_token: user.auth_token,
83
+ user_id: user.id,
84
+ }
85
+ response = Util.http_post("#{ Supso.supso_api_root }users/me/current_organization", data)
86
+
87
+ if response['success']
88
+ org = response['organization']
89
+ Organization.set_current_organization!(org['name'], org['id'])
90
+ else
91
+ puts response['reason']
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,218 @@
1
+ require 'json'
2
+ require 'openssl'
3
+ require 'base64'
4
+
5
+ module Supso
6
+ class Project
7
+ attr_accessor :name, :api_token, :client_data, :client_token, :source, :aliases
8
+
9
+ # Validities
10
+ MISSING_DATA = :missing_token
11
+ MISSING_TOKEN = :missing_token
12
+ DIFFERENT_API_TOKEN = :different_api_token
13
+ INVALID_TOKEN = :missing_token
14
+ MISSING_ORGANIZATION = :missing_organization
15
+ DIFFERENT_ORGANIZATION = :different_organization
16
+ VALID = :valid
17
+
18
+ def initialize(name, api_token, options = {})
19
+ @name = name
20
+ @api_token = api_token
21
+ @options = options
22
+ @client_data = self.load_client_data
23
+ @client_token = self.load_client_token
24
+ @source = options[:source] || options['source']
25
+ @aliases = options[:aliases] || options['aliases'] || []
26
+ end
27
+
28
+ def filename(filetype)
29
+ "#{ Supso.project_supso_config_root }/projects/#{ self.name }.#{ filetype }"
30
+ end
31
+
32
+ def data_filename
33
+ self.filename('json')
34
+ end
35
+
36
+ def identification_data
37
+ {
38
+ name: self.name,
39
+ api_token: self.api_token,
40
+ aliases: self.aliases,
41
+ source: self.source,
42
+ }
43
+ end
44
+
45
+ def puts_info
46
+ puts "#{ self.name }"
47
+
48
+ if self.source
49
+ human_readable_source = self.source == 'add' ? 'add (ruby)' : self.source
50
+ puts " Source: #{ human_readable_source }"
51
+ end
52
+
53
+ if self.valid?
54
+ puts " Valid: Yes"
55
+ else
56
+ puts " Valid: No"
57
+ puts " Reason: #{ self.validity_explanation }"
58
+ end
59
+ end
60
+
61
+ def load_client_data
62
+ if File.exist?(self.data_filename)
63
+ JSON.parse(File.read(self.data_filename))
64
+ else
65
+ {}
66
+ end
67
+ end
68
+
69
+ def load_client_token
70
+ if self.token_file_exists?
71
+ File.read(self.token_filename)
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
77
+ def token_filename
78
+ self.filename('token')
79
+ end
80
+
81
+ def token_file_exists?
82
+ File.exist?(self.token_filename)
83
+ end
84
+
85
+ def organization_id
86
+ self.client_data['organization_id']
87
+ end
88
+
89
+ def save_project_data!
90
+ if self.client_data.empty?
91
+ if File.exists?(self.data_filename)
92
+ File.delete(self.data_filename)
93
+ end
94
+ if File.exists?(self.token_filename)
95
+ File.delete(self.token_filename)
96
+ end
97
+ else
98
+ Project.save_project_directory_readme!
99
+
100
+ Util.ensure_path_exists!(self.data_filename)
101
+ file = File.open(self.data_filename, 'w')
102
+ file << self.client_data.to_json
103
+ file.close
104
+
105
+ Util.ensure_path_exists!(self.token_filename)
106
+ file = File.open(self.token_filename, 'w')
107
+ file << self.client_token
108
+ file.close
109
+ end
110
+ end
111
+
112
+ def valid?
113
+ self.validity == VALID
114
+ end
115
+
116
+ def validity
117
+ if !self.client_token
118
+ return MISSING_TOKEN
119
+ end
120
+
121
+ if !self.client_data
122
+ return MISSING_DATA
123
+ end
124
+
125
+ if self.client_data['project_api_token'] != self.api_token
126
+ return DIFFERENT_API_TOKEN
127
+ end
128
+
129
+ if !Organization.current_organization
130
+ return MISSING_ORGANIZATION
131
+ end
132
+
133
+ if self.organization_id != Organization.current_organization.id
134
+ return DIFFERENT_ORGANIZATION
135
+ end
136
+
137
+ public_key = OpenSSL::PKey::RSA.new File.read("#{ Supso.gem_root }/lib/other/supso2.pub")
138
+ digest = OpenSSL::Digest::SHA256.new
139
+
140
+ if !public_key.verify(digest, Base64.decode64(self.client_token), self.client_data.to_json)
141
+ return INVALID_TOKEN
142
+ end
143
+
144
+ return VALID
145
+ end
146
+
147
+ def validity_explanation
148
+ case self.validity
149
+ when MISSING_TOKEN
150
+ "Missing client token. Run `supso update` to update the token."
151
+ when MISSING_DATA
152
+ "Missing client data. Run `supso update` to update the data."
153
+ when DIFFERENT_API_TOKEN
154
+ "Different api token. The project's api token is different from the project api token listed in your client data. Make sure your projects are all up-to-date and you have the latest client token from `supso update`."
155
+ when MISSING_ORGANIZATION
156
+ "Missing organization. Run `supso update` to update your organization file."
157
+ when DIFFERENT_ORGANIZATION
158
+ "Different organization. The client token uses organization id #{ organization_id }, but " +
159
+ "your current organization id is #{ Organization.current_organization['id'] } (#{ Organization.current_organization['name'] })."
160
+ when INVALID_TOKEN
161
+ "Invalid client token. Run `supso update` to update the token."
162
+ when VALID
163
+ "Valid."
164
+ else
165
+ "Invalid."
166
+ end
167
+ end
168
+
169
+ def save_data!
170
+ self.save_project_data
171
+ end
172
+
173
+ class << self
174
+ attr_accessor :projects
175
+ end
176
+
177
+ def self.projects
178
+ @projects ||= []
179
+ end
180
+
181
+ def self.add(name, api_token, options = {})
182
+ # Correct for common mistakes:
183
+ options[:aliases] = options['aliases'] if options['aliases'] && !options[:aliases]
184
+ options[:source] = options['source'] if options['source'] && !options[:source]
185
+
186
+ options[:source] ||= 'add'
187
+ project = Project.new(name, api_token, options)
188
+ self.projects << project
189
+ end
190
+
191
+ def self.detect_all_projects!
192
+ Util.require_all_gems!
193
+ Javascript.detect_all_projects!
194
+ end
195
+
196
+ def self.save_project_directory_readme!
197
+ readme_path = "#{ Supso.project_supso_config_root }/README.txt"
198
+ if !File.exists?(readme_path)
199
+ readme_contents = File.open("#{ Supso.gem_root }/lib/templates/project_dir_readme.txt", 'r').read
200
+ Util.ensure_path_exists!(readme_path)
201
+ file = File.open(readme_path, 'w')
202
+ file << readme_contents
203
+ file.close
204
+ end
205
+ end
206
+
207
+ def self.aliases_match?(aliases1 = [], aliases2 = [])
208
+ aliases2.each do |first_alias|
209
+ if aliases1.any? { |second_alias| second_alias['name'] == first_alias['name'] &&
210
+ second_alias['platform'] == first_alias['platform'] }
211
+ return true
212
+ end
213
+ end
214
+
215
+ false
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,91 @@
1
+ module Supso
2
+ module Updater
3
+ def Updater.update
4
+ user = User.current_user
5
+ if user && user.auth_token
6
+ Updater.update_returning_user(user)
7
+ else
8
+ Updater.update_first_time_user
9
+ end
10
+ end
11
+
12
+ def Updater.update_first_time_user
13
+ puts "Super Source lets you subscribe to projects, so that you can receive urgent security announcements, important new versions, and other information via email."
14
+
15
+ Project.detect_all_projects!
16
+ puts "You are using the following projects with Super Source:"
17
+ Project.projects.each do |project|
18
+ puts " #{ project.name }"
19
+ end
20
+
21
+ puts "You can opt-out if you wish, however you must provide a valid email, in order to receive the confirmation token."
22
+
23
+ email = Commands.prompt_email
24
+ User.attach_to_email!(email)
25
+
26
+ succeeded = false
27
+ while !succeeded
28
+ token = Commands.prompt_confirmation_token(email)
29
+ succeeded, reason = User.log_in_with_confirmation_token!(email, token)
30
+ if !succeeded && reason
31
+ puts reason
32
+ end
33
+ end
34
+
35
+ Updater.update_projects!
36
+ end
37
+
38
+ def Updater.update_returning_user(user)
39
+ org = Organization.current_organization_or_fetch
40
+ Project.detect_all_projects!
41
+ Updater.update_projects!
42
+ end
43
+
44
+ def Updater.update_projects!(projects = nil)
45
+ if projects.nil?
46
+ projects = Project.projects
47
+ end
48
+
49
+ if projects.length == 0
50
+ puts "No projects in list to update."
51
+ return
52
+ end
53
+
54
+ puts "Updating #{ Util.pluralize(projects.length, 'project') }..."
55
+
56
+ user = User.current_user
57
+ organization = Organization.current_organization
58
+
59
+ data = {
60
+ auth_token: user.auth_token,
61
+ user_id: user.id,
62
+ projects: projects.map { |project| project.identification_data },
63
+ }
64
+
65
+ response = Util.http_post("#{ Supso.supso_api_root }organizations/#{ organization.id }/client_tokens", data)
66
+
67
+ if response['success']
68
+ response['projects'].each do |project_response|
69
+ client_data = project_response['client_data']
70
+ client_token = project_response['client_token']
71
+ api_token = client_data['project_api_token']
72
+ aliases = client_data['project_aliases'] || []
73
+ project = projects.find { |find_project| find_project.api_token == api_token }
74
+ if project.nil?
75
+ project = projects.find { |find_project| Project.aliases_match?(find_project.aliases, aliases) }
76
+
77
+ if project.nil?
78
+ next # Could log warning
79
+ end
80
+ end
81
+ project.client_data = client_data
82
+ project.client_token = client_token
83
+ project.aliases = aliases
84
+ project.save_project_data!
85
+ end
86
+ else
87
+ puts response['reason']
88
+ end
89
+ end
90
+ end
91
+ end
data/lib/supso/user.rb ADDED
@@ -0,0 +1,161 @@
1
+ require 'yaml'
2
+
3
+ module Supso
4
+ class User
5
+ attr_accessor :email, :name, :id, :auth_token
6
+
7
+ @@current_user = nil
8
+
9
+ def initialize(email, name, id, auth_token = nil)
10
+ @email = email
11
+ @name = name
12
+ @id = id
13
+ @auth_token = auth_token
14
+ end
15
+
16
+ def save_to_file!
17
+ Util.ensure_path_exists!(User.current_user_filename)
18
+ file = File.open(User.current_user_filename, 'w')
19
+ file << self.saved_data.to_json
20
+ file.close
21
+ User.save_user_supso_readme!
22
+ end
23
+
24
+ def saved_data
25
+ data = {
26
+ 'email' => self.email
27
+ }
28
+ data['name'] = self.name if self.name
29
+ data['id'] = self.id if self.id
30
+ data['auth_token'] = self.auth_token if self.auth_token
31
+ data
32
+ end
33
+
34
+ def self.current_user_filename
35
+ "#{ Supso.user_supso_config_root }/current_user.json"
36
+ end
37
+
38
+ def self.readme_filename
39
+ "#{ Supso.user_supso_config_root }/README.txt"
40
+ end
41
+
42
+ def self.current_user_from_file
43
+ if !File.exist?(User.current_user_filename)
44
+ return nil
45
+ end
46
+
47
+ user_data = {}
48
+ begin
49
+ user_data = JSON.parse(File.read(User.current_user_filename))
50
+ user_data = {} if !user_data.is_a?(Object)
51
+ rescue JSON::ParserError => err
52
+ user_data = {}
53
+ end
54
+
55
+ if user_data['email'] || user_data['auth_token']
56
+ User.new(user_data['email'], user_data['name'], user_data['id'], user_data['auth_token'])
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ def self.current_user
63
+ @@current_user ||= User.current_user_from_file
64
+ end
65
+
66
+ def self.set_current_user!(email, name, id, auth_token = nil)
67
+ @@current_user = User.new(email, name, id, auth_token)
68
+ @@current_user.save_to_file!
69
+ end
70
+
71
+ def self.attach_to_email!(email)
72
+ data = {
73
+ email: email,
74
+ }
75
+
76
+ response = Util.http_post("#{ Supso.supso_api_root }users/attach", data)
77
+
78
+ if response['success']
79
+ User.set_current_user!(response['user']['email'], response['user']['name'],
80
+ response['user']['id'])
81
+ else
82
+ puts response['reason']
83
+ # Anything here needed?
84
+ end
85
+ end
86
+
87
+ def self.log_in_with_password!(email, password)
88
+ data = {
89
+ email: email,
90
+ password: password,
91
+ }
92
+
93
+ response = Util.http_post("#{ Supso.supso_api_root }sign_in", data)
94
+
95
+ if response['success']
96
+ User.set_current_user!(response['user']['email'], response['user']['name'],
97
+ response['user']['id'], response['auth_token'])
98
+ if Organization.current_organization.nil?
99
+ Organization.set_current_organization!(response['organization']['name'], response['organization']['id'])
100
+ end
101
+ [true, nil]
102
+ else
103
+ [false, response['reason']]
104
+ end
105
+ end
106
+
107
+ def self.log_in_with_confirmation_token!(email, confirmation_token)
108
+ data = {
109
+ email: email,
110
+ confirmation_token: confirmation_token,
111
+ }
112
+
113
+ response = Util.http_post("#{ Supso.supso_api_root }users/confirm", data)
114
+
115
+ if response['success']
116
+ User.set_current_user!(response['user']['email'], response['user']['name'],
117
+ response['user']['id'], response['auth_token'])
118
+ if Organization.current_organization.nil?
119
+ Organization.set_current_organization!(response['organization']['name'], response['organization']['id'])
120
+ end
121
+ [true, nil]
122
+ else
123
+ [false, response['reason']]
124
+ end
125
+ end
126
+
127
+ def self.log_out!
128
+ if File.exists?(User.current_user_filename)
129
+ user = User.current_user
130
+ if user && user.auth_token
131
+ data = {
132
+ version: Supso::VERSION,
133
+ auth_token: user.auth_token,
134
+ user_id: user.id,
135
+ }
136
+
137
+ response = Util.http_post("#{ Supso.supso_api_root }sign_out", data)
138
+
139
+ if response['success']
140
+ # No need to say anything like 'logout succeeded'
141
+ else
142
+ puts response['reason']
143
+ end
144
+ end
145
+
146
+ File.delete(User.current_user_filename)
147
+ @@current_user = nil
148
+ end
149
+ end
150
+
151
+ def self.save_user_supso_readme!
152
+ if !File.exists?(User.readme_filename)
153
+ readme_contents = File.open("#{ Supso.gem_root }/lib/templates/user_dir_readme.txt", 'r').read
154
+ Util.ensure_path_exists!(User.readme_filename)
155
+ file = File.open(User.readme_filename, 'w')
156
+ file << readme_contents
157
+ file.close
158
+ end
159
+ end
160
+ end
161
+ end
data/lib/supso/util.rb ADDED
@@ -0,0 +1,129 @@
1
+ require 'fileutils'
2
+ require 'bundler'
3
+ require 'json'
4
+
5
+ module Supso
6
+ module Util
7
+ def Util.deep_merge(first, second)
8
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
9
+ first.merge(second, &merger)
10
+ end
11
+
12
+ def Util.detect_project_root
13
+ project_root = Dir.getwd
14
+ while true
15
+ if project_root == ""
16
+ project_root = nil
17
+ break
18
+ end
19
+
20
+ if File.exist?(project_root + '/Gemfile') ||
21
+ File.exist?(project_root + '/package.json') ||
22
+ File.exist?(project_root + '.supso')
23
+ break
24
+ end
25
+
26
+ detect_project_root_splits = project_root.split("/")
27
+ detect_project_root_splits = detect_project_root_splits[0..detect_project_root_splits.length - 2]
28
+ project_root = detect_project_root_splits.join("/")
29
+ end
30
+
31
+ project_root
32
+ end
33
+
34
+ def Util.ensure_path_exists!(full_path)
35
+ split_paths = full_path.split('/')
36
+ just_file_path = split_paths.pop
37
+ directory_path = split_paths.join('/')
38
+ FileUtils.mkdir_p(directory_path)
39
+ FileUtils.touch("#{ directory_path }/#{ just_file_path }")
40
+ end
41
+
42
+ def Util.has_command?(command)
43
+ !!Util.which(command)
44
+ end
45
+
46
+ def Util.which(command)
47
+ command = Util.sanitize_command(command)
48
+ response = `which #{ command }`
49
+ response && response.length > 0 ? response : nil
50
+ end
51
+
52
+ def Util.sanitize_command(command)
53
+ command.gsub(/[^-_\w]/, '')
54
+ end
55
+
56
+ def Util.http_get(url)
57
+ json_headers = {
58
+ "Content-Type" => "application/json",
59
+ "Accept" => "application/json",
60
+ }
61
+ uri = URI.parse(url)
62
+ http = Net::HTTP.new(uri.host, uri.port)
63
+ if url.start_with?('https://')
64
+ http.use_ssl = true
65
+ end
66
+ response = http.get(uri.path, json_headers)
67
+
68
+ if response.code.to_i == 200
69
+ return JSON.parse(response.body)
70
+ else
71
+ raise StandardError.new("Error #{ response } for #{ url }")
72
+ end
73
+ end
74
+
75
+ def Util.http_post(url, data = {})
76
+ json_headers = {
77
+ "Content-Type" => "application/json",
78
+ "Accept" => "application/json",
79
+ }
80
+
81
+ data[:version] = Supso::VERSION
82
+
83
+ uri = URI.parse(url)
84
+ http = Net::HTTP.new(uri.host, uri.port)
85
+ if url.start_with?('https://')
86
+ http.use_ssl = true
87
+ end
88
+ response = http.post(uri.path, data.to_json, json_headers)
89
+
90
+ if response.code.to_i == 200
91
+ return JSON.parse(response.body)
92
+ else
93
+ raise StandardError.new("Error #{ response } for #{ url }")
94
+ end
95
+ end
96
+
97
+ def Util.is_email?(email)
98
+ !!/\A[^@]+@([^@\.]+\.)+[^@\.]+\z/.match(email)
99
+ end
100
+
101
+ def Util.pluralize(count, word)
102
+ if count == 1
103
+ word
104
+ else
105
+ "#{ word }s"
106
+ end
107
+ end
108
+
109
+ def Util.require_all_gems!
110
+ begin
111
+ Bundler.require(:default, :development, :test, :production)
112
+ rescue Gem::LoadError, Bundler::GemfileNotFound
113
+ # Keep going
114
+ end
115
+ end
116
+
117
+ def Util.underscore_to_camelcase(str)
118
+ str.split('_').map{ |chunk| chunk.capitalize }.join
119
+ end
120
+
121
+ def Util.camelcase_to_underscore(str)
122
+ str.gsub(/::/, '/')
123
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
124
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
125
+ .tr("-", "_")
126
+ .downcase
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,8 @@
1
+ module Supso
2
+ if !defined?(Supso::VERSION)
3
+ VERSION_MAJOR = 0
4
+ VERSION_MINOR = 10
5
+ VERSION_PATCH = 0
6
+ VERSION = [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH].join('.')
7
+ end
8
+ end