Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions common/components/CloudinaryManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

namespace common\components;

use Yii;
use Cloudinary\Cloudinary;
use Cloudinary\Configuration\Configuration;
use Cloudinary\Api\Upload\UploadApi;
use Cloudinary\Api\Admin\AdminApi;
use yii\base\InvalidConfigException;

/**
*
Expand All @@ -21,7 +20,7 @@ class CloudinaryManager extends \yii\base\Component {
public $api_key;
public $api_secret;

private $cloudinary;
private $configured = false;

/**
* @inheritdoc
Expand All @@ -30,13 +29,16 @@ public function init()
{
parent::init();

foreach (['cloud_name', 'api_key', 'api_secret'] as $attribute) {
if ($this->$attribute === null) {
throw new yii\base\InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [
'{class}' => static::class,
'{attribute}' => '$' . $attribute
]));
}
$this->cloud_name = $this->normalizeConfigValue($this->cloud_name);
$this->api_key = $this->normalizeConfigValue($this->api_key);
$this->api_secret = $this->normalizeConfigValue($this->api_secret);

$this->configured = $this->cloud_name !== null
&& $this->api_key !== null
&& $this->api_secret !== null;

if (!$this->configured) {
return;
}

/*define('CLOUDINARY_CLOUD_NAME', $this->cloud_name);
Expand All @@ -55,6 +57,24 @@ public function init()
]);
}

private function normalizeConfigValue($value)
{
if ($value === null) {
return null;
}

$value = trim((string) $value);

return $value === '' ? null : $value;
}

private function assertConfigured()
{
if (!$this->configured) {
throw new InvalidConfigException('Cloudinary credentials are not configured.');
}
}

/**
* Upload image
* @param string $filePath
Expand All @@ -63,7 +83,9 @@ public function init()
*/
public function upload($filePath, $options)
{
return (new uploadApi())->upload(
$this->assertConfigured();

return (new UploadApi())->upload(
$filePath,
$options
);
Expand All @@ -76,14 +98,16 @@ public function upload($filePath, $options)
*/
public function delete($path, $type = "image")
{
$this->assertConfigured();

//remove extension from path to get public_id

$ext = pathinfo($path, PATHINFO_EXTENSION);

$public_id = str_replace(".".$ext, "", $path);
//$this->cloudinary->delete

$result = (new uploadApi())->destroy($public_id, [
$result = (new UploadApi())->destroy($public_id, [
"invalidate" => true,//remove from CDN cache if any
"resource_type" => $type
]);
Expand All @@ -98,7 +122,9 @@ public function delete($path, $type = "image")
*/
public function getUrl($public_id, $type = "image")
{
$result = (new adminApi())->asset($public_id);
$this->assertConfigured();

$result = (new AdminApi())->asset($public_id);

if ($result['secure_url']) {
return $result['secure_url'];
Expand Down
6 changes: 3 additions & 3 deletions common/config/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
],
'cloudinaryManager' => [
'class' => 'common\components\CloudinaryManager',
'cloud_name' => 'studenthub',
'api_key' => '251218449868375',
'api_secret' => 'FILAex7q93GUB-q1bEe1pAKOIvY'
'cloud_name' => getenv('CLOUDINARY_CLOUD_NAME') ?: null,
'api_key' => getenv('CLOUDINARY_API_KEY') ?: null,
'api_secret' => getenv('CLOUDINARY_API_SECRET') ?: null
/**
* You can access the bucket with:
* http://res.cloudinary.com/studenthub/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 23 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,26 @@ cd console && ../yii algolia/index candidate
```bash
./yii cron/update-candidate-stats
./yii cron/update-company-stats
```
```

## Cloudinary Runtime Configuration

Profile photos, brand logos, and company documents are stored in Cloudinary through
`common\components\CloudinaryManager`. Do not commit Cloudinary credentials to
the repository. Configure them through the runtime environment instead:

```bash
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret
```

If any of these variables are missing, Cloudinary upload, delete, and asset URL
lookups fail before calling Cloudinary. This prevents partial configuration from
accidentally using stale checked-in credentials.

Before submitting config changes, run:

```bash
python scripts/check-cloudinary-hardening.py
```
74 changes: 74 additions & 0 deletions scripts/check-cloudinary-hardening.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from pathlib import Path
import re
import sys


ROOT = Path(__file__).resolve().parents[1]
CONFIG = ROOT / "common" / "config" / "main.php"
MANAGER = ROOT / "common" / "components" / "CloudinaryManager.php"
DOCS = ROOT / "docs" / "setup.md"


def fail(message):
print(f"Cloudinary hardening check failed: {message}", file=sys.stderr)
raise SystemExit(1)


config = CONFIG.read_text(encoding="utf-8")
manager = MANAGER.read_text(encoding="utf-8")
docs = DOCS.read_text(encoding="utf-8")

for variable in [
"CLOUDINARY_CLOUD_NAME",
"CLOUDINARY_API_KEY",
"CLOUDINARY_API_SECRET",
]:
if variable not in config:
fail(f"{variable} is not wired in common/config/main.php")
if variable not in docs:
fail(f"{variable} is not documented in docs/setup.md")

cloudinary_block_match = re.search(
r"'cloudinaryManager'\s*=>\s*\[(.*?)\n\s*\],",
config,
flags=re.S,
)
if not cloudinary_block_match:
fail("cloudinaryManager config block was not found")

cloudinary_block = cloudinary_block_match.group(1)

literal_patterns = [
(r"'cloud_name'\s*=>\s*['\"][^'\"]+['\"]", "cloud_name literal"),
(r"'api_key'\s*=>\s*['\"][^'\"]+['\"]", "api_key literal"),
(r"'api_secret'\s*=>\s*['\"][^'\"]+['\"]", "api_secret literal"),
]
for pattern, description in literal_patterns:
if re.search(pattern, cloudinary_block):
fail(f"Cloudinary {description} remains in config")

required_manager_snippets = [
"private $configured = false;",
"normalizeConfigValue",
"assertConfigured",
"Cloudinary credentials are not configured.",
]
for snippet in required_manager_snippets:
if snippet not in manager:
fail(f"CloudinaryManager is missing {snippet!r}")

for method in ["upload", "delete", "getUrl"]:
method_match = re.search(
rf"public function {method}\([^)]*\)\s*\{{(.*?)\n\s*\}}",
manager,
flags=re.S,
)
if not method_match:
fail(f"CloudinaryManager::{method} was not found")
if "$this->assertConfigured();" not in method_match.group(1):
fail(f"CloudinaryManager::{method} does not assert configuration")

if "new uploadApi()" in manager or "new adminApi()" in manager:
fail("Cloudinary API classes should use their imported class names")

print("Cloudinary hardening check passed.")