Compare commits
4 Commits
feature/co
...
eeabbdb7df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeabbdb7df
|
||
|
ee42d68471
|
|||
|
7acc3b2106
|
|||
|
20c014607c
|
130
app/controllers/rs/oauth_controller.rb
Normal file
130
app/controllers/rs/oauth_controller.rb
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
class Rs::OauthController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
|
def new
|
||||||
|
username, org = params[:useraddress].split("@")
|
||||||
|
@user = User.where(cn: username.downcase, ou: org).first
|
||||||
|
@scopes = parse_scopes params[:scope]
|
||||||
|
@redirect_uri = params[:redirect_uri]
|
||||||
|
@client_id = params[:client_id]
|
||||||
|
@state = params[:state]
|
||||||
|
@root_access_requested = (@scopes & [":r",":rw"]).any?
|
||||||
|
|
||||||
|
@denial_url = url_with_state("#{@redirect_uri}#error=access_denied", @state)
|
||||||
|
|
||||||
|
@expire_at_dates = [["Never", nil],
|
||||||
|
["In 1 month", 1.month.from_now],
|
||||||
|
["In 1 day", 1.day.from_now]]
|
||||||
|
|
||||||
|
http_status :bad_request and return unless @redirect_uri.present?
|
||||||
|
|
||||||
|
unless current_user == @user
|
||||||
|
sign_out :user
|
||||||
|
|
||||||
|
redirect_to new_rs_oauth_url(@user.address,
|
||||||
|
scope: params[:scope],
|
||||||
|
redirect_uri: params[:redirect_uri],
|
||||||
|
client_id: params[:client_id],
|
||||||
|
state: params[:state])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @client_id.present?
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_request", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
if @scopes.empty?
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_scope", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_client", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
@client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
|
||||||
|
# redirect_to url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state), allow_other_host: true
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
unless current_user.id.to_s == params[:user_id]
|
||||||
|
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
|
||||||
|
http_status :forbidden and return
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions = parse_scopes params[:scope]
|
||||||
|
redirect_uri = params[:redirect_uri].presence
|
||||||
|
client_id = params[:client_id].presence
|
||||||
|
state = params[:state].presence
|
||||||
|
expire_at = params[:expire_at].presence
|
||||||
|
|
||||||
|
http_status :bad_request and return unless redirect_uri.present?
|
||||||
|
|
||||||
|
if permissions.empty?
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_scope", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless client_id.present?
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_request", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(client_id) == hostname_of(redirect_uri)
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_client", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
auth = current_user.remote_storage_authorizations.create!(
|
||||||
|
permissions: permissions,
|
||||||
|
client_id: client_id,
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
app_name: client_id, #TODO use user-defined name
|
||||||
|
expire_at: expire_at
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state), allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /rs/oauth/token/:id/launch_app
|
||||||
|
def launch_app
|
||||||
|
auth = current_user.remote_storage_authorizations.find(params[:id])
|
||||||
|
|
||||||
|
redirect_to app_auth_url(auth)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def app_auth_url(auth)
|
||||||
|
url = "#{auth.url}#remotestorage=#{current_user.address}"
|
||||||
|
url += "&access_token=#{auth.token}"
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def hostname_of(uri)
|
||||||
|
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_scopes(scope_string)
|
||||||
|
return [] if scope_string.blank?
|
||||||
|
|
||||||
|
scopes = scope_string.
|
||||||
|
gsub(/\[|\]/, "").
|
||||||
|
gsub(/\,/, " ").
|
||||||
|
gsub(/\/:/, ":").
|
||||||
|
split(/\s/).map(&:strip).
|
||||||
|
reject(&:empty?)
|
||||||
|
|
||||||
|
scopes = [":r"] if scopes.include?("*:r")
|
||||||
|
scopes = [":rw"] if scopes.include?("*:rw")
|
||||||
|
|
||||||
|
scopes
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_with_state(url, state)
|
||||||
|
state ? "#{url}&state=#{CGI.escape(state)}" : url
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
11
app/helpers/oauth_helper.rb
Normal file
11
app/helpers/oauth_helper.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module OauthHelper
|
||||||
|
|
||||||
|
def scope_name(scope)
|
||||||
|
scope.gsub(/(\:.+)/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_permissions(scope)
|
||||||
|
scope.match(/\:r$/) ? "r" : "rw"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
32
app/models/remote_storage_authorization.rb
Normal file
32
app/models/remote_storage_authorization.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
class RemoteStorageAuthorization < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
serialize :permissions
|
||||||
|
|
||||||
|
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.permisisons = [] if a.permissions == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
before_create :generate_token
|
||||||
|
|
||||||
|
def url
|
||||||
|
if self.redirect_uri
|
||||||
|
uri = URI.parse self.redirect_uri
|
||||||
|
"#{uri.scheme}://#{client_id}"
|
||||||
|
else
|
||||||
|
"http://#{client_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_token(length=16)
|
||||||
|
self.token = SecureRandom.hex(length) if self.token.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,6 +16,8 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
has_many :accounts, through: :lndhub_user
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
|
has_many :remote_storage_authorizations
|
||||||
|
|
||||||
validates_uniqueness_of :cn
|
validates_uniqueness_of :cn
|
||||||
validates_length_of :cn, :minimum => 3
|
validates_length_of :cn, :minimum => 3
|
||||||
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
||||||
|
|||||||
1
app/views/icons/_asterisk.html.erb
Normal file
1
app/views/icons/_asterisk.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512" fill="currentColor" stroke="currentColor" stroke-width="2" class="<%= custom_class %>"><path d="M475.31 364.144L288 256l187.31-108.144c5.74-3.314 7.706-10.653 4.392-16.392l-4-6.928c-3.314-5.74-10.653-7.706-16.392-4.392L272 228.287V12c0-6.627-5.373-12-12-12h-8c-6.627 0-12 5.373-12 12v216.287L52.69 120.144c-5.74-3.314-13.079-1.347-16.392 4.392l-4 6.928c-3.314 5.74-1.347 13.079 4.392 16.392L224 256 36.69 364.144c-5.74 3.314-7.706 10.653-4.392 16.392l4 6.928c3.314 5.74 10.653 7.706 16.392 4.392L240 283.713V500c0 6.627 5.373 12 12 12h8c6.627 0 12-5.373 12-12V283.713l187.31 108.143c5.74 3.314 13.079 1.347 16.392-4.392l4-6.928c3.314-5.74 1.347-13.079-4.392-16.392z"/></svg>
|
||||||
|
After Width: | Height: | Size: 760 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder <%= custom_class %>"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 331 B |
60
app/views/rs/oauth/new.html.erb
Normal file
60
app/views/rs/oauth/new.html.erb
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "App Authorization") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section class="px-16 pb-8 mt-8">
|
||||||
|
<p class="text-lg mb-8">
|
||||||
|
The app
|
||||||
|
<%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %>
|
||||||
|
is asking for access to these folders:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-xl mb-8">
|
||||||
|
<% if @root_access_requested %>
|
||||||
|
<span class="text-red-700">
|
||||||
|
<span class="text-red-700">
|
||||||
|
<%= render partial: "icons/asterisk", locals: { custom_class: "inline-block align-middle mb-1" } %>
|
||||||
|
All files and directories
|
||||||
|
</span>
|
||||||
|
<% if (@scopes & [":r"]).any? %>
|
||||||
|
<span class="text-sm text-gray-500">(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<% @scopes.each do |scope| %>
|
||||||
|
<span class="text-gray-500">
|
||||||
|
<span>
|
||||||
|
<%= render partial: "icons/folder", locals: { custom_class: "inline-block align-middle mb-2" } %>
|
||||||
|
<%= scope_name(scope) %>
|
||||||
|
</span>
|
||||||
|
<% if scope_permissions(scope) == "r" %>
|
||||||
|
<span class="text-sm text-gray-500">(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%= form_with(url: rs_oauth_path, method: :post, data: { turbo: false }) do |f| %>
|
||||||
|
<%= f.hidden_field :redirect_uri, value: @redirect_uri %>
|
||||||
|
<%= f.hidden_field :scope, value: @scopes.join(" ") %>
|
||||||
|
<%= f.hidden_field :user_id, value: @user.id %>
|
||||||
|
<%= f.hidden_field :client_id, value: @client_id %>
|
||||||
|
<%= f.hidden_field :state, value: @state %>
|
||||||
|
<p>
|
||||||
|
<%= f.label :expire_at, "Expire:" %>
|
||||||
|
<%= f.select :expire_at, options_for_select(@expire_at_dates) %>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 my-10">
|
||||||
|
You can revoke access for this app at any time on your storage dashboard.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<%= f.submit class: "btn-md btn-blue w-full sm:w-auto", data: { disable_with: "Saving..." } do %>
|
||||||
|
Allow
|
||||||
|
<% end %>
|
||||||
|
<%= link_to @denial_url, class: "btn-md btn-red w-full sm:w-auto" do %>
|
||||||
|
Deny
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
@@ -53,8 +53,15 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :rs do
|
||||||
|
resource :oauth, only: [:new, :create], path_names: { new: ':useraddress' },
|
||||||
|
controller: 'oauth', constraints: { useraddress: /[^\/]+/}
|
||||||
|
get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
|
||||||
|
end
|
||||||
|
|
||||||
get ".well-known/webfinger" => "webfinger#show"
|
get ".well-known/webfinger" => "webfinger#show"
|
||||||
|
|
||||||
|
|
||||||
authenticate :user, ->(user) { user.is_admin? } do
|
authenticate :user, ->(user) { user.is_admin? } do
|
||||||
mount Sidekiq::Web => '/sidekiq'
|
mount Sidekiq::Web => '/sidekiq'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class CreateRemoteStorageAuthorizations < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :remote_storage_authorizations do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.string :token
|
||||||
|
t.text :permissions, array: true, default: [].to_yaml
|
||||||
|
t.string :client_id
|
||||||
|
t.string :redirect_uri
|
||||||
|
t.string :app_name
|
||||||
|
t.datetime :expire_at
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :remote_storage_authorizations, :permissions, using: 'gin'
|
||||||
|
end
|
||||||
|
end
|
||||||
17
db/schema.rb
17
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
|
ActiveRecord::Schema[7.0].define(version: 2023_04_03_150200) do
|
||||||
create_table "donations", force: :cascade do |t|
|
create_table "donations", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "amount_sats"
|
t.integer "amount_sats"
|
||||||
@@ -34,6 +34,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
|
|||||||
t.index ["user_id"], name: "index_invitations_on_user_id"
|
t.index ["user_id"], name: "index_invitations_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "remote_storage_authorizations", force: :cascade do |t|
|
||||||
|
t.integer "user_id", null: false
|
||||||
|
t.string "token"
|
||||||
|
t.text "permissions", default: "--- []\n"
|
||||||
|
t.string "client_id"
|
||||||
|
t.string "redirect_uri"
|
||||||
|
t.string "app_name"
|
||||||
|
t.datetime "expire_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["permissions"], name: "index_remote_storage_authorizations_on_permissions"
|
||||||
|
t.index ["user_id"], name: "index_remote_storage_authorizations_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "settings", force: :cascade do |t|
|
create_table "settings", force: :cascade do |t|
|
||||||
t.string "var", null: false
|
t.string "var", null: false
|
||||||
t.text "value"
|
t.text "value"
|
||||||
@@ -65,4 +79,5 @@ ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
|
|||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_foreign_key "remote_storage_authorizations", "users"
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user