diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 937cd5502..bdc7575e0 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,9 +1,18 @@ class Api::BaseController < ApplicationController include AccessTokenAuthHelper + # POST-shaped endpoints that are semantically reads and should be reachable + # with a read-only token. Keep this list minimal; expand only when a clear + # read-with-complex-body use case appears. + READ_ONLY_POST_ALLOWLIST = %w[ + api/v1/accounts/conversations#filter + api/v1/accounts/contacts#filter + ].freeze + respond_to :json before_action :authenticate_access_token!, if: :authenticate_by_access_token? before_action :validate_bot_access_token!, if: :authenticate_by_access_token? before_action :authenticate_user!, unless: :authenticate_by_access_token? + before_action :enforce_read_only_token_scope private @@ -11,6 +20,15 @@ class Api::BaseController < ApplicationController request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present? end + def enforce_read_only_token_scope + return unless @access_token&.scope == 'read_only' + return if request.get? || request.head? || request.options? + return if READ_ONLY_POST_ALLOWLIST.include?("#{params[:controller]}##{params[:action]}") + + render json: { error: 'This access token is read-only and cannot perform write operations.' }, + status: :forbidden + end + def check_authorization(model = nil) model ||= controller_name.classify.constantize diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 141253d0d..0650b59a4 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -43,6 +43,12 @@ class Api::V1::ProfilesController < Api::BaseController @user.reload end + def reset_read_only_access_token + token = @user.read_only_access_token || AccessToken.create!(owner: @user, scope: 'read_only') + token.regenerate_token + @user.reload + end + private def set_user diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder index e856031c5..11ff6431e 100644 --- a/app/views/api/v1/models/_user.json.jbuilder +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -1,4 +1,5 @@ -json.access_token resource.access_token.token +json.access_token resource.access_token&.token +json.read_only_access_token resource.read_only_access_token&.token json.account_id resource.active_account_user&.account_id json.available_name resource.available_name json.avatar_url resource.avatar_url diff --git a/app/views/api/v1/profiles/reset_read_only_access_token.json.jbuilder b/app/views/api/v1/profiles/reset_read_only_access_token.json.jbuilder new file mode 100644 index 000000000..0a4b4f9fa --- /dev/null +++ b/app/views/api/v1/profiles/reset_read_only_access_token.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/user', formats: [:json], resource: @user diff --git a/config/routes.rb b/config/routes.rb index 355491d5b..ff3d13ffc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -422,6 +422,7 @@ Rails.application.routes.draw do put :set_active_account post :resend_confirmation post :reset_access_token + post :reset_read_only_access_token end # MFA routes