MetaData Sharing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1108 lines
31 KiB

  1. #!/usr/bin/env python
  2. #import pdb, sys; mypdb = pdb.Pdb(stdout=sys.stderr); mypdb.set_trace()
  3. from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey, \
  4. Ed448PublicKey
  5. from cryptography.hazmat.primitives.serialization import Encoding, \
  6. PrivateFormat, PublicFormat, NoEncryption
  7. import base58
  8. import copy
  9. import datetime
  10. import functools
  11. import hashlib
  12. import mock
  13. import os.path
  14. import pasn1
  15. import shutil
  16. import string
  17. import tempfile
  18. import unittest
  19. import uuid
  20. from contextlib import nested
  21. # The UUID for the namespace representing the path to a file
  22. _NAMESPACE_MEDASHARE_PATH = uuid.UUID('f6f36b62-3770-4a68-bc3d-dc3e31e429e6')
  23. _defaulthash = 'sha512'
  24. _validhashes = set([ 'sha256', 'sha512' ])
  25. _hashlengths = { len(getattr(hashlib, x)().hexdigest()): x for x in _validhashes }
  26. def _iterdictlist(obj):
  27. itms = obj.items()
  28. itms.sort()
  29. for k, v in itms:
  30. if isinstance(v, list):
  31. v = v[:]
  32. v.sort()
  33. for i in v:
  34. yield k, i
  35. else:
  36. yield k, v
  37. def _makeuuid(s):
  38. if isinstance(s, uuid.UUID):
  39. return s
  40. return uuid.UUID(bytes=s)
  41. # XXX - known issue, store is not atomic/safe, overwrites in place instead of renames
  42. # XXX - add validation
  43. # XXX - how to add singletons
  44. class MDBase(object):
  45. '''This is a simple wrapper that turns a JSON object into a pythonesc
  46. object where attribute accesses work.'''
  47. _type = 'invalid'
  48. _generated_properties = {
  49. 'uuid': uuid.uuid4,
  50. 'modified': datetime.datetime.utcnow
  51. }
  52. # When decoding, the decoded value should be passed to this function
  53. # to get the correct type
  54. _instance_properties = {
  55. 'uuid': _makeuuid,
  56. 'created_by_ref': _makeuuid,
  57. }
  58. _common_properties = [ 'type', 'created_by_ref' ] # XXX - add lang?
  59. _common_optional = set(('parent_refs', 'sig'))
  60. _common_names = set(_common_properties + _generated_properties.keys())
  61. def __init__(self, obj={}, **kwargs):
  62. obj = copy.deepcopy(obj)
  63. obj.update(kwargs)
  64. if self._type == MDBase._type:
  65. raise ValueError('call MDBase.create_obj instead so correct class is used.')
  66. if 'type' in obj and obj['type'] != self._type:
  67. raise ValueError(
  68. 'trying to create the wrong type of object, got: %s, expected: %s' %
  69. (`obj['type']`, `self._type`))
  70. if 'type' not in obj:
  71. obj['type'] = self._type
  72. for x in self._common_properties:
  73. if x not in obj:
  74. raise ValueError('common property %s not present' % `x`)
  75. for x, fun in self._instance_properties.iteritems():
  76. if x in obj:
  77. obj[x] = fun(obj[x])
  78. for x, fun in self._generated_properties.iteritems():
  79. if x not in obj:
  80. obj[x] = fun()
  81. self._obj = obj
  82. @classmethod
  83. def create_obj(cls, obj):
  84. '''Using obj as a base, create an instance of MDBase of the
  85. correct type.
  86. If the correct type is not found, a ValueError is raised.'''
  87. if isinstance(obj, cls):
  88. obj = obj._obj
  89. ty = obj['type']
  90. for i in MDBase.__subclasses__():
  91. if i._type == ty:
  92. return i(obj)
  93. else:
  94. raise ValueError('Unable to find class for type %s' %
  95. `ty`)
  96. def new_version(self, *args):
  97. '''For each k, v pari, add the property k as an additional one
  98. (or new one if first), with the value v.'''
  99. obj = copy.deepcopy(self._obj)
  100. common = self._common_names | self._common_optional
  101. for k, v in args:
  102. if k in common:
  103. obj[k] = v
  104. else:
  105. obj.setdefault(k, []).append(v)
  106. del obj['modified']
  107. return self.create_obj(obj)
  108. def __repr__(self): # pragma: no cover
  109. return '%s(%s)' % (self.__class__.__name__, `self._obj`)
  110. def __getattr__(self, k):
  111. try:
  112. return self._obj[k]
  113. except KeyError:
  114. raise AttributeError(k)
  115. def __setattr__(self, k, v):
  116. if k[0] == '_': # direct attribute
  117. self.__dict__[k] = v
  118. else:
  119. self._obj[k] = v
  120. def __getitem__(self, k):
  121. return self._obj[k]
  122. def __to_dict__(self):
  123. return self._obj
  124. def __eq__(self, o):
  125. return cmp(self._obj, o) == 0
  126. def __contains__(self, k):
  127. return k in self._obj
  128. def items(self, skipcommon=True):
  129. return [ (k, v) for k, v in self._obj.iteritems() if
  130. not skipcommon or k not in self._common_names ]
  131. def encode(self):
  132. return _asn1coder.dumps(self)
  133. @classmethod
  134. def decode(cls, s):
  135. return cls.create_obj(_asn1coder.loads(s))
  136. class MetaData(MDBase):
  137. _type = 'metadata'
  138. class Identity(MDBase):
  139. _type = 'identity'
  140. # Identites don't need a created by
  141. _common_properties = [ x for x in MDBase._common_properties if x !=
  142. 'created_by_ref' ]
  143. _common_optional = set([ x for x in MDBase._common_optional if x !=
  144. 'parent_refs' ] + [ 'name', 'pubkey' ])
  145. _common_names = set(_common_properties + MDBase._generated_properties.keys())
  146. def _trytodict(o):
  147. if isinstance(o, uuid.UUID):
  148. return 'bytes', o.bytes
  149. try:
  150. return 'dict', o.__to_dict__()
  151. except Exception: # pragma: no cover
  152. raise TypeError('unable to find __to_dict__ on %s: %s' % (type(o), `o`))
  153. class CanonicalCoder(pasn1.ASN1DictCoder):
  154. def enc_dict(self, obj, **kwargs):
  155. class FakeIter:
  156. def iteritems(self):
  157. itms = obj.items()
  158. itms.sort()
  159. return iter(itms)
  160. return pasn1.ASN1DictCoder.enc_dict(self, FakeIter(), **kwargs)
  161. _asn1coder = CanonicalCoder(coerce=_trytodict)
  162. class Persona(object):
  163. '''The object that represents a persona, or identity. It will
  164. create the proper identity object, serialize for saving keys,
  165. create objects for that persona and other management.'''
  166. def __init__(self, identity=None, key=None):
  167. if identity is None:
  168. self._identity = Identity()
  169. else:
  170. self._identity = identity
  171. self._key = key
  172. self._pubkey = None
  173. if 'pubkey' in self._identity:
  174. pubkeybytes = self._identity.pubkey
  175. self._pubkey = Ed448PublicKey.from_public_bytes(pubkeybytes)
  176. self._created_by_ref = self._identity.uuid
  177. def MetaData(self, *args, **kwargs):
  178. kwargs['created_by_ref'] = self.uuid
  179. return self.sign(MetaData(*args, **kwargs))
  180. @property
  181. def uuid(self):
  182. '''Return the UUID of the identity represented.'''
  183. return self._identity.uuid
  184. def __repr__(self): # pragma: no cover
  185. r = '<Persona: has key: %s, has pubkey: %s, identity: %s>' % \
  186. (self._key is not None, self._pubkey is not None,
  187. `self._identity`)
  188. return r
  189. @classmethod
  190. def from_pubkey(cls, pubkeystr):
  191. pubstr = base58.b58decode_check(pubkeystr)
  192. uuid, pubkey = _asn1coder.loads(pubstr)
  193. ident = Identity(uuid=uuid, pubkey=pubkey)
  194. return cls(ident)
  195. def get_identity(self):
  196. '''Return the Identity object for this Persona.'''
  197. return self._identity
  198. def get_pubkey(self):
  199. '''Get a printable version of the public key. This is used
  200. for importing into different programs, or for shared.'''
  201. idobj = self._identity
  202. pubstr = _asn1coder.dumps([ idobj.uuid, idobj.pubkey ])
  203. return base58.b58encode_check(pubstr)
  204. def new_version(self, *args):
  205. '''Update the Persona's Identity object.'''
  206. self._identity = self.sign(self._identity.new_version(*args))
  207. return self._identity
  208. def store(self, fname):
  209. '''Store the Persona to a file. If there is a private
  210. key associated w/ the Persona, it will be saved as well.'''
  211. with open(fname, 'w') as fp:
  212. obj = {
  213. 'identity': self._identity,
  214. }
  215. if self._key is not None:
  216. obj['key'] = \
  217. self._key.private_bytes(Encoding.Raw,
  218. PrivateFormat.Raw, NoEncryption())
  219. fp.write(_asn1coder.dumps(obj))
  220. @classmethod
  221. def load(cls, fname):
  222. '''Load the Persona from the provided file.'''
  223. with open(fname) as fp:
  224. objs = _asn1coder.loads(fp.read())
  225. kwargs = {}
  226. if 'key' in objs:
  227. kwargs['key'] = Ed448PrivateKey.from_private_bytes(objs['key'])
  228. return cls(Identity(objs['identity']), **kwargs)
  229. def generate_key(self):
  230. '''Generate a key for this Identity.
  231. Raises a RuntimeError if a key is already present.'''
  232. if self._key:
  233. raise RuntimeError('a key already exists')
  234. self._key = Ed448PrivateKey.generate()
  235. self._pubkey = self._key.public_key()
  236. pubkey = self._pubkey.public_bytes(Encoding.Raw,
  237. PublicFormat.Raw)
  238. self._identity = self.sign(self._identity.new_version(('pubkey',
  239. pubkey)))
  240. def _makesigbytes(self, obj):
  241. obj = dict(obj.items(False))
  242. try:
  243. del obj['sig']
  244. except KeyError:
  245. pass
  246. return _asn1coder.dumps(obj)
  247. def sign(self, obj):
  248. '''Takes the object, adds a signature, and returns the new
  249. object.'''
  250. sigbytes = self._makesigbytes(obj)
  251. sig = self._key.sign(sigbytes)
  252. newobj = MDBase.create_obj(obj)
  253. newobj.sig = sig
  254. return newobj
  255. def verify(self, obj):
  256. sigbytes = self._makesigbytes(obj)
  257. pubkey = self._pubkey.public_bytes(Encoding.Raw,
  258. PublicFormat.Raw)
  259. self._pubkey.verify(obj['sig'], sigbytes)
  260. return True
  261. def by_file(self, fname):
  262. '''Return a metadata object for the file named fname.'''
  263. fobj = FileObject.from_file(fname, self._created_by_ref)
  264. return self.sign(fobj)
  265. class ObjectStore(object):
  266. '''A container to store for the various Metadata objects.'''
  267. # The _uuids property contains both the UUIDv4 for objects, and
  268. # looking up the UUIDv5 for FileObjects.
  269. def __init__(self, created_by_ref):
  270. self._created_by_ref = created_by_ref
  271. self._uuids = {}
  272. self._hashes = {}
  273. @staticmethod
  274. def makehash(hashstr, strict=True):
  275. '''Take a hash or hash string, and return a valid hash
  276. string from it.
  277. This makes sure that it is of the correct type and length.
  278. If strict is False, the function will detect the length and
  279. return a valid hash string if one can be found.
  280. By default, the string must be prepended by the type,
  281. followed by a colon, followed by the value in hex in all
  282. lower case characters.'''
  283. try:
  284. hash, value = hashstr.split(':')
  285. except ValueError:
  286. if strict:
  287. raise
  288. hash = _hashlengths[len(hashstr)]
  289. value = hashstr
  290. if strict and len(str(value).translate(None, string.hexdigits.lower())) != 0:
  291. raise ValueError('value has invalid hex digits (must be lower case)', value)
  292. if hash in _validhashes:
  293. return ':'.join((hash, value))
  294. raise ValueError
  295. def __len__(self):
  296. return len(self._uuids)
  297. def store(self, fname):
  298. '''Write out the objects in the store to the file named
  299. fname.'''
  300. with open(fname, 'w') as fp:
  301. obj = {
  302. 'created_by_ref': self._created_by_ref,
  303. 'objects': self._uuids.values(),
  304. }
  305. fp.write(_asn1coder.dumps(obj))
  306. def loadobj(self, obj):
  307. '''Load obj into the data store.'''
  308. obj = MDBase.create_obj(obj)
  309. self._uuids[obj.uuid] = obj
  310. for j in obj.hashes:
  311. h = self.makehash(j)
  312. self._hashes.setdefault(h, []).append(obj)
  313. @classmethod
  314. def load(cls, fname):
  315. '''Load objects from the provided file name.
  316. Basic validation will be done on the objects in the file.
  317. The objects will be accessible via other methods.'''
  318. with open(fname) as fp:
  319. objs = _asn1coder.loads(fp.read())
  320. obj = cls(objs['created_by_ref'])
  321. for i in objs['objects']:
  322. obj.loadobj(i)
  323. return obj
  324. def by_id(self, id):
  325. '''Look up an object by it's UUID.'''
  326. if not isinstance(id, uuid.UUID):
  327. uid = uuid.UUID(id)
  328. else:
  329. uid = id
  330. return self._uuids[uid]
  331. def by_hash(self, hash):
  332. '''Look up an object by it's hash value.'''
  333. h = self.makehash(hash, strict=False)
  334. return self._hashes[h]
  335. def by_file(self, fname):
  336. '''Return a metadata object for the file named fname.'''
  337. fid = FileObject.make_id(fname)
  338. try:
  339. fobj = self.by_id(fid)
  340. except KeyError:
  341. # unable to find it
  342. fobj = FileObject.from_file(fname, self._created_by_ref)
  343. self.loadobj(fobj)
  344. for i in fobj.hashes:
  345. j = self.by_hash(i)
  346. # Filter out non-metadata objects
  347. j = [ x for x in j if x.type == 'metadata' ]
  348. if j:
  349. return j
  350. else:
  351. raise KeyError('unable to find metadata for file')
  352. def _hashfile(fname):
  353. hash = getattr(hashlib, _defaulthash)()
  354. with open(fname) as fp:
  355. r = fp.read()
  356. hash.update(r)
  357. return '%s:%s' % (_defaulthash, hash.hexdigest())
  358. class FileObject(MDBase):
  359. _type = 'file'
  360. @staticmethod
  361. def make_id(fname):
  362. '''Take a local file name, and make the id for it. Note that
  363. converts from the local path separator to a forward slash so
  364. that it will be the same between Windows and Unix systems.'''
  365. fname = os.path.realpath(fname)
  366. return uuid.uuid5(_NAMESPACE_MEDASHARE_PATH,
  367. '/'.join(os.path.split(fname)))
  368. @classmethod
  369. def from_file(cls, filename, created_by_ref):
  370. s = os.stat(filename)
  371. obj = {
  372. 'dir': os.path.dirname(filename),
  373. 'created_by_ref': created_by_ref,
  374. 'filename': os.path.basename(filename),
  375. 'id': cls.make_id(filename),
  376. 'mtime': datetime.datetime.utcfromtimestamp(s.st_mtime),
  377. 'size': s.st_size,
  378. 'hashes': [ _hashfile(filename), ],
  379. }
  380. return cls(obj)
  381. def enumeratedir(_dir, created_by_ref):
  382. '''Enumerate all the files and directories (not recursive) in _dir.
  383. Returned is a list of FileObjects.'''
  384. return map(lambda x: FileObject.from_file(os.path.join(_dir, x), created_by_ref),
  385. os.listdir(_dir))
  386. def main():
  387. from optparse import OptionParser
  388. import sys
  389. parser = OptionParser()
  390. parser.add_option('-a', action='append', dest='add',
  391. default=[], help='add the arg as metadata for files, tag=value')
  392. parser.add_option('-d', action='append', dest='delete',
  393. default=[], help='delete the arg as metadata from files. Either specify tag, and all tags are removed, or specify tag=value and that specific tag will be removed.')
  394. parser.add_option('-g', action='store_true', dest='generateident',
  395. default=False, help='generate an identity')
  396. parser.add_option('-i', action='store_true', dest='updateident',
  397. default=False, help='update the identity')
  398. parser.add_option('-l', action='store_true', dest='list',
  399. default=False, help='list metadata')
  400. parser.add_option('-p', action='store_true', dest='printpub',
  401. default=False, help='Print the public key of the identity')
  402. options, args = parser.parse_args()
  403. # this is shared between generateident and add
  404. addprops = map(lambda x: x.split('=', 1), options.add)
  405. if options.generateident or options.updateident or options.printpub:
  406. identfname = os.path.expanduser('~/.medashare_identity.pasn1')
  407. if options.generateident and os.path.exists(identfname):
  408. print >>sys.stderr, 'Error: Identity already created.'
  409. sys.exit(1)
  410. if options.generateident:
  411. persona = Persona()
  412. persona.generate_key()
  413. else:
  414. persona = Persona.load(identfname)
  415. if options.printpub:
  416. print persona.get_pubkey()
  417. return
  418. persona.new_version(*addprops)
  419. persona.store(identfname)
  420. return
  421. storefname = os.path.expanduser('~/.medashare_store.pasn1')
  422. import sys
  423. #print >>sys.stderr, `storefname`
  424. objstr = ObjectStore.load(storefname)
  425. if options.list:
  426. for i in args:
  427. for j in objstr.by_file(i):
  428. #print >>sys.stderr, `j._obj`
  429. for k, v in _iterdictlist(j):
  430. print '%s:\t%s' % (k, v)
  431. elif options.add:
  432. for i in args:
  433. for j in objstr.by_file(i):
  434. nobj = j.new_version(*addprops)
  435. objstr.loadobj(nobj)
  436. elif options.delete:
  437. for i in args:
  438. for j in objstr.by_file(i):
  439. obj = j.__to_dict__()
  440. for k in options.delete:
  441. try:
  442. key, v = k.split('=', 1)
  443. obj[key].remove(v)
  444. except ValueError:
  445. del obj[k]
  446. nobj = MDBase.create_obj(obj)
  447. objstr.loadobj(nobj)
  448. else: # pragma: no cover
  449. raise NotImplementedError
  450. objstr.store(storefname)
  451. if __name__ == '__main__': # pragma: no cover
  452. main()
  453. class _TestCononicalCoder(unittest.TestCase):
  454. def test_con(self):
  455. obja = { 'foo': 23984732, 'a': 5, 'b': 6, 'something': '2398472398723498273dfasdfjlaksdfj' }
  456. objaitems = obja.items()
  457. objaitems.sort()
  458. objb = dict(objaitems)
  459. self.assertEqual(obja, objb)
  460. # This is to make sure that item order changed
  461. self.assertNotEqual(obja.items(), objb.items())
  462. astr = pasn1.dumps(obja)
  463. bstr = pasn1.dumps(objb)
  464. self.assertNotEqual(astr, bstr)
  465. astr = _asn1coder.dumps(obja)
  466. bstr = _asn1coder.dumps(objb)
  467. self.assertEqual(astr, bstr)
  468. class _TestCases(unittest.TestCase):
  469. def setUp(self):
  470. d = os.path.realpath(tempfile.mkdtemp())
  471. self.basetempdir = d
  472. self.tempdir = os.path.join(d, 'subdir')
  473. persona = Persona.load(os.path.join('fixtures', 'sample.persona.pasn1'))
  474. self.created_by_ref = persona.get_identity().uuid
  475. shutil.copytree(os.path.join('fixtures', 'testfiles'),
  476. self.tempdir)
  477. def tearDown(self):
  478. shutil.rmtree(self.basetempdir)
  479. self.tempdir = None
  480. def test_mdbase(self):
  481. self.assertRaises(ValueError, MDBase, created_by_ref='')
  482. self.assertRaises(ValueError, MDBase.create_obj, { 'type': 'unknosldkfj' })
  483. self.assertRaises(ValueError, MDBase.create_obj, { 'type': 'metadata' })
  484. baseobj = {
  485. 'type': 'metadata',
  486. 'created_by_ref': self.created_by_ref,
  487. }
  488. origbase = copy.deepcopy(baseobj)
  489. # that when an MDBase object is created
  490. md = MDBase.create_obj(baseobj)
  491. # it doesn't modify the passed in object (when adding
  492. # generated properties)
  493. self.assertEqual(baseobj, origbase)
  494. # and it has the generted properties
  495. # Note: cannot mock the functions as they are already
  496. # referenced at creation time
  497. self.assertIn('uuid', md)
  498. self.assertIn('modified', md)
  499. # That you can create a new version using new_version
  500. md2 = md.new_version(('dc:creator', 'Jim Bob',))
  501. # that they are different
  502. self.assertNotEqual(md, md2)
  503. # and that the new modified time is different from the old
  504. self.assertNotEqual(md.modified, md2.modified)
  505. # and that the modification is present
  506. self.assertEqual(md2['dc:creator'], [ 'Jim Bob' ])
  507. # that providing a value from common property
  508. fvalue = 'fakesig'
  509. md3 = md.new_version(('sig', fvalue))
  510. # gets set directly, and is not a list
  511. self.assertEqual(md3.sig, fvalue)
  512. # that invalid attribute access raises correct exception
  513. self.assertRaises(AttributeError, getattr, md, 'somerandombogusattribute')
  514. def test_mdbase_encode_decode(self):
  515. # that an object
  516. baseobj = {
  517. 'type': 'metadata',
  518. 'created_by_ref': self.created_by_ref,
  519. }
  520. obj = MDBase.create_obj(baseobj)
  521. # can be encoded
  522. coded = obj.encode()
  523. # and that the rsults can be decoded
  524. decobj = MDBase.decode(coded)
  525. # and that they are equal
  526. self.assertEqual(obj, decobj)
  527. # and in the encoded object
  528. eobj = _asn1coder.loads(coded)
  529. # the uuid property is a str instance
  530. self.assertIsInstance(eobj['uuid'], str)
  531. # and has the length of 16
  532. self.assertEqual(len(eobj['uuid']), 16)
  533. def test_mdbase_wrong_type(self):
  534. # that created_by_ref can be passed by kw
  535. obj = MetaData(created_by_ref=self.created_by_ref)
  536. self.assertRaises(ValueError, FileObject, dict(obj.items(False)))
  537. def test_makehash(self):
  538. self.assertRaises(ValueError, ObjectStore.makehash, 'slkj')
  539. self.assertRaises(ValueError, ObjectStore.makehash, 'sha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ADA')
  540. self.assertRaises(ValueError, ObjectStore.makehash, 'bogushash:9e0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ADA', strict=False)
  541. self.assertEqual(ObjectStore.makehash('cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', strict=False), 'sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e')
  542. self.assertEqual(ObjectStore.makehash('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', strict=False), 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
  543. def test_enumeratedir(self):
  544. files = enumeratedir(self.tempdir, self.created_by_ref)
  545. ftest = files[0]
  546. fname = 'test.txt'
  547. # make sure that they are of type MDBase
  548. self.assertIsInstance(ftest, MDBase)
  549. oldid = ftest.id
  550. self.assertEqual(ftest.filename, fname)
  551. self.assertEqual(ftest.dir, self.tempdir)
  552. # XXX - do we add host information?
  553. self.assertEqual(ftest.id, uuid.uuid5(_NAMESPACE_MEDASHARE_PATH,
  554. '/'.join(os.path.split(self.tempdir) +
  555. ( fname, ))))
  556. self.assertEqual(ftest.mtime, datetime.datetime(2019, 5, 20, 21, 47, 36))
  557. self.assertEqual(ftest.size, 15)
  558. self.assertIn('sha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f', ftest.hashes)
  559. # XXX - make sure works w/ relative dirs
  560. files = enumeratedir(os.path.relpath(self.tempdir),
  561. self.created_by_ref)
  562. self.assertEqual(oldid, files[0].id)
  563. def test_mdbaseoverlay(self):
  564. objst = ObjectStore(self.created_by_ref)
  565. # that a base object
  566. bid = uuid.uuid4()
  567. objst.loadobj({
  568. 'type': 'metadata',
  569. 'uuid': bid,
  570. 'modified': datetime.datetime(2019, 6, 10, 14, 3, 10),
  571. 'created_by_ref': self.created_by_ref,
  572. 'hashes': [ 'sha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada' ],
  573. 'someprop': [ 'somevalue' ],
  574. 'lang': 'en',
  575. })
  576. # can have an overlay object
  577. oid = uuid.uuid4()
  578. dhash = 'sha256:a7c96262c21db9a06fd49e307d694fd95f624569f9b35bb3ffacd880440f9787'
  579. objst.loadobj({
  580. 'type': 'metadata',
  581. 'uuid': oid,
  582. 'modified': datetime.datetime(2019, 6, 10, 18, 3, 10),
  583. 'created_by_ref': self.created_by_ref,
  584. 'hashes': [ dhash ],
  585. 'parent_refs': [ bid ],
  586. 'lang': 'en',
  587. })
  588. # and that when you get it's properties
  589. oobj = objst.by_id(oid)
  590. odict = dict(oobj.items())
  591. # that is has the overlays property
  592. self.assertEqual(odict['parent_refs'], [ bid ])
  593. # that it doesn't have a common property
  594. self.assertNotIn('type', odict)
  595. # that when skipcommon is False
  596. odict = dict(oobj.items(False))
  597. # that it does have a common property
  598. self.assertIn('type', odict)
  599. def test_persona(self):
  600. # that a newly created persona
  601. persona = Persona()
  602. # has an identity object
  603. idobj = persona.get_identity()
  604. # and that it has a uuid attribute that matches
  605. self.assertEqual(persona.uuid, idobj['uuid'])
  606. # that a key can be generated
  607. persona.generate_key()
  608. # that the pubkey property is present
  609. idobj = persona.get_identity()
  610. self.assertIsInstance(idobj['pubkey'], str)
  611. # that get_pubkey returns the correct thing
  612. pubstr = _asn1coder.dumps([ idobj.uuid, idobj['pubkey'] ])
  613. self.assertEqual(persona.get_pubkey(),
  614. base58.b58encode_check(pubstr))
  615. # and that there is a signature
  616. self.assertIsInstance(idobj['sig'], str)
  617. # and that it can verify itself
  618. persona.verify(idobj)
  619. # and that a new persona can be created from the pubkey
  620. pkpersona = Persona.from_pubkey(persona.get_pubkey())
  621. # and that it can verify the old identity
  622. self.assertTrue(pkpersona.verify(idobj))
  623. # that a second time, it raises an exception
  624. self.assertRaises(RuntimeError, persona.generate_key)
  625. # that a file object created by it
  626. testfname = os.path.join(self.tempdir, 'test.txt')
  627. testobj = persona.by_file(testfname)
  628. # has the correct created_by_ref
  629. self.assertEqual(testobj.created_by_ref, idobj.uuid)
  630. # and has a signature
  631. self.assertIn('sig', testobj)
  632. # that a persona created from the identity object
  633. vpersona = Persona(idobj)
  634. # can verify the sig
  635. self.assertTrue(vpersona.verify(testobj))
  636. # and that a bogus signature
  637. bogussig = 'somebogussig'
  638. bogusobj = MDBase.create_obj(testobj)
  639. bogusobj.sig = bogussig
  640. # fails to verify
  641. self.assertRaises(Exception, vpersona.verify, bogusobj)
  642. # and that a modified object
  643. otherobj = testobj.new_version(('customprop', 'value'))
  644. # fails to verify
  645. self.assertRaises(Exception, vpersona.verify, otherobj)
  646. # that a persona object can be written
  647. perpath = os.path.join(self.basetempdir, 'persona.pasn1')
  648. persona.store(perpath)
  649. # and that when loaded back
  650. loadpersona = Persona.load(perpath)
  651. # the new persona object can sign an object
  652. nvtestobj = loadpersona.sign(testobj.new_version())
  653. # and the old persona can verify it.
  654. self.assertTrue(vpersona.verify(nvtestobj))
  655. def test_persona_metadata(self):
  656. # that a persona
  657. persona = Persona()
  658. persona.generate_key()
  659. # can create a metadata object
  660. hashobj = ['asdlfkj']
  661. mdobj = persona.MetaData(hashes=hashobj)
  662. # that the object has the correct created_by_ref
  663. self.assertEqual(mdobj.created_by_ref, persona.uuid)
  664. # and has the provided hashes
  665. self.assertEqual(mdobj.hashes, hashobj)
  666. # and that it can be verified
  667. persona.verify(mdobj)
  668. def test_objectstore(self):
  669. objst = ObjectStore.load(os.path.join('fixtures', 'sample.data.pasn1'))
  670. objst.loadobj({
  671. 'type': 'metadata',
  672. 'uuid': uuid.UUID('c9a1d1e2-3109-4efd-8948-577dc15e44e7'),
  673. 'modified': datetime.datetime(2019, 5, 31, 14, 3, 10),
  674. 'created_by_ref': self.created_by_ref,
  675. 'hashes': [ 'sha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada' ],
  676. 'lang': 'en',
  677. })
  678. lst = objst.by_hash('91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada')
  679. self.assertEqual(len(lst), 2)
  680. byid = objst.by_id('3e466e06-45de-4ecc-84ba-2d2a3d970e96')
  681. self.assertIsInstance(byid, MetaData)
  682. self.assertIn(byid, lst)
  683. r = byid
  684. self.assertEqual(r.uuid, uuid.UUID('3e466e06-45de-4ecc-84ba-2d2a3d970e96'))
  685. self.assertEqual(r['dc:creator'], [ u'John-Mark Gurney' ])
  686. fname = 'testfile.pasn1'
  687. objst.store(fname)
  688. with open(fname) as fp:
  689. objs = _asn1coder.loads(fp.read())
  690. os.unlink(fname)
  691. self.assertEqual(len(objs), len(objst))
  692. self.assertEqual(objs['created_by_ref'], self.created_by_ref.bytes)
  693. for i in objs['objects']:
  694. i['created_by_ref'] = uuid.UUID(bytes=i['created_by_ref'])
  695. i['uuid'] = uuid.UUID(bytes=i['uuid'])
  696. self.assertEqual(objst.by_id(i['uuid']), i)
  697. testfname = os.path.join(self.tempdir, 'test.txt')
  698. self.assertEqual(objst.by_file(testfname), [ byid ])
  699. self.assertEqual(objst.by_file(testfname), [ byid ])
  700. self.assertRaises(KeyError, objst.by_file, '/dev/null')
  701. # XXX make sure that object store contains fileobject
  702. # Tests to add:
  703. # Non-duplicates when same metadata is located by multiple hashes.
  704. def test_main(self):
  705. # Test the main runner, this is only testing things that are
  706. # specific to running the program, like where the store is
  707. # created.
  708. # setup object store
  709. storefname = os.path.join(self.tempdir, 'storefname')
  710. identfname = os.path.join(self.tempdir, 'identfname')
  711. shutil.copy(os.path.join('fixtures', 'sample.data.pasn1'), storefname)
  712. # setup path mapping
  713. def expandusermock(arg):
  714. if arg == '~/.medashare_store.pasn1':
  715. return storefname
  716. elif arg == '~/.medashare_identity.pasn1':
  717. return identfname
  718. # setup test fname
  719. testfname = os.path.join(self.tempdir, 'test.txt')
  720. import sys
  721. import StringIO
  722. import itertools
  723. with mock.patch('os.path.expanduser', side_effect=expandusermock) \
  724. as eu:
  725. # that generating a new identity
  726. with nested(mock.patch('sys.stdout',
  727. StringIO.StringIO()), mock.patch('sys.argv',
  728. [ 'progname', '-g', '-a', 'name=A Test User' ])) as (stdout, argv):
  729. main()
  730. # does not output anything
  731. self.assertEqual(stdout.getvalue(), '')
  732. # looks up the correct file
  733. eu.assert_called_with('~/.medashare_identity.pasn1')
  734. # and that the identity
  735. persona = Persona.load(identfname)
  736. pident = persona.get_identity()
  737. # has the correct name
  738. self.assertEqual(pident.name, 'A Test User')
  739. # that when generating an identity when one already exists
  740. with nested(mock.patch('sys.stderr',
  741. StringIO.StringIO()), mock.patch('sys.argv',
  742. [ 'progname', '-g', '-a', 'name=A Test User' ])) as (stderr, argv):
  743. # that it exits
  744. with self.assertRaises(SystemExit) as cm:
  745. main()
  746. # with error code 1
  747. self.assertEqual(cm.exception[0], 1)
  748. # and outputs an error message
  749. self.assertEqual(stderr.getvalue(),
  750. 'Error: Identity already created.\n')
  751. # and looked up the correct file
  752. eu.assert_called_with('~/.medashare_identity.pasn1')
  753. # that when updating the identity
  754. with nested(mock.patch('sys.stdout',
  755. StringIO.StringIO()), mock.patch('sys.argv',
  756. [ 'progname', '-i', '-a', 'name=Changed Name' ])) as (stdout, argv):
  757. main()
  758. # it doesn't output anything
  759. self.assertEqual(stdout.getvalue(), '')
  760. # and looked up the correct file
  761. eu.assert_called_with('~/.medashare_identity.pasn1')
  762. npersona = Persona.load(identfname)
  763. nident = npersona.get_identity()
  764. # and has the new name
  765. self.assertEqual(nident.name, 'Changed Name')
  766. # and has the same old uuid
  767. self.assertEqual(nident.uuid, pident.uuid)
  768. # and that the modified date has changed
  769. self.assertNotEqual(pident.modified, nident.modified)
  770. # and that the old Persona can verify the new one
  771. self.assertTrue(persona.verify(nident))
  772. # that when asked to print the public key
  773. with nested(mock.patch('sys.stdout',
  774. StringIO.StringIO()), mock.patch('sys.argv',
  775. [ 'progname', '-p' ])) as (stdout, argv):
  776. main()
  777. # the correct key is printed
  778. self.assertEqual(stdout.getvalue(),
  779. '%s\n' % persona.get_pubkey())
  780. # and looked up the correct file
  781. eu.assert_called_with('~/.medashare_identity.pasn1')
  782. with nested(mock.patch('sys.stdout',
  783. StringIO.StringIO()), mock.patch('sys.argv',
  784. [ 'progname', '-l', testfname ])) as (stdout, argv):
  785. main()
  786. self.assertEqual(stdout.getvalue(),
  787. 'dc:creator:\tJohn-Mark Gurney\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n')
  788. eu.assert_called_with('~/.medashare_store.pasn1')
  789. with nested(mock.patch('sys.stdout',
  790. StringIO.StringIO()), mock.patch('sys.argv',
  791. [ 'progname', '-a', 'dc:creator=Another user', '-a', 'foo=bar=baz', testfname ])) as (stdout, argv):
  792. main()
  793. with nested(mock.patch('sys.stdout',
  794. StringIO.StringIO()), mock.patch('sys.argv',
  795. [ 'progname', '-l', testfname ])) as (stdout, argv):
  796. main()
  797. self.assertEqual(stdout.getvalue(),
  798. 'dc:creator:\tAnother user\ndc:creator:\tJohn-Mark Gurney\nfoo:\tbar=baz\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n')
  799. with nested(mock.patch('sys.stdout',
  800. StringIO.StringIO()), mock.patch('sys.argv',
  801. [ 'progname', '-d', 'dc:creator', testfname ])) as (stdout, argv):
  802. main()
  803. with nested(mock.patch('sys.stdout',
  804. StringIO.StringIO()), mock.patch('sys.argv',
  805. [ 'progname', '-l', testfname ])) as (stdout, argv):
  806. main()
  807. self.assertEqual(stdout.getvalue(),
  808. 'foo:\tbar=baz\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n')
  809. with nested(mock.patch('sys.stdout',
  810. StringIO.StringIO()), mock.patch('sys.argv',
  811. [ 'progname', '-a', 'foo=bleh', testfname ])) as (stdout, argv):
  812. main()
  813. with nested(mock.patch('sys.stdout',
  814. StringIO.StringIO()), mock.patch('sys.argv',
  815. [ 'progname', '-l', testfname ])) as (stdout, argv):
  816. main()
  817. self.assertEqual(stdout.getvalue(),
  818. 'foo:\tbar=baz\nfoo:\tbleh\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n')
  819. with nested(mock.patch('sys.stdout',
  820. StringIO.StringIO()), mock.patch('sys.argv',
  821. [ 'progname', '-d', 'foo=bar=baz', testfname ])) as (stdout, argv):
  822. main()
  823. with nested(mock.patch('sys.stdout',
  824. StringIO.StringIO()), mock.patch('sys.argv',
  825. [ 'progname', '-l', testfname ])) as (stdout, argv):
  826. main()
  827. self.assertEqual(stdout.getvalue(),
  828. 'foo:\tbleh\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n')