Browse Source

Adding OAuth access scopes, fixing OAuth authorization UI, adding rate limiting

to the API
master
Eugen Rochko 7 years ago
parent
commit
a9e40a3d80
26 changed files with 195 additions and 99 deletions
  1. +50
    -4
      .rubocop.yml
  2. +46
    -42
      app/assets/stylesheets/forms.scss
  3. +1
    -1
      app/channels/public_channel.rb
  4. +3
    -1
      app/controllers/api/v1/accounts_controller.rb
  5. +1
    -1
      app/controllers/api/v1/follows_controller.rb
  6. +1
    -1
      app/controllers/api/v1/media_controller.rb
  7. +3
    -1
      app/controllers/api/v1/statuses_controller.rb
  8. +22
    -0
      app/controllers/api_controller.rb
  9. +1
    -1
      app/controllers/home_controller.rb
  10. +9
    -0
      app/controllers/oauth/authorizations_controller.rb
  11. +1
    -1
      app/models/feed.rb
  12. +2
    -1
      app/models/media_attachment.rb
  13. +0
    -4
      app/views/doorkeeper/authorizations/error.html.haml
  14. +0
    -26
      app/views/doorkeeper/authorizations/new.html.haml
  15. +0
    -2
      app/views/doorkeeper/authorizations/show.html.haml
  16. +2
    -0
      app/views/oauth/authorizations/error.html.haml
  17. +25
    -0
      app/views/oauth/authorizations/new.html.haml
  18. +1
    -0
      app/views/oauth/authorizations/show.html.haml
  19. +1
    -1
      config/environments/production.rb
  20. +2
    -2
      config/initializers/doorkeeper.rb
  21. +1
    -1
      config/initializers/rabl_init.rb
  22. +14
    -4
      config/initializers/rack-attack.rb
  23. +4
    -0
      config/locales/doorkeeper.en.yml
  24. +3
    -1
      config/routes.rb
  25. +1
    -1
      db/seeds.rb
  26. +1
    -3
      lib/tasks/mastodon.rake

+ 50
- 4
.rubocop.yml View File

@@ -1,14 +1,60 @@
Rails:
Enabled: true

Metrics/LineLength:
Enabled: false

Style/PerlBackrefs:
AutoCorrect: false

Style/ClassAndModuleChildren:
Enabled: false

Documentation:
Metrics/BlockNesting:
Max: 2

Metrics/LineLength:
AllowURI: true
Enabled: false

Metrics/MethodLength:
CountComments: false
Max: 10

Metrics/ModuleLength:
Max: 100

Metrics/ParameterLists:
Max: 4
CountKeywordArgs: true

Style/AccessModifierIndentation:
EnforcedStyle: indent

Style/CollectionMethods:
Enabled: true
PreferredMethods:
find_all: 'select'

Style/Documentation:
Enabled: false

Style/DoubleNegation:
Enabled: false

Style/FrozenStringLiteralComment:
Enabled: false

Style/SpaceInsideHashLiteralBraces:
EnforcedStyle: space

Style/TrailingCommaInLiteral:
EnforcedStyleForMultiline: 'comma'

Style/RegexpLiteral:
Enabled: false

AllCops:
TargetRubyVersion: 2.2
Exclude:
- 'spec/**/*'
- 'db/**/*'
- 'app/views/**/*'
- 'config/**/*'

+ 46
- 42
app/assets/stylesheets/forms.scss View File

@@ -85,18 +85,7 @@
}
}

.prompt {
font-size: 16px;
color: #9baec8;
text-align: center;

.prompt-highlight {
font-weight: 500;
color: #fff;
}
}

code.copypasteable {
code {
display: block;
font-family: 'Roboto Mono', monospace;
font-weight: 400;
@@ -110,42 +99,42 @@

.actions {
margin-top: 30px;
}

button {
display: block;
width: 100%;
border: 0;
border-radius: 4px;
background: #2b90d9;
color: #fff;
font-size: 18px;
padding: 10px;
text-transform: uppercase;
cursor: pointer;
font-weight: 500;
outline: 0;
margin-bottom: 10px;
button {
display: block;
width: 100%;
border: 0;
border-radius: 4px;
background: #2b90d9;
color: #fff;
font-size: 18px;
padding: 10px;
text-transform: uppercase;
cursor: pointer;
font-weight: 500;
outline: 0;
margin-bottom: 10px;

&:hover {
background-color: lighten(#2b90d9, 5%);
}
&:hover {
background-color: lighten(#2b90d9, 5%);
}

&:active, &:focus {
position: relative;
top: 1px;
background-color: darken(#2b90d9, 5%);
}
&:active, &:focus {
position: relative;
top: 1px;
background-color: darken(#2b90d9, 5%);
}

&.negative {
background: #df405a;
&.negative {
background: #df405a;

&:hover {
background-color: lighten(#df405a, 5%);
}
&:hover {
background-color: lighten(#df405a, 5%);
}

&:active, &:focus {
background-color: darken(#df405a, 5%);
}
&:active, &:focus {
background-color: darken(#df405a, 5%);
}
}
}
@@ -180,3 +169,18 @@
}
}

.oauth-prompt {
margin-bottom: 30px;
text-align: center;
color: #9baec8;

h2 {
font-size: 16px;
margin-bottom: 30px;
}

strong {
color: #d9e1e8;
font-weight: 500;
}
}

+ 1
- 1
app/channels/public_channel.rb View File

@@ -1,7 +1,7 @@
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class PublicChannel < ApplicationCable::Channel
def subscribed
stream_from 'timeline:public', -> (encoded_message) do
stream_from 'timeline:public', lambda do |encoded_message|
message = ActiveSupport::JSON.decode(encoded_message)

status = Status.find_by(id: message['id'])


+ 3
- 1
app/controllers/api/v1/accounts_controller.rb View File

@@ -1,5 +1,7 @@
class Api::V1::AccountsController < ApiController
before_action :doorkeeper_authorize!
before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock]
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock]

before_action :set_account, except: [:verify_credentials, :suggestions]
respond_to :json



+ 1
- 1
app/controllers/api/v1/follows_controller.rb View File

@@ -1,5 +1,5 @@
class Api::V1::FollowsController < ApiController
before_action :doorkeeper_authorize!
before_action -> { doorkeeper_authorize! :follow }
respond_to :json

def create


+ 1
- 1
app/controllers/api/v1/media_controller.rb View File

@@ -1,5 +1,5 @@
class Api::V1::MediaController < ApiController
before_action :doorkeeper_authorize!
before_action -> { doorkeeper_authorize! :write }
respond_to :json

def create


+ 3
- 1
app/controllers/api/v1/statuses_controller.rb View File

@@ -1,5 +1,7 @@
class Api::V1::StatusesController < ApiController
before_action :doorkeeper_authorize!
before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]
before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]

respond_to :json

def show


+ 22
- 0
app/controllers/api_controller.rb View File

@@ -1,7 +1,10 @@
class ApiController < ApplicationController
protect_from_forgery with: :null_session

skip_before_action :verify_authenticity_token

before_action :set_rate_limit_headers

rescue_from ActiveRecord::RecordInvalid do |e|
render json: { error: e.to_s }, status: 422
end
@@ -22,8 +25,27 @@ class ApiController < ApplicationController
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
end

def doorkeeper_unauthorized_render_options(*)
{ json: { error: 'Not authorized' } }
end

def doorkeeper_forbidden_render_options(*)
{ json: { error: 'This action is outside the authorized scopes' } }
end

protected

def set_rate_limit_headers
return if request.env['rack.attack.throttle_data'].nil?

now = Time.now.utc
match_data = request.env['rack.attack.throttle_data']['api']

response.headers['X-RateLimit-Limit'] = match_data[:limit].to_s
response.headers['X-RateLimit-Remaining'] = (match_data[:limit] - match_data[:count]).to_s
response.headers['X-RateLimit-Reset'] = (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
end

def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end


+ 1
- 1
app/controllers/home_controller.rb View File

@@ -15,6 +15,6 @@ class HomeController < ApplicationController
end

def find_or_create_access_token
Doorkeeper::AccessToken.find_or_create_for(Doorkeeper::Application.where(superapp: true).first, current_user.id, nil, Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled?)
Doorkeeper::AccessToken.find_or_create_for(Doorkeeper::Application.where(superapp: true).first, current_user.id, 'read write follow', Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled?)
end
end

+ 9
- 0
app/controllers/oauth/authorizations_controller.rb View File

@@ -0,0 +1,9 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :store_current_location

private

def store_current_location
store_location_for(:user, request.url)
end
end

+ 1
- 1
app/models/feed.rb View File

@@ -7,7 +7,7 @@ class Feed
def get(limit, max_id = nil, since_id = nil)
max_id = '+inf' if max_id.blank?
since_id = '-inf' if since_id.blank?
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).collect(&:last).map(&:to_i)
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)

# If we're after most recent items and none are there, we need to precompute the feed
if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'


+ 2
- 1
app/models/media_attachment.rb View File

@@ -34,7 +34,8 @@ class MediaAttachment < ApplicationRecord
image? ? 'image' : 'video'
end

private
private

def self.file_styles(f)
if f.instance.image?
{


+ 0
- 4
app/views/doorkeeper/authorizations/error.html.haml View File

@@ -1,4 +0,0 @@
.prompt= t('doorkeeper.authorizations.error.title')

#error_explanation
= @pre_auth.error_response.body[:error_description]

+ 0
- 26
app/views/doorkeeper/authorizations/new.html.haml View File

@@ -1,26 +0,0 @@
.prompt= raw t('.prompt', client_name: "<strong class=\"prompt-highlight\">#{ @pre_auth.client.name }</strong>")

/- if @pre_auth.scopes.count > 0
/ .scope-permission-prompt
/ %p= t('.able_to')

/ %ul.scope-permissions
/ - @pre_auth.scopes.each do |scope|
/ %li= t scope, scope: [:doorkeeper, :scopes]

.actions
= form_tag oauth_authorization_path, method: :post do
= hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit

= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'

+ 0
- 2
app/views/doorkeeper/authorizations/show.html.haml View File

@@ -1,2 +0,0 @@
.prompt= t('.title')
%code.copypasteable= params[:code]

+ 2
- 0
app/views/oauth/authorizations/error.html.haml View File

@@ -0,0 +1,2 @@
.flash-message#error_explanation
= @pre_auth.error_response.body[:error_description]

+ 25
- 0
app/views/oauth/authorizations/new.html.haml View File

@@ -0,0 +1,25 @@
.oauth-prompt
%h2
Application
%strong=@pre_auth.client.name
requests access to your account

%p
It will be able to
= @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe

= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
= hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit

= form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
= hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'

+ 1
- 0
app/views/oauth/authorizations/show.html.haml View File

@@ -0,0 +1 @@
%code= params[:code]

+ 1
- 1
config/environments/production.rb View File

@@ -12,7 +12,7 @@ Rails.application.configure do

# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = false
config.action_controller.perform_caching = true

# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.


+ 2
- 2
config/initializers/doorkeeper.rb View File

@@ -50,8 +50,8 @@ Doorkeeper.configure do
# Define access token scopes for your provider
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
# default_scopes :public
# optional_scopes :write, :follow
default_scopes :read
optional_scopes :write, :follow

# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then


+ 1
- 1
config/initializers/rabl_init.rb View File

@@ -1,5 +1,5 @@
Rabl.configure do |config|
config.cache_all_output = true
config.cache_all_output = false
config.cache_sources = !!Rails.env.production?
config.include_json_root = false
config.view_paths = [Rails.root.join('app/views')]


+ 14
- 4
config/initializers/rack-attack.rb View File

@@ -1,9 +1,19 @@
class Rack::Attack
throttle('get-req/ip', limit: 300, period: 5.minutes) do |req|
req.ip if req.get?
# Rate limits for the API
throttle('api', limit: 150, period: 5.minutes) do |req|
req.ip if req.path.match(/\A\/api\//)
end

throttle('post-req/ip', limit: 100, period: 5.minutes) do |req|
req.ip if req.post?
self.throttled_response = lambda do |env|
now = Time.now.utc
match_data = env['rack.attack.match_data']

headers = {
'X-RateLimit-Limit' => match_data[:limit].to_s,
'X-RateLimit-Remaining' => '0',
'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
}

[429, headers, [{ error: 'Throttled' }.to_json]]
end
end

+ 4
- 0
config/locales/doorkeeper.en.yml View File

@@ -15,6 +15,10 @@ en:
secured_uri: 'must be an HTTPS/SSL URI.'

doorkeeper:
scopes:
read: read your account's data
write: post on your behalf
follow: follow, block, unblock and unfollow accounts
applications:
confirmations:
destroy: 'Are you sure?'


+ 3
- 1
config/routes.rb View File

@@ -7,7 +7,9 @@ Rails.application.routes.draw do
mount Sidekiq::Web => '/sidekiq'
end

use_doorkeeper
use_doorkeeper do
controllers authorizations: 'oauth/authorizations'
end

get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta
get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger


+ 1
- 1
db/seeds.rb View File

@@ -1,2 +1,2 @@
web_app = Doorkeeper::Application.new(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri)
web_app = Doorkeeper::Application.new(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow')
web_app.save!

+ 1
- 3
lib/tasks/mastodon.rake View File

@@ -2,9 +2,7 @@ namespace :mastodon do
namespace :media do
desc 'Removes media attachments that have not been assigned to any status for longer than a day'
task clear: :environment do
MediaAttachment.where(status_id: nil).where('created_at < ?', 1.day.ago).find_each do |m|
m.destroy
end
MediaAttachment.where(status_id: nil).where('created_at < ?', 1.day.ago).find_each(&:destroy)
end
end



Loading…
Cancel
Save