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

Project creation auth for user without Admin rights Fixes #6187 #6593

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions pootle/apps/pootle_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
from django.conf.urls import include, url

from .views.admin import urls as admin_urls
from .views.user import urls as user_urls


urlpatterns = [
url(r'^admin/',
include(admin_urls)),
url(r'^xhr/admin/',
include(admin_urls.api_patterns)),
url(r'^my/',
include(user_urls)),
url(r'^xhr/my/',
include(user_urls.api_patterns)),
url(r'',
include('pootle_app.views.index.urls')),
]
6 changes: 4 additions & 2 deletions pootle/apps/pootle_app/views/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
# AUTHORS file for copyright and authorship information.

from .languages import LanguageAdminView, LanguageAPIView
from .projects import ProjectAdminView, ProjectAPIView
from .projects import (
ProjectGenericAdminView, ProjectAdminView, ProjectAPIView)
from .users import UserAdminView, UserAPIView


__all__ = (
'LanguageAdminView', 'LanguageAPIView', 'ProjectAdminView',
'LanguageAdminView', 'LanguageAPIView',
'ProjectGenericAdminView', 'ProjectAdminView',
'ProjectAPIView', 'UserAdminView', 'UserAPIView')
5 changes: 4 additions & 1 deletion pootle/apps/pootle_app/views/admin/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
User = get_user_model()

PERMISSIONS = {
'positive': ['view', 'suggest', 'translate', 'review', 'administrate'],
'positive': [
'view', 'suggest', 'translate', 'review',
'administrate', 'create_project'
],
'negative': ['hide'],
}

Expand Down
68 changes: 64 additions & 4 deletions pootle/apps/pootle_app/views/admin/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.

import json

from django.views.generic import TemplateView

from pootle.core.delegate import formats
from pootle.core.http import JsonResponse
from pootle.core.views import APIView
from pootle.core.views.decorators import (requires_permission_class,
set_permissions)
from pootle.core.views.mixins import SuperuserRequiredMixin
from pootle_app.forms import ProjectForm
from pootle_app.models.directory import Directory
from pootle_app.models.permissions import (PermissionSet,
check_user_permission,
get_pootle_permission)
from pootle_language.models import Language
from pootle_project.models import PROJECT_CHECKERS, Project


__all__ = ('ProjectAdminView', 'ProjectAPIView')


class ProjectAdminView(SuperuserRequiredMixin, TemplateView):
class ProjectGenericAdminView(TemplateView):
template_name = 'admin/projects.html'
page_code = 'admin-projects'

def get_context_data(self, **kwargs):
languages = Language.objects.exclude(code='templates')
Expand All @@ -42,7 +52,7 @@ def get_context_data(self, **kwargs):
in sorted(PROJECT_CHECKERS.keys())]

return {
'page': 'admin-projects',
'page': self.page_code,
'form_choices': {
'checkstyle': project_checker_choices,
'filetypes': filetypes,
Expand All @@ -55,11 +65,61 @@ def get_context_data(self, **kwargs):
}


class ProjectAPIView(SuperuserRequiredMixin, APIView):
class ProjectAdminView(SuperuserRequiredMixin, ProjectGenericAdminView):
pass


class ProjectAPIView(APIView):
model = Project
base_queryset = Project.objects.order_by('-id')
add_form_class = ProjectForm
edit_form_class = ProjectForm
page_size = 10
search_fields = ('code', 'fullname', 'disabled')
m2m = ("filetypes", )
m2m = ("filetypes",)

@property
def permission_context(self):
return Directory.objects.root

@set_permissions
@requires_permission_class("create_project")
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
exclude_projects = [project.pk
for project in self.base_queryset.all()
if not check_user_permission(
request.user,
"administrate",
project.directory
)]
self.base_queryset = self.base_queryset.exclude(
pk__in=exclude_projects)
return super(ProjectAPIView, self).dispatch(request,
*args, **kwargs)

def post(self, request, *args, **kwargs):
try:
request_dict = json.loads(request.body)
except ValueError:
return self.status_msg('Invalid JSON data', status=400)

form = self.add_form_class(request_dict)

if form.is_valid():
new_object = form.save()
permissionset = PermissionSet.objects.create(
user=request.user,
directory=new_object.directory
)
permissionset.positive_permissions.add(
get_pootle_permission("administrate")
)
request.user.permissionset_set.add(permissionset)

wrapper_qs = self.base_queryset.filter(pk=new_object.pk)
return JsonResponse(
self.qs_to_values(wrapper_qs, single_object=True)
)

return self.form_invalid(form)
13 changes: 13 additions & 0 deletions pootle/apps/pootle_app/views/user/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) Pootle contributors.
#
# This file is a part of the Pootle project. It is distributed under the GPL3
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.

from pootle_app.views.admin import ProjectAPIView
from .projects import ProjectUserView


__all__ = ('ProjectAPIView', 'ProjectUserView')
30 changes: 30 additions & 0 deletions pootle/apps/pootle_app/views/user/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) Pootle contributors.
#
# This file is a part of the Pootle project. It is distributed under the GPL3
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.


from pootle.core.views.decorators import (requires_permission_class,
set_permissions)
from pootle_app.models.directory import Directory
from pootle_app.views.admin import ProjectGenericAdminView


__all__ = ('ProjectUserView',)


class ProjectUserView(ProjectGenericAdminView):
template_name = 'projects/user/projects.html'
page_code = 'user-projects'

@property
def permission_context(self):
return Directory.objects.root

@set_permissions
@requires_permission_class("create_project")
def dispatch(self, request, *args, **kwargs):
return super(ProjectUserView, self).dispatch(request, *args, **kwargs)
31 changes: 31 additions & 0 deletions pootle/apps/pootle_app/views/user/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) Pootle contributors.
#
# This file is a part of the Pootle project. It is distributed under the GPL3
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.

from django.conf.urls import url

from . import ProjectAPIView, ProjectUserView


urlpatterns = [
url(r'^projects/$',
ProjectUserView.as_view(),
name='pootle-user-projects'),
url(r'^projects/(?P<id>[0-9]*)/?$',
ProjectUserView.as_view(),
name='pootle-user-project-edit'),
]


api_patterns = [
url(r'^projects/?$',
ProjectAPIView.as_view(),
name='pootle-xhr-user-projects'),
url(r'^projects/(?P<id>[0-9]*)/?$',
ProjectAPIView.as_view(),
name='pootle-xhr-user-project'),
]
2 changes: 2 additions & 0 deletions pootle/apps/pootle_misc/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def pootle_context(request):
'POOTLE_SIGNUP_ENABLED': settings.POOTLE_SIGNUP_ENABLED,
'SCRIPT_NAME': settings.SCRIPT_NAME,
'POOTLE_CACHE_TIMEOUT': settings.POOTLE_CACHE_TIMEOUT,
'POOTLE_PROJECTADMIN_CAN_EDITPROJECTS':
settings.POOTLE_PROJECTADMIN_CAN_EDITPROJECTS,
'DEBUG': settings.DEBUG,
},
'custom': settings.POOTLE_CUSTOM_TEMPLATE_CONTEXT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.10 on 2016-10-06 13:51
from __future__ import unicode_literals

from django.contrib.contenttypes.management import update_contenttypes
from django.db import migrations

def create_permission_add_project(apps, schema_editor):
update_contenttypes(apps.app_configs['pootle_project'])
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
try:
pootle_project_model = ContentType.objects.get(
app_label="pootle_app",
model="directory"
)
try:
add_project_permission = Permission.objects.get(
codename="create_project",
content_type=pootle_project_model
)
except Permission.DoesNotExist:
pootle_project_model = ContentType.objects.get(
app_label="pootle_app",
model="directory"
)
add_project_permission = Permission(
content_type=pootle_project_model,
name="Can create a project",
codename="create_project"
)
add_project_permission.save()
except ContentType.DoesNotExist:
# this means the content types has not been
# created yet by the migration tool, thus the migration
# is starting from beginning (?). That's why, this migration
# is not required and the permission will be created when
# populating the database with initdb script.
pass




class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0001_initial'),
('auth', '0001_initial'),
('pootle_project', '0016_change_treestyle_choices_label'),
]

operations = [
migrations.RunPython(create_permission_add_project),
]
Empty file.
24 changes: 24 additions & 0 deletions pootle/apps/pootle_project/templatetags/check_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) Pootle contributors.
#
# This file is a part of the Pootle project. It is distributed under the GPL3
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.

from django import template

from pootle.core.views.decorators import check_directory_permission
from pootle_app.models import Directory


register = template.Library()


@register.filter('can_create_project')
def can_create_project(request):
return check_directory_permission(
"create_project",
request,
Directory.objects.root
)
4 changes: 4 additions & 0 deletions pootle/core/initdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def create_pootle_permissions(self):
'name': _("Can perform administrative tasks"),
'codename': "administrate",
},
{
'name': _("Can create a project"),
'codename': "create_project",
},
]

criteria = {
Expand Down
21 changes: 20 additions & 1 deletion pootle/core/views/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from django.core.exceptions import PermissionDenied

from pootle.i18n.gettext import ugettext as _
from pootle_app.models.permissions import get_matching_permissions
from pootle_app.models.permissions import (check_permission,
get_matching_permissions)


def check_directory_permission(permission_codename, request, directory):
Expand Down Expand Up @@ -71,3 +72,21 @@ def method_wrapper(self, request, *args, **kwargs):
return f(self, request, *args, **kwargs)
return method_wrapper
return class_wrapper


def requires_permission_class(permission):

def class_wrapper(f):

@functools.wraps(f)
def method_wrapper(self, request, *args, **kwargs):
has_permission = (
request.user.is_authenticated
and check_permission(permission, request))

if not has_permission:
raise PermissionDenied(
_("Insufficient rights to access this page."), )
return f(self, request, *args, **kwargs)
return method_wrapper
return class_wrapper
3 changes: 3 additions & 0 deletions pootle/settings/30-site.conf
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ POOTLE_CONTACT_ENABLED = True
# Whether to email reviewer's feedback to suggesters.
POOTLE_EMAIL_FEEDBACK_ENABLED = False

# Whether administrators of projects can edit projects
POOTLE_PROJECTADMIN_CAN_EDITPROJECTS = True

# By default Pootle uses SMTP server on localhost, if the server is
# not configured for sending emails use these settings to setup an
# external outgoing SMTP server.
Expand Down
5 changes: 4 additions & 1 deletion pootle/templates/layout.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load core i18n assets locale profile_tags static statici18n %}
{% load check_permissions core i18n assets locale profile_tags static statici18n %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" dir="{% locale_dir %}" id="{% block html_id %}pootle-base{% endblock %}" class="{{ settings.POOTLE_INSTANCE_ID }}">
Expand Down Expand Up @@ -130,6 +130,9 @@ <h1 class="brand">
<ul class="dropdown-menu">
<li><a href="{{ user.get_absolute_url }}">{% trans 'Public Profile' %}</a></li>
<li><a href="{% url 'pootle-user-settings' user.username %}">{% trans 'Settings' %}</a></li>
{% if user.is_superuser or request|can_create_project %}
<li><a href="{% url 'pootle-user-projects' %}">{% trans "My Projects" %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a class="admin" href="{% url 'pootle-admin' %}">{% trans 'Admin' %}</a></li>
{% endif %}
Expand Down
Loading