openproject-token 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 +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: []
|