yiffspace 0.0.14 → 0.0.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98aa533af69b280415d1d0bdea4653e69ea058fdd10cd826e4d47c5dee07c883
4
- data.tar.gz: 534b56e9af66e0cd7ba951b4c47ab5dd0a30dbc68a0453381649f4f0d706e5b1
3
+ metadata.gz: a12391067375c3fe9b67ce2cd6b798471109847146659bde7e831e97b5512f8d
4
+ data.tar.gz: 603e6b07f15adc922e2665ed15f6eafd4a65ffefdb63ce618cb29e09fc82956e
5
5
  SHA512:
6
- metadata.gz: 690c8f47088aaade9958a2b39e014f94d5a61ae4f0fb1437150ad5fd47ea4c38248d5bf9a78e046ea7ee7675352282a2ab0fd625349fc73129fdf9dc33e4006c
7
- data.tar.gz: cda63776835628c1a63deb30f264b107d0766884aab61313fa836bdd650d22185fb21aa723c713072a3010e4f9aaed7316347aa6e525d707bca736035fa60bf9
6
+ metadata.gz: 423e58dc486f4c20e4990b6850dccde2066d75fe79ca82707f3601ffe364db811039d5663f2ec201659f17f3312d375207c0b415eb992e07a3bedf503cfdaf2e
7
+ data.tar.gz: 8f6b52d3b39fd93fc3bc23b30e699e3fa2bfc51dcbeff7e0dc65e6cd5bfb67cfb24b882455fcfae0c1aa0fe36a82bd933ec744f8ac61ab304d2c81830d5c5aca
@@ -5,6 +5,8 @@ module YiffSpace
5
5
  class RootController < ApplicationController
6
6
  include(Helper)
7
7
 
8
+ before_action(:sync_auth_if_dirty!)
9
+
8
10
  def show
9
11
  client.sign_in(redirect_uri: auth_client_config.redirect_uri, post_redirect_uri: params[:path] || "/")
10
12
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("openssl")
4
+ require("active_support/security_utils")
5
+
6
+ module YiffSpace
7
+ module Auth
8
+ class WebhookController < ApplicationController
9
+ skip_before_action(:verify_authenticity_token)
10
+
11
+ HANDLED_EVENTS = %w[
12
+ User.Roles.Updated
13
+ User.SuspensionStatus.Updated
14
+ User.Deleted
15
+ Role.Scopes.Updated
16
+ ].freeze
17
+ DIRTY_FLAG_TTL = 24.hours
18
+
19
+ def create
20
+ unless verify_signature
21
+ render(plain: "Unauthorized", status: :unauthorized)
22
+ return
23
+ end
24
+
25
+ payload = JSON.parse(request.raw_post)
26
+ event = payload["event"]
27
+
28
+ handle_event(event, payload) if HANDLED_EVENTS.include?(event)
29
+
30
+ head(:ok)
31
+ rescue JSON::ParserError
32
+ render(plain: "Bad Request", status: :bad_request)
33
+ end
34
+
35
+ private
36
+
37
+ def verify_signature
38
+ secret = auth_client_config.logto_webhook_secret
39
+ return true if secret.blank?
40
+
41
+ received = request.headers["logto-signature-sha-256"].to_s
42
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
43
+ ActiveSupport::SecurityUtils.secure_compare(received, expected)
44
+ end
45
+
46
+ def handle_event(event, payload)
47
+ data = payload["data"] || {}
48
+
49
+ if event == "Role.Scopes.Updated"
50
+ handle_role_scopes_updated(data)
51
+ else
52
+ user_id = data["id"]
53
+ mark_dirty(user_id) if user_id.present?
54
+ end
55
+ end
56
+
57
+ def handle_role_scopes_updated(data)
58
+ role_id = data["id"]
59
+ return if role_id.blank?
60
+
61
+ management = auth_client_config.logto_management
62
+ users = management.get_users_with_role(role_id)
63
+ users.each { |u| mark_dirty(u["id"]) }
64
+ rescue StandardError => e
65
+ Rails.logger.error("[YiffSpace::Auth::WebhookController] Role.Scopes.Updated fan-out failed: #{e.message}")
66
+ end
67
+
68
+ def mark_dirty(user_id)
69
+ Rails.cache.write(format(Helper::DIRTY_FLAG_KEY, user_id), true, expires_in: DIRTY_FLAG_TTL)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  YiffSpace::Auth::Engine.routes.draw do
4
4
  constraints(YiffSpace::Auth::SetClientName.default) do
5
+ post(:webhook, controller: :webhook)
5
6
  get(:cb, controller: :root)
6
7
  get(:logout, controller: :root)
7
8
  get(:permissions, controller: :root)
@@ -9,7 +9,7 @@ module YiffSpace
9
9
  @data = Utils::OpenHash.from(data)
10
10
  end
11
11
 
12
- delegate(:id, :username, to: :data)
12
+ delegate(:id, :name, to: :data)
13
13
 
14
14
  def discord_id
15
15
  data.identities.discord.userId
@@ -8,7 +8,8 @@ module YiffSpace
8
8
  class Client
9
9
  attr_accessor(:client_id, :client_secret, :scopes, :permissions,
10
10
  :resource, :redirect_uri, :server_url, :auth_session_key,
11
- :user_session_key, :update_discord_images, :permissions_separator)
11
+ :user_session_key, :update_discord_images, :permissions_separator,
12
+ :logto_webhook_secret)
12
13
 
13
14
  attr_reader(:name)
14
15
 
@@ -22,6 +23,7 @@ module YiffSpace
22
23
  @user_session_key = :"yiffspace_user_#{name}"
23
24
  @update_discord_images = true
24
25
  @permissions_separator = ":"
26
+ @logto_webhook_secret = nil
25
27
  end
26
28
 
27
29
  def logto(controller)
@@ -41,6 +41,7 @@ module YiffSpace
41
41
  subclass.routes.default_scope = { module: "yiff_space/auth" }
42
42
  subclass.routes.draw do
43
43
  constraints(SetClientName.new(name)) do
44
+ post(:webhook, controller: :webhook)
44
45
  get(:cb, controller: :root)
45
46
  get(:logout, controller: :root)
46
47
  get(:permissions, controller: :root)
@@ -78,19 +78,55 @@ module YiffSpace
78
78
  end
79
79
 
80
80
  def require_auth(path)
81
- redirect_to(path) unless user?
81
+ redirect_to(path) unless logged_in?
82
82
  end
83
83
 
84
84
  def logged_in?
85
- user?
85
+ auth? && user?
86
86
  end
87
87
 
88
88
  def has_permission?(name)
89
- return false unless user?
89
+ return false unless logged_in?
90
90
 
91
91
  auth.permissions.has?(name)
92
92
  end
93
93
 
94
+ DIRTY_FLAG_KEY = "yiffspace:auth:dirty:%s"
95
+
96
+ # Checks the dirty flag written by the Logto webhook handler. If set, re-fetches
97
+ # the user's current roles and permissions from the Logto Management API and
98
+ # rewrites the session — without waiting for the access token to expire.
99
+ # Call this as a before_action in any controller that needs instant revocation.
100
+ def sync_auth_if_dirty!
101
+ return unless auth?
102
+
103
+ flag_key = format(DIRTY_FLAG_KEY, auth.id)
104
+ return unless Rails.cache.exist?(flag_key)
105
+
106
+ Rails.cache.delete(flag_key)
107
+
108
+ management = auth_client_config.logto_management
109
+ api_user = management.get_user_by_id(auth.id)
110
+
111
+ if api_user.nil? || api_user.data["isSuspended"]
112
+ full_reset!
113
+ return
114
+ end
115
+
116
+ roles = management.get_user_roles(auth.id)
117
+ permissions = roles.flat_map { |role| management.get_role_scopes(role["id"]) }
118
+ .pluck("name")
119
+ .uniq
120
+
121
+ self.auth = AuthInfo.new(
122
+ id: auth.id,
123
+ token: auth.token,
124
+ roles: roles.pluck("name"),
125
+ permissions: permissions,
126
+ client_id: auth.client_id,
127
+ )
128
+ end
129
+
94
130
  def url_helpers
95
131
  YiffSpace::Auth::Engine.for(client_name).routes.url_helpers
96
132
  end
@@ -50,7 +50,7 @@ module YiffSpace
50
50
 
51
51
  response = HTTParty.post("#{auth.server_url}/api/users", {
52
52
  headers: { "Authorization" => "Bearer #{get_token}", "Content-Type" => "application/json" },
53
- body: { avatar: "https://cdn.discordapp.com/avatars/#{details['id']}/#{details['avatar']}", username: details["username"] }.to_json,
53
+ body: { avatar: "https://cdn.discordapp.com/avatars/#{details['id']}/#{details['avatar']}", name: details["username"] }.to_json,
54
54
  })
55
55
  raise("failed to create user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
56
56
 
@@ -74,5 +74,34 @@ module YiffSpace
74
74
  def get_or_create_user(id)
75
75
  get_user(id) || create_user(id)
76
76
  end
77
+
78
+ def get_user_by_id(logto_id)
79
+ response = HTTParty.get("#{auth.server_url}/api/users/#{logto_id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
80
+ return nil if response.code == 404
81
+ raise("failed to get user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
82
+
83
+ Auth::ApiUser.new(response.parsed_response)
84
+ end
85
+
86
+ def get_user_roles(logto_id)
87
+ response = HTTParty.get("#{auth.server_url}/api/users/#{logto_id}/roles", { headers: { "Authorization" => "Bearer #{get_token}" } })
88
+ raise("failed to get user roles: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
89
+
90
+ response.parsed_response
91
+ end
92
+
93
+ def get_role_scopes(role_id)
94
+ response = HTTParty.get("#{auth.server_url}/api/roles/#{role_id}/scopes", { headers: { "Authorization" => "Bearer #{get_token}" } })
95
+ raise("failed to get role scopes: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
96
+
97
+ response.parsed_response
98
+ end
99
+
100
+ def get_users_with_role(role_id)
101
+ response = HTTParty.get("#{auth.server_url}/api/roles/#{role_id}/users", { headers: { "Authorization" => "Bearer #{get_token}" } })
102
+ raise("failed to get users with role: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
103
+
104
+ response.parsed_response
105
+ end
77
106
  end
78
107
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YiffSpace
4
- VERSION = "0.0.14"
4
+ VERSION = "0.0.16"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yiffspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donovan_DMC
@@ -114,6 +114,7 @@ files:
114
114
  - app/views/yiff_space/error.html.erb
115
115
  - engines/auth/app/controllers/yiff_space/auth/application_controller.rb
116
116
  - engines/auth/app/controllers/yiff_space/auth/root_controller.rb
117
+ - engines/auth/app/controllers/yiff_space/auth/webhook_controller.rb
117
118
  - engines/auth/app/views/yiff_space/auth/root/permissions.html.erb
118
119
  - engines/auth/config/routes.rb
119
120
  - lib/tasks/yiff_space_tasks.rake