Skip to content

Commit 40bb231

Browse files
committed
Merged in [11064] [11082] from housley@vigilsec.com:
The secretariat and the Team Chair can now edit team groups. In addition, if the team in within the IETF, Area Directors can edit it. And, if the team is within the IRTF, the IRTF Chair can edit it. Cleaned up the checking permission for a user to manage a group. Also, cleanly handle a set of group parent links did for a loop. Fixes ietf-tools#1915. - Legacy-Id: 11091 Note: SVN reference [11064] has been migrated to Git commit 1c509cd Note: SVN reference [11082] has been migrated to Git commit 858530c
2 parents d2cd382 + 858530c commit 40bb231

8 files changed

Lines changed: 107 additions & 26 deletions

File tree

ietf/doc/views_charter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
derive_new_work_text )
2424
from ietf.doc.mails import email_state_changed, email_charter_internal_review
2525
from ietf.group.models import ChangeStateGroupEvent, MilestoneGroupEvent
26-
from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_group_type
26+
from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_group
2727
from ietf.ietfauth.utils import has_role, role_required
2828
from ietf.name.models import GroupStateName
2929
from ietf.person.models import Person
@@ -58,7 +58,7 @@ def change_state(request, name, option=None):
5858
charter = get_object_or_404(Document, type="charter", name=name)
5959
group = charter.group
6060

61-
if not can_manage_group_type(request.user, group.type_id):
61+
if not can_manage_group(request.user, group):
6262
return HttpResponseForbidden("You don't have permission to access this view")
6363

6464
chartering_type = get_chartering_type(charter)
@@ -246,7 +246,7 @@ def change_title(request, name, option=None):
246246
logging the title as a comment."""
247247
charter = get_object_or_404(Document, type="charter", name=name)
248248
group = charter.group
249-
if not can_manage_group_type(request.user, group.type_id):
249+
if not can_manage_group(request.user, group):
250250
return HttpResponseForbidden("You don't have permission to access this view")
251251
login = request.user.person
252252
if request.method == 'POST':
@@ -359,7 +359,7 @@ def submit(request, name=None, option=None):
359359
charter = get_object_or_404(Document, type="charter", name=name)
360360
group = charter.group
361361

362-
if not can_manage_group_type(request.user, group.type_id):
362+
if not can_manage_group(request.user, group):
363363
return HttpResponseForbidden("You don't have permission to access this view")
364364

365365
path = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev))

ietf/doc/views_doc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
5353
from ietf.community.models import CommunityList
5454
from ietf.group.models import Role
55-
from ietf.group.utils import can_manage_group_type, can_manage_materials
55+
from ietf.group.utils import can_manage_group, can_manage_materials
5656
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
5757
from ietf.name.models import StreamName, BallotPositionName
5858
from ietf.person.models import Email
@@ -445,7 +445,7 @@ def document_main(request, name, rev=None):
445445
if chartering and not snapshot:
446446
milestones = doc.group.groupmilestone_set.filter(state="charter")
447447

448-
can_manage = can_manage_group_type(request.user, doc.group.type_id)
448+
can_manage = can_manage_group(request.user, doc.group)
449449

450450
return render_to_response("doc/document_charter.html",
451451
dict(doc=doc,

ietf/group/edit.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ietf.doc.utils_charter import charter_name_for_group
1717
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
1818
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
19-
from ietf.group.utils import save_group_in_history, can_manage_group_type
19+
from ietf.group.utils import save_group_in_history, can_manage_group
2020
from ietf.group.utils import get_group_or_404
2121
from ietf.ietfauth.utils import has_role
2222
from ietf.person.fields import SearchableEmailsField
@@ -186,7 +186,7 @@ def submit_initial_charter(request, group_type=None, acronym=None):
186186
# This is where we start ignoring the passed in group_type
187187
group_type = group.type_id
188188

189-
if not can_manage_group_type(request.user, group_type):
189+
if not can_manage_group(request.user, group):
190190
return HttpResponseForbidden("You don't have permission to access this view")
191191

192192
if not group.charter:
@@ -199,20 +199,20 @@ def submit_initial_charter(request, group_type=None, acronym=None):
199199
def edit(request, group_type=None, acronym=None, action="edit"):
200200
"""Edit or create a group, notifying parties as
201201
necessary and logging changes as group events."""
202-
if not can_manage_group_type(request.user, group_type):
203-
return HttpResponseForbidden("You don't have permission to access this view")
204-
205202
if action == "edit":
206-
group = get_object_or_404(Group, acronym=acronym)
207203
new_group = False
208204
elif action in ("create","charter"):
209205
group = None
210206
new_group = True
211207
else:
212208
raise Http404
213209

214-
if not group_type and group:
215-
group_type = group.type_id
210+
if not new_group:
211+
group = get_group_or_404(acronym, group_type)
212+
if not group_type and group:
213+
group_type = group.type_id
214+
if not (can_manage_group(request.user, group) or group.has_role(request.user, "chair")):
215+
return HttpResponseForbidden("You don't have permission to access this view")
216216

217217
if request.method == 'POST':
218218
form = GroupForm(request.POST, group=group, group_type=group_type)
@@ -364,7 +364,7 @@ def conclude(request, acronym, group_type=None):
364364
"""Request the closing of group, prompting for instructions."""
365365
group = get_group_or_404(acronym, group_type)
366366

367-
if not can_manage_group_type(request.user, group.type_id):
367+
if not can_manage_group(request.user, group):
368368
return HttpResponseForbidden("You don't have permission to access this view")
369369

370370
if request.method == 'POST':

ietf/group/info.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
from ietf.doc.templatetags.ietf_filters import clean_whitespace
5656
from ietf.group.models import Group, Role, ChangeStateGroupEvent
5757
from ietf.name.models import GroupTypeName
58-
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type, can_provide_status_update
58+
from ietf.group.utils import get_charter_text, can_manage_group_type, can_manage_group, milestone_reviewer_for_group_type, can_provide_status_update
5959
from ietf.group.utils import can_manage_materials, get_group_or_404
6060
from ietf.utils.pipe import pipe
6161
from ietf.utils.textupload import get_cleaned_text_file_content
@@ -365,7 +365,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
365365
actions = []
366366

367367
is_chair = group.has_role(request.user, "chair")
368-
can_manage = can_manage_group_type(request.user, group.type_id)
368+
can_manage = can_manage_group(request.user, group)
369369

370370
if group.features.has_milestones:
371371
if group.state_id != "proposed" and (is_chair or can_manage):
@@ -374,7 +374,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
374374
if group.features.has_materials and can_manage_materials(request.user, group):
375375
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
376376

377-
if group.type_id in ("rg", "wg") and group.state_id != "conclude" and can_manage:
377+
if group.state_id != "conclude" and (is_chair or can_manage):
378378
actions.append((u"Edit group", urlreverse("group_edit", kwargs=kwargs)))
379379

380380
if group.features.customize_workflow and (is_chair or can_manage):
@@ -489,7 +489,7 @@ def group_about(request, acronym, group_type=None):
489489
e = group.latest_event(type__in=("changed_state", "requested_close",))
490490
requested_close = group.state_id != "conclude" and e and e.type == "requested_close"
491491

492-
can_manage = can_manage_group_type(request.user, group.type_id)
492+
can_manage = can_manage_group(request.user, group)
493493

494494
can_provide_update = can_provide_status_update(request.user, group)
495495
status_update = group.latest_event(type="status_update")

ietf/group/milestones.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ietf.doc.utils import get_chartering_type
1313
from ietf.doc.fields import SearchableDocumentsField
1414
from ietf.group.models import GroupMilestone, MilestoneGroupEvent
15-
from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type,
15+
from ietf.group.utils import (save_milestone_in_history, can_manage_group, milestone_reviewer_for_group_type,
1616
get_group_or_404)
1717
from ietf.name.models import GroupMilestoneStateName
1818
from ietf.group.mails import email_milestones_changed
@@ -93,8 +93,8 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
9393
raise Http404
9494

9595
needs_review = False
96-
if not can_manage_group_type(request.user, group.type_id):
97-
if group.role_set.filter(name="chair", person__user=request.user):
96+
if not can_manage_group(request.user, group):
97+
if group.has_role(request.user, "chair"):
9898
if milestone_set == "current":
9999
needs_review = True
100100
else:
@@ -329,8 +329,9 @@ def reset_charter_milestones(request, group_type, acronym):
329329
if not group.features.has_milestones:
330330
raise Http404
331331

332-
if (not can_manage_group_type(request.user, group_type) and
333-
not group.role_set.filter(name="chair", person__user=request.user)):
332+
can_manage = can_manage_group(request.user, group)
333+
is_chair = group.has_role(request.user, "chair")
334+
if (not can_manage) and (not is_chair):
334335
return HttpResponseForbidden("You are not chair of this group.")
335336

336337
current_milestones = group.groupmilestone_set.filter(state="active")

ietf/group/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ def has_role(self, user, role_names):
8080
role_names = [role_names]
8181
return user.is_authenticated() and self.role_set.filter(name__in=role_names, person__user=user).exists()
8282

83+
def is_decendant_of(self, sought_parent):
84+
p = self.parent
85+
while ((p != None) and (p != self)):
86+
if p.acronym == sought_parent:
87+
return True
88+
p = p.parent
89+
return False
90+
8391
def is_bof(self):
8492
return (self.state.slug in ["bof", "bof-conc"])
8593

ietf/group/tests_info.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ietf.person.models import Person, Email
2626
from ietf.utils.test_utils import TestCase, unicontent
2727
from ietf.utils.mail import outbox, empty_outbox
28-
from ietf.utils.test_data import make_test_data
28+
from ietf.utils.test_data import make_test_data, create_person
2929
from ietf.utils.test_utils import login_testing_unauthorized
3030
from ietf.group.factories import GroupFactory, RoleFactory, GroupEventFactory
3131
from ietf.meeting.factories import SessionFactory
@@ -240,14 +240,27 @@ def test_group_charter(self):
240240
self.assertTrue(milestone.docs.all()[0].name in unicontent(r))
241241

242242
def test_group_about(self):
243+
244+
def verify_cannot_edit_group(username):
245+
self.client.login(username=username, password=username+"+password")
246+
r = self.client.get(url)
247+
self.assertEqual(r.status_code, 403)
248+
249+
def verify_can_edit_group(username):
250+
self.client.login(username=username, password=username+"+password")
251+
r = self.client.get(url)
252+
self.assertEqual(r.status_code, 200)
253+
243254
make_test_data()
244255
group = Group.objects.create(
245256
type_id="team",
246257
acronym="testteam",
247258
name="Test Team",
248259
description="The test team is testing.",
249260
state_id="active",
261+
parent = Group.objects.get(acronym="farfut"),
250262
)
263+
create_person(group, "chair", name="Testteam Chairman", username="teamchairman")
251264

252265
for url in [group.about_url(),
253266
urlreverse('ietf.group.info.group_about',kwargs=dict(acronym=group.acronym)),
@@ -260,6 +273,14 @@ def test_group_about(self):
260273
self.assertTrue(group.acronym in unicontent(r))
261274
self.assertTrue(group.description in unicontent(r))
262275

276+
url = urlreverse('ietf.group.edit.edit', kwargs=dict(acronym=group.acronym))
277+
278+
for username in ['plain','iana','iab chair','irtf chair','marschairman']:
279+
verify_cannot_edit_group(username)
280+
281+
for username in ['secretary','teamchairman','ad']:
282+
verify_can_edit_group(username)
283+
263284
def test_materials(self):
264285
make_test_data()
265286
group = Group.objects.create(type_id="team", acronym="testteam", name="Test Team", state_id="active")
@@ -1104,5 +1125,44 @@ def test_edit_status_update(self):
11041125
self.assertEqual(response.status_code, 302)
11051126
self.assertEqual(chair.group.latest_event(type='status_update').desc,'This came from a file.')
11061127

1128+
class GroupParentLoopTests(TestCase):
11071129

1108-
1130+
def test_group_parent_loop(self):
1131+
make_test_data()
1132+
mars = Group.objects.get(acronym="mars")
1133+
test1 = Group.objects.create(
1134+
type_id="team",
1135+
acronym="testteam1",
1136+
name="Test One",
1137+
description="The test team 1 is testing.",
1138+
state_id="active",
1139+
parent = mars,
1140+
)
1141+
test2 = Group.objects.create(
1142+
type_id="team",
1143+
acronym="testteam2",
1144+
name="Test Two",
1145+
description="The test team 2 is testing.",
1146+
state_id="active",
1147+
parent = test1,
1148+
)
1149+
# Change the parent of Mars to make a loop
1150+
mars.parent = test2
1151+
1152+
# In face of the loop in the parent links, the code should not loop forever
1153+
import signal
1154+
1155+
def timeout_handler(signum, frame):
1156+
raise Exception("Infinite loop in parent links is not handeled properly.")
1157+
1158+
signal.signal(signal.SIGALRM, timeout_handler)
1159+
signal.alarm(1) # One second
1160+
try:
1161+
test2.is_decendant_of("ietf")
1162+
except Exception:
1163+
raise
1164+
finally:
1165+
signal.alarm(0)
1166+
1167+
# If we get here, then there is not an infinite loop
1168+
return

ietf/group/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ def can_manage_group_type(user, group_type):
9191

9292
return has_role(user, 'Secretariat')
9393

94+
def can_manage_group(user, group):
95+
if group.type_id == "rg":
96+
return has_role(user, ('IRTF Chair', 'Secretariat'))
97+
elif group.type_id == "wg":
98+
return has_role(user, ('Area Director', 'Secretariat'))
99+
elif group.type_id == "team":
100+
if group.is_decendant_of("ietf"):
101+
return has_role(user, ('Area Director', 'Secretariat'))
102+
elif group.is_decendant_of("irtf"):
103+
return has_role(user, ('IRTF Chair', 'Secretariat'))
104+
return has_role(user, ('Secretariat'))
105+
94106
def milestone_reviewer_for_group_type(group_type):
95107
if group_type == "rg":
96108
return "IRTF Chair"

0 commit comments

Comments
 (0)