Ctrl K
ComunidadGuías…

test/integration/oauth_connected_apps_preview_test.rb

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
require "test_helper"

# While connecting apps is in preview (Oauth::Availability), only previewers,
# 37signals staff and the reviewers listed by identity id, can connect an app
# or use what they connected, whichever client asks.
class OauthConnectedAppsPreviewTest < ActionDispatch::IntegrationTest
  include OauthAvailabilityTestHelper

  CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

  test "staff see the consent screen and connect" do
    with_oauth_availability preview: true do
      sign_in_as :david
      get_consent_screen
      assert_response :success
      assert_select "form[action$=?]", "/oauth/authorization"

      post_consent
      assert_response :redirect
      assert_match %r{\Ahttp://127\.0\.0\.1:8888/callback\?code=}, response.location
    end
  end

  test "anyone else is told connecting apps is in preview, and gets no code" do
    with_oauth_availability preview: true do
      sign_in_as :jz
      get_consent_screen
      assert_response :forbidden
      assert_match "Connecting apps to Fizzy is in preview", response.body

      post_consent
      assert_response :forbidden
      assert_nil response.location
    end
  end

  test "anyone else can still decline" do
    with_oauth_availability preview: true do
      sign_in_as :jz
      post_consent error: "access_denied"

      assert_response :redirect
      assert_match "error=access_denied", response.location
    end
  end

  test "everyone connects when it's off" do
    sign_in_as :jz
    get_consent_screen

    assert_response :success
  end

  test "a code issued to someone no longer staff mints nothing" do
    code = authorization_code_for(identities(:david))
    identities(:david).update!(staff: false)

    with_oauth_availability preview: true do
      redeem code
    end

    assert_response :bad_request
    assert_equal "invalid_grant", response.parsed_body["error"]
  end

  test "a code replayed once its owner isn't staff still revokes the grant it minted" do
    code = authorization_code_for(identities(:david))

    with_oauth_availability preview: true do
      redeem code
      assert_response :success
      grant = Identity::AccessToken.find_by_raw_token(response.parsed_body["access_token"])

      identities(:david).update!(staff: false)
      redeem code

      assert_response :bad_request
      assert_equal "invalid_grant", response.parsed_body["error"]
      assert_not Identity::AccessToken.exists?(grant.id), "a replay revokes the grant whoever owns it"
    end
  end

  test "anyone else asking for consent as JSON is refused as JSON" do
    with_oauth_availability preview: true do
      sign_in_as :jz
      untenanted { get new_oauth_authorization_path(format: :json), params: consent_params }

      assert_response :forbidden
      assert_equal "access_denied", response.parsed_body["error"]
      assert_equal "Connecting apps to Fizzy is in preview for now", response.parsed_body["error_description"]
    end
  end

  test "a staff code redeems" do
    with_oauth_availability preview: true do
      redeem authorization_code_for(identities(:david))
    end

    assert_response :success
  end

  test "a grant stops working, and refreshing, once its owner isn't staff" do
    grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))

    with_oauth_availability preview: true do
      get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
      assert_response :success

      identities(:david).update!(staff: false)

      get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
      assert_response :unauthorized
      assert_match 'error="invalid_token"', response.headers["WWW-Authenticate"]

      refresh grant
      assert_response :bad_request
      assert_equal "invalid_grant", response.parsed_body["error"]
    end

    assert grant.reload.persisted?, "a refused refresh leaves the grant to its owner to revoke"
  end

  test "a replayed refresh token still revokes the grant once its owner isn't staff" do
    grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))
    presented = grant.raw_refresh_token

    with_oauth_availability preview: true do
      refresh grant
      assert_response :success

      identities(:david).update!(staff: false)
      travel Oauth::RetiredRefreshToken::GRACE + 1.second

      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: presented, client_id: oauth_clients(:mcp_client).client_id }, as: :json
      end
      assert_response :bad_request
    end

    assert_not Identity::AccessToken.exists?(grant.id), "a replay revokes the grant whoever owns it"
  end

  test "a staff grant refreshes" do
    grant = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))

    with_oauth_availability preview: true do
      refresh grant
    end

    assert_response :success
  end

  test "a listed reviewer sees the consent screen and connects, like staff" do
    with_oauth_availability preview: true, reviewer_ids: [ identities(:jz).id ] do
      sign_in_as :jz
      get_consent_screen
      assert_response :success

      post_consent
      assert_response :redirect
      assert_match %r{\Ahttp://127\.0\.0\.1:8888/callback\?code=}, response.location
    end
  end

  test "a listed reviewer's code redeems, and the grant works and refreshes, until they're delisted" do
    code = authorization_code_for(identities(:jz))

    with_oauth_availability preview: true, reviewer_ids: [ identities(:jz).id ] do
      redeem code
      assert_response :success
      access_token, refresh_token = response.parsed_body.values_at("access_token", "refresh_token")

      get user_path(users(:jz)), env: { "HTTP_AUTHORIZATION" => "Bearer #{access_token}" }, as: :json
      assert_response :success

      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: refresh_token, client_id: oauth_clients(:mcp_client).client_id }, as: :json
      end
      assert_response :success
    end

    grant = identities(:jz).access_tokens.create!(oauth_client: oauth_clients(:mcp_client))

    with_oauth_availability preview: true, reviewer_ids: [ identities(:mike).id ] do
      get user_path(users(:jz)), env: { "HTTP_AUTHORIZATION" => "Bearer #{grant.raw_token}" }, as: :json
      assert_response :unauthorized

      refresh grant
      assert_response :bad_request
      assert_equal "invalid_grant", response.parsed_body["error"]

      redeem authorization_code_for(identities(:jz))
      assert_response :bad_request
      assert_equal "invalid_grant", response.parsed_body["error"]

      sign_in_as :jz
      get_consent_screen
      assert_response :forbidden
    end
  end

  test "a reviewer list naming someone else admits no one beyond staff" do
    with_oauth_availability preview: true, reviewer_ids: [ identities(:mike).id ] do
      sign_in_as :jz
      get_consent_screen
      assert_response :forbidden
    end
  end

  test "personal access tokens work for anyone" do
    identities(:david).update!(staff: false)

    with_oauth_availability preview: true do
      get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" }, as: :json
    end

    assert_response :success
  end

  test "personal access tokens issued by the first-party client work for anyone" do
    token = identities(:david).access_tokens.create!(oauth_client: oauth_clients(:personal_access), permission: :read, description: "Script")
    identities(:david).update!(staff: false)

    with_oauth_availability preview: true do
      get user_path(users(:david)), env: { "HTTP_AUTHORIZATION" => "Bearer #{token.raw_token}" }, as: :json
    end

    assert_response :success
  end

  private
    def get_consent_screen(**params)
      untenanted { get new_oauth_authorization_path, params: consent_params(**params) }
    end

    def post_consent(**params)
      untenanted { post oauth_authorization_path, params: consent_params(**params) }
    end

    def consent_params(**overrides)
      {
        client_id: oauth_clients(:mcp_client).client_id,
        redirect_uri: "http://127.0.0.1:8888/callback",
        response_type: "code",
        code_challenge: code_challenge,
        code_challenge_method: "S256",
        scope: "read",
        state: "xyz123"
      }.merge(overrides)
    end

    def code_challenge
      Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false)
    end

    def authorization_code_for(identity)
      Oauth::AuthorizationCode.generate \
        client_id: oauth_clients(:mcp_client).client_id,
        identity_id: identity.id,
        code_challenge: code_challenge,
        redirect_uri: "http://127.0.0.1:8888/callback",
        scope: "read"
    end

    def redeem(code)
      untenanted do
        post oauth_token_path, params: {
          grant_type: "authorization_code",
          client_id: oauth_clients(:mcp_client).client_id,
          code: code,
          redirect_uri: "http://127.0.0.1:8888/callback",
          code_verifier: CODE_VERIFIER
        }, as: :json
      end
    end

    def refresh(grant)
      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: grant.raw_refresh_token, client_id: oauth_clients(:mcp_client).client_id }, as: :json
      end
    end
end