* Change authorized applications page * Hide revoke button for superapps and suspended accounts * Clean up db/schema.rbclosed-social-glitch-2
@ -0,0 +1,21 @@ | |||
# frozen_string_literal: true | |||
module AccessTokenTrackingConcern | |||
extend ActiveSupport::Concern | |||
ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze | |||
included do | |||
before_action :update_access_token_last_used | |||
end | |||
private | |||
def update_access_token_last_used | |||
doorkeeper_token.update_last_used(request) if access_token_needs_update? | |||
end | |||
def access_token_needs_update? | |||
doorkeeper_token.present? && (doorkeeper_token.last_used_at.nil? || doorkeeper_token.last_used_at < ACCESS_TOKEN_UPDATE_FREQUENCY.ago) | |||
end | |||
end |
@ -0,0 +1,10 @@ | |||
# frozen_string_literal: true | |||
class ScopeParser < Parslet::Parser | |||
rule(:term) { match('[a-z]').repeat(1).as(:term) } | |||
rule(:colon) { str(':') } | |||
rule(:access) { (str('write') | str('read')).as(:access) } | |||
rule(:namespace) { str('admin').as(:namespace) } | |||
rule(:scope) { ((namespace >> colon).maybe >> ((access >> colon >> term) | access | term)).as(:scope) } | |||
root(:scope) | |||
end |
@ -0,0 +1,40 @@ | |||
# frozen_string_literal: true | |||
class ScopeTransformer < Parslet::Transform | |||
class Scope | |||
DEFAULT_TERM = 'all' | |||
DEFAULT_ACCESS = %w(read write).freeze | |||
attr_reader :namespace, :term | |||
def initialize(scope) | |||
@namespace = scope[:namespace]&.to_s | |||
@access = scope[:access] ? [scope[:access].to_s] : DEFAULT_ACCESS.dup | |||
@term = scope[:term]&.to_s || DEFAULT_TERM | |||
end | |||
def key | |||
@key ||= [@namespace, @term].compact.join('/') | |||
end | |||
def access | |||
@access.join('/') | |||
end | |||
def merge(other_scope) | |||
clone.merge!(other_scope) | |||
end | |||
def merge!(other_scope) | |||
raise ArgumentError unless other_scope.namespace == namespace && other_scope.term == term | |||
@access.concat(other_scope.instance_variable_get('@access')) | |||
@access.uniq! | |||
@access.sort! | |||
self | |||
end | |||
end | |||
rule(scope: subtree(:scope)) { Scope.new(scope) } | |||
end |
@ -1,26 +1,38 @@ | |||
- content_for :page_title do | |||
= t('doorkeeper.authorizations.new.title') | |||
.form-container | |||
.form-container.simple_form | |||
.oauth-prompt | |||
%h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name) | |||
%h3= t('doorkeeper.authorizations.new.title') | |||
%p | |||
= t('doorkeeper.authorizations.new.able_to') | |||
!= @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>" }.to_sentence | |||
%p= t('doorkeeper.authorizations.new.prompt_html', client_name: content_tag(:strong, @pre_auth.client.name)) | |||
= 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 | |||
%h3= t('doorkeeper.authorizations.new.review_permissions') | |||
= 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' | |||
%ul.permissions-list | |||
- grouped_scopes(@pre_auth.scopes).each do |scope| | |||
%li.permissions-list__item | |||
.permissions-list__item__icon | |||
= fa_icon('check') | |||
.permissions-list__item__text | |||
.permissions-list__item__text__title | |||
= t(scope.key, scope: [:doorkeeper, :grouped_scopes, :title]) | |||
.permissions-list__item__text__type | |||
= t(scope.access, scope: [:doorkeeper, :grouped_scopes, :access]) | |||
.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' |
@ -1,24 +1,44 @@ | |||
- content_for :page_title do | |||
= t('doorkeeper.authorized_applications.index.title') | |||
.table-wrapper | |||
%table.table | |||
%thead | |||
%tr | |||
%th= t('doorkeeper.authorized_applications.index.application') | |||
%th= t('doorkeeper.authorized_applications.index.scopes') | |||
%th= t('doorkeeper.authorized_applications.index.created_at') | |||
%th | |||
%tbody | |||
- @applications.each do |application| | |||
%tr | |||
%td | |||
- if application.website.blank? | |||
= application.name | |||
- else | |||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer' | |||
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ') | |||
%td= l application.created_at | |||
%td | |||
- unless application.superapp? || current_account.suspended? | |||
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } | |||
%p= t('doorkeeper.authorized_applications.index.description_html') | |||
%hr.spacer/ | |||
.announcements-list | |||
- @applications.each do |application| | |||
.announcements-list__item | |||
- if application.website.present? | |||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer', class: 'announcements-list__item__title' | |||
- else | |||
%strong.announcements-list__item__title | |||
= application.name | |||
- if application.superapp? | |||
%span.account-role.moderator= t('doorkeeper.authorized_applications.index.superapp') | |||
.announcements-list__item__action-bar | |||
.announcements-list__item__meta | |||
- if application.most_recently_used_access_token | |||
= t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date)) | |||
- else | |||
= t('doorkeeper.authorized_applications.index.never_used') | |||
• | |||
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date)) | |||
- unless application.superapp? || current_account.suspended? | |||
%div | |||
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } | |||
.announcements-list__item__permissions | |||
%ul.permissions-list | |||
- grouped_scopes(application.scopes).each do |scope| | |||
%li.permissions-list__item | |||
.permissions-list__item__icon | |||
= fa_icon('check') | |||
.permissions-list__item__text | |||
.permissions-list__item__text__title | |||
= t(scope.key, scope: [:doorkeeper, :grouped_scopes, :title]) | |||
.permissions-list__item__text__type | |||
= t(scope.access, scope: [:doorkeeper, :grouped_scopes, :access]) |
@ -0,0 +1,6 @@ | |||
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1] | |||
def change | |||
add_column :oauth_access_tokens, :last_used_at, :datetime | |||
add_column :oauth_access_tokens, :last_used_ip, :inet | |||
end | |||
end |
@ -0,0 +1,89 @@ | |||
# frozen_string_literal: true | |||
require 'rails_helper' | |||
describe ScopeTransformer do | |||
describe '#apply' do | |||
subject { described_class.new.apply(ScopeParser.new.parse(input)) } | |||
shared_examples 'a scope' do |namespace, term, access| | |||
it 'parses the term' do | |||
expect(subject.term).to eq term | |||
end | |||
it 'parses the namespace' do | |||
expect(subject.namespace).to eq namespace | |||
end | |||
it 'parses the access' do | |||
expect(subject.access).to eq access | |||
end | |||
end | |||
context 'for scope "read"' do | |||
let(:input) { 'read' } | |||
it_behaves_like 'a scope', nil, 'all', 'read' | |||
end | |||
context 'for scope "write"' do | |||
let(:input) { 'write' } | |||
it_behaves_like 'a scope', nil, 'all', 'write' | |||
end | |||
context 'for scope "follow"' do | |||
let(:input) { 'follow' } | |||
it_behaves_like 'a scope', nil, 'follow', 'read/write' | |||
end | |||
context 'for scope "crypto"' do | |||
let(:input) { 'crypto' } | |||
it_behaves_like 'a scope', nil, 'crypto', 'read/write' | |||
end | |||
context 'for scope "push"' do | |||
let(:input) { 'push' } | |||
it_behaves_like 'a scope', nil, 'push', 'read/write' | |||
end | |||
context 'for scope "admin:read"' do | |||
let(:input) { 'admin:read' } | |||
it_behaves_like 'a scope', 'admin', 'all', 'read' | |||
end | |||
context 'for scope "admin:write"' do | |||
let(:input) { 'admin:write' } | |||
it_behaves_like 'a scope', 'admin', 'all', 'write' | |||
end | |||
context 'for scope "admin:read:accounts"' do | |||
let(:input) { 'admin:read:accounts' } | |||
it_behaves_like 'a scope', 'admin', 'accounts', 'read' | |||
end | |||
context 'for scope "admin:write:accounts"' do | |||
let(:input) { 'admin:write:accounts' } | |||
it_behaves_like 'a scope', 'admin', 'accounts', 'write' | |||
end | |||
context 'for scope "read:accounts"' do | |||
let(:input) { 'read:accounts' } | |||
it_behaves_like 'a scope', nil, 'accounts', 'read' | |||
end | |||
context 'for scope "write:accounts"' do | |||
let(:input) { 'write:accounts' } | |||
it_behaves_like 'a scope', nil, 'accounts', 'write' | |||
end | |||
end | |||
end |