Skip to content

Commit

Permalink
Implement members permissions (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
akondas authored Apr 29, 2020
1 parent bca561f commit 673037d
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 81 deletions.
2 changes: 2 additions & 0 deletions src/Controller/OAuth/BitbucketController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Buddy\Repman\Service\BitbucketApi;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Token\AccessToken;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -46,6 +47,7 @@ public function registerCheck(Request $request, BitbucketApi $api): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-bitbucket", name="fetch_bitbucket_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromBitbucket(Organization $organization): Response
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/OAuth/GitHubController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Buddy\Repman\Query\User\Model\Organization;
use Buddy\Repman\Service\GitHubApi;
use League\OAuth2\Client\Token\AccessToken;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -48,6 +49,7 @@ public function registerCheck(Request $request, GitHubApi $api): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-github", name="fetch_github_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromGithub(Organization $organization): Response
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/OAuth/GitLabController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Buddy\Repman\Query\User\Model\Organization;
use League\OAuth2\Client\Token\AccessToken;
use Omines\OAuth2\Client\Provider\GitlabResourceOwner;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -50,6 +51,7 @@ function (): string {
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-gitlab", name="fetch_gitlab_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromGitLab(Organization $organization): Response
Expand Down
45 changes: 25 additions & 20 deletions src/Controller/Organization/MembersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Buddy\Repman\Query\User\Model\Organization\Invitation;
use Buddy\Repman\Query\User\OrganizationQuery;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -47,6 +48,27 @@ public function listMembers(Organization $organization, Request $request): Respo
}

/**
* @Route("/user/invitation/{token}", name="organization_accept_invitation", methods={"GET"}, requirements={"token"="%uuid_pattern%"})
*/
public function acceptInvitation(string $token): Response
{
/** @var User $user */
$user = $this->getUser();
$organization = $this->organizations->getByInvitation($token, $user->getEmail());
if ($organization->isEmpty()) {
$this->addFlash('danger', 'Invitation not found or belongs to different user');
$this->tokenStorage->setToken();
throw new AuthenticationException();
}

$this->dispatchMessage(new AcceptInvitation($token, $user->id()->toString()));
$this->addFlash('success', sprintf('The invitation to %s organization has been accepted', $organization->get()->name()));

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

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/member/invite", name="organization_invite_member", methods={"GET", "POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function invite(Organization $organization, Request $request): Response
Expand Down Expand Up @@ -74,6 +96,7 @@ public function invite(Organization $organization, Request $request): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/invitation", name="organization_invitations", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function listInvitations(Organization $organization, Request $request): Response
Expand All @@ -86,26 +109,7 @@ public function listInvitations(Organization $organization, Request $request): R
}

/**
* @Route("/user/invitation/{token}", name="organization_accept_invitation", methods={"GET"}, requirements={"token"="%uuid_pattern%"})
*/
public function acceptInvitation(string $token): Response
{
/** @var User $user */
$user = $this->getUser();
$organization = $this->organizations->getByInvitation($token, $user->getEmail());
if ($organization->isEmpty()) {
$this->addFlash('danger', 'Invitation not found or belongs to different user');
$this->tokenStorage->setToken();
throw new AuthenticationException();
}

$this->dispatchMessage(new AcceptInvitation($token, $user->id()->toString()));
$this->addFlash('success', sprintf('The invitation to %s organization has been accepted', $organization->get()->name()));

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

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/invitation/{token}", name="organization_remove_invitation", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeInvitation(Organization $organization, string $token): Response
Expand All @@ -117,6 +121,7 @@ public function removeInvitation(Organization $organization, string $token): Res
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/member/{user}", name="organization_remove_member", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeMember(Organization $organization, UserReadModel $user): Response
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/Organization/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Buddy\Repman\Service\GitLabApi;
use Http\Client\Exception as HttpException;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
Expand All @@ -33,6 +34,7 @@
final class PackageController extends AbstractController
{
/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/new/{type?}", name="organization_package_new", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageNew(Organization $organization, Request $request, GithubApi $githubApi, GitlabApi $gitlabApi, BitbucketApi $bitbucketApi, ?string $type): Response
Expand Down
4 changes: 4 additions & 0 deletions src/Controller/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Buddy\Repman\Service\ExceptionHandler;
use Buddy\Repman\Service\Organization\AliasGenerator;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -119,6 +120,7 @@ public function updatePackage(Organization $organization, Package $package): Res
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/{package}", name="organization_package_remove", methods={"DELETE"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"})
*/
public function removePackage(Organization $organization, Package $package): Response
Expand Down Expand Up @@ -247,6 +249,7 @@ public function removeToken(Organization $organization, string $token): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/settings", name="organization_settings", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function settings(Organization $organization, Request $request): Response
Expand Down Expand Up @@ -277,6 +280,7 @@ public function settings(Organization $organization, Request $request): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}", name="organization_remove", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeOrganization(Organization $organization): Response
Expand Down
2 changes: 0 additions & 2 deletions src/Entity/Organization/Member.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
class Member
{
public const ROLE_OWNER = 'owner';
public const ROLE_ADMIN = 'admin';
public const ROLE_MEMBER = 'member';

/**
Expand Down Expand Up @@ -89,7 +88,6 @@ public static function availableRoles(): array
{
return [
self::ROLE_MEMBER,
self::ROLE_ADMIN,
self::ROLE_OWNER,
];
}
Expand Down
34 changes: 25 additions & 9 deletions src/Service/Organization/OrganizationVoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ public function __construct(OrganizationQuery $organizationQuery)

protected function supports(string $attribute, $subject): bool
{
return $attribute === 'ROLE_ORGANIZATION_MEMBER';
return in_array($attribute, [
'ROLE_ORGANIZATION_MEMBER',
'ROLE_ORGANIZATION_OWNER',
], true);
}

/**
Expand All @@ -31,17 +34,30 @@ protected function supports(string $attribute, $subject): bool
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User || !$subject instanceof Request) {
if (!$user instanceof User) {
return false;
}
$userId = $user->id()->toString();

return $this->organizationQuery
->getByAlias($subject->get('organization'))
->map(function (Organization $organization) use ($user, $subject): bool {
$subject->attributes->set('organization', $organization);
if ($subject instanceof Organization) {
return $attribute === 'ROLE_ORGANIZATION_OWNER' ? $subject->isOwner($userId) : $subject->isMember($userId);
}

if ($subject instanceof Request) {
return $this->organizationQuery
->getByAlias($subject->get('organization'))
->map(function (Organization $organization) use ($userId, $subject, $attribute): bool {
$subject->attributes->set('organization', $organization);

if ($attribute === 'ROLE_ORGANIZATION_OWNER') {
return $organization->isOwner($userId);
}

return $organization->isMember($userId);
})
->getOrElse(false);
}

return $organization->isMember($user->id()->toString());
})
->getOrElse(false);
return false;
}
}
18 changes: 10 additions & 8 deletions templates/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@
<span class="d-none d-sm-block">Members</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ current_route == 'organization_settings' ? 'active' : '' }}" href="{{ path('organization_settings', {"organization":organization.alias}) }}">
<span class="nav-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
</span>
<span class="d-none d-sm-block">Settings</span>
</a>
</li>
{% if is_granted('ROLE_ORGANIZATION_OWNER', organization) %}
<li class="nav-item">
<a class="nav-link {{ current_route == 'organization_settings' ? 'active' : '' }}" href="{{ path('organization_settings', {"organization":organization.alias}) }}">
<span class="nav-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
</span>
<span class="d-none d-sm-block">Settings</span>
</a>
</li>
{% endif %}
{% else %}
<li class="nav-item">
<a class="nav-link {{ current_route == 'index' ? 'active' : '' }}" href="{{ path('index') }}">
Expand Down
14 changes: 8 additions & 6 deletions templates/bundles/TwigBundle/Exception/error.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
<p class="empty-subtitle text-muted">
{% block status_description %}{% endblock %}
</p>
<div class="empty-action">
<a href="{{ path('index') }}" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
Take me home
</a>
</div>
{% block button %}
<div class="empty-action">
<a href="{{ path('index') }}" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
Take me home
</a>
</div>
{% endblock %}
</div>
</div>
</body>
Expand Down
11 changes: 11 additions & 0 deletions templates/bundles/TwigBundle/Exception/error403.html.twig
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
{% extends "bundles/TwigBundle/Exception/error.html.twig" %}

{% block status_text %}Oops&hellip; Access Denied{% endblock %}

{% block status_description %}We are sorry but you do not have permission to access this page{% endblock %}

{% block button %}
<div class="empty-action">
<a href="#" onclick="window.history.back();" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
Take me back
</a>
</div>
{% endblock %}
44 changes: 25 additions & 19 deletions templates/organization/member/members.html.twig
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
{% extends "base.html.twig" %}

{% block header_btn %}
{% if invitations > 0 %}
<a href="{{ path('organization_invitations', {"organization":organization.alias}) }}" class="mr-5">
Pending invitations: {{ invitations }}
{% if is_granted('ROLE_ORGANIZATION_OWNER', organization) %}
{% if invitations > 0 %}
<a href="{{ path('organization_invitations', {"organization":organization.alias}) }}" class="mr-5">
Pending invitations: {{ invitations }}
</a>
{% endif %}
<a href="{{ path('organization_invite_member', {"organization":organization.alias}) }}" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
Invite new member
</a>
{% endif %}
<a href="{{ path('organization_invite_member', {"organization":organization.alias}) }}" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
Invite new member
</a>
{% endblock %}
{% block header %} {{ organization.name }} members:{% endblock %}

Expand All @@ -21,25 +23,29 @@
<tr>
<th>E-mail</th>
<th>Role</th>
<th>Options</th>
{% if is_granted('ROLE_ORGANIZATION_OWNER', organization) %}
<th>Options</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td>{{ member.email }}</td>
<td>{{ member.role }}</td>
<td>
<button
class="btn btn-danger btn-sm"
type="submit"
data-target="confirmation"
data-action="{{ path('organization_remove_member', {organization: organization.alias, user: member.userId}) }}"
data-method="DELETE"
>
Remove
</button>
</td>
{% if is_granted('ROLE_ORGANIZATION_OWNER', organization) %}
<td>
<button
class="btn btn-danger btn-sm"
type="submit"
data-target="confirmation"
data-action="{{ path('organization_remove_member', {organization: organization.alias, user: member.userId}) }}"
data-method="DELETE"
>
Remove
</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
Expand Down
Loading

0 comments on commit 673037d

Please sign in to comment.