Files
akkounts/app/models/remote_storage_authorization.rb
T
raucao 614ead90a9 Distinguish tokens without a recorded device
Tokens issued before user agents were captured cannot be attributed to a
single device, and the per-app tokens issued earlier could even be reused
from several devices at once.

- Add RemoteStorageAuthorization#device_label, falling back to
  "Unknown device(s)" so the card and the details modal share one source
- Show a lower-bound device count with a "+" whenever an app has such a
  token, so it reads as "could be more" instead of claiming a number we
  cannot support
- Drop the now-redundant device-name wrapper from the details modal

Part of #254
2026-11-03 16:55:05 +04:00

159 lines
4.7 KiB
Ruby

class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
# Tokens issued before user agents were recorded may have been used from any
# number of devices, so they cannot be attributed to a single one.
UNKNOWN_DEVICE = "Unknown device(s)"
serialize :permissions, coder: YAML unless Rails.env.production?
validates_presence_of :permissions
validates_presence_of :client_id
scope :valid, -> { where(expire_at: nil).or(where(expire_at: (DateTime.now)..)) }
scope :expired, -> { where(expire_at: ..(DateTime.now)) }
after_initialize do |a|
a.permissions = [] if a.permissions == nil
end
before_create :generate_token
before_create :store_token_in_redis
before_create :find_or_create_web_app
after_commit :schedule_token_expiry, on: :create
after_commit :notify_user, on: :create
before_destroy :delete_token_from_redis
after_destroy :remove_token_expiry_job
def url
uri = URI.parse self.redirect_uri
"#{uri.scheme}://#{client_id}"
end
def launch_url
return url unless web_app && web_app.metadata[:start_url].present?
start_url = web_app.metadata[:start_url]
if start_url.match("^https?:\/\/")
return start_url.start_with?(url) ? start_url : url
else
path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
"#{url}/#{path}"
end
end
def expired?
expire_at.present? && expire_at.past?
end
def device_name
return nil if user_agent.blank?
client = DeviceDetector.new(user_agent)
name = client.name.presence
os = client.os_name.presence
os = "Linux" if os == "GNU/Linux"
os = "macOS" if os == "Mac"
# Safari never runs on Linux. A WebKit "Safari" user agent on Linux is a
# WebKitGTK browser (e.g. GNOME Web), which sends a Safari-compatible UA.
name = "WebKit" if name == "Safari" && os == "Linux"
[name, os].compact.join(" on ").presence
end
def device_label
device_name || UNKNOWN_DEVICE
end
def display_name
web_app&.name.presence || app_name.presence || client_id
end
def root_access?
(permissions & [":r", ":rw"]).any?
end
def delete_token_from_redis
key = "authorizations:#{user.cn}:#{token}"
redis.srem? key, redis.smembers(key)
rescue => e
Rails.logger.error e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
private
def redis
@redis ||= Redis.new(url: Setting.rs_redis_url)
end
def generate_token(length=16)
self.token = SecureRandom.hex(length) if self.token.blank?
end
def store_token_in_redis
redis.sadd "authorizations:#{user.cn}:#{token}", permissions
end
def schedule_token_expiry
return unless expire_at.present?
RemoteStorageExpireAuthorizationJob.set(wait_until: expire_at)
.perform_later(id)
end
def remove_token_expiry_job
job_class = RemoteStorageExpireAuthorizationJob
job_args = [id]
query = SolidQueue::Job.where(class_name: job_class.to_s)
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlite/
query.where("json_extract(arguments, '$.arguments') = ?", job_args.to_json)
when /postgres/
query.where("CAST(arguments AS jsonb)->>'arguments' = ?", job_args.to_json)
else
raise "Unsupported database adapter"
end.destroy_all
end
def find_or_create_web_app
if looks_like_hosted_origin?
web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url)
web_app.update_metadata unless web_app.name.present?
self.web_app = web_app
self.app_name = web_app.name.presence || client_id
else
self.app_name = client_id
end
end
def looks_like_hosted_origin?
uri = URI.parse self.redirect_uri
!!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/)
rescue URI::InvalidURIError
false
end
def notify_user
notify = user.preferences[:remotestorage_notify_auth_created]
case notify
when "xmpp"
router = Router.new
payload = {
type: "normal", to: user.address,
from: Setting.xmpp_notifications_from_address,
body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}"
}
XmppSendMessageJob.perform_later(payload)
when "email"
NotificationMailer.with(user: user, auth: self)
.remotestorage_auth_created.deliver_later
end
end
end