openproject-token 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/open_project/token.rb +176 -0
- data/lib/open_project/token/armor.rb +38 -0
- data/lib/open_project/token/extractor.rb +68 -0
- data/lib/open_project/token/version.rb +5 -0
- data/lib/openproject-token.rb +1 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1c30facc4e5d73dae3a1260b332edd3032fe1fd5
|
4
|
+
data.tar.gz: f4bff0f4de4653c09109a2429bf782ce8b394991
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1b2556482fddcd2576379c2f9c0bf61338a6c373408a18bc3e4bb72300ed6b586fddbaec709cf7aa0f7e16758c3d630fc44f35baa9d1bd68cae29e08dc890ae
|
7
|
+
data.tar.gz: 5f9341a35e78b775a8114461626dd18040506736ccbcc938338c8ae24d92909de39a011aa8e630ce4fda220eaeee3bd1f106a484e513bc89958055fe91158d5f
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "date"
|
3
|
+
require "json"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
require 'active_model'
|
7
|
+
|
8
|
+
require "open_project/token/version"
|
9
|
+
require "open_project/token/extractor"
|
10
|
+
require "open_project/token/armor"
|
11
|
+
|
12
|
+
module OpenProject
|
13
|
+
class Token
|
14
|
+
class Error < StandardError; end
|
15
|
+
class ImportError < Error; end
|
16
|
+
class ParseError < Error; end
|
17
|
+
class ValidationError < Error; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_reader :key, :extractor
|
21
|
+
|
22
|
+
def key=(key)
|
23
|
+
if key && !key.is_a?(OpenSSL::PKey::RSA)
|
24
|
+
raise ArgumentError, "Key is missing."
|
25
|
+
end
|
26
|
+
|
27
|
+
@key = key
|
28
|
+
@extractor = Extractor.new(self.key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def import(data)
|
32
|
+
raise ImportError, "Missing key." if key.nil?
|
33
|
+
raise ImportError, "No token data." if data.nil?
|
34
|
+
|
35
|
+
data = Armor.decode(data)
|
36
|
+
json = extractor.read(data)
|
37
|
+
attributes = JSON.parse(json)
|
38
|
+
|
39
|
+
new(attributes)
|
40
|
+
rescue Extractor::Error
|
41
|
+
raise ImportError, "Token value could not be read."
|
42
|
+
rescue JSON::ParserError
|
43
|
+
raise ImportError, "Token value is invalid JSON."
|
44
|
+
rescue Armor::ParseError
|
45
|
+
raise ImportError, "Token value could not be parsed."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
include ActiveModel::Validations
|
50
|
+
|
51
|
+
attr_reader :version
|
52
|
+
attr_accessor :subscriber, :mail
|
53
|
+
attr_accessor :starts_at, :issued_at, :expires_at
|
54
|
+
attr_accessor :notify_admins_at, :notify_users_at, :block_changes_at
|
55
|
+
attr_accessor :restrictions
|
56
|
+
|
57
|
+
validates_presence_of :subscriber
|
58
|
+
validates_presence_of :mail
|
59
|
+
|
60
|
+
validates_each(
|
61
|
+
:starts_at, :issued_at, :expires_at, :notify_admins_at, :notify_users_at, :block_changes_at,
|
62
|
+
allow_blank: true) do |record, attr, value|
|
63
|
+
|
64
|
+
record.errors.add attr, 'is not a date' if !value.is_a?(Date)
|
65
|
+
end
|
66
|
+
|
67
|
+
validates_each :restrictions, allow_nil: true do |record, attr, value|
|
68
|
+
record.errors.add attr, :invalid if !value.is_a?(Hash)
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(attributes = {})
|
72
|
+
load_attributes(attributes)
|
73
|
+
end
|
74
|
+
|
75
|
+
def will_expire?
|
76
|
+
self.expires_at
|
77
|
+
end
|
78
|
+
|
79
|
+
def will_notify_admins?
|
80
|
+
self.notify_admins_at
|
81
|
+
end
|
82
|
+
|
83
|
+
def will_notify_users?
|
84
|
+
self.notify_users_at
|
85
|
+
end
|
86
|
+
|
87
|
+
def will_block_changes?
|
88
|
+
self.block_changes_at
|
89
|
+
end
|
90
|
+
|
91
|
+
def expired?
|
92
|
+
will_expire? && Date.today >= self.expires_at
|
93
|
+
end
|
94
|
+
|
95
|
+
def notify_admins?
|
96
|
+
will_notify_admins? && Date.today >= self.notify_admins_at
|
97
|
+
end
|
98
|
+
|
99
|
+
def notify_users?
|
100
|
+
will_notify_users? && Date.today >= self.notify_users_at
|
101
|
+
end
|
102
|
+
|
103
|
+
def block_changes?
|
104
|
+
will_block_changes? && Date.today >= self.block_changes_at
|
105
|
+
end
|
106
|
+
|
107
|
+
def restricted?(key = nil)
|
108
|
+
if key
|
109
|
+
restricted? && restrictions.has_key?(key)
|
110
|
+
else
|
111
|
+
restrictions && restrictions.length >= 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def attributes
|
116
|
+
hash = {}
|
117
|
+
|
118
|
+
hash["version"] = self.version
|
119
|
+
hash["subscriber"] = self.subscriber
|
120
|
+
hash["mail"] = self.mail
|
121
|
+
|
122
|
+
hash["issued_at"] = self.issued_at
|
123
|
+
hash["starts_at"] = self.starts_at
|
124
|
+
hash["expires_at"] = self.expires_at if self.will_expire?
|
125
|
+
|
126
|
+
hash["notify_admins_at"] = self.notify_admins_at if self.will_notify_admins?
|
127
|
+
hash["notify_users_at"] = self.notify_users_at if self.will_notify_users?
|
128
|
+
hash["block_changes_at"] = self.block_changes_at if self.will_block_changes?
|
129
|
+
|
130
|
+
hash["restrictions"] = self.restrictions if self.restricted?
|
131
|
+
|
132
|
+
hash
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_json
|
136
|
+
JSON.dump(self.attributes)
|
137
|
+
end
|
138
|
+
|
139
|
+
def from_json(json)
|
140
|
+
load_attributes(JSON.parse(json))
|
141
|
+
rescue => e
|
142
|
+
raise ParseError, "Failed to load from json: #{e}"
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def load_attributes(attributes)
|
148
|
+
attributes = Hash[attributes.map { |k, v| [k.to_s, v] }]
|
149
|
+
|
150
|
+
version = attributes["version"] || 1
|
151
|
+
unless version && version == 1
|
152
|
+
raise ArgumentError, "Version is too new"
|
153
|
+
end
|
154
|
+
|
155
|
+
@version = version
|
156
|
+
@subscriber = attributes["subscriber"]
|
157
|
+
@mail = attributes["mail"]
|
158
|
+
|
159
|
+
%w(starts_at issued_at expires_at
|
160
|
+
notify_admins_at notify_users_at block_changes_at).each do |attr|
|
161
|
+
value = attributes[attr]
|
162
|
+
value = Date.parse(value) rescue nil if value.is_a?(String)
|
163
|
+
|
164
|
+
next unless value
|
165
|
+
|
166
|
+
send("#{attr}=", value)
|
167
|
+
end
|
168
|
+
|
169
|
+
restrictions = attributes["restrictions"]
|
170
|
+
if restrictions && restrictions.is_a?(Hash)
|
171
|
+
restrictions = Hash[restrictions.map { |k, v| [k.to_sym, v] }]
|
172
|
+
@restrictions = restrictions
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module OpenProject
|
2
|
+
class Token
|
3
|
+
module Armor
|
4
|
+
class ParseError < StandardError; end
|
5
|
+
|
6
|
+
MARKER = 'OPENPROJECT-EE TOKEN'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def header
|
10
|
+
"-----BEGIN #{MARKER}-----"
|
11
|
+
end
|
12
|
+
|
13
|
+
def footer
|
14
|
+
"-----END #{MARKER}-----"
|
15
|
+
end
|
16
|
+
|
17
|
+
def encode(data)
|
18
|
+
''.tap do |s|
|
19
|
+
s << header << "\n"
|
20
|
+
|
21
|
+
s << data.strip << "\n"
|
22
|
+
|
23
|
+
s << footer
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def decode(data)
|
28
|
+
match = data.match /#{header}\r?\n(.+?)\r?\n#{footer}/m
|
29
|
+
if match.nil?
|
30
|
+
raise ParseError, 'Failed to parse armored text.'
|
31
|
+
end
|
32
|
+
|
33
|
+
match[1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module OpenProject
|
2
|
+
class Token
|
3
|
+
class Extractor
|
4
|
+
class Error < StandardError; end
|
5
|
+
class KeyError < Error; end
|
6
|
+
class DecryptionError < Error; end
|
7
|
+
|
8
|
+
attr_accessor :key
|
9
|
+
|
10
|
+
def initialize(key)
|
11
|
+
@key = key
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(data)
|
15
|
+
unless key.public?
|
16
|
+
raise KeyError, "Provided key is not a public key."
|
17
|
+
end
|
18
|
+
|
19
|
+
json_data = Base64.decode64(data.chomp)
|
20
|
+
|
21
|
+
begin
|
22
|
+
encryption_data = JSON.parse(json_data)
|
23
|
+
rescue JSON::ParserError
|
24
|
+
raise DecryptionError, "Encryption data is invalid JSON."
|
25
|
+
end
|
26
|
+
|
27
|
+
unless %w(data key iv).all? { |key| encryption_data[key] }
|
28
|
+
raise DecryptionError, "Required field missing from encryption data."
|
29
|
+
end
|
30
|
+
|
31
|
+
encrypted_data = Base64.decode64(encryption_data["data"])
|
32
|
+
encrypted_key = Base64.decode64(encryption_data["key"])
|
33
|
+
aes_iv = Base64.decode64(encryption_data["iv"])
|
34
|
+
|
35
|
+
begin
|
36
|
+
# Decrypt the AES key using asymmetric RSA encryption.
|
37
|
+
aes_key = self.key.public_decrypt(encrypted_key)
|
38
|
+
rescue OpenSSL::PKey::RSAError
|
39
|
+
raise DecryptionError, "AES encryption key could not be decrypted."
|
40
|
+
end
|
41
|
+
|
42
|
+
# Decrypt the data using symmetric AES encryption.
|
43
|
+
cipher = OpenSSL::Cipher::AES128.new(:CBC)
|
44
|
+
cipher.decrypt
|
45
|
+
|
46
|
+
begin
|
47
|
+
cipher.key = aes_key
|
48
|
+
rescue OpenSSL::Cipher::CipherError
|
49
|
+
raise DecryptionError, "AES encryption key is invalid."
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
cipher.iv = aes_iv
|
54
|
+
rescue OpenSSL::Cipher::CipherError
|
55
|
+
raise DecryptionError, "AES IV is invalid."
|
56
|
+
end
|
57
|
+
|
58
|
+
begin
|
59
|
+
data = cipher.update(encrypted_data) + cipher.final
|
60
|
+
rescue OpenSSL::Cipher::CipherError
|
61
|
+
raise DecryptionError, "Data could not be decrypted."
|
62
|
+
end
|
63
|
+
|
64
|
+
data
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'open_project/token'
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: openproject-token
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- OpenProject GmbH
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.5'
|
55
|
+
description:
|
56
|
+
email: info@openproject.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- lib/open_project/token.rb
|
62
|
+
- lib/open_project/token/armor.rb
|
63
|
+
- lib/open_project/token/extractor.rb
|
64
|
+
- lib/open_project/token/version.rb
|
65
|
+
- lib/openproject-token.rb
|
66
|
+
homepage: https://www.openproject.org
|
67
|
+
licenses:
|
68
|
+
- GPLv3
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.4.5.1
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: OpenProject EE token reader
|
90
|
+
test_files: []
|