Skip to content
Draft
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
2 changes: 2 additions & 0 deletions api/fixtures/camps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ App\Entity\Camp:
addressCity: <city()>
owner: '@admin'
creator: '@admin'
isPublic: true
isPrototype: true
isShared: false
sharedBy: null
Expand All @@ -69,6 +70,7 @@ App\Entity\Camp:
addressCity: <city()>
owner: '@admin'
creator: '@admin'
isPublic: true
isPrototype: false
isShared: true
sharedBy: '@admin'
Expand Down
56 changes: 56 additions & 0 deletions api/migrations/schema/Version20250903154700.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250903154700 extends AbstractMigration {
public function getDescription(): string {
return 'Add another view for filtering out person-related data in public camps';
}

public function up(Schema $schema): void {
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps
AS
select cc.id::TEXT, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps_with_public
AS
SELECT CONCAT(u.id, c.id) id, u.id userid, c.id campid
from camp c, "user" u
where c.isprototype = TRUE or c.isshared = TRUE
union all
select cc.id, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
}

public function down(Schema $schema): void {
$this->addSql('DROP VIEW IF EXISTS public.view_user_camps_with_public');
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps
AS
SELECT CONCAT(u.id, c.id) id, u.id userid, c.id campid
from camp c, "user" u
where c.isprototype = TRUE
union all
select cc.id, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
}
}
58 changes: 58 additions & 0 deletions api/migrations/schema/Version20251004093025.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251004093025 extends AbstractMigration {
public function getDescription(): string {
return '';
}

public function up(Schema $schema): void {
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE camp ADD isPublic BOOLEAN DEFAULT false NOT NULL');
$this->addSql('UPDATE camp SET isPublic = (isShared OR isPrototype)');
$this->addSql('ALTER TABLE camp ADD CONSTRAINT enforce_public_flag CHECK (isPublic = (isShared OR isPrototype))');
$this->addSql('CREATE INDEX IDX_C1944230FADC24C7 ON camp (isPublic)');
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps
AS
SELECT CONCAT(u.id, c.id) id, u.id userid, c.id campid
from camp c, "user" u
where c.ispublic = TRUE
union all
select cc.id, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
}

public function down(Schema $schema): void {
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps
AS
SELECT CONCAT(u.id, c.id) id, u.id userid, c.id campid
from camp c, "user" u
where c.isprototype = TRUE or c.isshared = TRUE
union all
select cc.id, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
$this->addSql('DROP INDEX IDX_C1944230FADC24C7');
$this->addSql('ALTER TABLE camp DROP CONSTRAINT enforce_public_flag');
$this->addSql('ALTER TABLE camp DROP isPublic');
}
}
18 changes: 13 additions & 5 deletions api/src/Entity/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
new Get(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object)'
is_granted("CAMP_IS_PUBLIC", object)'
),
new Patch(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
Expand All @@ -56,8 +55,7 @@
toProperty: 'camp',
fromClass: Camp::class,
security: 'is_granted("CAMP_COLLABORATOR", camp) or
is_granted("CAMP_IS_SHARED", camp) or
is_granted("CAMP_IS_PROTOTYPE", camp)'
is_granted("CAMP_IS_PUBLIC", camp)'
),
],
normalizationContext: self::COLLECTION_NORMALIZATION_CONTEXT,
Expand Down Expand Up @@ -275,7 +273,17 @@ public function removeScheduleEntry(ScheduleEntry $scheduleEntry): self {
/**
* @return ActivityResponsible[]
*/
#[ApiProperty(readableLink: true)]
#[ApiProperty(readableLink: true, security: '!is_granted("CAMP_COLLABORATOR", object)')]
#[SerializedName('activityResponsibles')]
#[Groups(['Activity:ActivityResponsibles'])]
public function getRedactedEmbeddedActivityResponsibles(): array {
return [];
}

/**
* @return ActivityResponsible[]
*/
#[ApiProperty(readableLink: true, security: 'is_granted("CAMP_COLLABORATOR", object)')]
#[SerializedName('activityResponsibles')]
#[Groups(['Activity:ActivityResponsibles'])]
public function getEmbeddedActivityResponsibles(): array {
Expand Down
6 changes: 2 additions & 4 deletions api/src/Entity/ActivityProgressLabel.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
new Get(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object)'
is_granted("CAMP_IS_PUBLIC", object)'
),
new Patch(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
Expand All @@ -53,8 +52,7 @@
toProperty: 'camp',
fromClass: Camp::class,
security: 'is_granted("CAMP_COLLABORATOR", camp) or
is_granted("CAMP_IS_SHARED", camp) or
is_granted("CAMP_IS_PROTOTYPE", camp)'
is_granted("CAMP_IS_PUBLIC", camp)'
),
],
security: 'is_fully_authenticated()',
Expand Down
2 changes: 1 addition & 1 deletion api/src/Entity/ActivityResponsible.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#[ApiResource(
operations: [
new Get(
security: 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_SHARED", object) or is_granted("CAMP_IS_PROTOTYPE", object)'
security: 'is_granted("CAMP_COLLABORATOR", object)'
),
new Delete(
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)'
Expand Down
36 changes: 30 additions & 6 deletions api/src/Entity/Camp.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
operations: [
new Get(
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object)',
is_granted("CAMP_IS_PUBLIC", object)',
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
),
new Patch(
Expand Down Expand Up @@ -69,6 +68,7 @@
#[ORM\Entity(repositoryClass: CampRepository::class)]
#[ORM\Index(columns: ['isPrototype'])]
#[ORM\Index(columns: ['isShared'])]
#[ORM\Index(columns: ['isPublic'])]
#[ORM\Index(columns: ['updateTime'])] // TODO unclear why this is necessary, but doctrine forgot about this index from BaseEntity...
class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface {
public const ITEM_NORMALIZATION_CONTEXT = [
Expand All @@ -83,12 +83,21 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
public Collection $collaborations;

/**
* UserCamp Collections
* Based von view_user_camps; lists all user who can see this camp.
* UserCamp Collection
* Based von view_user_camps; lists all user who can see this camp through campCollaborations.
*/
#[ORM\OneToMany(targetEntity: UserCamp::class, mappedBy: 'camp')]
public Collection $userCamps;

/**
* UserCampWithPublic Collection
* Based von view_user_camps_with_public; lists all user who can see this camp, through
* campCollaborations or because the camps are prototypes or shared.
*/
#[ORM\OneToMany(targetEntity: UserCampWithPublic::class, mappedBy: 'camp')]
#[Assert\DisableAutoMapping]
public Collection $userCampsWithPublic;

/**
* The time periods of the camp, there must be at least one. Periods in a camp may not overlap.
* When creating a camp, the initial periods may be specified as nested payload, but updating,
Expand Down Expand Up @@ -190,7 +199,7 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
public ?Camp $campPrototype = null;

/**
* Whether the programme of this camp is publicly available to anyone (including
* Whether the programme of this camp is publicly available to anyone (except for
* personal data such as camp collaborations, personal material lists,
* responsibilities and comments).
*/
Expand Down Expand Up @@ -228,6 +237,14 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
#[ORM\Column(type: 'boolean')]
public bool $isPrototype = false;

/**
* Automatically set to the value (isShared || isPrototype). Used for more efficient
* database filtering operations, since OR queries are very expensive to compute.
* This is only used in the database, and therefore not available on the API.
*/
#[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])]
public bool $isPublic = false;

/**
* An optional short title for the camp. Can be used in the UI where space is tight. If
* not present, frontends may auto-shorten the title if the shortTitle is not set.
Expand Down Expand Up @@ -484,13 +501,20 @@ public function getCampCollaborations(): array {
return [];
}

#[ApiProperty(writable: false, readableLink: true, security: '!is_granted("CAMP_COLLABORATOR", object)')]
#[SerializedName('campCollaborations')]
#[Groups('Camp:CampCollaborations')]
public function getRedactedEmbeddedCampCollaborations(): array {
return [];
}

/**
* The people working on planning and carrying out the camp. Only collaborators have access
* to the camp's contents.
*
* @return CampCollaboration[]
*/
#[ApiProperty(writable: false, readableLink: true)]
#[ApiProperty(writable: false, readableLink: true, security: 'is_granted("CAMP_COLLABORATOR", object)')]
#[SerializedName('campCollaborations')]
#[Groups('Camp:CampCollaborations')]
public function getEmbeddedCampCollaborations(): array {
Expand Down
5 changes: 2 additions & 3 deletions api/src/Entity/CampCollaboration.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
operations: [
new Get(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
security: 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_SHARED", object) or is_granted("CAMP_IS_PROTOTYPE", object)'
security: 'is_granted("CAMP_COLLABORATOR", object)'
),
new Patch(
processor: CampCollaborationUpdateProcessor::class,
Expand Down Expand Up @@ -68,8 +68,7 @@
toProperty: 'camp',
fromClass: Camp::class,
security: 'is_granted("CAMP_COLLABORATOR", camp) or
is_granted("CAMP_IS_SHARED", camp) or
is_granted("CAMP_IS_PROTOTYPE", camp)'
is_granted("CAMP_IS_PUBLIC", camp)'
Copy link
Member

Choose a reason for hiding this comment

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

When /camps/camp-id/camp_collaborations is accessed, and the camp has set 'isPublic', then this endpoint may be displayed, but as an empty array.
Am I correctly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Exactly. This allows the frontend to "fail" gracefully. If someone visits the team page (even though it's not in the menu), they will see an empty list

),
],
security: 'is_fully_authenticated()',
Expand Down
6 changes: 2 additions & 4 deletions api/src/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@
new Get(
normalizationContext: self::ITEM_NORMALIZATION_CONTEXT,
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object)'
is_granted("CAMP_IS_PUBLIC", object)'
),
new Patch(
denormalizationContext: ['groups' => ['write', 'update']],
Expand Down Expand Up @@ -66,8 +65,7 @@
fromClass: Camp::class,
toProperty: 'camp',
security: 'is_granted("CAMP_COLLABORATOR", camp) or
is_granted("CAMP_IS_SHARED", camp) or
is_granted("CAMP_IS_PROTOTYPE", camp)'
is_granted("CAMP_IS_PUBLIC", camp)'
),
],
extraProperties: [
Expand Down
6 changes: 2 additions & 4 deletions api/src/Entity/Checklist.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
operations: [
new Get(
security: 'is_granted("CHECKLIST_IS_PROTOTYPE", object) or
is_granted("CAMP_IS_PROTOTYPE", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PUBLIC", object) or
is_granted("CAMP_COLLABORATOR", object)
'
),
Expand Down Expand Up @@ -64,8 +63,7 @@
toProperty: 'camp',
fromClass: Camp::class,
security: 'is_granted("CAMP_COLLABORATOR", camp) or
is_granted("CAMP_IS_SHARED", camp) or
is_granted("CAMP_IS_PROTOTYPE", camp)'
is_granted("CAMP_IS_PUBLIC", camp)'
),
],
extraProperties: [
Expand Down
6 changes: 2 additions & 4 deletions api/src/Entity/ChecklistItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
operations: [
new Get(
security: 'is_granted("CHECKLIST_IS_PROTOTYPE", object) or
is_granted("CAMP_IS_PROTOTYPE", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PUBLIC", object) or
is_granted("CAMP_COLLABORATOR", object)
'
),
Expand Down Expand Up @@ -65,8 +64,7 @@
fromClass: Checklist::class,
toProperty: 'checklist',
security: 'is_granted("CHECKLIST_IS_PROTOTYPE", checklist) or
is_granted("CAMP_IS_PROTOTYPE", checklist) or
is_granted("CAMP_IS_SHARED", checklist) or
is_granted("CAMP_IS_PUBLIC", checklist) or
is_granted("CAMP_COLLABORATOR", checklist)'
),
],
Expand Down
5 changes: 1 addition & 4 deletions api/src/Entity/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
operations: [
new Get(
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object) or
object.author === user',
),
new Delete(
Expand All @@ -49,8 +47,7 @@
toProperty: 'activity',
fromClass: Activity::class,
security: 'is_granted("CAMP_COLLABORATOR", activity) or
is_granted("CAMP_IS_SHARED", activity) or
is_granted("CAMP_IS_PROTOTYPE", activity)',
is_granted("CAMP_IS_PUBLIC", activity)',
),
],
security: 'is_fully_authenticated()',
Expand Down
3 changes: 1 addition & 2 deletions api/src/Entity/ContentNode/ChecklistNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
operations: [
new Get(
security: 'is_granted("CAMP_COLLABORATOR", object) or
is_granted("CAMP_IS_SHARED", object) or
is_granted("CAMP_IS_PROTOTYPE", object)'
is_granted("CAMP_IS_PUBLIC", object)'
),
new Patch(
processor: ChecklistNodePersistProcessor::class,
Expand Down
Loading
Loading