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 members permissions #128

Merged
merged 1 commit into from
Apr 29, 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
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