From 7acc3b21060dd97be743dc9348fc61f056c6dc1c Mon Sep 17 00:00:00 2001 From: Garret Alfert Date: Sun, 12 Mar 2023 21:46:03 +0100 Subject: [PATCH] RemoteStorage OAuth dialog --- app/controllers/rs/oauth_controller.rb | 131 +++++++++++++++++++++++++ app/helpers/oauth_helper.rb | 11 +++ app/services/remote_storage.rb | 18 ++++ app/views/icons/_asterisk.html.erb | 1 + app/views/icons/_folder.html.erb | 2 +- app/views/rs/oauth/new.html.erb | 60 +++++++++++ config/routes.rb | 6 ++ 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 app/controllers/rs/oauth_controller.rb create mode 100644 app/helpers/oauth_helper.rb create mode 100644 app/services/remote_storage.rb create mode 100644 app/views/icons/_asterisk.html.erb create mode 100644 app/views/rs/oauth/new.html.erb diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb new file mode 100644 index 0000000..c403bcf --- /dev/null +++ b/app/controllers/rs/oauth_controller.rb @@ -0,0 +1,131 @@ +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)?:\/\//, "") + + rs = RemoteStorage.new + auth = rs.create_authorization(current_user, { + 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 diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb new file mode 100644 index 0000000..12f76a5 --- /dev/null +++ b/app/helpers/oauth_helper.rb @@ -0,0 +1,11 @@ +module OauthHelper + + def scope_name(scope) + scope.gsub(/(\:.+)/, '') + end + + def scope_permissions(scope) + scope.match(/\:r$/) ? "r" : "rw" + end + +end diff --git a/app/services/remote_storage.rb b/app/services/remote_storage.rb new file mode 100644 index 0000000..a6a80af --- /dev/null +++ b/app/services/remote_storage.rb @@ -0,0 +1,18 @@ +require 'ostruct' + +class RemoteStorage + + def initialize + end + + def create_authorization(user, auth_data) + + return OpenStruct.new(token: "SOME-FANCY-TOKEN") + # permissions: permissions, + # client_id: client_id, + # redirect_uri: redirect_uri, + # app_name: client_id, #TODO use user-defined name + # expire_at: expire_at + end + +end diff --git a/app/views/icons/_asterisk.html.erb b/app/views/icons/_asterisk.html.erb new file mode 100644 index 0000000..a97802b --- /dev/null +++ b/app/views/icons/_asterisk.html.erb @@ -0,0 +1 @@ + diff --git a/app/views/icons/_folder.html.erb b/app/views/icons/_folder.html.erb index 134458b..69e55bb 100644 --- a/app/views/icons/_folder.html.erb +++ b/app/views/icons/_folder.html.erb @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/rs/oauth/new.html.erb b/app/views/rs/oauth/new.html.erb new file mode 100644 index 0000000..5cee5b4 --- /dev/null +++ b/app/views/rs/oauth/new.html.erb @@ -0,0 +1,60 @@ +<%= render HeaderComponent.new(title: "App Authorization") %> + +<%= render MainSimpleComponent.new do %> +
+

+ The app + <%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %> + is asking for access to these folders: +

+ +

+ <% if @root_access_requested %> + + + <%= render partial: "icons/asterisk", locals: { custom_class: "inline-block align-middle mb-1" } %> + All files and directories + + <% if (@scopes & [":r"]).any? %> + (read only) + <% end %> + + <% else %> + <% @scopes.each do |scope| %> + + + <%= render partial: "icons/folder", locals: { custom_class: "inline-block align-middle mb-2" } %> + <%= scope_name(scope) %> + + <% if scope_permissions(scope) == "r" %> + (read only) + <% end %> + + <% end %> + <% end %> +

+ + <%= 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 %> +

+ <%= f.label :expire_at, "Expire:" %> + <%= f.select :expire_at, options_for_select(@expire_at_dates) %> +

+

+ You can revoke access for this app at any time on your storage dashboard. +

+
+ <%= 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 %> +
+ <% end %> +
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index 8b4866b..b83d631 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,12 @@ Rails.application.routes.draw do 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 + authenticate :user, ->(user) { user.is_admin? } do mount Sidekiq::Web => '/sidekiq' end