Skip to content

Commit

Permalink
Implement anonymous access to organization (#201)
Browse files Browse the repository at this point in the history
* Implement anonymous access to organization

* Code review fixes
  • Loading branch information
karniv00l authored Jun 19, 2020
1 parent 353a177 commit cebb61b
Show file tree
Hide file tree
Showing 28 changed files with 622 additions and 123 deletions.
4 changes: 4 additions & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ security:
guard:
authenticators:
- Buddy\Repman\Security\TokenAuthenticator
- Buddy\Repman\Security\AnonymousOrganizationUserAuthenticator
entry_point: Buddy\Repman\Security\AnonymousOrganizationUserAuthenticator
main:
anonymous: lazy
provider: user_provider
Expand All @@ -45,6 +47,8 @@ security:
- { path: ^/user, roles: ROLE_USER }
- { path: ^/organization/new, roles: ROLE_USER }
- { path: ^/$, roles: ROLE_USER }
- { path: ^/organization/.+/overview$, roles: ROLE_ORGANIZATION_ANONYMOUS_USER }
- { path: ^/organization/.+/package$, roles: ROLE_ORGANIZATION_ANONYMOUS_USER }
- { path: ^/organization/.+(/.+)*, roles: ROLE_ORGANIZATION_MEMBER }
- { path: ^/downloads, host: '([a-z0-9_-]+)\.repo\.(.+)', roles: IS_AUTHENTICATED_ANONYMOUSLY}
- { path: ^/, host: '([a-z0-9_-]+)\.repo\.(.+)', roles: ROLE_ORGANIZATION }
15 changes: 14 additions & 1 deletion src/Controller/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
namespace Buddy\Repman\Controller;

use Buddy\Repman\Form\Type\Organization\ChangeAliasType;
use Buddy\Repman\Form\Type\Organization\ChangeAnonymousAccessType;
use Buddy\Repman\Form\Type\Organization\ChangeNameType;
use Buddy\Repman\Form\Type\Organization\CreateType;
use Buddy\Repman\Form\Type\Organization\GenerateTokenType;
use Buddy\Repman\Message\Organization\ChangeAlias;
use Buddy\Repman\Message\Organization\ChangeAnonymousAccess;
use Buddy\Repman\Message\Organization\ChangeName;
use Buddy\Repman\Message\Organization\CreateOrganization;
use Buddy\Repman\Message\Organization\GenerateToken;
Expand Down Expand Up @@ -97,7 +99,8 @@ public function overview(Organization $organization): Response
public function packages(Organization $organization, Request $request): Response
{
$count = $this->packageQuery->count($organization->id());
if ($count === 0 && $organization->isOwner($this->getUser()->id())) {
$user = parent::getUser();
if ($count === 0 && $user instanceof User && $organization->isOwner($user->id())) {
return $this->redirectToRoute('organization_package_new', ['organization' => $organization->alias()]);
}

Expand Down Expand Up @@ -287,10 +290,20 @@ public function settings(Organization $organization, Request $request): Response
return $this->redirectToRoute('organization_settings', ['organization' => $aliasForm->get('alias')->getData()]);
}

$anonymousAccessForm = $this->createForm(ChangeAnonymousAccessType::class, ['hasAnonymousAccess' => $organization->hasAnonymousAccess()]);
$anonymousAccessForm->handleRequest($request);
if ($anonymousAccessForm->isSubmitted() && $anonymousAccessForm->isValid()) {
$this->dispatchMessage(new ChangeAnonymousAccess($organization->id(), $anonymousAccessForm->get('hasAnonymousAccess')->getData()));
$this->addFlash('success', 'Anonymous access has been successfully changed.');

return $this->redirectToRoute('organization_settings', ['organization' => $organization->alias()]);
}

return $this->render('organization/settings.html.twig', [
'organization' => $organization,
'renameForm' => $renameForm->createView(),
'aliasForm' => $aliasForm->createView(),
'anonymousAccessForm' => $anonymousAccessForm->createView(),
]);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Entity/Organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ class Organization
*/
private ?Collection $members = null;

/**
* @ORM\Column(type="boolean")
*/
private bool $hasAnonymousAccess = false;

public function __construct(UuidInterface $id, User $owner, string $name, string $alias)
{
$this->id = $id;
Expand Down Expand Up @@ -246,6 +251,11 @@ public function members(): Collection
return $this->members;
}

public function changeAnonymousAccess(bool $hasAnonymousAccess): void
{
$this->hasAnonymousAccess = $hasAnonymousAccess;
}

private function isLastOwner(User $user): bool
{
$owners = $this->members->filter(fn (Member $member) => $member->isOwner());
Expand Down
32 changes: 32 additions & 0 deletions src/Form/Type/Organization/ChangeAnonymousAccessType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Form\Type\Organization;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class ChangeAnonymousAccessType extends AbstractType
{
public function getBlockPrefix(): string
{
return '';
}

/**
* @param array<mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('hasAnonymousAccess', CheckboxType::class, [
'label' => 'Allow anonymous users',
'required' => false,
])
->add('changeAnonymousAccess', SubmitType::class, ['label' => 'Change'])
;
}
}
27 changes: 27 additions & 0 deletions src/Message/Organization/ChangeAnonymousAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Message\Organization;

final class ChangeAnonymousAccess
{
private string $organizationId;
private bool $hasAnonymousAccess;

public function __construct(string $organizationId, bool $hasAnonymousAccess)
{
$this->organizationId = $organizationId;
$this->hasAnonymousAccess = $hasAnonymousAccess;
}

public function organizationId(): string
{
return $this->organizationId;
}

public function hasAnonymousAccess(): bool
{
return $this->hasAnonymousAccess;
}
}
28 changes: 28 additions & 0 deletions src/MessageHandler/Organization/ChangeAnonymousAccessHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\MessageHandler\Organization;

use Buddy\Repman\Message\Organization\ChangeAnonymousAccess;
use Buddy\Repman\Repository\OrganizationRepository;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

final class ChangeAnonymousAccessHandler implements MessageHandlerInterface
{
private OrganizationRepository $repositories;

public function __construct(OrganizationRepository $repositories)
{
$this->repositories = $repositories;
}

public function __invoke(ChangeAnonymousAccess $message): void
{
$this->repositories
->getById(Uuid::fromString($message->organizationId()))
->changeAnonymousAccess($message->hasAnonymousAccess())
;
}
}
37 changes: 37 additions & 0 deletions src/Migrations/Version20200615181216.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Migrations;

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

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200615181216 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->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql('ALTER TABLE organization ADD has_anonymous_access BOOLEAN NOT NULL DEFAULT false');
$this->addSql('ALTER TABLE "user" ALTER email_scan_result DROP DEFAULT');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql('ALTER TABLE organization DROP has_anonymous_access');
$this->addSql('ALTER TABLE "user" ALTER email_scan_result SET DEFAULT \'true\'');
}
}
10 changes: 9 additions & 1 deletion src/Query/User/Model/Organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ final class Organization
private string $id;
private string $name;
private string $alias;
private bool $hasAnonymousAccess;

/**
* @var Member[]
*/
Expand All @@ -22,12 +24,13 @@ final class Organization
/**
* @param Member[] $members
*/
public function __construct(string $id, string $name, string $alias, array $members, ?string $token = null)
public function __construct(string $id, string $name, string $alias, array $members, bool $hasAnonymousAccess, ?string $token = null)
{
$this->id = $id;
$this->name = $name;
$this->alias = $alias;
$this->members = array_map(fn (Member $member) => $member, $members);
$this->hasAnonymousAccess = $hasAnonymousAccess;
$this->token = $token;
}

Expand Down Expand Up @@ -93,4 +96,9 @@ public function getMember(string $userId): Option

return Option::none();
}

public function hasAnonymousAccess(): bool
{
return $this->hasAnonymousAccess;
}
}
15 changes: 9 additions & 6 deletions src/Query/User/OrganizationQuery/DbalOrganizationQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public function __construct(Connection $connection)
public function getByAlias(string $alias): Option
{
$data = $this->connection->fetchAssoc(
'SELECT id, name, alias FROM "organization" WHERE alias = :alias', [
'SELECT id, name, alias, has_anonymous_access
FROM "organization" WHERE alias = :alias', [
':alias' => $alias,
]);

Expand All @@ -41,8 +42,9 @@ public function getByAlias(string $alias): Option

public function getByInvitation(string $token, string $email): Option
{
$data = $this->connection->fetchAssoc('SELECT o.id, o.name, o.alias
FROM "organization" o
$data = $this->connection->fetchAssoc(
'SELECT o.id, o.name, o.alias, o.has_anonymous_access
FROM "organization" o
JOIN organization_invitation i ON o.id = i.organization_id
WHERE i.token = :token AND i.email = :email
', [
Expand Down Expand Up @@ -70,8 +72,8 @@ public function findAllTokens(string $organizationId, int $limit = 20, int $offs
$data['last_used_at'] !== null ? new \DateTimeImmutable($data['last_used_at']) : null
);
}, $this->connection->fetchAll('
SELECT name, value, created_at, last_used_at
FROM organization_token
SELECT name, value, created_at, last_used_at
FROM organization_token
WHERE organization_id = :id
ORDER BY UPPER(name) ASC
LIMIT :limit OFFSET :offset', [
Expand Down Expand Up @@ -149,7 +151,7 @@ public function findAllMembers(string $organizationId, int $limit = 20, int $off
$row['role']
);
}, $this->connection->fetchAll('
SELECT u.id, u.email, m.role
SELECT u.id, u.email, m.role
FROM organization_member AS m
JOIN "user" u ON u.id = m.user_id
WHERE m.organization_id = :id
Expand Down Expand Up @@ -214,6 +216,7 @@ private function hydrateOrganization(array $data): Organization
$data['name'],
$data['alias'],
array_map(fn (array $row) => new Member($row['user_id'], $row['email'], $row['role']), $members),
$data['has_anonymous_access'],
$token !== false ? $token : null
);
}
Expand Down
81 changes: 81 additions & 0 deletions src/Security/AnonymousOrganizationUserAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

final class AnonymousOrganizationUserAuthenticator extends AbstractGuardAuthenticator
{
/**
* @codeCoverageIgnore
*
* @return Response
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new JsonResponse([
'message' => 'Authentication Required',
], Response::HTTP_UNAUTHORIZED);
}

public function supports(Request $request)
{
return $request->get('_route') !== 'repo_package_downloads'
&& !$request->headers->has('PHP_AUTH_USER')
&& !$request->headers->has('PHP_AUTH_PW');
}

public function getCredentials(Request $request)
{
$organizationAlias = $request->get('organization');
if ($organizationAlias === null) {
throw new BadCredentialsException();
}

return $organizationAlias;
}

public function getUser($credentials, UserProviderInterface $userProvider)
{
if (!$userProvider instanceof OrganizationProvider) {
throw new \InvalidArgumentException();
}

return $userProvider->loadUserByAlias($credentials);
}

public function checkCredentials($credentials, UserInterface $user)
{
return true;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return new JsonResponse([
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
], Response::HTTP_FORBIDDEN);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
return null;
}

/**
* @codeCoverageIgnore
*/
public function supportsRememberMe(): bool
{
return false;
}
}
Loading

0 comments on commit cebb61b

Please sign in to comment.