Commit 01231fe0 authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Merge branch 'release-0.13' into develop

Conflicts:
	snf-astakos-app/astakos/im/messages.py
	snf-astakos-app/astakos/im/views.py
	version
parents 2e673d33 db4b5872
......@@ -166,7 +166,11 @@ class ObjectMigration(Migration):
self.create_tags(headerid, nodeid, vserials)
#set object's publicity
if public:
self.backend.permissions.public_set(object)
self.backend.permissions.public_set(
object,
self.backend.public_url_security,
self.backend.public_url_alphabet
)
#set object's permissions
self.create_permissions(headerid, object, username, is_folder=False)
......
......@@ -565,14 +565,33 @@ class ContainerGet(BaseTestCase):
self.assertEqual(objs, ['folder', 'folder/object'])
def test_list_public(self):
token = OTHER_ACCOUNTS.keys()[0]
account = OTHER_ACCOUNTS[token]
cl = Pithos_Client(get_url(), token, account)
self.client.publish_object(self.container[0], self.obj[0]['name'])
objs = self.client.list_objects(self.container[0], public=True)
self.assertEqual(objs, [self.obj[0]['name']])
self.assert_raises_fault(
403, cl.list_objects, self.container[0], public=True,
account=get_user()
)
self.client.share_object(
self.container[0], self.obj[1]['name'], [account]
)
objs = cl.list_objects(
self.container[0], public=True, account=get_user()
)
self.assertTrue(self.obj[0]['name'] not in objs)
# create child object
self.upload_random_data(self.container[0], strnextling(self.obj[0]['name']))
objs = self.client.list_objects(self.container[0], public=True)
self.assertEqual(objs, [self.obj[0]['name']])
objs = cl.list_objects(
self.container[0], public=True, account=get_user()
)
self.assertTrue(self.obj[0]['name'] not in objs)
# test inheritance
self.client.create_folder(self.container[1], 'folder')
......@@ -580,18 +599,35 @@ class ContainerGet(BaseTestCase):
self.upload_random_data(self.container[1], 'folder/object')
objs = self.client.list_objects(self.container[1], public=True)
self.assertEqual(objs, ['folder'])
self.assert_raises_fault(
403, cl.list_objects, self.container[1], public=True,
account=get_user()
)
def test_list_shared_public(self):
token = OTHER_ACCOUNTS.keys()[0]
account = OTHER_ACCOUNTS[token]
cl = Pithos_Client(get_url(), token, account)
self.client.share_object(self.container[0], self.obj[0]['name'], ('*',))
self.client.publish_object(self.container[0], self.obj[1]['name'])
objs = self.client.list_objects(self.container[0], shared=True, public=True)
self.assertEqual(objs, [self.obj[0]['name'], self.obj[1]['name']])
objs = cl.list_objects(
self.container[0], shared=True, public=True, account=get_user()
)
self.assertEqual(objs, [self.obj[0]['name']])
# create child object
self.upload_random_data(self.container[0], strnextling(self.obj[0]['name']))
self.upload_random_data(self.container[0], strnextling(self.obj[1]['name']))
objs = self.client.list_objects(self.container[0], shared=True, public=True)
self.assertEqual(objs, [self.obj[0]['name'], self.obj[1]['name']])
objs = cl.list_objects(
self.container[0], shared=True, public=True, account=get_user()
)
self.assertEqual(objs, [self.obj[0]['name']])
# test inheritance
self.client.create_folder(self.container[1], 'folder1')
......@@ -602,6 +638,10 @@ class ContainerGet(BaseTestCase):
o = self.upload_random_data(self.container[1], 'folder2/object')
objs = self.client.list_objects(self.container[1], shared=True, public=True)
self.assertEqual(objs, ['folder1', 'folder1/object', 'folder2'])
objs = cl.list_objects(
self.container[1], shared=True, public=True, account=get_user()
)
self.assertEqual(objs, ['folder1', 'folder1/object'])
def test_list_objects(self):
objects = self.client.list_objects(self.container[0])
......@@ -2357,6 +2397,14 @@ class TestPublish(BaseTestCase):
data = resp.read(length)
self.assertEqual(o_data, data)
token = OTHER_ACCOUNTS.keys()[0]
account = OTHER_ACCOUNTS[token]
cl = Pithos_Client(get_url(), token, account)
self.client.share_object('c', 'o', (account,))
meta = cl.retrieve_object_metadata('c', 'o', account=get_user())
self.assertTrue('x-object-public' not in meta)
class TestPolicies(BaseTestCase):
def test_none_versioning(self):
self.client.create_container('c', policies={'versioning':'none'})
......
......@@ -1166,15 +1166,15 @@ Synnefo Upgrade Notes
.. toctree::
:maxdepth: 1
upgrade-0.13
v0.12 -> v0.13 <upgrade/upgrade-0.13>
Older Cyclades upgrade notes
Older Cyclades Upgrade Notes
============================
.. toctree::
:maxdepth: 2
cyclades-upgrade
upgrade/cyclades-upgrade
Changelog
=========
......@@ -27,6 +27,8 @@ Document Revisions
========================= ================================
Revision Description
========================= ================================
0.13 (Mar 27, 2013) Restrict public object listing only to the owner.
Do not propagate public URL information in shared objects.
0.13 (Jan 21, 2013) Proxy identity management services
\ UUID to displayname translation
0.9 (Feb 17, 2012) Change permissions model.
......@@ -367,7 +369,7 @@ limit The amount of results requested (default is 10000)
marker Return containers with name lexicographically after marker
format Optional extended reply type (can be ``json`` or ``xml``)
shared Show only shared containers (no value parameter)
public Show only public containers (no value parameter)
public Show only public containers (no value parameter / avalaible only for owner requests)
until Optional timestamp
====================== =========================
......@@ -542,8 +544,8 @@ delimiter Return objects up to the delimiter (discussion follows)
path Assume ``prefix=path`` and ``delimiter=/``
format Optional extended reply type (can be ``json`` or ``xml``)
meta Return objects that satisfy the key queries in the specified comma separated list (use ``<key>``, ``!<key>`` for existence queries, ``<key><op><value>`` for value queries, where ``<op>`` can be one of ``=``, ``!=``, ``<=``, ``>=``, ``<``, ``>``)
shared Show only shared objects (no value parameter)
public Show only public containers (no value parameter)
shared Show only objects (no value parameter)
public Show only public objects (no value parameter / avalaible only for owner reqeusts)
until Optional timestamp
====================== ===================================
......@@ -1136,6 +1138,8 @@ Read and write control in Pithos is managed by setting appropriate permissions w
A user may ``GET`` another account or container. The result will include a limited reply, containing only the allowed containers or objects respectively. A top-level request with an authentication token, will return a list of allowed accounts, so the user can easily find out which other users share objects. The ``X-Object-Allowed-To`` header lists the actions allowed on an object, if it does not belong to the requesting user.
Shared objects that are also public do not expose the ``X-Object-Public`` meta information.
Objects that are marked as public, via the ``X-Object-Public`` meta, are also available at the corresponding URI returned for ``HEAD`` or ``GET``. Requests for public objects do not need to include an ``X-Auth-Token``. Pithos will ignore request parameters and only include the following headers in the reply (all ``X-Object-*`` meta is hidden):
========================== ===============================
......
......@@ -103,6 +103,7 @@ for a single user from the command line
raise CommandError("Cannot combine option `--from-file' with "
"`--set-capacity'.")
self.import_from_file(from_file)
return
if set_capacity is not None:
user, resource, capacity = set_capacity
......
......@@ -124,8 +124,7 @@ VERIFICATION_SEND_ERR = EMAIL_SEND_ERR % 'verification'
INVITATION_SEND_ERR = EMAIL_SEND_ERR % 'invitation'
GREETING_SEND_ERR = EMAIL_SEND_ERR % 'greeting'
FEEDBACK_SEND_ERR = EMAIL_SEND_ERR % 'feedback'
CHANGE_EMAIL_SEND_ERR = EMAIL_SEND_ERR % 'feedback'
CHANGE_EMAIL_SEND_ERR = EMAIL_SEND_ERR % 'email change'
NOTIFICATION_SEND_ERR = EMAIL_SEND_ERR % 'notification'
DETAILED_NOTIFICATION_SEND_ERR = 'Failed to send %(subject)s notification to %(recipients)s.'
......
......@@ -299,7 +299,7 @@ RESOURCES_PRESENTATION_DATA = getattr(
'is_abbreviation':False,
'report_desc':'Private Networks',
'placeholder':'eg. 1',
'verbose_name':'private network'
'verbose_name':'Private Network'
}
},
......@@ -347,7 +347,7 @@ PROJECT_ADMINS = getattr(settings, 'ASTAKOS_PROJECT_ADMINS', set())
# This is to reduce the volume of applications
# in case users abuse the mechanism.
PENDING_APPLICATION_LIMIT = getattr(settings,
'ASTAKOS_PENDING_APPLICATION_LIMIT', 1)
'ASTAKOS_PENDING_APPLICATION_LIMIT', 0)
# OAuth2 Twitter credentials.
TWITTER_TOKEN = getattr(settings, 'ASTAKOS_TWITTER_TOKEN', '')
......
......@@ -253,6 +253,7 @@ dl.alt-style dd { overflow:hidden; }
.projects h3 { font-size:1.154em; }
.projects .submit-rt { margin:0; text-align:right; }
.projects +.buttons-list.fixpos { left:0; right:auto; }
.project-actions a { font-size: 0.7em }
/* new faq-userguide styles */
......@@ -512,10 +513,17 @@ form input[type="text"]:-ms-input-placeholder,
.projects .editable form textarea { width:70%; height:50px; max-width:70%; width:270px; height:120px;}
table .msg-wrap { position:relative; display:inline-block; }
table .msg-wrap .dialog { position:absolute; border:1px dashed #ccc; padding:15px; width:200px; bottom:30px; right:0; background:#fff; display:none; }
table .msg-wrap .dialog .submit { min-width:30px; padding:5px 22px; }
table .msg-wrap .dialog .no.submit { float:right; }
h2 .msg-wrap { font-size: 0.7em }
.msg-wrap { position:relative; display:inline-block; }
.msg-wrap .dialog { position:absolute; display: none;}
.msg-wrap .dialog-content { border:1px dashed #ccc; padding:15px; width:200px; bottom:30px; right:0; background:#fff;}
.msg-wrap .dialog .submit { min-width:30px; padding:5px 22px; }
.msg-wrap .dialog .no.submit { float:right; }
.msg-wrap.inline form.link-like { float: none !important; padding:0 !important; margin: 0 !important}
.msg-wrap.inline { display: inline; }
.msg-wrap textarea { width: 90%; height: 100px; padding:4%; font-size: 1.1em;}
table.alt-style .centered { text-align:center; }
table.alt-style form.link-like { float:none}
table.alt-style .project_action div:first-child form { margin:0; }
......
......@@ -229,9 +229,27 @@ $(document).ready(function() {
});
$("input.leave, input.join").click(function () {
$('dialog').hide();
$("input.leave, input.join").click(function (e) {
e.preventDefault();
var form = $(this).parents('form');
var dialog = $(this).parents('.msg-wrap').find('.dialog');
$('.dialog').hide();
$(this).parents('.msg-wrap').find('.dialog').show();
var offset = dialog.offset();
if (offset.left <= 10) {
dialog.css({'left': '10px'})
}
if (offset.top <= 10) {
dialog.css({'top': '10px'})
}
if (dialog.find('textarea').length > 0) {
dialog.find('textarea').val('');
dialog.find('textarea').focus();
}
return false;
});
......@@ -243,7 +261,20 @@ $(document).ready(function() {
$('.msg-wrap .yes').click( function(e){
e.preventDefault();
$(this).parents('.dialog').siblings('form').submit();
var dialog = $(this).parents('.msg-wrap').find('.dialog');
var form = $(this).parents('.msg-wrap').find('form');
var fields = dialog.find('input, textarea')
var toremove = [];
fields.each(function(){
var f = $(this).clone();
f.hide();
form.append(f);
f.val($(this).val());
toremove.push(f);
});
form.submit();
})
$('.hidden-submit input[readonly!="True"]').focus(function () {
......@@ -338,4 +369,4 @@ $(window).resize(function() {
setContainerMinHeight('.container .wrapper');
});
\ No newline at end of file
});
......@@ -278,7 +278,7 @@ class UserProjectApplicationsTable(UserTable):
def render_membership_status(self, record, *args, **kwargs):
if self.user.owns_application(record):
if self.user.owns_application(record) or self.user.is_project_admin():
return record.project_state_display()
else:
try:
......
{% load astakos_tags i18n %}
<!-- make room for buttons -->
{% if owner_mode or admin_mode or can_join_request or can_leave_request %}
<br />
{% endif %}
<div class="project-actions">
{% if owner_mode or admin_mode %}
<a class="owner-action" href="{% url astakos.im.views.project_modify object.pk %}">MODIFY</a>
{% if owner_mode %}
{% with object.last_pending_incl_me as last_pending %}
{% if last_pending %}
{% if object.project_exists %}
- {% confirm_link "CANCEL PROJECT MODIFICATION" "project_modification_cancel" "project_app_cancel" last_pending.pk "" "OK" %}
{% else %}
- {% confirm_link "CANCEL PROJECT APPLICATION" "project_app_cancel" "project_app_cancel" last_pending.pk "" "OK" %}
{% endif %}
{% endif %}
{% endwith %}
{% endif %}
{% if admin_mode %}
{% if object.can_approve %}
- {% confirm_link "APPROVE" "project_app_approve" "project_app_approve" object.pk "" "OK" %}
- {% confirm_link "DENY" "project_app_deny" "project_app_deny" object.pk %}
{% endif %}
{% endif %}
{% if owner_mode %}
{% if object.can_dismiss %}
- {% confirm_link "DISMISS" "project_app_dismiss" "project_app_dismiss" object.pk %}
{% endif %}
{% endif %}
<!-- only one is possible, perhaps add cancel button too -->
{% if can_join_request or can_leave_request %}
-
{% endif %}
{% endif %}
{% if can_join_request %}
{% confirm_link "JOIN" "project_join" "project_join" project.pk %}
{% endif %}
{% if can_leave_request %}
{% confirm_link "LEAVE" "project_leave" "project_leave" project.pk %}
{% endif %}
</div>
......@@ -47,62 +47,10 @@
{% endif %}
{{ object.name|upper }}
</span>
<!-- make room for buttons -->
{% if owner_mode or admin_mode or can_join_request or can_leave_request %}
<br />
{% endif %}
{% if owner_mode or admin_mode %}
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_modify object.pk %}">MODIFY</a>
{% if owner_mode %}
{% with object.last_pending_incl_me as last_pending %}
{% if last_pending %}
-
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_cancel last_pending.pk %}">
CANCEL PROJECT {% if object.project_exists %} MODIFICATION {% else %}
APPLICATION {% endif %}
</a>
{% endif %}
{% endwith %}
{% endif %}
{% if admin_mode %}
{% if object.can_approve %}
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_approve object.pk %}">
APPROVE</a>
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_deny object.pk %}">
DENY</a>
{% endif %}
{% endif %}
{% if owner_mode %}
{% if object.can_dismiss %}
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_dismiss object.pk %}">
DISMISS</a>
{% endif %}
{% endif %}
<!-- only one is possible, perhaps add cancel button too -->
{% if can_join_request or can_leave_request %}
<br />
{% endif %}
{% endif %}
{% if can_join_request %}
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_join project.pk %}">JOIN</a>
{% endif %}
{% if can_leave_request %}
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_leave project.pk %}">LEAVE</a>
{% endif %}
{% block project.actions %}
{% include "im/projects/_project_detail_actions.html" %}
{% endblock %}
</h2>
<div class="full-dotted">
......
......@@ -79,7 +79,7 @@
<div class="form-row submit">
<input type="submit" value="BACK" class="submit lt" onclick='this.form.action="?edit=1&verify=0";'>
<input type="submit" value="SUBMIT" class="submit" >
<a href="{% url project_add %}" class="rt-link">CANCEL</a>
<a href="{% url project_list %}" class="rt-link">CANCEL</a>
</div>
</form>
......
<div class="msg-wrap">
<div class="msg-wrap {% if inline %}inline{% endif %}">
{% if url %}
{% if confirm %}
<form method="{{ col.method }}"
......@@ -8,6 +8,7 @@
<input type="submit" value="{{ action }}" class="join_group join" />
</form>
<div class="dialog" style="display: none">
<div class="dialog-content">
{{ prompt }}
<br />
<br />
......@@ -15,6 +16,7 @@
&nbsp;
<a href="#" class="no submit">{{ col.cancel_prompt }}</a>
</div>
</div>
{% else %}
<a href="{{ url }}">{{ action }}</a>
{% endif %}
......
......@@ -39,6 +39,11 @@ from django import template
from django.core.urlresolvers import resolve
from django.conf import settings
from django.template import TemplateSyntaxError, Variable
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
from django.template import RequestContext
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
register = template.Library()
......@@ -207,3 +212,61 @@ def provider_login_url(context, provider, from_login=False):
return "%s%s%s" % (url, joinchar, urllib.urlencode(attrs))
EXTRA_CONTENT_MAP = {
'confirm_text': '<textarea name="reason"></textarea>'
}
CONFIRM_LINK_PROMPT_MAP = {
'project_modification_cancel': _('Are you sure you want to dismiss this '
'project ?'),
'project_app_cancel': _('Are you sure you want to cancel this project ?'),
'project_app_approve': _('Are you sure you want to approve this '
'project ?'),
'project_app_deny': _('Are you sure you want to deny this project ? '
'<br /><br />You '
'may optionally provide denial reason in the '
'following field: <br /><br /><textarea '
'class="deny_reason" name="reason"></textarea>'),
'project_app_dismiss': _('Are you sure you want to dismiss this '
'project ?'),
'project_join': _('Are you sure you want to join this project ?'),
'project_leave': _('Are you sure you want to leave from the project ?'),
}
@register.tag(name="confirm_link")
@basictag(takes_context=True)
def confirm_link(context, title, prompt='', url=None, urlarg=None,
extracontent='',
confirm_prompt=None,
inline=True,
template="im/table_rich_link_column.html"):
urlargs = None
if urlarg:
urlargs = (urlarg,)
if CONFIRM_LINK_PROMPT_MAP.get(prompt, None):
prompt = mark_safe(CONFIRM_LINK_PROMPT_MAP.get(prompt))
url = reverse(url, args=urlargs)
title = _(title)
tpl_context = RequestContext(context.get('request'))
tpl_context.update({
'col': {
'method': 'POST',
'cancel_prompt': 'CANCEL',
'confirm_prompt': confirm_prompt or title
},
'inline': inline,
'url': url,
'action': title,
'prompt': prompt,
'extra_form_content': EXTRA_CONTENT_MAP.get(extracontent, ''),
'confirm': True
})
content = render_to_string(template, tpl_context)
return content
......@@ -549,7 +549,6 @@ def signup(request, template_name='im/signup.html', on_success='index', extra_co
return HttpResponseRedirect(reverse(on_success))
except SendMailError, e:
logger.exception(e)
status = messages.ERROR
message = e.message
messages.error(request, message)
......@@ -612,6 +611,7 @@ def feedback(request, template_name='im/feedback.html', email_template_name='im/
try:
send_feedback(msg, data, request.user, email_template_name)
except SendMailError, e:
message = e.message
messages.error(request, message)
else:
message = _(astakos_messages.FEEDBACK_SENT)
......@@ -1122,7 +1122,7 @@ def project_list(request):
})
@require_http_methods(["GET", "POST"])
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context()
def project_app_cancel(request, application_id, ctx=None):
......@@ -1355,7 +1355,7 @@ def project_search(request):
'table': table
})
@require_http_methods(["POST", "GET"])
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_join(request, chain_id, ctx=None):
......@@ -1382,7 +1382,7 @@ def project_join(request, chain_id, ctx=None):
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["POST", "GET"])
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_leave(request, chain_id, ctx=None):
......@@ -1505,7 +1505,7 @@ def project_reject_member(request, chain_id, user_id, ctx=None):
messages.success(request, msg)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST", "GET"])
@require_http_methods(["POST"])
@signed_terms_required
@login_required
@project_transaction_context(sync=True)
......@@ -1524,12 +1524,16 @@ def project_app_approve(request, application_id, ctx=None):
chain_id = get_related_project_id(application_id)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST", "GET"])
@require_http_methods(["POST"])
@signed_terms_required
@login_required
@project_transaction_context()
def project_app_deny(request, application_id, ctx=None):