Skip to content
3 changes: 3 additions & 0 deletions .github/workflows/nightly_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
SSL_ENABLED: false
SESSION_DRIVER: redis
PHP_VERSION: 8.3
FACEBOOK_CLIENT_ID: 214242500242860
FACEBOOK_CLIENT_SECRET: e62fa81aa898699d8cebf14bf5e586aa
FACEBOOK_REDIRECT_URI: /auth/login/facebook/callback
services:
mysql:
image: mysql:8.0
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pull_request_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
PHP_VERSION: 8.3
OTEL_SDK_DISABLED: true
OTEL_SERVICE_ENABLED: false
FACEBOOK_CLIENT_ID: 214242500242860
FACEBOOK_CLIENT_SECRET: e62fa81aa898699d8cebf14bf5e586aa

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the hard-coded Facebook application secret from all CI workflows.

The same credential is committed in three files and is exposed to job steps. Replace each value with ${{ secrets.FACEBOOK_CLIENT_SECRET }} or a non-sensitive test fixture. Rotate the exposed credential before merge.

  • .github/workflows/pull_request_unit_tests.yml#L41-L41: replace the plaintext secret in the pull-request test job.
  • .github/workflows/nightly_unit_tests.yml#L36-L36: replace the plaintext secret in the nightly test job.
  • .github/workflows/push.yml#L37-L37: replace the plaintext secret in the push test job.
🧰 Tools
🪛 Betterleaks (1.7.3)

[high] 41-41: Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure.

(facebook-secret)

📍 Affects 3 files
  • .github/workflows/pull_request_unit_tests.yml#L41-L41 (this comment)
  • .github/workflows/nightly_unit_tests.yml#L36-L36
  • .github/workflows/push.yml#L37-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pull_request_unit_tests.yml at line 41, Remove the
hard-coded Facebook application secret and use the GitHub Actions secret
reference or a non-sensitive test fixture in all affected workflows:
.github/workflows/pull_request_unit_tests.yml lines 41-41,
.github/workflows/nightly_unit_tests.yml lines 36-36, and
.github/workflows/push.yml lines 37-37. Rotate the exposed credential before
merging.

Source: Linters/SAST tools

FACEBOOK_REDIRECT_URI: /auth/login/facebook/callback
services:
mysql:
image: mysql:8.0
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
PHP_VERSION: 8.3
OTEL_SDK_DISABLED: true
OTEL_SERVICE_ENABLED: false
FACEBOOK_CLIENT_ID: 214242500242860
FACEBOOK_CLIENT_SECRET: e62fa81aa898699d8cebf14bf5e586aa
FACEBOOK_REDIRECT_URI: /auth/login/facebook/callback
services:
mysql:
image: mysql:8.0
Expand Down
108 changes: 108 additions & 0 deletions app/Console/Commands/FacebookDataDeletionBackfill.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php namespace App\Console\Commands;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Services\Auth\IFacebookDataDeletionService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

/**
* Class FacebookDataDeletionBackfill
* Processes the "Download User Identifiers" CSV Facebook lets admins export
* from the app dashboard's Advanced Settings, unlinking each app-scoped ID
* from its matching OpenStackID user (same logic as the live callback).
* @package App\Console\Commands
*/
final class FacebookDataDeletionBackfill extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'idp:facebook-data-deletion-backfill';

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'idp:facebook-data-deletion-backfill {path : Absolute path to the Facebook user identifiers CSV}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Process a Facebook "Download User Identifiers" CSV export, unlinking each app-scoped ID from its OpenStackID user.';

/**
* @var IFacebookDataDeletionService
*/
private $service;

/**
* FacebookDataDeletionBackfill constructor.
* @param IFacebookDataDeletionService $service
*/
public function __construct(IFacebookDataDeletionService $service)
{
parent::__construct();
$this->service = $service;
}

/**
* @return int
*/
public function handle()
{
$path = $this->argument('path');

if (!is_readable($path)) {
$this->error(sprintf("File %s is not readable.", $path));
return 1;
}

$matched = 0;
$not_found = 0;
$skipped = 0;

$handle = @fopen($path, 'r');
if ($handle === false) {
$this->error(sprintf("Unable to open file %s.", $path));
return 1;
}

while (($line = fgets($handle)) !== false) {
$external_id = trim($line, " \t\n\r\0\x0B\"'");

if ($external_id === '') {
$skipped++;
continue;
}

$result = $this->service->processDeletionRequest($external_id);

if ($result['status'] === 'completed') {
$matched++;
} else {
$not_found++;
}

Log::debug(sprintf("FacebookDataDeletionBackfill::handle processed request with status %s", $result['status']));
}
fclose($handle);

$this->info(sprintf("Processed CSV: %d matched, %d not found, %d skipped blank lines.", $matched, $not_found, $skipped));
return 0;
}
}
1 change: 1 addition & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Kernel extends ConsoleKernel
Commands\CleanOAuth2StaleData::class,
Commands\CleanOpenIdStaleData::class,
Commands\CreateSuperAdmin::class,
Commands\FacebookDataDeletionBackfill::class,
Commands\SpammerProcess\RebuildUserSpammerEstimator::class,
Commands\SpammerProcess\UserSpammerProcessor::class,
];
Expand Down
91 changes: 91 additions & 0 deletions app/Http/Controllers/Api/FacebookDataDeletionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php namespace App\Http\Controllers\Api;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Http\Controllers\Controller;
use App\libs\Auth\FacebookSignedRequestParser;
use App\Services\Auth\IFacebookDataDeletionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;

/**
* Class FacebookDataDeletionController
* Implements Facebook's Data Deletion Request Callback.
* @see https://developers.facebook.com/documentation/development/create-an-app/app-dashboard/data-deletion-callback
* @package App\Http\Controllers\Api
*/
final class FacebookDataDeletionController extends Controller
{
/**
* @var IFacebookDataDeletionService
*/
private $service;

/**
* FacebookDataDeletionController constructor.
* @param IFacebookDataDeletionService $service
*/
public function __construct(IFacebookDataDeletionService $service)
{
$this->service = $service;
}

/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function handle(Request $request)
{
$signed_request = $request->input('signed_request');

if (!is_string($signed_request) || $signed_request === '') {
Log::warning("FacebookDataDeletionController::handle missing signed_request");
return response()->json([
'error' => ['code' => 'invalid_request', 'message' => 'signed_request is required.']
], 400);
}

$secret = Config::get('services.facebook.client_secret');
$data = FacebookSignedRequestParser::parse($signed_request, $secret);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Comment on lines +58 to +60
if (is_null($data)) {
Log::warning("FacebookDataDeletionController::handle invalid signed_request");
return response()->json([
'error' => ['code' => 'invalid_signature', 'message' => 'Invalid signed_request.']
], 400);
}

$result = $this->service->processDeletionRequest((string)$data['user_id']);

return response()->json([
'url' => $result['url'],
'confirmation_code' => $result['confirmation_code'],
], 200);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* @param Request $request
* @param string $confirmation_code
* @return \Illuminate\Contracts\View\View
*/
public function status(Request $request, string $confirmation_code)
{
$result = $this->service->getStatus($confirmation_code);

if (is_null($result)) {
abort(404);
}

return view('auth.facebook_data_deletion_status', ['result' => $result]);
}
}
18 changes: 18 additions & 0 deletions app/Repositories/DoctrineUserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,22 @@ public function getByIdWithGroups(int $id): ?User

return $qb->getQuery()->getOneOrNullResult();
}

/**
* @param string $provider
* @param string $external_id
* @return User|null
*/
public function getByExternalId(string $provider, string $external_id): ?User
{
return $this->getEntityManager()
->createQueryBuilder()
->select("e")
->from($this->getBaseEntity(), "e")
->Where("e.external_provider = :provider AND e.external_id = :external_id")
->setParameter("provider", $provider)
->setParameter("external_id", $external_id)
->getQuery()
->getOneOrNullResult();
}
}
Loading
Loading