From: Tom Hughes Date: Thu, 27 Aug 2020 17:44:50 +0000 (+0100) Subject: Add support for OAuth2 using doorkeeper X-Git-Tag: live~1583^2~8 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/e222329d043592af299eed12cf6ad16969c1b46f Add support for OAuth2 using doorkeeper --- diff --git a/.rubocop.yml b/.rubocop.yml index 1931eb55f..628589d69 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -58,6 +58,9 @@ Rails/HttpPositionalArguments: Rails/InverseOf: Enabled: false +Rails/ReflectionClassName: + Enabled: false + Rails/SkipsModelValidations: Exclude: - 'db/migrate/*.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3059d5773..e922a4b79 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -142,6 +142,10 @@ Rails/HelperInstanceVariable: Exclude: - 'app/helpers/title_helper.rb' +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/oauth2_applications_controller.rb' + # Offense count: 5 # Configuration parameters: Include. # Include: db/migrate/*.rb diff --git a/Gemfile b/Gemfile index 10e34638e..cdc5c7e26 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,10 @@ gem "omniauth-openid" gem "omniauth-rails_csrf_protection", "~> 1.0" gem "omniauth-windowslive" +# Doorkeeper for OAuth2 +gem "doorkeeper" +gem "doorkeeper-i18n" + # Markdown formatting support gem "kramdown" diff --git a/Gemfile.lock b/Gemfile.lock index be1d70183..1effb0b34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,6 +151,10 @@ GEM activerecord (>= 3.0, < 6.2) delayed_job (>= 3.0, < 5) docile (1.4.0) + doorkeeper (5.5.1) + railties (>= 5) + doorkeeper-i18n (5.2.2) + doorkeeper (>= 5.2) dry-configurable (0.12.1) concurrent-ruby (~> 1.0) dry-core (~> 0.5, >= 0.5.0) @@ -504,6 +508,8 @@ DEPENDENCIES dalli debug_inspector delayed_job_active_record + doorkeeper + doorkeeper-i18n erb_lint factory_bot_rails faraday diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index 9c832b43a..c03ab288c 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -39,6 +39,9 @@ class Ability if Settings.status != "database_offline" can [:index, :new, :create, :show, :edit, :update, :destroy], ClientApplication + can [:index, :new, :create, :show, :edit, :update, :destroy], :oauth2_application + can [:index, :destroy], :oauth2_authorized_application + can [:new, :show, :create, :destroy], :oauth2_authorization can [:new, :create, :edit, :update, :comment, :subscribe, :unsubscribe], DiaryEntry can [:make_friend, :remove_friend], Friendship can [:new, :create, :reply, :show, :inbox, :outbox, :mark, :destroy], Message diff --git a/app/abilities/api_capability.rb b/app/abilities/api_capability.rb index beb4d39bf..04d7fe10a 100644 --- a/app/abilities/api_capability.rb +++ b/app/abilities/api_capability.rb @@ -5,29 +5,35 @@ class ApiCapability def initialize(token) if Settings.status != "database_offline" - can [:create, :comment, :close, :reopen], Note if capability?(token, :allow_write_notes) - can [:show, :data], Trace if capability?(token, :allow_read_gpx) - can [:create, :update, :destroy], Trace if capability?(token, :allow_write_gpx) - can [:details], User if capability?(token, :allow_read_prefs) - can [:gpx_files], User if capability?(token, :allow_read_gpx) - can [:index, :show], UserPreference if capability?(token, :allow_read_prefs) - can [:update, :update_all, :destroy], UserPreference if capability?(token, :allow_write_prefs) + user = if token.respond_to?(:resource_owner_id) + User.find(token.resource_owner_id) + elsif token.respond_to?(:user) + token.user + end - if token&.user&.terms_agreed? - can [:create, :update, :upload, :close, :subscribe, :unsubscribe], Changeset if capability?(token, :allow_write_api) - can :create, ChangesetComment if capability?(token, :allow_write_api) - can [:create, :update, :delete], Node if capability?(token, :allow_write_api) - can [:create, :update, :delete], Way if capability?(token, :allow_write_api) - can [:create, :update, :delete], Relation if capability?(token, :allow_write_api) + can [:create, :comment, :close, :reopen], Note if scope?(token, :write_notes) + can [:show, :data], Trace if scope?(token, :read_gpx) + can [:create, :update, :destroy], Trace if scope?(token, :write_gpx) + can [:details], User if scope?(token, :read_prefs) + can [:gpx_files], User if scope?(token, :read_gpx) + can [:index, :show], UserPreference if scope?(token, :read_prefs) + can [:update, :update_all, :destroy], UserPreference if scope?(token, :write_prefs) + + if user&.terms_agreed? + can [:create, :update, :upload, :close, :subscribe, :unsubscribe], Changeset if scope?(token, :write_api) + can :create, ChangesetComment if scope?(token, :write_api) + can [:create, :update, :delete], Node if scope?(token, :write_api) + can [:create, :update, :delete], Way if scope?(token, :write_api) + can [:create, :update, :delete], Relation if scope?(token, :write_api) end - if token&.user&.moderator? - can [:destroy, :restore], ChangesetComment if capability?(token, :allow_write_api) - can :destroy, Note if capability?(token, :allow_write_notes) - if token&.user&.terms_agreed? - can :redact, OldNode if capability?(token, :allow_write_api) - can :redact, OldWay if capability?(token, :allow_write_api) - can :redact, OldRelation if capability?(token, :allow_write_api) + if user&.moderator? + can [:destroy, :restore], ChangesetComment if scope?(token, :write_api) + can :destroy, Note if scope?(token, :write_notes) + if user&.terms_agreed? + can :redact, OldNode if scope?(token, :write_api) + can :redact, OldWay if scope?(token, :write_api) + can :redact, OldRelation if scope?(token, :write_api) end end end @@ -35,7 +41,7 @@ class ApiCapability private - def capability?(token, cap) - token&.read_attribute(cap) + def scope?(token, scope) + token&.includes_scope?(scope) end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c905b24ce..a13897640 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -61,7 +61,9 @@ class ApiController < ApplicationController def current_ability # Use capabilities from the oauth token if it exists and is a valid access token - if Authenticator.new(self, [:token]).allow? + if doorkeeper_token&.accessible? + ApiAbility.new(nil).merge(ApiCapability.new(doorkeeper_token)) + elsif Authenticator.new(self, [:token]).allow? ApiAbility.new(nil).merge(ApiCapability.new(current_token)) else ApiAbility.new(current_user) @@ -69,7 +71,7 @@ class ApiController < ApplicationController end def deny_access(_exception) - if current_token + if doorkeeper_token || current_token set_locale report_error t("oauth.permissions.missing"), :forbidden elsif current_user @@ -94,7 +96,11 @@ class ApiController < ApplicationController # is optional. def setup_user_auth # try and setup using OAuth - unless Authenticator.new(self, [:token]).allow? + if doorkeeper_token&.accessible? + self.current_user = User.find(doorkeeper_token.resource_owner_id) + elsif Authenticator.new(self, [:token]).allow? + # self.current_user setup by OAuth + else username, passwd = get_auth_data # parse from headers # authenticate per-scheme self.current_user = if username.nil? diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b4c0fbc23..fc8b75b60 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -345,7 +345,7 @@ class ApplicationController < ActionController::Base end def deny_access(_exception) - if current_token + if doorkeeper_token || current_token set_locale report_error t("oauth.permissions.missing"), :forbidden elsif current_user diff --git a/app/controllers/oauth2_applications_controller.rb b/app/controllers/oauth2_applications_controller.rb new file mode 100644 index 000000000..63b77be4b --- /dev/null +++ b/app/controllers/oauth2_applications_controller.rb @@ -0,0 +1,28 @@ +class Oauth2ApplicationsController < Doorkeeper::ApplicationsController + layout "site" + + prepend_before_action :authorize_web + before_action :set_locale + before_action :set_application, :only => [:show, :edit, :update, :destroy] + + authorize_resource :class => false + + def index + @applications = current_resource_owner.oauth2_applications.ordered_by(:created_at) + end + + private + + def set_application + @application = current_resource_owner&.oauth2_applications&.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render :action => "not_found", :status => :not_found + end + + def application_params + params[:doorkeeper_application][:scopes]&.delete("") + params.require(:doorkeeper_application) + .permit(:name, :redirect_uri, :confidential, :scopes => []) + .merge(:owner => current_resource_owner) + end +end diff --git a/app/controllers/oauth2_authorizations_controller.rb b/app/controllers/oauth2_authorizations_controller.rb new file mode 100644 index 000000000..9c2bce2d1 --- /dev/null +++ b/app/controllers/oauth2_authorizations_controller.rb @@ -0,0 +1,8 @@ +class Oauth2AuthorizationsController < Doorkeeper::AuthorizationsController + layout "site" + + prepend_before_action :authorize_web + before_action :set_locale + + authorize_resource :class => false +end diff --git a/app/controllers/oauth2_authorized_applications_controller.rb b/app/controllers/oauth2_authorized_applications_controller.rb new file mode 100644 index 000000000..369908b87 --- /dev/null +++ b/app/controllers/oauth2_authorized_applications_controller.rb @@ -0,0 +1,8 @@ +class Oauth2AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController + layout "site" + + prepend_before_action :authorize_web + before_action :set_locale + + authorize_resource :class => false +end diff --git a/app/models/access_token.rb b/app/models/access_token.rb index e5ba2e240..1a5ff8553 100644 --- a/app/models/access_token.rb +++ b/app/models/access_token.rb @@ -45,6 +45,10 @@ class AccessToken < OauthToken before_create :set_authorized_at + def includes_scope?(scope) + self[:"allow_#{scope}"] + end + protected def set_authorized_at diff --git a/app/models/client_application.rb b/app/models/client_application.rb index ee39c294b..1b2faafbb 100644 --- a/app/models/client_application.rb +++ b/app/models/client_application.rb @@ -62,7 +62,7 @@ class ClientApplication < ApplicationRecord end def self.all_permissions - PERMISSIONS + Oauth.scopes.collect { |s| :"allow_#{s.name}" } end def oauth_server @@ -102,11 +102,6 @@ class ClientApplication < ApplicationRecord protected - # this is the set of permissions that the client can ask for. clients - # have to say up-front what permissions they want and when users sign up they - # can agree or not agree to each of them. - PERMISSIONS = [:allow_read_prefs, :allow_write_prefs, :allow_write_diary, :allow_write_api, :allow_read_gpx, :allow_write_gpx, :allow_write_notes].freeze - def generate_keys self.key = OAuth::Helper.generate_key(40)[0, 40] self.secret = OAuth::Helper.generate_key(40)[0, 40] diff --git a/app/models/user.rb b/app/models/user.rb index 3dbaa688c..964359e9c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -68,6 +68,10 @@ class User < ApplicationRecord has_many :client_applications has_many :oauth_tokens, -> { order(:authorized_at => :desc).preload(:client_application) }, :class_name => "OauthToken" + has_many :oauth2_applications, :class_name => Doorkeeper.config.application_model.name, :foreign_key => :owner_id + has_many :access_grants, :class_name => Doorkeeper.config.access_grant_model.name, :foreign_key => :resource_owner_id + has_many :access_tokens, :class_name => Doorkeeper.config.access_token_model.name, :foreign_key => :resource_owner_id + has_many :blocks, :class_name => "UserBlock" has_many :blocks_created, :class_name => "UserBlock", :foreign_key => :creator_id has_many :blocks_revoked, :class_name => "UserBlock", :foreign_key => :revoker_id diff --git a/app/views/oauth2_applications/_application.html.erb b/app/views/oauth2_applications/_application.html.erb new file mode 100644 index 000000000..eb7f7ee0e --- /dev/null +++ b/app/views/oauth2_applications/_application.html.erb @@ -0,0 +1,23 @@ + + + + + + + + + <%= link_to t(".edit"), edit_oauth_application_path(application), :class => "btn btn-outline-primary" %> + + + <%= link_to t(".delete"), oauth_application_path(application), { :method => :delete, :class => "btn btn-outline-danger", :data => { :confirm => t(".confirm_delete") } } %> + + diff --git a/app/views/oauth2_applications/_form.html.erb b/app/views/oauth2_applications/_form.html.erb new file mode 100644 index 000000000..c61d351b7 --- /dev/null +++ b/app/views/oauth2_applications/_form.html.erb @@ -0,0 +1,7 @@ +<%= f.text_field :name %> +<%= f.text_area :redirect_uri, :help => t(".redirect_uri_help") %> +<%= f.form_group :confidential, :help => t(".confidential_help") do %> + <%= f.check_box :confidential %> +<% end %> +<%= f.collection_check_boxes :scopes, Oauth.scopes, :name, :description %> +<%= f.primary %> diff --git a/app/views/oauth2_applications/edit.html.erb b/app/views/oauth2_applications/edit.html.erb new file mode 100644 index 000000000..94b51893e --- /dev/null +++ b/app/views/oauth2_applications/edit.html.erb @@ -0,0 +1,7 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +<%= bootstrap_form_for @application, :url => oauth_application_path(@application), :html => { :method => :put } do |f| %> + <%= render :partial => "form", :locals => { :f => f } %> +<% end %> diff --git a/app/views/oauth2_applications/index.html.erb b/app/views/oauth2_applications/index.html.erb new file mode 100644 index 000000000..a2d4cc089 --- /dev/null +++ b/app/views/oauth2_applications/index.html.erb @@ -0,0 +1,13 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +

+ <%= link_to t(".new"), new_oauth_application_path, :class => "btn btn-outline-primary" %> +

+ + + + <%= render :partial => "application", :collection => @applications %> + +
diff --git a/app/views/oauth2_applications/new.html.erb b/app/views/oauth2_applications/new.html.erb new file mode 100644 index 000000000..a9d6f4a49 --- /dev/null +++ b/app/views/oauth2_applications/new.html.erb @@ -0,0 +1,7 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +<%= bootstrap_form_for @application, :url => { :action => :create } do |f| %> + <%= render :partial => "form", :locals => { :f => f } %> +<% end %> diff --git a/app/views/oauth2_applications/not_found.html.erb b/app/views/oauth2_applications/not_found.html.erb new file mode 100644 index 000000000..641b1dfb8 --- /dev/null +++ b/app/views/oauth2_applications/not_found.html.erb @@ -0,0 +1 @@ +

<%= t ".sorry" %>

diff --git a/app/views/oauth2_applications/show.html.erb b/app/views/oauth2_applications/show.html.erb new file mode 100644 index 000000000..e6fe3d3e8 --- /dev/null +++ b/app/views/oauth2_applications/show.html.erb @@ -0,0 +1,44 @@ +<% content_for :heading do %> +

<%= @application.name %>

+<% end %> + +<% secret = flash[:application_secret].presence || @application.plaintext_secret %> + + + + + + + <% unless secret.blank? && Doorkeeper.config.application_secret_hashed? -%> + + + + + <% end -%> + + + + + + + + +
<%= t ".client_id" %><%= @application.uid %>
<%= t ".client_secret" %> + <%= secret %> + <% if Doorkeeper.config.application_secret_hashed? -%> +
+ <%= t ".client_secret_warning" %> + <% end -%> +
<%= t ".permissions" %> +
    + <% @application.scopes.each do |scope| -%> +
  • <%= t "oauth.scopes.#{scope}" %>
  • + <% end -%> +
+
<%= t ".redirect_uris" %> +
    + <% @application.redirect_uri.split.each do |uri| -%> +
  • <%= uri %>
  • + <% end -%> +
+
diff --git a/app/views/oauth2_authorizations/error.html.erb b/app/views/oauth2_authorizations/error.html.erb new file mode 100644 index 000000000..7df81dabe --- /dev/null +++ b/app/views/oauth2_authorizations/error.html.erb @@ -0,0 +1,5 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +

<%= @pre_auth.error_response.body[:error_description] %>

diff --git a/app/views/oauth2_authorizations/new.html.erb b/app/views/oauth2_authorizations/new.html.erb new file mode 100644 index 000000000..00d2a01af --- /dev/null +++ b/app/views/oauth2_authorizations/new.html.erb @@ -0,0 +1,40 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +

<%= t ".introduction", :application => @pre_auth.client.name %>

+ + + +
+
+
+ <%= bootstrap_form_tag :action => :create do |f| %> + <%= f.hidden_field :client_id, :value => @pre_auth.client.uid %> + <%= f.hidden_field :redirect_uri, :value => @pre_auth.redirect_uri %> + <%= f.hidden_field :state, :value => @pre_auth.state %> + <%= f.hidden_field :response_type, :value => @pre_auth.response_type %> + <%= f.hidden_field :scope, :value => @pre_auth.scope %> + <%= f.hidden_field :code_challenge, :value => @pre_auth.code_challenge %> + <%= f.hidden_field :code_challenge_method, :value => @pre_auth.code_challenge_method %> + <%= f.primary t(".authorize") %> + <% end %> +
+
+ <%= bootstrap_form_tag :action => :destroy, :html => { :method => :delete } do |f| %> + <%= f.hidden_field :client_id, :value => @pre_auth.client.uid %> + <%= f.hidden_field :redirect_uri, :value => @pre_auth.redirect_uri %> + <%= f.hidden_field :state, :value => @pre_auth.state %> + <%= f.hidden_field :response_type, :value => @pre_auth.response_type %> + <%= f.hidden_field :scope, :value => @pre_auth.scope %> + <%= f.hidden_field :code_challenge, :value => @pre_auth.code_challenge %> + <%= f.hidden_field :code_challenge_method, :value => @pre_auth.code_challenge_method %> + <%= f.submit t(".deny") %> + <% end %> +
+
+
diff --git a/app/views/oauth2_authorizations/show.html.erb b/app/views/oauth2_authorizations/show.html.erb new file mode 100644 index 000000000..b55757b50 --- /dev/null +++ b/app/views/oauth2_authorizations/show.html.erb @@ -0,0 +1,5 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +<%= params[:code] %> diff --git a/app/views/oauth2_authorized_applications/_application.html.erb b/app/views/oauth2_authorized_applications/_application.html.erb new file mode 100644 index 000000000..f8e9a1185 --- /dev/null +++ b/app/views/oauth2_authorized_applications/_application.html.erb @@ -0,0 +1,15 @@ + + + <%= application.name %> + + + + + + <%= link_to t(".revoke"), oauth_authorized_application_path(application), { :method => :delete, :class => "btn btn-outline-danger", :data => { :confirm => t(".confirm_revoke") } } %> + + diff --git a/app/views/oauth2_authorized_applications/index.html.erb b/app/views/oauth2_authorized_applications/index.html.erb new file mode 100644 index 000000000..dca65e9f9 --- /dev/null +++ b/app/views/oauth2_authorized_applications/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + + + + + + + + + <%= render :partial => "application", :collection => @applications %> + +
<%= t ".application" %><%= t ".permissions" %>
diff --git a/app/views/users/account.html.erb b/app/views/users/account.html.erb index 10b33063e..dc31de97a 100644 --- a/app/views/users/account.html.erb +++ b/app/views/users/account.html.erb @@ -6,7 +6,9 @@

<%= t ".my settings" %>

<% end %> diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 000000000..bc70f02f0 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,459 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + current_user + end + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + + admin_authenticator do + current_user + end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + # api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + + enforce_content_type + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + + access_token_expires_in nil + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + # custom_access_token_expires_in do |context| + # context.client.application.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-base-controller + + base_controller "ApplicationController" + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old valid one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper + # doesn't updates existing token expiration time, it will create a new token instead. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without enabling the additional + # setting `fallback_to_plain_secrets` below. + + hash_token_secrets + + # Hash application secrets before persisting them. + + hash_application_secrets + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # fallback_to_plain_secrets + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default: false) if you want to enforce ownership of + # a registered application + # NOTE: you must also run the rails g doorkeeper:application_owner generator + # to provide the necessary support + + enable_application_owner :confirmation => true + + # Define access token scopes for your provider + # For more information go to + # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + + # default_scopes :public + optional_scopes(*Oauth::SCOPES) + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + + enforce_configured_scopes + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + + access_token_methods :from_bearer_authorization + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + + force_ssl_in_redirect_uri !Rails.env.development? + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Allows to set blank redirect URIs for Applications in case Doorkeeper configured + # to use URI-less OAuth grant flows like Client Credentials or Resource Owner + # Password Credentials. The option is on by default and checks configured grant + # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` + # column for `oauth_applications` database table. + # + # You can completely disable this feature with: + # + # allow_blank_redirect_uri false + # + # Or you can define your custom check: + # + # allow_blank_redirect_uri do |grant_flows, client| + # client.superapp? + # end + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + + # Customize token introspection response. + # Allows to add your own fields to default one that are required by the OAuth spec + # for the introspection response. It could be `sub`, `aud` and so on. + # This configuration option can be a proc, lambda or any Ruby object responds + # to `.call` method and result of it's invocation must be a Hash. + # + # custom_introspection_response do |token, context| + # { + # "sub": "Z5O3upPC88QrAjx00dis", + # "aud": "https://protected.example.net/resource", + # "username": User.find(token.resource_owner_id).username + # } + # end + # + # or + # + # custom_introspection_response CustomIntrospectionResponder + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + + grant_flows %w[authorization_code] + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # Be default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_whitelist.include?(resource_owner) + # end + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/locales/en.yml b/config/locales/en.yml index e04a6ac87..dfb8c25eb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -95,6 +95,11 @@ en: latitude: "Latitude" longitude: "Longitude" language: "Language" + doorkeeper/application: + name: Name + redirect_uri: Redirect URIs + confidential: Confidential application? + scopes: Permissions friend: user: "User" friend: "Friend" @@ -2274,6 +2279,14 @@ en: flash: "You've revoked the token for %{application}" permissions: missing: "You have not permitted the application access to this facility" + scopes: + read_prefs: Read user preferences + write_prefs: Modify user preferences + write_diary: Create diary entries, comments and make friends + write_api: Modify the map + read_gpx: Read private GPS traces + write_gpx: Upload GPS traces + write_notes: Modify notes oauth_clients: new: title: "Register a new application" @@ -2313,6 +2326,47 @@ en: flash: "Updated the client information successfully" destroy: flash: "Destroyed the client application registration" + oauth2_applications: + index: + title: "My client applications" + new: "Create new application" + application: + edit: "Edit" + delete: "Delete" + confirm_delete: "Delete this application?" + new: + title: "Register a new application" + edit: + title: "Edit your application" + show: + client_id: "Client ID" + client_secret: "Client Secret" + client_secret_warning: "Make sure to save this secret - it will not be accessible again" + permissions: "Permissions" + redirect_uris: "Redirect URIs" + form: + redirect_uri_help: "Use one line per URI" + confidential_help: "Application will be used where the client secret can be kept confidential (native mobile apps and single page apps are not confidential)" + not_found: + sorry: "Sorry, that application could not be found." + oauth2_authorizations: + new: + title: "Authorization required" + introduction: "Authorize %{application} to access your account with the following permissions?" + authorize: "Authorize" + deny: "Deny" + error: + title: "An error has occurred" + show: + title: "Authorization code" + oauth2_authorized_applications: + index: + title: "My authorized applications" + application: "Application" + permissions: "Permissions" + application: + revoke: "Revoke Access" + confirm_revoke: "Revoke access for this application?" users: new: title: "Sign Up" @@ -2372,7 +2426,6 @@ en: my profile: My Profile my settings: My Settings my comments: My Comments - oauth settings: oauth settings blocks on me: Blocks on Me blocks by me: Blocks by Me send message: Send Message @@ -2474,6 +2527,9 @@ en: save changes button: Save Changes make edits public button: Make all my edits public return to profile: Return to profile + oauth1 settings: OAuth 1 settings + oauth2 applications: OAuth 2 applications + oauth2 authorizations: OAuth 2 authorizations flash update success confirm needed: "User information updated successfully. Check your email for a note to confirm your new email address." flash update success: "User information updated successfully." set_home: diff --git a/config/routes.rb b/config/routes.rb index da3921e4a..048db8b33 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,10 @@ OpenStreetMap::Application.routes.draw do + use_doorkeeper :scope => "oauth2" do + controllers :authorizations => "oauth2_authorizations", + :applications => "oauth2_applications", + :authorized_applications => "oauth2_authorized_applications" + end + # API namespace :api do get "capabilities" => "capabilities#show" # Deprecated, remove when 0.6 support is removed diff --git a/db/migrate/20201004105659_create_doorkeeper_tables.rb b/db/migrate/20201004105659_create_doorkeeper_tables.rb new file mode 100644 index 000000000..66456bc2c --- /dev/null +++ b/db/migrate/20201004105659_create_doorkeeper_tables.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CreateDoorkeeperTables < ActiveRecord::Migration[6.0] + def change + create_table :oauth_applications do |t| + t.references :owner, :null => false, :type => :bigint, :polymorphic => true + t.string :name, :null => false + t.string :uid, :null => false + t.string :secret, :null => false + t.text :redirect_uri, :null => false + t.string :scopes, :null => false, :default => "" + t.boolean :confidential, :null => false, :default => true + t.timestamps :null => false + end + + add_index :oauth_applications, :uid, :unique => true + add_foreign_key :oauth_applications, :users, :column => :owner_id, :validate => false + + create_table :oauth_access_grants do |t| + t.references :resource_owner, :null => false, :type => :bigint + t.references :application, :null => false + t.string :token, :null => false + t.integer :expires_in, :null => false + t.text :redirect_uri, :null => false + t.datetime :created_at, :null => false + t.datetime :revoked_at + t.string :scopes, :null => false, :default => "" + t.column :code_challenge, :string, :null => true + t.column :code_challenge_method, :string, :null => true + end + + add_index :oauth_access_grants, :token, :unique => true + add_foreign_key :oauth_access_grants, :users, :column => :resource_owner_id, :validate => false + add_foreign_key :oauth_access_grants, :oauth_applications, :column => :application_id, :validate => false + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, :index => true, :type => :bigint + t.references :application, :null => false + t.string :token, :null => false + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, :null => false + t.string :scopes + t.string :previous_refresh_token, :null => false, :default => "" + end + + add_index :oauth_access_tokens, :token, :unique => true + add_index :oauth_access_tokens, :refresh_token, :unique => true + add_foreign_key :oauth_access_tokens, :users, :column => :resource_owner_id, :validate => false + add_foreign_key :oauth_access_tokens, :oauth_applications, :column => :application_id, :validate => false + end +end diff --git a/db/structure.sql b/db/structure.sql index 53202f2d8..a45bb0a70 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1070,6 +1070,119 @@ CREATE SEQUENCE public.notes_id_seq ALTER SEQUENCE public.notes_id_seq OWNED BY public.notes.id; +-- +-- Name: oauth_access_grants; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.oauth_access_grants ( + id bigint NOT NULL, + resource_owner_id bigint NOT NULL, + application_id bigint NOT NULL, + token character varying NOT NULL, + expires_in integer NOT NULL, + redirect_uri text NOT NULL, + created_at timestamp without time zone NOT NULL, + revoked_at timestamp without time zone, + scopes character varying DEFAULT ''::character varying NOT NULL, + code_challenge character varying, + code_challenge_method character varying +); + + +-- +-- Name: oauth_access_grants_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.oauth_access_grants_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: oauth_access_grants_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.oauth_access_grants_id_seq OWNED BY public.oauth_access_grants.id; + + +-- +-- Name: oauth_access_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.oauth_access_tokens ( + id bigint NOT NULL, + resource_owner_id bigint, + application_id bigint NOT NULL, + token character varying NOT NULL, + refresh_token character varying, + expires_in integer, + revoked_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + scopes character varying, + previous_refresh_token character varying DEFAULT ''::character varying NOT NULL +); + + +-- +-- Name: oauth_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.oauth_access_tokens_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: oauth_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.oauth_access_tokens_id_seq OWNED BY public.oauth_access_tokens.id; + + +-- +-- Name: oauth_applications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.oauth_applications ( + id bigint NOT NULL, + owner_type character varying NOT NULL, + owner_id bigint NOT NULL, + name character varying NOT NULL, + uid character varying NOT NULL, + secret character varying NOT NULL, + redirect_uri text NOT NULL, + scopes character varying DEFAULT ''::character varying NOT NULL, + confidential boolean DEFAULT true NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: oauth_applications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.oauth_applications_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: oauth_applications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.oauth_applications_id_seq OWNED BY public.oauth_applications.id; + + -- -- Name: oauth_nonces; Type: TABLE; Schema: public; Owner: - -- @@ -1627,6 +1740,27 @@ ALTER TABLE ONLY public.note_comments ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.notes ALTER COLUMN id SET DEFAULT nextval('public.notes_id_seq'::regclass); +-- +-- Name: oauth_access_grants id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_grants ALTER COLUMN id SET DEFAULT nextval('public.oauth_access_grants_id_seq'::regclass); + + +-- +-- Name: oauth_access_tokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.oauth_access_tokens_id_seq'::regclass); + + +-- +-- Name: oauth_applications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_applications ALTER COLUMN id SET DEFAULT nextval('public.oauth_applications_id_seq'::regclass); + + -- -- Name: oauth_nonces id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1931,6 +2065,30 @@ ALTER TABLE ONLY public.notes ADD CONSTRAINT notes_pkey PRIMARY KEY (id); +-- +-- Name: oauth_access_grants oauth_access_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_grants + ADD CONSTRAINT oauth_access_grants_pkey PRIMARY KEY (id); + + +-- +-- Name: oauth_access_tokens oauth_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_tokens + ADD CONSTRAINT oauth_access_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: oauth_applications oauth_applications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_applications + ADD CONSTRAINT oauth_applications_pkey PRIMARY KEY (id); + + -- -- Name: oauth_nonces oauth_nonces_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2395,6 +2553,69 @@ CREATE INDEX index_note_comments_on_body ON public.note_comments USING gin (to_t CREATE INDEX index_note_comments_on_created_at ON public.note_comments USING btree (created_at); +-- +-- Name: index_oauth_access_grants_on_application_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_oauth_access_grants_on_application_id ON public.oauth_access_grants USING btree (application_id); + + +-- +-- Name: index_oauth_access_grants_on_resource_owner_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_oauth_access_grants_on_resource_owner_id ON public.oauth_access_grants USING btree (resource_owner_id); + + +-- +-- Name: index_oauth_access_grants_on_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON public.oauth_access_grants USING btree (token); + + +-- +-- Name: index_oauth_access_tokens_on_application_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_oauth_access_tokens_on_application_id ON public.oauth_access_tokens USING btree (application_id); + + +-- +-- Name: index_oauth_access_tokens_on_refresh_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_oauth_access_tokens_on_refresh_token ON public.oauth_access_tokens USING btree (refresh_token); + + +-- +-- Name: index_oauth_access_tokens_on_resource_owner_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_oauth_access_tokens_on_resource_owner_id ON public.oauth_access_tokens USING btree (resource_owner_id); + + +-- +-- Name: index_oauth_access_tokens_on_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_oauth_access_tokens_on_token ON public.oauth_access_tokens USING btree (token); + + +-- +-- Name: index_oauth_applications_on_owner_type_and_owner_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_oauth_applications_on_owner_type_and_owner_id ON public.oauth_applications USING btree (owner_type, owner_id); + + +-- +-- Name: index_oauth_applications_on_uid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_oauth_applications_on_uid ON public.oauth_applications USING btree (uid); + + -- -- Name: index_oauth_nonces_on_nonce_and_timestamp; Type: INDEX; Schema: public; Owner: - -- @@ -2802,6 +3023,22 @@ ALTER TABLE ONLY public.diary_entry_subscriptions ADD CONSTRAINT diary_entry_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id); +-- +-- Name: oauth_access_grants fk_rails_330c32d8d9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_grants + ADD CONSTRAINT fk_rails_330c32d8d9 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id) NOT VALID; + + +-- +-- Name: oauth_access_tokens fk_rails_732cb83ab7; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_tokens + ADD CONSTRAINT fk_rails_732cb83ab7 FOREIGN KEY (application_id) REFERENCES public.oauth_applications(id) NOT VALID; + + -- -- Name: active_storage_variant_records fk_rails_993965df05; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2810,6 +3047,14 @@ ALTER TABLE ONLY public.active_storage_variant_records ADD CONSTRAINT fk_rails_993965df05 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id); +-- +-- Name: oauth_access_grants fk_rails_b4b53e07b8; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_grants + ADD CONSTRAINT fk_rails_b4b53e07b8 FOREIGN KEY (application_id) REFERENCES public.oauth_applications(id) NOT VALID; + + -- -- Name: active_storage_attachments fk_rails_c3b3935057; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2818,6 +3063,22 @@ ALTER TABLE ONLY public.active_storage_attachments ADD CONSTRAINT fk_rails_c3b3935057 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id); +-- +-- Name: oauth_applications fk_rails_cc886e315a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_applications + ADD CONSTRAINT fk_rails_cc886e315a FOREIGN KEY (owner_id) REFERENCES public.users(id) NOT VALID; + + +-- +-- Name: oauth_access_tokens fk_rails_ee63f25419; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_access_tokens + ADD CONSTRAINT fk_rails_ee63f25419 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id) NOT VALID; + + -- -- Name: friends friends_friend_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3181,6 +3442,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20190702193519'), ('20190716173946'), ('20191120140058'), +('20201004105659'), ('20201006213836'), ('20201006220807'), ('20201214144017'), diff --git a/lib/oauth.rb b/lib/oauth.rb new file mode 100644 index 000000000..8f45a3b4b --- /dev/null +++ b/lib/oauth.rb @@ -0,0 +1,19 @@ +module Oauth + SCOPES = %w[read_prefs write_prefs write_diary write_api read_gpx write_gpx write_notes].freeze + + class Scope + attr_reader :name + + def initialize(name) + @name = name + end + + def description + I18n.t("oauth.scopes.#{name}") + end + end + + def self.scopes + SCOPES.collect { |s| Scope.new(s) } + end +end