config_preview_deploy-1.0.0-alpha3/tests/src/Kernel/SecurityEdgeCaseTest.php

tests/src/Kernel/SecurityEdgeCaseTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\config_preview_deploy\Kernel;

use Drupal\config_preview_deploy\Controller\ProductionController;
use Drupal\consumers\Entity\Consumer;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\simple_oauth\Entity\Oauth2Token;
use Drupal\user\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Tests security edge cases and attack vectors.
 *
 * @group config_preview_deploy
 */
class SecurityEdgeCaseTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'config_preview_deploy',
    'simple_oauth',
    'simple_oauth_static_scope',
    'consumers',
    'key',
    'system',
    'user',
    'serialization',
    'options',
    'file',
    'image',
  ];

  /**
   * Test user.
   */
  protected User $testUser;

  /**
   * OAuth consumer.
   */
  protected Consumer $consumer;

  /**
   * Production controller.
   */
  protected ProductionController $productionController;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Set HTTP_HOST for environment detection.
    $_SERVER['HTTP_HOST'] = 'test.example.com';

    $this->installEntitySchema('user');
    $this->installEntitySchema('consumer');
    $this->installEntitySchema('oauth2_token');
    $this->installEntitySchema('oauth2_scope');
    $this->installEntitySchema('oauth2_token_type');
    $this->installEntitySchema('key');
    $this->installConfig([
      'system',
      'user',
      'simple_oauth',
      'consumers',
      'config_preview_deploy',
    ]);

    // Create temporary directory for OAuth keys.
    $keysDir = sys_get_temp_dir() . '/oauth_keys_' . uniqid();
    mkdir($keysDir, 0777, TRUE);

    // Generate test keys.
    $this->generateTestKeys($keysDir);

    // Configure simple_oauth to use our test keys.
    $this->config('simple_oauth.settings')
      ->set('public_key', $keysDir . '/public.key')
      ->set('private_key', $keysDir . '/private.key')
      ->save();

    // Create test user with deployment permissions.
    $this->testUser = User::create([
      'name' => 'deploy_user',
      'mail' => 'deploy@example.com',
      'status' => 1,
    ]);
    $this->testUser->save();

    // Grant the user the required permission.
    user_role_grant_permissions('authenticated', ['accept config deployments']);

    // Run module install to create OAuth consumer.
    \Drupal::moduleHandler()->loadInclude('config_preview_deploy', 'install');
    config_preview_deploy_install();

    // Load the created consumer.
    $consumers = \Drupal::entityTypeManager()
      ->getStorage('consumer')
      ->loadByProperties(['client_id' => 'config_preview_deploy']);
    $this->consumer = reset($consumers);

    // Create controller instance.
    $this->productionController = ProductionController::create($this->container);
  }

  /**
   * Helper to issue a request and return the response.
   */
  protected function request(string $path, string $method = 'GET', array $query = [], array $headers = [], string $content = ''): Response {
    $request = Request::create($path, $method, $query, [], [], [], $content);

    // Add headers.
    foreach ($headers as $name => $value) {
      $request->headers->set($name, $value);
    }

    return $this->container->get('http_kernel')->handle($request);
  }

  /**
   * Generate test RSA key pair for OAuth.
   */
  protected function generateTestKeys(string $keysDir): void {
    // Generate private key.
    $private_key = openssl_pkey_new([
      'digest_alg' => 'sha256',
      'private_key_bits' => 2048,
      'private_key_type' => OPENSSL_KEYTYPE_RSA,
    ]);

    // Export private key.
    openssl_pkey_export_to_file($private_key, $keysDir . '/private.key');

    // Export public key.
    $public_key_details = openssl_pkey_get_details($private_key);
    file_put_contents($keysDir . '/public.key', $public_key_details['key']);
  }

  /**
   * Tests protection against token replay attacks.
   */
  public function testTokenReplayAttackProtection(): void {
    // Create a valid token with valid timestamp.
    $timestamp = time();
    $hashVerification = $this->container->get('config_preview_deploy.hash_verification');
    // Get the host from Drupal's base URL to match verification logic.
    $currentUrl = Url::fromRoute('system.admin', [], ['absolute' => TRUE])->toString();
    $productionHost = parse_url($currentUrl, PHP_URL_HOST);
    $token = $hashVerification->generateVerificationHash($productionHost, $timestamp);

    $request = Request::create('/api/config-preview-deploy/status', 'GET', [
      'timestamp' => $timestamp,
      'token' => $token,
    ]);

    // First request should succeed.
    $response = $this->productionController->getStatus($request);
    $this->assertEquals(200, $response->getStatusCode());

    // Try to replay the same token with an old timestamp.
    // More than 5 minutes old.
    $old_timestamp = $timestamp - 400;
    $old_token = $hashVerification->generateVerificationHash($productionHost, $old_timestamp);

    $replay_request = Request::create('/api/config-preview-deploy/status', 'GET', [
      'timestamp' => $old_timestamp,
      'token' => $old_token,
    ]);

    // Replay attack should be blocked.
    $response = $this->productionController->getStatus($replay_request);
    $this->assertEquals(403, $response->getStatusCode());

    $data = json_decode($response->getContent(), TRUE);
    $this->assertEquals('Invalid verification hash', $data['error']);
  }

  /**
   * Tests OAuth token scope restriction.
   */
  public function testOauthTokenScopeRestriction(): void {
    // Create OAuth token with wrong scope.
    $wrong_scope_token = Oauth2Token::create([
      'bundle' => 'access_token',
      'auth_user_id' => $this->testUser->id(),
      'client' => $this->consumer->id(),
      'scopes' => [['value' => 'wrong_scope']],
      'value' => 'wrong-scope-token-' . bin2hex(random_bytes(16)),
      'expire' => time() + 3600,
      'resource_owner' => $this->testUser->id(),
    ]);
    $wrong_scope_token->save();

    // Try to use token with wrong scope via HTTP kernel.
    $data = json_encode([
      'diff' => 'test diff content',
      'environment' => 'test',
      'auth_hash' => 'test_hash',
      'timestamp' => time(),
    ]);

    $response = $this->request('/admin/config/development/config-preview-deploy/deploy-endpoint', 'POST', [], [
      'Content-Type' => 'application/json',
      'Authorization' => 'Bearer ' . $wrong_scope_token->get('value')->value,
    ], $data);

    // Should be rejected due to wrong scope (OAuth validation at route level).
    // 401 = invalid token, 403 = valid token but insufficient scope.
    $this->assertTrue(in_array($response->getStatusCode(), [401, 403]),
      'OAuth should reject wrong scope token with 401 or 403, got: ' . $response->getStatusCode() . ' - ' . $response->getContent());
  }

  /**
   * Tests expired OAuth token handling.
   */
  public function testExpiredOauthTokenHandling(): void {
    // Create expired OAuth token.
    $token = Oauth2Token::create([
      'bundle' => 'access_token',
      'auth_user_id' => $this->testUser->id(),
      'client' => $this->consumer->id(),
      'scopes' => [['value' => 'config_preview_deploy']],
      'value' => 'expired-token-' . bin2hex(random_bytes(16)),
    // Expired 1 hour ago.
      'expire' => time() - 3600,
      'resource_owner' => $this->testUser->id(),
    ]);
    $token->save();

    // Try to use expired token via HTTP kernel.
    $data = json_encode([
      'diff' => 'test diff content',
      'environment' => 'test',
      'auth_hash' => 'test_hash',
      'timestamp' => time(),
    ]);

    $response = $this->request('/admin/config/development/config-preview-deploy/deploy-endpoint', 'POST', [], [
      'Content-Type' => 'application/json',
      'Authorization' => 'Bearer ' . $token->get('value')->value,
    ], $data);

    // Should be rejected due to expiration (OAuth validation at route level).
    // 401 = invalid token, 403 = valid token but expired.
    $this->assertTrue(in_array($response->getStatusCode(), [401, 403]),
      'OAuth should reject expired token with 401 or 403, got: ' . $response->getStatusCode());
  }

  /**
   * Tests environment variable security for deployment secrets.
   */
  public function testEnvironmentVariableSecurity(): void {
    // Test that deployment secret key configuration is secure.
    $key_storage = \Drupal::entityTypeManager()->getStorage('key');
    $deployment_key = $key_storage->load('config_deploy_secret');

    $this->assertNotNull($deployment_key, 'Deployment key should exist');

    // Verify key configuration.
    $key_provider = $deployment_key->get('key_provider');
    $key_provider_value = is_object($key_provider) ? $key_provider->value : $key_provider;
    $this->assertTrue(in_array($key_provider_value, ['config', 'env']), 'Key provider should be config or env');

    // Test secret value is not empty.
    $secret = $deployment_key->getKeyValue();
    $this->assertNotEmpty($secret, 'Secret should not be empty');
    $this->assertIsString($secret, 'Secret should be a string');

    // Test secret has sufficient entropy.
    $this->assertGreaterThan(16, strlen($secret), 'Secret should be at least 16 characters');
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc