Change authorized applications page (#17656)
* Change authorized applications page * Hide revoke button for superapps and suspended accounts * Clean up db/schema.rb
This commit is contained in:
		| @@ -5,6 +5,7 @@ class Api::BaseController < ApplicationController | ||||
|   DEFAULT_ACCOUNTS_LIMIT = 40 | ||||
|  | ||||
|   include RateLimitHeaders | ||||
|   include AccessTokenTrackingConcern | ||||
|  | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :require_functional!, unless: :whitelist_mode? | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/controllers/concerns/access_token_tracking_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/controllers/concerns/access_token_tracking_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -3,7 +3,7 @@ | ||||
| module SessionTrackingConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   UPDATE_SIGN_IN_HOURS = 24 | ||||
|   SESSION_UPDATE_FREQUENCY = 24.hours.freeze | ||||
|  | ||||
|   included do | ||||
|     before_action :set_session_activity | ||||
| @@ -17,6 +17,6 @@ module SessionTrackingConcern | ||||
|   end | ||||
|  | ||||
|   def session_needs_update? | ||||
|     !current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago | ||||
|     !current_session.nil? && current_session.updated_at < SESSION_UPDATE_FREQUENCY.ago | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| module UserTrackingConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze | ||||
|   SIGN_IN_UPDATE_FREQUENCY = 24.hours.freeze | ||||
|  | ||||
|   included do | ||||
|     before_action :update_user_sign_in | ||||
| @@ -16,6 +16,6 @@ module UserTrackingConcern | ||||
|   end | ||||
|  | ||||
|   def user_needs_sign_in_update? | ||||
|     user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago) | ||||
|     user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < SIGN_IN_UPDATE_FREQUENCY.ago) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -224,4 +224,19 @@ module ApplicationHelper | ||||
|     content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') | ||||
|     # rubocop:enable Rails/OutputSafety | ||||
|   end | ||||
|  | ||||
|   def grouped_scopes(scopes) | ||||
|     scope_parser      = ScopeParser.new | ||||
|     scope_transformer = ScopeTransformer.new | ||||
|  | ||||
|     scopes.each_with_object({}) do |str, h| | ||||
|       scope = scope_transformer.apply(scope_parser.parse(str)) | ||||
|  | ||||
|       if h[scope.key] | ||||
|         h[scope.key].merge!(scope) | ||||
|       else | ||||
|         h[scope.key] = scope | ||||
|       end | ||||
|     end.values | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -907,6 +907,12 @@ a.name-tag, | ||||
|       text-decoration: none; | ||||
|       margin-bottom: 10px; | ||||
|  | ||||
|       .account-role { | ||||
|         vertical-align: middle; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     a.announcements-list__item__title { | ||||
|       &:hover, | ||||
|       &:focus, | ||||
|       &:active { | ||||
| @@ -925,6 +931,10 @@ a.name-tag, | ||||
|       align-items: center; | ||||
|     } | ||||
|  | ||||
|     &__permissions { | ||||
|       margin-top: 10px; | ||||
|     } | ||||
|  | ||||
|     &:last-child { | ||||
|       border-bottom: 0; | ||||
|     } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| .container-alt { | ||||
|   width: 700px; | ||||
|   margin: 0 auto; | ||||
|   margin-top: 40px; | ||||
|  | ||||
|   @media screen and (max-width: 740px) { | ||||
|     width: 100%; | ||||
| @@ -67,22 +66,20 @@ | ||||
|   line-height: 18px; | ||||
|   box-sizing: border-box; | ||||
|   padding: 20px 0; | ||||
|   padding-bottom: 0; | ||||
|   margin-bottom: -30px; | ||||
|   margin-top: 40px; | ||||
|   margin-bottom: 10px; | ||||
|   border-bottom: 1px solid $ui-base-color; | ||||
|  | ||||
|   @media screen and (max-width: 440px) { | ||||
|     width: 100%; | ||||
|     margin: 0; | ||||
|     margin-bottom: 10px; | ||||
|     padding: 20px; | ||||
|     padding-bottom: 0; | ||||
|   } | ||||
|  | ||||
|   .avatar { | ||||
|     width: 40px; | ||||
|     height: 40px; | ||||
|     margin-right: 8px; | ||||
|     margin-right: 10px; | ||||
|  | ||||
|     img { | ||||
|       width: 100%; | ||||
| @@ -96,7 +93,7 @@ | ||||
|   .name { | ||||
|     flex: 1 1 auto; | ||||
|     color: $secondary-text-color; | ||||
|     width: calc(100% - 88px); | ||||
|     width: calc(100% - 90px); | ||||
|  | ||||
|     .username { | ||||
|       display: block; | ||||
| @@ -110,7 +107,7 @@ | ||||
|     display: block; | ||||
|     font-size: 32px; | ||||
|     line-height: 40px; | ||||
|     margin-left: 8px; | ||||
|     margin-left: 10px; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -800,9 +800,41 @@ code { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   @media screen and (max-width: 740px) and (min-width: 441px) { | ||||
|     margin-top: 40px; | ||||
| .oauth-prompt { | ||||
|   h3 { | ||||
|     color: $ui-secondary-color; | ||||
|     font-size: 17px; | ||||
|     line-height: 22px; | ||||
|     font-weight: 500; | ||||
|     margin-bottom: 30px; | ||||
|   } | ||||
|  | ||||
|   p { | ||||
|     font-size: 14px; | ||||
|     line-height: 18px; | ||||
|     margin-bottom: 30px; | ||||
|   } | ||||
|  | ||||
|   .permissions-list { | ||||
|     border: 1px solid $ui-base-color; | ||||
|     border-radius: 4px; | ||||
|     background: darken($ui-base-color, 4%); | ||||
|     margin-bottom: 30px; | ||||
|   } | ||||
|  | ||||
|   .actions { | ||||
|     margin: 0 -10px; | ||||
|     display: flex; | ||||
|  | ||||
|     form { | ||||
|       box-sizing: border-box; | ||||
|       padding: 0 10px; | ||||
|       flex: 1 1 auto; | ||||
|       min-height: 1px; | ||||
|       width: 50%; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1005,3 +1037,38 @@ code { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .permissions-list { | ||||
|   &__item { | ||||
|     padding: 15px; | ||||
|     color: $ui-secondary-color; | ||||
|     border-bottom: 1px solid lighten($ui-base-color, 4%); | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|  | ||||
|     &__text { | ||||
|       flex: 1 1 auto; | ||||
|  | ||||
|       &__title { | ||||
|         font-weight: 500; | ||||
|       } | ||||
|  | ||||
|       &__type { | ||||
|         color: $darker-text-color; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__icon { | ||||
|       flex: 0 0 auto; | ||||
|       font-size: 18px; | ||||
|       width: 30px; | ||||
|       color: $valid-value-color; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|     } | ||||
|  | ||||
|     &:last-child { | ||||
|       border-bottom: 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,10 @@ module AccessTokenExtension | ||||
|     update(revoked_at: clock.now.utc) | ||||
|   end | ||||
|  | ||||
|   def update_last_used(request, clock = Time) | ||||
|     update(last_used_at: clock.now.utc, last_used_ip: request.remote_ip) | ||||
|   end | ||||
|  | ||||
|   def push_to_streaming_api | ||||
|     Redis.current.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed? | ||||
|   end | ||||
|   | ||||
| @@ -8,4 +8,8 @@ module ApplicationExtension | ||||
|     validates :website, url: true, length: { maximum: 2_000 }, if: :website? | ||||
|     validates :redirect_uri, length: { maximum: 2_000 } | ||||
|   end | ||||
|  | ||||
|   def most_recently_used_access_token | ||||
|     @most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										10
									
								
								app/lib/scope_parser.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/lib/scope_parser.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										40
									
								
								app/lib/scope_transformer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/lib/scope_transformer.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -12,8 +12,9 @@ | ||||
|         = fa_icon 'sign-out' | ||||
|  | ||||
|   .container-alt= yield | ||||
|  | ||||
|   .modal-layout__mastodon | ||||
|     %div | ||||
|       %img{alt:'', draggable:'false', src:"#{mascot_url}"} | ||||
|       %img{alt: '', draggable: 'false', src: mascot_url } | ||||
|  | ||||
| = render template: 'layouts/application' | ||||
|   | ||||
| @@ -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]) | ||||
|   | ||||
| @@ -18,6 +18,7 @@ class Scheduler::IpCleanupScheduler | ||||
|     SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all | ||||
|     User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(sign_up_ip: nil) | ||||
|     LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all | ||||
|     Doorkeeper::AccessToken.where('last_used_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_used_ip: nil) | ||||
|   end | ||||
|  | ||||
|   def clean_expired_ip_blocks! | ||||
|   | ||||
| @@ -60,8 +60,8 @@ en: | ||||
|       error: | ||||
|         title: An error has occurred | ||||
|       new: | ||||
|         able_to: It will be able to | ||||
|         prompt: Application %{client_name} requests access to your account | ||||
|         prompt_html: "%{client_name} would like permission to access your account. It is a third-party application. <strong>If you do not trust it, then you should not authorize it.</strong>" | ||||
|         review_permissions: Review permissions | ||||
|         title: Authorization required | ||||
|       show: | ||||
|         title: Copy this authorization code and paste it to the application. | ||||
| @@ -71,10 +71,12 @@ en: | ||||
|       confirmations: | ||||
|         revoke: Are you sure? | ||||
|       index: | ||||
|         application: Application | ||||
|         created_at: Authorized | ||||
|         date_format: "%Y-%m-%d %H:%M:%S" | ||||
|         scopes: Scopes | ||||
|         authorized_at: Authorized on %{date} | ||||
|         description_html: These are applications that can access your account using the API. If there are applications you do not recognize here, or an application is misbehaving, you can revoke its access. | ||||
|         last_used_at: Last used on %{date} | ||||
|         never_used: Never used | ||||
|         scopes: Permissions | ||||
|         superapp: Internal | ||||
|         title: Your authorized applications | ||||
|     errors: | ||||
|       messages: | ||||
| @@ -110,6 +112,33 @@ en: | ||||
|       authorized_applications: | ||||
|         destroy: | ||||
|           notice: Application revoked. | ||||
|     grouped_scopes: | ||||
|       access: | ||||
|         read: Read-only access | ||||
|         read/write: Read and write access | ||||
|         write: Write-only access | ||||
|       title: | ||||
|         accounts: Accounts | ||||
|         admin/accounts: Administration of accounts | ||||
|         admin/all: All administrative functions | ||||
|         admin/reports: Administration of reports | ||||
|         all: Everything | ||||
|         blocks: Blocks | ||||
|         bookmarks: Bookmarks | ||||
|         conversations: Conversations | ||||
|         crypto: End-to-end encryption | ||||
|         favourites: Favourites | ||||
|         filters: Filters | ||||
|         follow: Relationships | ||||
|         follows: Follows | ||||
|         lists: Lists | ||||
|         media: Media attachments | ||||
|         mutes: Mutes | ||||
|         notifications: Notifications | ||||
|         push: Push notifications | ||||
|         reports: Reports | ||||
|         search: Search | ||||
|         statuses: Posts | ||||
|     layouts: | ||||
|       admin: | ||||
|         nav: | ||||
| @@ -124,6 +153,7 @@ en: | ||||
|       admin:write: modify all data on the server | ||||
|       admin:write:accounts: perform moderation actions on accounts | ||||
|       admin:write:reports: perform moderation actions on reports | ||||
|       crypto: use end-to-end encryption | ||||
|       follow: modify account relationships | ||||
|       push: receive your push notifications | ||||
|       read: read all your account's data | ||||
| @@ -143,6 +173,7 @@ en: | ||||
|       write:accounts: modify your profile | ||||
|       write:blocks: block accounts and domains | ||||
|       write:bookmarks: bookmark posts | ||||
|       write:conversations: mute and delete conversations | ||||
|       write:favourites: favourite posts | ||||
|       write:filters: create filters | ||||
|       write:follows: follow people | ||||
|   | ||||
| @@ -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 | ||||
| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 2022_02_24_010024) do | ||||
| ActiveRecord::Schema.define(version: 2022_02_27_041951) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @@ -630,6 +630,8 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do | ||||
|     t.string "scopes" | ||||
|     t.bigint "application_id" | ||||
|     t.bigint "resource_owner_id" | ||||
|     t.datetime "last_used_at" | ||||
|     t.inet "last_used_ip" | ||||
|     t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true | ||||
|     t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" | ||||
|     t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true | ||||
|   | ||||
							
								
								
									
										89
									
								
								spec/lib/scope_transformer_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								spec/lib/scope_transformer_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
		Reference in New Issue
	
	Block a user