yiffspace 0.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +10 -0
- data/app/assets/config/yiff_space_manifest.js +1 -0
- data/app/assets/stylesheets/yiff_space/application.css +30 -0
- data/app/controllers/yiff_space/application_controller.rb +6 -0
- data/app/helpers/yiff_space/application_helper.rb +6 -0
- data/app/helpers/yiff_space/auth_helper.rb +6 -0
- data/app/models/yiff_space/application_record.rb +7 -0
- data/app/views/layouts/yiff_space/application.html.erb +17 -0
- data/app/views/yiff_space/error.html.erb +3 -0
- data/engines/auth/app/controllers/yiff_space/auth/application_controller.rb +11 -0
- data/engines/auth/app/controllers/yiff_space/auth/root_controller.rb +67 -0
- data/engines/auth/app/views/yiff_space/auth/root/permissions.html.erb +14 -0
- data/engines/auth/config/routes.rb +11 -0
- data/lib/tasks/yiff_space_tasks.rake +6 -0
- data/lib/yiffspace/auth/auth_info/anonymous.rb +64 -0
- data/lib/yiffspace/auth/auth_info.rb +74 -0
- data/lib/yiffspace/auth/client.rb +58 -0
- data/lib/yiffspace/auth/discord_info.rb +63 -0
- data/lib/yiffspace/auth/engine.rb +57 -0
- data/lib/yiffspace/auth/helper.rb +167 -0
- data/lib/yiffspace/auth/permissions.rb +85 -0
- data/lib/yiffspace/auth/set_client_name.rb +15 -0
- data/lib/yiffspace/auth/user_info/anonymous.rb +72 -0
- data/lib/yiffspace/auth/user_info.rb +91 -0
- data/lib/yiffspace/auth.rb +84 -0
- data/lib/yiffspace/config_builder.rb +189 -0
- data/lib/yiffspace/configuration/images.rb +22 -0
- data/lib/yiffspace/configuration.rb +30 -0
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client/scoped.rb +13 -0
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client.rb +13 -0
- data/lib/yiffspace/core_ext/active_record/all.rb +3 -0
- data/lib/yiffspace/core_ext/active_record/where_chain.rb +6 -0
- data/lib/yiffspace/core_ext/all.rb +8 -0
- data/lib/yiffspace/core_ext/arel/all.rb +4 -0
- data/lib/yiffspace/core_ext/arel/cross_join_lateral.rb +21 -0
- data/lib/yiffspace/core_ext/arel/left_join_lateral.rb +21 -0
- data/lib/yiffspace/core_ext/enumerable/all.rb +3 -0
- data/lib/yiffspace/core_ext/enumerable/parallel.rb +7 -0
- data/lib/yiffspace/core_ext/hash/all.rb +3 -0
- data/lib/yiffspace/core_ext/hash/to_open_hash.rb +7 -0
- data/lib/yiffspace/core_ext/open_hash.rb +3 -0
- data/lib/yiffspace/core_ext/string/all.rb +5 -0
- data/lib/yiffspace/core_ext/string/sql.rb +7 -0
- data/lib/yiffspace/core_ext/string/to_b.rb +7 -0
- data/lib/yiffspace/core_ext/string/truthy_falsy.rb +7 -0
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client/scoped.rb +15 -0
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client.rb +13 -0
- data/lib/yiffspace/extensions/active_record/where_chain.rb +107 -0
- data/lib/yiffspace/extensions/arel/nodes/cross_join_lateral.rb +17 -0
- data/lib/yiffspace/extensions/arel/nodes/left_join_lateral.rb +17 -0
- data/lib/yiffspace/extensions/arel/table/cross_join_lateral.rb +15 -0
- data/lib/yiffspace/extensions/arel/table/left_join_lateral.rb +15 -0
- data/lib/yiffspace/extensions/arel/visitors/postgresql/cross_join_lateral.rb +19 -0
- data/lib/yiffspace/extensions/arel/visitors/postgresql/left_join_lateral.rb +23 -0
- data/lib/yiffspace/extensions/enumerable/parallel.rb +39 -0
- data/lib/yiffspace/extensions/hash/to_open_hash.rb +15 -0
- data/lib/yiffspace/extensions/string/sql.rb +21 -0
- data/lib/yiffspace/extensions/string/to_b.rb +13 -0
- data/lib/yiffspace/extensions/string/truthy_falsy.rb +17 -0
- data/lib/yiffspace/images/avatar/base.rb +26 -0
- data/lib/yiffspace/images/avatar/discord.rb +37 -0
- data/lib/yiffspace/images/avatar/gravatar.rb +21 -0
- data/lib/yiffspace/images/avatar.rb +29 -0
- data/lib/yiffspace/images/banner/base.rb +26 -0
- data/lib/yiffspace/images/banner/discord.rb +37 -0
- data/lib/yiffspace/images/banner.rb +29 -0
- data/lib/yiffspace/images.rb +6 -0
- data/lib/yiffspace/utils/open_hash.rb +54 -0
- data/lib/yiffspace/utils/set_env_constraint.rb +18 -0
- data/lib/yiffspace/utils.rb +6 -0
- data/lib/yiffspace/version.rb +5 -0
- data/lib/yiffspace.rb +11 -0
- metadata +176 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("active_support/concern")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Auth
|
|
7
|
+
module Helper
|
|
8
|
+
extend(ActiveSupport::Concern)
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def set_client_name(name) # rubocop:disable Naming/AccessorMethodName
|
|
12
|
+
before_action do |controller|
|
|
13
|
+
controller.request.env["yiffspace.auth.client_name"] = name
|
|
14
|
+
if controller.respond_to?(:yiffspace_client_name=)
|
|
15
|
+
controller.yiffspace_client_name = name
|
|
16
|
+
elsif controller.respond_to?(:client_name=)
|
|
17
|
+
controller.client_name = name
|
|
18
|
+
elsif controller.respond_to?(:helpers)
|
|
19
|
+
if controller.helpers.respond_to?(:yiffspace_client_name=)
|
|
20
|
+
controller.helpers.yiffspace_client_name = name
|
|
21
|
+
elsif controller.helpers.respond_to?(:client_name=)
|
|
22
|
+
controller.helpers.client_name = name
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def auth_raw
|
|
30
|
+
session[auth_client_config.auth_session_key]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def auth
|
|
34
|
+
return AuthInfo::Anonymous.instance if auth_raw.blank?
|
|
35
|
+
|
|
36
|
+
AuthInfo.from_session(auth_raw)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def auth?
|
|
40
|
+
auth_raw.present? && !auth.anonymous?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def auth=(value)
|
|
44
|
+
value = nil if value.is_a?(AuthInfo::Anonymous)
|
|
45
|
+
session[auth_client_config.auth_session_key] = value&.to_session
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset_auth!
|
|
49
|
+
session.delete(auth_client_config.auth_session_key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def user_raw
|
|
53
|
+
session[auth_client_config.user_session_key]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def user
|
|
57
|
+
return UserInfo::Anonymous.instance if user_raw.blank?
|
|
58
|
+
|
|
59
|
+
UserInfo.from_session(user_raw)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def user?
|
|
63
|
+
user_raw.present? && !user.anonymous?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def user=(value)
|
|
67
|
+
value = nil if value.is_a?(UserInfo::Anonymous)
|
|
68
|
+
session[auth_client_config.user_session_key] = value&.to_session
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reset_user!
|
|
72
|
+
session.delete(auth_client_config.user_session_key)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def state
|
|
76
|
+
session[auth_client_config.state_session_key]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def state=(value)
|
|
80
|
+
session[auth_client_config.state_session_key] = value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reset_state!
|
|
84
|
+
session.delete(auth_client_config.state_session_key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def generate_state!
|
|
88
|
+
generator = auth_client_config.state_generator
|
|
89
|
+
self.state = generator.call(*(generator.arity.zero? ? [] : [self]))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def return_path
|
|
93
|
+
session[auth_client_config.return_path_session_key]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def return_path=(value)
|
|
97
|
+
return if value && (!value.start_with?("/") || value.start_with?("//"))
|
|
98
|
+
|
|
99
|
+
session[auth_client_config.return_path_session_key] = value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reset_return_path!
|
|
103
|
+
session.delete(auth_client_config.return_path_session_key)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def full_reset!
|
|
107
|
+
reset_state!
|
|
108
|
+
reset_auth!
|
|
109
|
+
reset_user!
|
|
110
|
+
reset_return_path!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def require_auth(path)
|
|
114
|
+
redirect_to(path) unless auth?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def has_permission?(name)
|
|
118
|
+
return false unless auth?
|
|
119
|
+
|
|
120
|
+
auth.permissions.has?(name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def url_helpers
|
|
124
|
+
YiffSpace::Auth::Engine.for(client_name).routes.url_helpers
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns the Auth::Client for the current request. In auth engine controllers this is
|
|
128
|
+
# resolved from the routing default set by Engine.for; in host app controllers it falls
|
|
129
|
+
# back to the default registered client. Override in your controller to choose a specific
|
|
130
|
+
# client when multiple are registered.
|
|
131
|
+
def auth_client_config
|
|
132
|
+
client_name = self.client_name
|
|
133
|
+
client_name.present? ? YiffSpace::Auth[client_name.to_sym] : YiffSpace::Auth.default
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def client_name
|
|
137
|
+
respond_to?(:request, true) && request.env[CLIENT_NAME_ENV]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def client_name=(value)
|
|
141
|
+
request.env[CLIENT_NAME_ENV] = value.to_sym
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
module Scoped
|
|
145
|
+
extend(ActiveSupport::Concern)
|
|
146
|
+
include(Helper)
|
|
147
|
+
|
|
148
|
+
included do
|
|
149
|
+
private(*Helper.instance_methods(false))
|
|
150
|
+
private_class_method(*Helper::ClassMethods.instance_methods(false))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
Helper.instance_methods(false).each do |name|
|
|
154
|
+
define_method("yiffspace_#{name}") { |*args, **kwargs, &block| send(name, *args, **kwargs, &block) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
module ClassMethods
|
|
158
|
+
include(Helper::ClassMethods)
|
|
159
|
+
|
|
160
|
+
Helper::ClassMethods.instance_methods(false).each do |name|
|
|
161
|
+
define_method("yiffspace_#{name}") { |*args, **kwargs, &block| send(name, *args, **kwargs, &block) }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class Permissions
|
|
6
|
+
attr_accessor(:values)
|
|
7
|
+
|
|
8
|
+
delegate(:each, to: :values)
|
|
9
|
+
|
|
10
|
+
include(Enumerable)
|
|
11
|
+
|
|
12
|
+
def initialize(perms)
|
|
13
|
+
@values = perms
|
|
14
|
+
@tree = {}
|
|
15
|
+
|
|
16
|
+
perms.each do |perm|
|
|
17
|
+
current = @tree
|
|
18
|
+
parts = perm.split(".")
|
|
19
|
+
|
|
20
|
+
parts.each do |part|
|
|
21
|
+
current[part] ||= {}
|
|
22
|
+
current = current[part]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# mark leaf
|
|
26
|
+
current[:__leaf__] = true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def has?(perm)
|
|
31
|
+
current = @tree
|
|
32
|
+
parts = perm.split(".")
|
|
33
|
+
|
|
34
|
+
parts.each do |part|
|
|
35
|
+
return false unless current[part]
|
|
36
|
+
|
|
37
|
+
current = current[part]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
current[:__leaf__] == true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias include? has?
|
|
44
|
+
|
|
45
|
+
def method_missing(name, *_args)
|
|
46
|
+
str = name.to_s
|
|
47
|
+
|
|
48
|
+
if str.end_with?("?")
|
|
49
|
+
key = str[0..-2]
|
|
50
|
+
return @tree[key]&.dig(:__leaf__) == true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if @tree.key?(str)
|
|
54
|
+
self.class.new_from_subtree(@tree[str])
|
|
55
|
+
else
|
|
56
|
+
# return empty node instead of raising
|
|
57
|
+
self.class.new([])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.new_from_subtree(tree)
|
|
66
|
+
obj = allocate
|
|
67
|
+
obj.instance_variable_set(:@tree, tree)
|
|
68
|
+
obj.instance_variable_set(:@values, leaves_from_tree(tree))
|
|
69
|
+
obj
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.leaves_from_tree(node, prefix = "")
|
|
73
|
+
result = []
|
|
74
|
+
node.each do |key, subtree|
|
|
75
|
+
next if key == :__leaf__
|
|
76
|
+
|
|
77
|
+
path = prefix.empty? ? key : "#{prefix}.#{key}"
|
|
78
|
+
result << path if subtree[:__leaf__]
|
|
79
|
+
result.concat(leaves_from_tree(subtree, path))
|
|
80
|
+
end
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class SetClientName < Utils::SetEnvConstraint
|
|
6
|
+
def initialize(value)
|
|
7
|
+
super(CLIENT_NAME_ENV, value.to_sym)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.default
|
|
11
|
+
new(DEFAULT_CLIENT_NAME)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("singleton")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Auth
|
|
7
|
+
class UserInfo
|
|
8
|
+
class Anonymous
|
|
9
|
+
include(::Singleton)
|
|
10
|
+
|
|
11
|
+
%i[id user discord avatar].each do |attr|
|
|
12
|
+
define_method(attr) { |*, **| raise(NotImplementedError, "#{attr} is not present on anonymous user") }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def username
|
|
16
|
+
"anonymous"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def display_name
|
|
20
|
+
"Anonymous"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def avatar
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def avatar_url
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def banner
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def banner_url
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def anonymous?
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# this feels wrong, but it hopefully shouldn't break anything
|
|
44
|
+
def present?
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def blank?
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def serializable_hash(*)
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_session
|
|
57
|
+
serializable_hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.from_json(*)
|
|
61
|
+
Anonymous.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.from_session(data)
|
|
65
|
+
return nil if data.blank?
|
|
66
|
+
|
|
67
|
+
from_json(data)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class UserInfo
|
|
6
|
+
attr_reader(:id, :user, :discord, :client_id)
|
|
7
|
+
|
|
8
|
+
# @param id String
|
|
9
|
+
# @param user OpenIDConnect::ResponseObject::UserInfo
|
|
10
|
+
# @param discord Hash
|
|
11
|
+
# @param client_id String
|
|
12
|
+
def initialize(id:, user:, discord:, client_id:)
|
|
13
|
+
raise(ArgumentError, "no id provided") if id.blank?
|
|
14
|
+
raise(ArgumentError, "no user provided") if user.blank?
|
|
15
|
+
|
|
16
|
+
@id = id
|
|
17
|
+
@user = user
|
|
18
|
+
@discord = DiscordInfo.from_json(discord)
|
|
19
|
+
@client_id = client_id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def anonymous?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# this feels wrong, but it hopefully shouldn't break anything
|
|
27
|
+
def present?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def blank?
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def avatar(type = nil)
|
|
36
|
+
type.present? ? Images::Avatar.get_for(id, type) : Images::Avatar.default_for(id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def avatar_url(type = nil)
|
|
40
|
+
avatar(type).url
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def banner(type = nil)
|
|
44
|
+
type.present? ? Images::Banner.get_for(id, type) : Images::Banner.default_for(id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def banner_url(type = nil)
|
|
48
|
+
banner(type).url
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
delegate(:username, to: :discord)
|
|
52
|
+
|
|
53
|
+
def display_name
|
|
54
|
+
discord.global_name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serializable_hash(_options = {})
|
|
58
|
+
{
|
|
59
|
+
"id" => id,
|
|
60
|
+
"discord" => discord.serializable_hash,
|
|
61
|
+
"user" => ::YiffSpace::Auth.serialize_user(user, client_id: client_id),
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_session
|
|
66
|
+
serializable_hash.without("discord").merge(discord: discord.to_session)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.from_json(data)
|
|
70
|
+
raise(ArgumentError, "invalid data") if data.blank?
|
|
71
|
+
|
|
72
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
73
|
+
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
74
|
+
|
|
75
|
+
user_data = ::YiffSpace::Utils::OpenHash.from(data.user)
|
|
76
|
+
new(
|
|
77
|
+
id: data.id,
|
|
78
|
+
discord: data.discord,
|
|
79
|
+
user: ::YiffSpace::Auth.unserialize_user(user_data),
|
|
80
|
+
client_id: user_data.client_id,
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.from_session(data)
|
|
85
|
+
return nil if data.blank?
|
|
86
|
+
|
|
87
|
+
from_json(data)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("openid_connect")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Auth
|
|
7
|
+
class SerializeError < StandardError; end
|
|
8
|
+
|
|
9
|
+
ExchangeResponse = Struct.new(:auth, :user)
|
|
10
|
+
CLIENT_NAME_ENV = "yiffspace.auth.client_name"
|
|
11
|
+
DEFAULT_CLIENT_NAME = :default
|
|
12
|
+
|
|
13
|
+
@clients = {}
|
|
14
|
+
@enable_debug_action = false
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def register(name, &block)
|
|
19
|
+
client = Client.new(name)
|
|
20
|
+
block&.call(client)
|
|
21
|
+
@clients[name.to_sym] = client
|
|
22
|
+
client
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def [](name)
|
|
26
|
+
@clients[name.to_sym] or raise(KeyError, "unknown auth client: #{name.inspect}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default
|
|
30
|
+
@clients[DEFAULT_CLIENT_NAME] || raise("no default client configured")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_by_client_id(client_id)
|
|
34
|
+
@clients.values.find { |c| c.oidc_client.identifier == client_id }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def serialize_token(token)
|
|
38
|
+
{ attributes: token.raw_attributes.without("client"), client_id: token.client.identifier }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unserialize_token(data)
|
|
42
|
+
raise(SerializeError, "no token data provided") if data.blank?
|
|
43
|
+
return data if data.is_a?(OpenIDConnect::AccessToken)
|
|
44
|
+
|
|
45
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
46
|
+
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
47
|
+
raise(SerializeError, "no client id for token, refusing to reconstruct") if data.client_id.nil?
|
|
48
|
+
|
|
49
|
+
client_config = find_by_client_id(data.client_id)
|
|
50
|
+
raise(SerializeError, "unknown client_id #{data.client_id.inspect}") unless client_config
|
|
51
|
+
|
|
52
|
+
OpenIDConnect::AccessToken.new(data.attributes.merge(client: client_config.oidc_client))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def serialize_user(user, client_id:)
|
|
56
|
+
{ attributes: user.raw_attributes, client_id: client_id }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unserialize_user(data)
|
|
60
|
+
raise(SerializeError, "no user data provided") if data.blank?
|
|
61
|
+
return data if data.is_a?(OpenIDConnect::ResponseObject::UserInfo)
|
|
62
|
+
|
|
63
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
64
|
+
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
65
|
+
raise(SerializeError, "no client id for user, refusing to reconstruct") if data.client_id.nil?
|
|
66
|
+
|
|
67
|
+
find_by_client_id(data.client_id) or raise(SerializeError, "unknown client_id #{data.client_id.inspect}")
|
|
68
|
+
|
|
69
|
+
OpenIDConnect::ResponseObject::UserInfo.new(data.attributes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enable_debug_action?
|
|
73
|
+
@enable_debug_action
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def enable_debug_action!
|
|
77
|
+
@enable_debug_action = true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def disable_debug_action!
|
|
81
|
+
@enable_debug_action = false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This is intended to only be used externally, it is NOT reusable, do not use it within this project, or attempt to use it multiple times!!
|
|
4
|
+
module YiffSpace
|
|
5
|
+
class ConfigBuilder
|
|
6
|
+
include(Singleton)
|
|
7
|
+
|
|
8
|
+
cattr_accessor(:list, default: [])
|
|
9
|
+
cattr_accessor(:env_set, default: {})
|
|
10
|
+
cattr_accessor(:unset, default: [])
|
|
11
|
+
cattr_accessor(:required, default: [])
|
|
12
|
+
cattr_accessor(:reviver_map, default: {})
|
|
13
|
+
cattr_accessor(:subconfigs, default: Hash.new { |h, k| h[k] = {} })
|
|
14
|
+
cattr_accessor(:env_name, default: "CONFIG")
|
|
15
|
+
|
|
16
|
+
def self.config(name, type = :string, env: true, required: false, blank: false, &block)
|
|
17
|
+
name = name.to_sym
|
|
18
|
+
remove_config(name) if list.include?(name)
|
|
19
|
+
list << name
|
|
20
|
+
self.required << name if required
|
|
21
|
+
if block.nil?
|
|
22
|
+
unset << name
|
|
23
|
+
block = -> { raise(NotImplementedError, "Config option #{name} is not set") }
|
|
24
|
+
end
|
|
25
|
+
define_method(name) do |*args|
|
|
26
|
+
env_or_value(name, args, blank: blank, &block)
|
|
27
|
+
end
|
|
28
|
+
if type == :boolean
|
|
29
|
+
define_method("#{name}?") do |*args|
|
|
30
|
+
public_send(name, *args)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
reviver(name, type) if type && type != :string
|
|
34
|
+
env_set[name] = env
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.remove_config(name, reviver: false)
|
|
38
|
+
name = name.to_sym
|
|
39
|
+
list.delete(name)
|
|
40
|
+
required.delete(name)
|
|
41
|
+
unset.delete(name)
|
|
42
|
+
reviver_map.delete(name) if reviver
|
|
43
|
+
env_set.delete(name)
|
|
44
|
+
remove_method(name)
|
|
45
|
+
remove_method("#{name}?") if method_defined?("#{name}?")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def env_or_value(name, args, blank: false, &)
|
|
49
|
+
value = env(name)
|
|
50
|
+
value = nil if !value.nil? && value.blank? && !blank
|
|
51
|
+
if value.nil?
|
|
52
|
+
instance_exec(*args, &)
|
|
53
|
+
else
|
|
54
|
+
reviver = reviver_map.fetch(name, proc(&:itself))
|
|
55
|
+
instance_exec(value, *args, &reviver)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def env(name)
|
|
60
|
+
ENV.fetch("#{self.class.env_name}_#{name.to_s.upcase}", nil)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.reviver(name, type = nil, &block)
|
|
64
|
+
if block
|
|
65
|
+
reviver_map[name] = block
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
method = case type
|
|
69
|
+
when :boolean
|
|
70
|
+
->(v) { !v.match?(/\A(false|f|no|n|off|0)\z/i) }
|
|
71
|
+
when :integer
|
|
72
|
+
->(v) { v.to_i }
|
|
73
|
+
when :symbol
|
|
74
|
+
->(v) { v.to_sym }
|
|
75
|
+
when :array
|
|
76
|
+
->(v) { v.split(/\s*,\s*/) }
|
|
77
|
+
else
|
|
78
|
+
raise(ArgumentError, "not sure how to revive #{type} for #{method}")
|
|
79
|
+
end
|
|
80
|
+
reviver_map[name] = method
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.subconfig(prefix, parent: [], &block)
|
|
84
|
+
raise(ArgumentError, "block required") unless block
|
|
85
|
+
|
|
86
|
+
prefix = prefix.to_sym
|
|
87
|
+
path = parent + [prefix]
|
|
88
|
+
|
|
89
|
+
node = subconfigs
|
|
90
|
+
path.each do |p|
|
|
91
|
+
node[p] ||= {}
|
|
92
|
+
node = node[p]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
define_method(prefix) { SubconfigProxy.new(self, path) } unless method_defined?(prefix)
|
|
96
|
+
|
|
97
|
+
collector = Module.new do
|
|
98
|
+
def self.collected
|
|
99
|
+
@collected ||= []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.config(name, type = :string, env: true, required: false, blank: false, &block)
|
|
103
|
+
collected << [:config, name, type, env, required, blank, block]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.reviver(name, type = nil, &block)
|
|
107
|
+
collected << [:reviver, name, type, block]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.subconfig(name, &block)
|
|
111
|
+
collected << [:subconfig, name, block]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
collector.module_eval(&block)
|
|
116
|
+
|
|
117
|
+
collector.collected.each do |kind, name, *args|
|
|
118
|
+
case kind
|
|
119
|
+
when :config
|
|
120
|
+
full = (path + [name]).join("_").to_sym
|
|
121
|
+
config(full, args[0], env: args[1], required: args[2], blank: args[3], &args[4])
|
|
122
|
+
when :reviver
|
|
123
|
+
full = (path + [name]).join("_").to_sym
|
|
124
|
+
reviver(full, args[0], &args[1])
|
|
125
|
+
when :subconfig
|
|
126
|
+
subconfig(name, parent: path, &args[0])
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.ensure_required_set!
|
|
132
|
+
unset = []
|
|
133
|
+
required.each do |name|
|
|
134
|
+
value = begin
|
|
135
|
+
instance.public_send(name)
|
|
136
|
+
rescue StandardError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
unset << name if value.blank?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
raise("Missing required configuration options: #{unset.join(', ')}") if unset.any?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def present?(*names)
|
|
146
|
+
names.flatten.all? { |name| public_send(name).present? }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class SubconfigProxy
|
|
150
|
+
def initialize(owner, path)
|
|
151
|
+
@owner = owner
|
|
152
|
+
@path = path
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def method_missing(name, *, &)
|
|
156
|
+
name = name.to_sym
|
|
157
|
+
full_path = @path + [name]
|
|
158
|
+
|
|
159
|
+
flat = full_path.join("_").to_sym
|
|
160
|
+
return @owner.public_send(flat, *, &) if @owner.respond_to?(flat)
|
|
161
|
+
|
|
162
|
+
return SubconfigProxy.new(@owner, full_path) if subconfig_path?(full_path)
|
|
163
|
+
|
|
164
|
+
super
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def respond_to_missing?(name, include_private = false)
|
|
168
|
+
name = name.to_sym
|
|
169
|
+
flat = (@path + [name]).join("_").to_sym
|
|
170
|
+
|
|
171
|
+
@owner.respond_to?(flat, include_private) ||
|
|
172
|
+
subconfig_path?(@path + [name]) ||
|
|
173
|
+
super
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def subconfig_path?(path)
|
|
179
|
+
node = ConfigBuilder.subconfigs
|
|
180
|
+
path.each do |p|
|
|
181
|
+
return false unless node.key?(p)
|
|
182
|
+
|
|
183
|
+
node = node[p]
|
|
184
|
+
end
|
|
185
|
+
true
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|