Revision 3436eeb0

b/pithos/api/functions.py
44 44
    LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
45 45
from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
46 46
    put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
47
    update_manifest_meta, validate_modification_preconditions, validate_matching_preconditions,
48
    split_container_object_string, copy_or_move_object, get_int_parameter, get_content_length,
49
    get_content_range, raw_input_socket, socket_read_iterator, object_data_response,
50
    put_object_block, hashmap_hash, api_method)
47
    update_manifest_meta, format_permissions, validate_modification_preconditions,
48
    validate_matching_preconditions, split_container_object_string, copy_or_move_object,
49
    get_int_parameter, get_content_length, get_content_range, get_sharing, raw_input_socket,
50
    socket_read_iterator, object_data_response, put_object_block, hashmap_hash, api_method)
51 51
from pithos.backends import backend
52 52

  
53 53

  
......
348 348
        else:
349 349
            try:
350 350
                meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
351
                object_meta.append(printable_meta_dict(meta))
351
                permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
352 352
            except NameError:
353 353
                pass
354
            if permissions:
355
                meta['X-Object-Sharing'] = format_permissions(permissions)
356
            object_meta.append(printable_meta_dict(meta))
354 357
    if request.serialization == 'xml':
355 358
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
356 359
    elif request.serialization  == 'json':
......
370 373
    version = get_int_parameter(request, 'version')
371 374
    try:
372 375
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
376
        permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
373 377
    except NameError:
374 378
        raise ItemNotFound('Object does not exist')
375 379
    except IndexError:
376 380
        raise ItemNotFound('Version does not exist')
377 381
    
382
    if permissions:
383
        meta['X-Object-Sharing'] = format_permissions(permissions)
378 384
    update_manifest_meta(request, v_account, meta)
379 385
    
380 386
    response = HttpResponse(status=200)
......
398 404
        version_list = True
399 405
    try:
400 406
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
407
        permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
401 408
    except NameError:
402 409
        raise ItemNotFound('Object does not exist')
403 410
    except IndexError:
404 411
        raise ItemNotFound('Version does not exist')
405 412
    
413
    if permissions:
414
        meta['X-Object-Sharing'] = format_permissions(permissions)
406 415
    update_manifest_meta(request, v_account, meta)
407 416
    
408 417
    # Evaluate conditions.
......
485 494
    # Error Response Codes: serviceUnavailable (503),
486 495
    #                       unprocessableEntity (422),
487 496
    #                       lengthRequired (411),
497
    #                       conflict (409),
488 498
    #                       itemNotFound (404),
489 499
    #                       unauthorized (401),
490 500
    #                       badRequest (400)
......
510 520
        return HttpResponse(status=201)
511 521
    
512 522
    meta = get_object_meta(request)
523
    permissions = get_sharing(request)
513 524
    content_length = -1
514 525
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
515 526
        content_length = get_content_length(request)
......
534 545
        raise UnprocessableEntity('Object ETag does not match')
535 546
    
536 547
    try:
537
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
548
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
538 549
    except NameError:
539 550
        raise ItemNotFound('Container does not exist')
551
    except ValueError:
552
        raise BadRequest('Invalid sharing header')
553
    except AttributeError:
554
        raise Conflict('Sharing already set above or below this path in the hierarchy')
540 555
    
541 556
    response = HttpResponse(status=201)
542 557
    response['ETag'] = meta['hash']
......
582 597
def object_update(request, v_account, v_container, v_object):
583 598
    # Normal Response Codes: 202, 204
584 599
    # Error Response Codes: serviceUnavailable (503),
600
    #                       conflict (409),
585 601
    #                       itemNotFound (404),
586 602
    #                       unauthorized (401),
587 603
    #                       badRequest (400)
588 604
    
589 605
    meta = get_object_meta(request)
606
    permissions = get_sharing(request)
590 607
    content_type = meta.get('Content-Type')
591 608
    if content_type:
592 609
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
......
607 624
        except NameError:
608 625
            raise ItemNotFound('Object does not exist')
609 626
    
627
    # Handle permission changes.
628
    if permissions:
629
        try:
630
            backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
631
        except NameError:
632
            raise ItemNotFound('Object does not exist')
633
        except ValueError:
634
            raise BadRequest('Invalid sharing header')
635
        except AttributeError:
636
            raise Conflict('Sharing already set above or below this path in the hierarchy')
637
    
638
    # TODO: Merge above functions with updating the hashmap if there is data in the request.
639
    
610 640
    # A Content-Type or Content-Range header may indicate data updates.
611 641
    if content_type is None:
612 642
        return HttpResponse(status=202)
......
665 695
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, False)
666 696
    except NameError:
667 697
        raise ItemNotFound('Container does not exist')
668
        
698
    except ValueError:
699
        raise BadRequest('Invalid sharing header')
700
    except AttributeError:
701
        raise Conflict('Sharing already set above or below this path in the hierarchy')
702
    
669 703
    response = HttpResponse(status=204)
670 704
    response['ETag'] = meta['hash']
671 705
    return response
b/pithos/api/util.py
129 129
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
130 130
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
131 131
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
132
    if request.META.get('HTTP_X_OBJECT_PUBLIC'):
133
        meta['X-Object-Public'] = request.META['HTTP_X_OBJECT_PUBLIC']
134 132
    return meta
135 133

  
136 134
def put_object_meta(response, meta, public=False):
......
145 143
        response['X-Object-Version-Timestamp'] = meta['version_timestamp']
146 144
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
147 145
            response[k.encode('utf-8')] = meta[k].encode('utf-8')
148
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Public'):
146
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing'):
149 147
            if k in meta:
150 148
                response[k] = meta[k]
151 149
    else:
......
174 172
        md5.update(hash)
175 173
        meta['hash'] = md5.hexdigest().lower()
176 174

  
175
def format_permissions(permissions):
176
    ret = []
177
    if 'public' in permissions:
178
        ret.append('public')
179
    if 'private' in permissions:
180
        ret.append('private')
181
    r = ','.join(permissions.get('read', []))
182
    if r:
183
        ret.append('read=' + r)
184
    w = ','.join(permissions.get('write', []))
185
    if w:
186
        ret.append('write=' + w)
187
    return '; '.join(ret)
188

  
177 189
def validate_modification_preconditions(request, meta):
178 190
    """Check that the modified timestamp conforms with the preconditions set."""
179 191
    
......
221 233
    """Copy or move an object."""
222 234
    
223 235
    meta = get_object_meta(request)
236
    permissions = get_sharing(request)
224 237
    # Keep previous values of 'Content-Type' (if a new one is absent) and 'hash'.
225 238
    try:
226 239
        src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name)
......
234 247
    
235 248
    try:
236 249
        if move:
237
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True)
250
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions)
238 251
        else:
239 252
            src_version = request.META.get('HTTP_X_SOURCE_VERSION')
240
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, src_version)
253
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions, src_version)
241 254
    except NameError:
242 255
        raise ItemNotFound('Container or object does not exist')
256
    except ValueError:
257
        raise BadRequest('Invalid sharing header')
258
    except AttributeError:
259
        raise Conflict('Sharing already set above or below this path in the hierarchy')
243 260

  
244 261
def get_int_parameter(request, name):
245 262
    p = request.GET.get(name)
......
341 358
        length = upto - offset + 1
342 359
    return (offset, length, total)
343 360

  
361
def get_sharing(request):
362
    """Parse an X-Object-Sharing header from the request.
363
    
364
    Raises BadRequest on error.
365
    """
366
    
367
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
368
    if permissions is None or permissions == '':
369
        return None
370
    
371
    ret = {}
372
    for perm in (x.replace(' ','') for x in permissions.split(';')):
373
        if perm == 'public':
374
            ret['public'] = True
375
            continue
376
        elif perm == 'private':
377
            ret['private'] = True
378
            continue
379
        elif perm.startswith('read='):
380
            ret['read'] = [v.replace(' ','') for v in perm[5:].split(',')]
381
            if len(ret['read']) == 0:
382
                raise BadRequest('Bad X-Object-Sharing header value')
383
        elif perm.startswith('write='):
384
            ret['write'] = [v.replace(' ','') for v in perm[6:].split(',')]
385
            if len(ret['write']) == 0:
386
                raise BadRequest('Bad X-Object-Sharing header value')
387
        else:
388
            raise BadRequest('Bad X-Object-Sharing header value')
389
    return ret
390

  
344 391
def raw_input_socket(request):
345 392
    """Return the socket for reading the rest of the request."""
346 393
    
b/pithos/backends/base.py
182 182
        """
183 183
        return
184 184
    
185
    def get_object_permissions(self, user, account, container, name):
186
        """Return a dictionary with the object permissions.
187
        
188
        The keys are:
189
            'public': The object is readable by all and available at a public URL
190
            'private': No permissions set
191
            'read': The object is readable by the users/groups in the list
192
            'write': The object is writable by the users/groups in the list
193
        
194
        Raises:
195
            NameError: Container/object does not exist
196
        """
197
        return {}
198
    
199
    def update_object_permissions(self, user, account, container, name, permissions):
200
        """Update the permissions associated with the object.
201
        
202
        Parameters:
203
            'permissions': Dictionary with permissions to update
204
        
205
        Raises:
206
            NameError: Container/object does not exist
207
            ValueError: Invalid users/groups in permissions
208
            AttributeError: Can not set permissions, as this object\
209
                is already shared/private by another object higher\
210
                in the hierarchy, or setting permissions here will\
211
                invalidate other permissions deeper in the hierarchy
212
        """
213
        return
214
    
185 215
    def get_object_hashmap(self, user, account, container, name, version=None):
186 216
        """Return the object's size and a list with partial hashes.
187 217
        
......
191 221
        """
192 222
        return 0, []
193 223
    
194
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False):
224
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions={}):
195 225
        """Create/update an object with the specified size and partial hashes.
196 226
        
227
        Parameters:
228
            'dest_meta': Dictionary with metadata to change
229
            'replace_meta': Replace metadata instead of update
230
            'permissions': Updated object permissions
231
        
197 232
        Raises:
198 233
            NameError: Container does not exist
234
            ValueError: Invalid users/groups in permissions
235
            AttributeError: Can not set permissions
199 236
        """
200 237
        return
201 238
    
202
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, src_version=None):
239
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}, src_version=None):
203 240
        """Copy an object's data and metadata.
204 241
        
205 242
        Parameters:
206
            'dest_meta': Dictionary with metadata to changes from source to destination
243
            'dest_meta': Dictionary with metadata to change from source to destination
207 244
            'replace_meta': Replace metadata instead of update
208
            'src_version': Copy from the version provided.
245
            'permissions': New object permissions
246
            'src_version': Copy from the version provided
209 247
        
210 248
        Raises:
211 249
            NameError: Container/object does not exist
212 250
            IndexError: Version does not exist
251
            ValueError: Invalid users/groups in permissions
252
            AttributeError: Can not set permissions
213 253
        """
214 254
        return
215 255
    
216
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False):
256
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}):
217 257
        """Move an object's data and metadata.
218 258
        
219 259
        Parameters:
220
            'dest_meta': Dictionary with metadata to changes from source to destination
260
            'dest_meta': Dictionary with metadata to change from source to destination
221 261
            'replace_meta': Replace metadata instead of update
262
            'permissions': New object permissions
222 263
        
223 264
        Raises:
224 265
            NameError: Container/object does not exist
266
            ValueError: Invalid users/groups in permissions
267
            AttributeError: Can not set permissions
225 268
        """
226 269
        return
227 270
    
b/pithos/backends/simple.py
79 79
        sql = '''create table if not exists hashmaps (
80 80
                    version_id integer, pos integer, block_id text, primary key (version_id, pos))'''
81 81
        self.con.execute(sql)
82
        sql = '''create table if not exists permissions (
83
                    name text, read text, write text, primary key (name))'''
84
        self.con.execute(sql)
82 85
        self.con.commit()
83 86
    
84 87
    def delete_account(self, user, account):
......
227 230
        path, version_id, mtime, size = self._get_objectinfo(account, container, name)
228 231
        self._put_metadata(path, meta, replace)
229 232
    
233
    def get_object_permissions(self, user, account, container, name):
234
        """Return a dictionary with the object permissions."""
235
        
236
        logger.debug("get_object_permissions: %s %s %s", account, container, name)
237
        path = self._get_objectinfo(account, container, name)[0]
238
        return self._get_permissions(path)
239
    
240
    def update_object_permissions(self, user, account, container, name, permissions):
241
        """Update the permissions associated with the object."""
242
        
243
        logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
244
        path = self._get_objectinfo(account, container, name)[0]
245
        r, w = self._check_permissions(path, permissions)
246
        self._put_permissions(path, r, w)
247
    
230 248
    def get_object_hashmap(self, user, account, container, name, version=None):
231 249
        """Return the object's size and a list with partial hashes."""
232 250
        
......
237 255
        hashmap = [x[0] for x in c.fetchall()]
238 256
        return size, hashmap
239 257
    
240
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False):
258
    def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions={}):
241 259
        """Create/update an object with the specified size and partial hashes."""
242 260
        
243 261
        logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
244 262
        path = self._get_containerinfo(account, container)[0]
245 263
        path = os.path.join(path, name)
264
        if permissions:
265
            r, w = self._check_permissions(path, permissions)
246 266
        src_version_id, dest_version_id = self._copy_version(path, path, not replace_meta, False)
247 267
        sql = 'update versions set size = ? where version_id = ?'
248 268
        self.con.execute(sql, (size, dest_version_id))
......
253 273
        for k, v in meta.iteritems():
254 274
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
255 275
            self.con.execute(sql, (dest_version_id, k, v))
276
        if permissions:
277
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
278
            self.con.execute(sql, (path, r, w))
256 279
        self.con.commit()
257 280
    
258
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, src_version=None):
281
    def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}, src_version=None):
259 282
        """Copy an object's data and metadata."""
260 283
        
261
        logger.debug("copy_object: %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, src_version)
284
        logger.debug("copy_object: %s %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
262 285
        self._get_containerinfo(account, src_container)
263 286
        if src_version is None:
264 287
            src_path = self._get_objectinfo(account, src_container, src_name)[0]
......
266 289
            src_path = os.path.join(account, src_container, src_name)
267 290
        dest_path = self._get_containerinfo(account, dest_container)[0]
268 291
        dest_path = os.path.join(dest_path, dest_name)
292
        if permissions:
293
            r, w = self._check_permissions(dest_path, permissions)
269 294
        src_version_id, dest_version_id = self._copy_version(src_path, dest_path, not replace_meta, True, src_version)
270 295
        for k, v in dest_meta.iteritems():
271 296
            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
272 297
            self.con.execute(sql, (dest_version_id, k, v))
298
        if permissions:
299
            sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
300
            self.con.execute(sql, (dest_path, r, w))
273 301
        self.con.commit()
274 302
    
275
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False):
303
    def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}):
276 304
        """Move an object's data and metadata."""
277 305
        
278
        logger.debug("move_object: %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta)
279
        self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, None)
306
        logger.debug("move_object: %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions)
307
        self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
280 308
        self.delete_object(user, account, src_container, src_name)
281 309
    
282 310
    def delete_object(self, user, account, container, name):
......
285 313
        logger.debug("delete_object: %s %s %s", account, container, name)
286 314
        path, version_id, mtime, size = self._get_objectinfo(account, container, name)
287 315
        self._put_version(path, 0, 1)
316
        sql = 'delete from permissions where name = ?'
317
        self.con.execute(sql, (path,))
318
        self.con.commit()
288 319
    
289 320
    def list_versions(self, user, account, container, name):
290 321
        """Return a list of all (version, version_timestamp) tuples for an object."""
......
449 480
            self.con.execute(sql, (dest_version_id, k, v))
450 481
        self.con.commit()
451 482
    
483
    def _can_read(self, user, path):
484
        return True
485
    
486
    def _can_write(self, user, path):
487
        return True
488
    
489
    def _check_permissions(self, path, permissions):
490
        # Check for existing permissions.
491
        sql = '''select name from permissions
492
                    where name != ? and (name like ? or ? like name || ?)'''
493
        c = self.con.execute(sql, (path, path + '%', path, '%'))
494
        if c.fetchall() is not None:
495
            raise AttributeError('Permissions already set')
496
        
497
        # Format given permissions set.
498
        r = permissions.get('read', [])
499
        w = permissions.get('write', [])
500
        if True in [False or '*' in x or ',' in x for x in r]:
501
            raise ValueError('Bad characters in read permissions')
502
        if True in [False or '*' in x or ',' in x for x in w]:
503
            raise ValueError('Bad characters in write permissions')
504
        r = ','.join(r)
505
        w = ','.join(w)
506
        if 'public' in permissions:
507
            r = '*'
508
        if 'private' in permissions:
509
            r = ''
510
            w = ''
511
        return r, w
512
    
513
    def _get_permissions(self, path):
514
        sql = 'select read, write from permissions where name = ?'
515
        c = self.con.execute(sql, (path,))
516
        row = c.fetchone()
517
        if not row:
518
            return {}
519
        
520
        r, w = row
521
        if r == '' and w == '':
522
            return {'private': True}
523
        ret = {}
524
        if w != '':
525
            ret['write'] = w.split(',')
526
        if r != '':
527
            if r == '*':
528
                ret['public'] = True
529
            else:
530
                ret['read'] = r.split(',')        
531
        return ret
532
    
533
    def _put_permissions(self, path, r, w):
534
        sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
535
        self.con.execute(sql, (path, r, w))
536
        self.con.commit()
537
    
452 538
    def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
453 539
        cont_prefix = path + '/'
454 540
        if keys and len(keys) > 0:
......
502 588
        self.con.execute(sql, (path,))
503 589
        sql = '''delete from versions where name = ?'''
504 590
        self.con.execute(sql, (path,))
591
        sql = '''delete from permissions where name like ?'''
592
        self.con.execute(sql, (path + '%',)) # Redundant.
505 593
        self.con.commit()
b/pithos/public/functions.py
65 65
    
66 66
    try:
67 67
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
68
        permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
68 69
    except NameError:
69 70
        raise ItemNotFound('Object does not exist')
70 71
    
71
    if 'X-Object-Public' not in meta:
72
    if 'public' not in permissions:
72 73
        raise ItemNotFound('Object does not exist')
73 74
    update_manifest_meta(request, v_account, meta)
74 75
    
......
89 90
    
90 91
    try:
91 92
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
93
        permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
92 94
    except NameError:
93 95
        raise ItemNotFound('Object does not exist')
94 96
    
95
    if 'X-Object-Public' not in meta:
97
    if 'public' not in permissions:
96 98
        raise ItemNotFound('Object does not exist')
97 99
    update_manifest_meta(request, v_account, meta)
98 100
    

Also available in: Unified diff