Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement anonymous access to organization #201

Merged
merged 2 commits into from
Jun 19, 2020
Merged
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
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,
akondas marked this conversation as resolved.
Show resolved Hide resolved
])
->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