mirror of https://github.com/mastodon/mastodon
Change authorized applications page (#17656)
* Change authorized applications page * Hide revoke button for superapps and suspended accounts * Clean up db/schema.rbpull/17682/head
parent
233f7e6174
commit
50ea54b3ed
@ -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,24 +1,44 @@
|
|||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('doorkeeper.authorized_applications.index.title')
|
= t('doorkeeper.authorized_applications.index.title')
|
||||||
|
|
||||||
.table-wrapper
|
%p= t('doorkeeper.authorized_applications.index.description_html')
|
||||||
%table.table
|
|
||||||
%thead
|
%hr.spacer/
|
||||||
%tr
|
|
||||||
%th= t('doorkeeper.authorized_applications.index.application')
|
.announcements-list
|
||||||
%th= t('doorkeeper.authorized_applications.index.scopes')
|
|
||||||
%th= t('doorkeeper.authorized_applications.index.created_at')
|
|
||||||
%th
|
|
||||||
%tbody
|
|
||||||
- @applications.each do |application|
|
- @applications.each do |application|
|
||||||
%tr
|
.announcements-list__item
|
||||||
%td
|
- if application.website.present?
|
||||||
- if application.website.blank?
|
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer', class: 'announcements-list__item__title'
|
||||||
|
- else
|
||||||
|
%strong.announcements-list__item__title
|
||||||
= application.name
|
= 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
|
- else
|
||||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
|
= t('doorkeeper.authorized_applications.index.never_used')
|
||||||
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
|
|
||||||
%td= l application.created_at
|
•
|
||||||
%td
|
|
||||||
|
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
|
||||||
|
|
||||||
- unless application.superapp? || current_account.suspended?
|
- 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') }
|
= 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
|
Loading…
Reference in New Issue