RemoteStorage OAuth dialog
This commit is contained in:
		
							parent
							
								
									20c014607c
								
							
						
					
					
						commit
						7acc3b2106
					
				
							
								
								
									
										131
									
								
								app/controllers/rs/oauth_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								app/controllers/rs/oauth_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										18
									
								
								app/services/remote_storage.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/services/remote_storage.rb
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										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 %> | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user