Skip to content

Commit e3766fc

Browse files
frankdekkerCopilot
andauthored
Allow image assets download from email (#1211)
* Add hash validation to get asset controller * Add coverage * Add coverage * Add coverage * Update src/Security/Voter/AssetVoter.php Co-authored-by: Copilot <[email protected]> * Add coverage --------- Co-authored-by: Copilot <[email protected]>
1 parent ceacf52 commit e3766fc

File tree

11 files changed

+298
-8
lines changed

11 files changed

+298
-8
lines changed

.github/copilot-instructions.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Agents.md
2+
3+
## Project Overview
4+
5+
123view is a Symfony-based code review and commit notification application built with PHP 8.3+ and Symfony 7.2. It provides features for creating code reviews, managing commit notifications, and integrating with version control systems.
6+
7+
## Core Architecture
8+
9+
**Backend (PHP/Symfony):**
10+
- **Namespace**: `DR\Review\` - all PHP classes use this base namespace
11+
- **Entity Layer**: Domain entities in `src/Entity/` organized by feature (Review, Repository, User, etc.)
12+
- **Controller Layer**: Split into `Api/` and `App/` controllers, with mail controllers in `Mail/`
13+
- **Service Layer**: Business logic in `src/Service/` with feature-based organization
14+
- **Repository Layer**: Doctrine repositories in `src/Repository/` following entity structure
15+
- **Message/Event System**: Async messaging in `src/Message/` with handlers in `src/MessageHandler/`
16+
- **Form Layer**: Symfony forms in `src/Form/` organized by domain
17+
18+
**Frontend (TypeScript/Stimulus):**
19+
- **Assets**: TypeScript controllers in `assets/ts/controllers/`
20+
- **Styling**: SCSS files in `assets/styles/` with theme support (dark/light)
21+
- **Build System**: Webpack Encore configuration in `webpack.config.js`
22+
23+
## Development Commands
24+
25+
**Frontend Development:**
26+
```bash
27+
npm run dev # Development build
28+
npm run watch # Watch mode for development
29+
npm run build # Production build
30+
npm run stylelint # Lint SCSS files
31+
npm run eslint # Lint TypeScript files
32+
```
33+
34+
**PHP Development:**
35+
```bash
36+
composer check # Run all checks (PHPStan, PHPMD, PHPCS)
37+
composer check:phpstan # Static analysis
38+
composer check:phpmd # Mess detection
39+
composer check:phpcs # Code style check
40+
composer fix:phpcbf # Auto-fix code style
41+
composer test # Run all tests
42+
composer test:unit # Unit tests only
43+
composer test:integration # Integration tests only
44+
composer test:functional # Functional tests only
45+
```
46+
47+
**Docker Environment:**
48+
```bash
49+
./bin/start.sh # Start development environment
50+
./bin/install.sh # Run installation wizard
51+
```
52+
53+
## Key Domain Concepts
54+
55+
**Code Reviews:**
56+
- Reviews are created from specific revisions (commits)
57+
- Support for attaching/detaching revisions
58+
- Comment system with threading and reactions
59+
- Reviewer workflow with accept/reject states
60+
61+
**Repositories:**
62+
- Git repository integration with credential management
63+
- Branch and revision tracking
64+
- External tool integrations (GitLab, etc.)
65+
66+
**Notifications:**
67+
- Rule-based commit notifications
68+
- Frequency controls (hourly, daily, weekly)
69+
- Filter system for including/excluding commits
70+
- Email delivery with theme support
71+
72+
## Test Structure
73+
74+
Tests are organized in three directories:
75+
- `tests/Unit/` - Isolated unit tests
76+
- `tests/Integration/` - Integration tests with database
77+
- `tests/Functional/` - Full application tests
78+
79+
Use the specific test suites when working on particular areas to speed up feedback loops.
80+
81+
## Configuration Files
82+
83+
- `webpack.config.js` - Frontend build configuration
84+
- `tsconfig.json` - TypeScript compiler settings
85+
- `composer.json` - PHP dependencies and scripts
86+
- `phpstan.neon` - Static analysis configuration
87+
- `phpunit.xml.dist` - Test configuration

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/config.xml
88
/docker/db
99
/.eslintcache
10+
/.claude/settings.local.json
1011

1112
###> symfony/framework-bundle ###
1213
/.env.local

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# CLAUDE.md
1+
# Agents.md
22

33
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44

@@ -83,7 +83,7 @@ Use the specific test suites when working on particular areas to speed up feedba
8383
## Configuration Files
8484

8585
- `webpack.config.js` - Frontend build configuration
86-
- `tsconfig.json` - TypeScript compiler settings
86+
- `tsconfig.json` - TypeScript compiler settings
8787
- `composer.json` - PHP dependencies and scripts
8888
- `phpstan.neon` - Static analysis configuration
89-
- `phpunit.xml.dist` - Test configuration
89+
- `phpunit.xml.dist` - Test configuration

config/packages/security.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use DR\Review\Security\AzureAd\AzureAdAuthenticator;
1111
use DR\Review\Security\Role\Roles;
1212
use DR\Review\Security\UserChecker;
13+
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
1314
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
1415
use Symfony\Config\SecurityConfig;
1516

@@ -56,6 +57,7 @@
5657
->path(LogoutController::class)
5758
->target(LoginController::class);
5859

59-
$security->accessControl()->path('^/app')->roles(['IS_AUTHENTICATED_FULLY']);
60+
$security->accessControl()->path('^/app/assets/')->roles(AuthenticatedVoter::PUBLIC_ACCESS);
61+
$security->accessControl()->path('^/app')->roles(AuthenticatedVoter::IS_AUTHENTICATED_FULLY);
6062
$security->accessControl()->path('^/log-viewer')->roles([Roles::ROLE_ADMIN]);
6163
};

src/Controller/App/Asset/AddAssetController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ public function __invoke(AddAssetRequest $request): JsonResponse
2828
// save entity
2929
$this->assetRepository->save($asset, true);
3030

31-
return new JsonResponse(['url' => $this->generateUrl(GetAssetController::class, ['id' => $asset->getId()])]);
31+
return new JsonResponse(['url' => $this->generateUrl(GetAssetController::class, ['id' => $asset->getId(), 'hash' => $asset->getHash()])]);
3232
}
3333
}

src/Controller/App/Asset/GetAssetController.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
namespace DR\Review\Controller\App\Asset;
55

66
use DR\Review\Entity\Asset\Asset;
7-
use DR\Review\Security\Role\Roles;
87
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
8+
use Symfony\Component\ExpressionLanguage\Expression;
99
use Symfony\Component\HttpFoundation\Response;
1010
use Symfony\Component\Routing\Annotation\Route;
1111
use Symfony\Component\Security\Http\Attribute\IsGranted;
1212

1313
class GetAssetController
1414
{
1515
#[Route('app/assets/{id<\d+>}', name: self::class, methods: 'GET')]
16-
#[IsGranted(Roles::ROLE_USER)]
16+
#[IsGranted(
17+
new Expression('is_granted("ROLE_USER") or is_granted("VIEW", subject)'),
18+
new Expression('args["asset"]')
19+
)]
1720
public function __invoke(#[MapEntity] Asset $asset): Response
1821
{
1922
return (new Response($asset->getData(), 200, ['Content-Type' => $asset->getMimeType()]))->setPublic();

src/Entity/Asset/Asset.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ public function setData(string $data): Asset
7171
return $this;
7272
}
7373

74+
public function getHash(): string
75+
{
76+
return substr(hash('sha256', $this->data), 0, 8);
77+
}
78+
7479
public function getCreateTimestamp(): int
7580
{
7681
return $this->createTimestamp;

src/Security/Voter/AssetVoter.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace DR\Review\Security\Voter;
5+
6+
use DR\Review\Entity\Asset\Asset;
7+
use DR\Utils\Assert;
8+
use Symfony\Component\HttpFoundation\RequestStack;
9+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
10+
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
11+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
12+
13+
/**
14+
* @extends Voter<string, Asset>
15+
*/
16+
class AssetVoter extends Voter
17+
{
18+
public const VIEW = 'VIEW';
19+
20+
public function __construct(private readonly RequestStack $requestStack)
21+
{
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
protected function supports(string $attribute, mixed $subject): bool
28+
{
29+
// only support assets, and correct attributes
30+
return $this->requestStack->getCurrentRequest() !== null && $subject instanceof Asset && $attribute === self::VIEW;
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
37+
{
38+
$hash = Assert::notNull($this->requestStack->getCurrentRequest())->query->getString('hash');
39+
$asset = Assert::isInstanceOf($subject, Asset::class);
40+
41+
return $hash !== '' && hash_equals($asset->getHash(), $hash);
42+
}
43+
}

tests/Unit/Controller/App/Asset/AddAssetControllerTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function testInvoke(): void
3535
{
3636
$asset = new Asset();
3737
$asset->setId(123);
38+
$asset->setData('foobar');
3839
$user = new User();
3940

4041
$request = $this->createMock(AddAssetRequest::class);
@@ -44,7 +45,7 @@ public function testInvoke(): void
4445
$this->expectGetUser($user);
4546
$this->assetFactory->expects($this->once())->method('create')->with($user, 'mime-type', 'data')->willReturn($asset);
4647
$this->assetRepository->expects($this->once())->method('save')->with($asset, true);
47-
$this->expectGenerateUrl(GetAssetController::class, ['id' => 123]);
48+
$this->expectGenerateUrl(GetAssetController::class, ['id' => 123, 'hash' => 'c3ab8ff1']);
4849

4950
($this->controller)($request);
5051
}

tests/Unit/Entity/Asset/AssetTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,10 @@ public function testAccessorPairs(): void
1717
{
1818
static::assertAccessorPairs(Asset::class);
1919
}
20+
21+
public function testGetHash(): void
22+
{
23+
$asset = (new Asset())->setData('foobar');
24+
static::assertSame('c3ab8ff1', $asset->getHash());
25+
}
2026
}

0 commit comments

Comments
 (0)