def registered(): if not request.args['state'] == session.pop('state_token'): abort(403) data = { 'client_id': app.config['ORCID_CLIENT_ID'], 'client_secret': app.config['ORCID_CLIENT_SECRET'], 'code': request.args['code'], 'grant_type': 'authorization_code', 'redirect_uri': url_for('registered', _external=True), 'scope': '/authenticate', } response = requests.post( 'https://pub.orcid.org/oauth/token', headers={'Accept': 'application/json'}, allow_redirects=True, data=data) credentials = response.json() if not response.status_code == 200: app.logger.error('Response to request for ORCID credentials was not OK') app.logger.error('Request: %s', data) app.logger.error('Response: %s', response.text) identity = auth.add_user_or_update_credentials(credentials) database.get_db().commit() return make_response( """ <!doctype html> <head> <script type="text/javascript"> localStorage.auth = '{}'; window.close(); </script> </head> <body> """.format(json.dumps( {'name': credentials['name'], 'token': identity.b64token.decode()} )))
def set_permissions(orcid, permissions=None): if permissions is None: permissions = [] needs = set() with app.app_context(): db = database.get_db() cursor = db.cursor() cursor.execute("select id from user where id=?", [orcid]) user = cursor.fetchone() if user is None: raise ValueError('No user with orcid "{}" in database.'.format(orcid)) for permission_name in permissions: permission_attr = "{}_permission".format(permission_name) permission = getattr(auth, permission_attr, None) if permission is None: raise ValueError("No such permission: {}".format(permission_name)) for need in permission.needs: needs.add(tuple(need)) cursor.execute("UPDATE user SET permissions = ? WHERE id = ?", [json.dumps(list(needs)), orcid]) db.commit()
def put(self, id): permission = auth.UpdatePatchPermission(id) if not permission.can(): raise auth.PermissionDenied(permission) try: patch = JsonPatch(parse_json(request)) affected_entities = patching.validate( patch, database.get_dataset()) except ResourceError as e: return e.response() except patching.InvalidPatchError as e: if str(e) != 'Could not apply JSON patch to dataset.': return {'status': 400, 'message': str(e)}, 400 db = database.get_db() curs = db.cursor() curs.execute(''' UPDATE patch_request SET original_patch = ?, updated_entities = ?, removed_entities = ?, updated_by = ? WHERE id = ? ''', (patch.to_string(), json.dumps(sorted(affected_entities['updated'])), json.dumps(sorted(affected_entities['removed'])), g.identity.id, id) ) db.commit()
def get_identity(b64token, cursor=None): if cursor is None: cursor = database.get_db().cursor() rows = cursor.execute(''' SELECT user.id AS user_id, user.permissions AS user_permissions, patch_request.id AS patch_request_id, strftime("%s","now") > token_expires_at AS token_expired FROM user LEFT JOIN patch_request ON user.id = patch_request.created_by AND patch_request.open = 1 WHERE user.b64token = ?''', (b64token,)).fetchall() if not rows: return UnauthenticatedIdentity( 'invalid_token', 'The access token is invalid') if rows[0]['token_expired']: return UnauthenticatedIdentity( 'invalid_token', 'The access token expired') identity = Identity(rows[0]['user_id'], auth_type='bearer') identity.b64token = b64token for p in json.loads(rows[0]['user_permissions']): identity.provides.add(tuple(p)) for r in rows: if r['patch_request_id'] is not None: identity.provides.add(UpdatePatchNeed(value=r['patch_request_id'])) return identity
def setUp(self): self.db_fd, app.config['DATABASE'] = tempfile.mkstemp() app.config['TESTING'] = True commands.init_db() commands.load_data(filepath('test-data.json')) self.client = app.test_client() with open(filepath('test-patch-replace-values-1.json')) as f: self.patch = f.read() with app.app_context(): self.unauthorized_identity = auth.add_user_or_update_credentials({ 'name': 'Dangerous Dan', 'access_token': 'f7e00c02-6f97-4636-8499-037446d95446', 'expires_in': 631138518, 'orcid': '0000-0000-0000-000X', }) db = database.get_db() curs = db.cursor() curs.execute('UPDATE user SET permissions = ? WHERE name = ?', ('[]', 'Dangerous Dan')) self.user_identity = auth.add_user_or_update_credentials({ 'name': 'Regular Gal', 'access_token': '5005eb18-be6b-4ac0-b084-0443289b3378', 'expires_in': 631138518, 'orcid': '1234-5678-9101-112X', }) self.admin_identity = auth.add_user_or_update_credentials({ 'name': 'Super Admin', 'access_token': 'f7c64584-0750-4cb6-8c81-2932f5daabb8', 'expires_in': 3600, 'orcid': '1211-1098-7654-321X', }, (ActionNeed('accept-patch'),)) db.commit()
def registered(): if not request.args['state'] == session.pop('state_token', None): abort(403) data = { 'client_id': app.config['ORCID_CLIENT_ID'], 'client_secret': app.config['ORCID_CLIENT_SECRET'], 'code': request.args['code'], 'grant_type': 'authorization_code', 'redirect_uri': build_redirect_uri(cli=('cli' in request.args)), 'scope': '/authenticate', } response = requests.post( 'https://orcid.org/oauth/token', headers={'Accept': 'application/json'}, allow_redirects=True, data=data) if not response.status_code == 200: app.logger.error('Response to request for ORCID credential was not OK') app.logger.error('Request: %s', data) app.logger.error('Response: %s', response.text) credentials = response.json() if 'name' not in credentials or len(credentials['name']) == 0: # User has made their name private, so just use their ORCID as name credentials['name'] = credentials['orcid'] identity = auth.add_user_or_update_credentials(credentials) database.get_db().commit() if 'cli' in request.args: return make_response( ('Your token is: {}'.format(identity.b64token.decode()), {'Content-Type': 'text/plain'})) else: return make_response(""" <!doctype html> <head> <script type="text/javascript"> parent.postMessage( {{ name: {}, token: {} }}, "{}" ) window.close(); </script> </head> <body> """.format( json.dumps(credentials['name']), json.dumps(identity.b64token.decode()), request.host_url ))
def add_new_version_of_dataset(data): now = database.query_db( "SELECT CAST(strftime('%s', 'now') AS INTEGER) AS now", one=True)['now'] cursor = database.get_db().cursor() cursor.execute( 'INSERT into DATASET (data, description, created_at) VALUES (?,?,?)', (json.dumps(data), void.describe_dataset(data, now), now)) return cursor.lastrowid
def merge(patch_id, user_id): row = database.query_db( 'SELECT * FROM patch_request WHERE id = ?', (patch_id,), one=True) if not row: raise MergeError('No patch with ID {}.'.format(patch_id)) if row['merged']: raise MergeError('Patch is already merged.') if not row['open']: raise MergeError('Closed patches cannot be merged.') dataset = database.get_dataset() mergeable = is_mergeable(row['original_patch'], dataset) if not mergeable: raise UnmergeablePatchError('Patch is not mergeable.') data = json.loads(dataset['data']) original_patch = from_text(row['original_patch']) applied_patch, id_map = replace_skolem_ids( original_patch, data, database.get_removed_entity_keys()) created_entities = set(id_map.values()) # Should this be ordered? new_data = applied_patch.apply(data) db = database.get_db() curs = db.cursor() curs.execute( ''' UPDATE patch_request SET merged = 1, open = 0, merged_at = strftime('%s', 'now'), merged_by = ?, applied_to = ?, created_entities = ?, identifier_map = ?, applied_patch = ? WHERE id = ?; ''', (user_id, dataset['id'], json.dumps(sorted(created_entities)), json.dumps(id_map), applied_patch.to_string(), row['id']) ) version_id = add_new_version_of_dataset(new_data) curs.execute( ''' UPDATE patch_request SET resulted_in = ? WHERE id = ?; ''', (version_id, row['id']) )
def create_request(patch, user_id): dataset = database.get_dataset() affected_entities = validate(patch, dataset) cursor = database.get_db().cursor() cursor.execute(''' INSERT INTO patch_request (created_by, updated_by, created_from, updated_entities, removed_entities, original_patch) VALUES (?, ?, ?, ?, ?, ?) ''', (user_id, user_id, dataset['id'], json.dumps(sorted(affected_entities['updated'])), json.dumps(sorted(affected_entities['removed'])), patch.to_string())) return cursor.lastrowid
def add_comment(patch_id, user_id, message): row = database.query_db( 'SELECT * FROM patch_request WHERE id = ?', (patch_id,), one=True) if not row: raise MergeError('No patch with ID {}.'.format(patch_id)) db = database.get_db() curs = db.cursor() curs.execute( ''' INSERT INTO patch_request_comment (patch_request_id, author, message) VALUES (?, ?, ?) ''', (patch_id, user_id, message) )
def reject(patch_id, user_id): row = database.query_db( 'SELECT * FROM patch_request WHERE id = ?', (patch_id,), one=True) if not row: raise MergeError('No patch with ID {}.'.format(patch_id)) if row['merged']: raise MergeError('Patch is already merged.') if not row['open']: raise MergeError('Closed patches cannot be merged.') db = database.get_db() curs = db.cursor() curs.execute( ''' UPDATE patch_request SET merged = 0, open = 0, merged_at = strftime('%s', 'now'), merged_by = ? WHERE id = ?; ''', (user_id, row['id'],) )
def add_user_or_update_credentials(credentials, extra_permissions=()): orcid = 'http://orcid.org/{}'.format(credentials['orcid']) b64token = b64encode(credentials['access_token'].encode()) permissions = (ActionNeed('submit-patch'),) + extra_permissions db = database.get_db() cursor = db.cursor() cursor.execute(''' INSERT OR IGNORE INTO user ( id, name, permissions, b64token, token_expires_at, credentials) VALUES (?, ?, ?, ?, strftime('%s','now') + ?, ?) ''', (orcid, credentials['name'], json.dumps(permissions), b64token, credentials['expires_in'], json.dumps(credentials))) if not cursor.lastrowid: # user with this id already in DB cursor.execute(''' UPDATE user SET name = ?, b64token = ?, token_expires_at = strftime('%s','now') + ?, credentials = ? WHERE id = ?''', (credentials['name'], b64token, credentials['expires_in'], json.dumps(credentials), orcid)) return get_identity(b64token, cursor)
def init_db(): with app.app_context(): db = database.get_db() with app.open_resource("schema.sql", mode="r") as schema_file: db.cursor().executescript(schema_file.read()) db.commit()
def make_nanopub(period_id, version): cursor = database.get_db().cursor() cursor.execute( ''' SELECT patch.id as patch_id, patch.merged_at, patch.merged_by, patch.created_by, dataset.data FROM patch_request AS patch LEFT JOIN dataset ON patch.resulted_in = dataset.id WHERE patch.created_entities LIKE ? OR patch.updated_entities LIKE ? ORDER BY patch.id ASC LIMIT ?, 1; ''', ('%"' + identifier.prefix(period_id) + '"%', '%"' + identifier.prefix(period_id) + '"%', version - 1) ) result = cursor.fetchone() if not result: raise PeriodNotFoundError( 'Could not find version {} of period {}'.format( version, period_id)) data = json.loads(result['data']) authority_id = identifier.prefix( period_id[:identifier.AUTHORITY_SEQUENCE_LENGTH + 1]) authority = data['authorities'][authority_id] source = authority['source'] period = authority['periods'][identifier.prefix(period_id)] period['authority'] = authority_id nanopub_uri = '{}/nanopub{}'.format( identifier.prefix(period_id), version) patch_uri = identifier.prefix('h#change-{}'.format(result['patch_id'])) context = data['@context'].copy() context['np'] = 'http://nanopub.org/nschema#' context['pub'] = data['@context']['@base'] + nanopub_uri + '#' context['prov'] = 'http://www.w3.org/ns/prov#' # TODO: Pop "source" from period and include it in the provenance # graph? return { "@context": context, "@graph": [ { "@id": "pub:head", "@graph": { "@id": nanopub_uri, "@type": "np:Nanopublication", "np:hasAssertion": as_uri("pub:assertion"), "np:hasProvenance": as_uri("pub:provenance"), "np:hasPublicationInfo": as_uri("pub:pubinfo"), } }, { "@id": "pub:assertion", "@graph": [period] }, { "@id": "pub:provenance", "@graph": [ { "@id": 'pub:assertion', "dc:source": source } ] }, { "@id": "pub:pubinfo", "@graph": [ { "@id": nanopub_uri, "prov:wasGeneratedBy": as_uri(patch_uri), "prov:asGeneratedAtTime": result['merged_at'], "prov:wasAttributedTo": [ as_uri(result['merged_by']), as_uri(result['created_by']) ] } ] } ] }
def describe_dataset(data, created_at): cursor = database.get_db().cursor() contributors = cursor.execute(''' SELECT DISTINCT created_by, updated_by FROM patch_request WHERE merged = 1 AND id > 1''').fetchall() with open(os.path.join(os.path.dirname(__file__), 'void-stub.ttl')) as f: description_g = Graph().parse(file=f, format='turtle') ns = Namespace(description_g.value( predicate=RDF.type, object=VOID.DatasetDescription)) dataset_g = Graph().parse(data=json.dumps(data), format='json-ld') partitions = description_g.objects( subject=ns.d, predicate=VOID.classPartition) for part in partitions: clazz = description_g.value(subject=part, predicate=VOID['class']) entity_count = len(dataset_g.query(''' SELECT DISTINCT ?s WHERE { ?s a <%s> . FILTER (STRSTARTS(STR(?s), "%s")) }''' % (clazz, ns))) description_g.add( (part, VOID.entities, Literal(entity_count, datatype=XSD.integer))) linksets = description_g.subjects(predicate=RDF.type, object=VOID.Linkset) for linkset in linksets: target = description_g.value( subject=linkset, predicate=VOID.objectsTarget) predicate = description_g.value( subject=linkset, predicate=VOID.linkPredicate) uriSpace = description_g.value( subject=target, predicate=VOID.uriSpace).value triples = len(dataset_g.query(''' SELECT ?s ?p ?o WHERE { ?s <%s> ?o . FILTER (STRSTARTS(STR(?o), "%s")) . }''' % (predicate, uriSpace))) description_g.add( (linkset, VOID.triples, Literal(triples, datatype=XSD.integer))) def add_to_description(p, o): description_g.add((ns.d, p, o)) add_to_description( DCTERMS.modified, Literal(utils.isoformat(created_at), datatype=XSD.dateTime)) add_to_description( DCTERMS.provenance, URIRef(utils.absolute_url(data['@context']['@base'], 'history') + '#changes') ) add_to_description( VOID.triples, Literal(len(dataset_g), datatype=XSD.integer)) for row in contributors: add_to_description( DCTERMS.contributor, URIRef(row['created_by'])) if row['updated_by']: add_to_description( DCTERMS.contributor, URIRef(row['updated_by'])) return description_g.serialize(format='turtle')
def history(): g = Graph() changelog = Collection(g, URIRef("#changelog")) cursor = database.get_db().cursor() for row in cursor.execute( """ SELECT id, created_at, created_by, updated_by, merged_at, merged_by, applied_to, resulted_in, created_entities, updated_entities, removed_entities FROM patch_request WHERE merged = 1 ORDER BY id ASC """ ).fetchall(): change = URIRef("#change-{}".format(row["id"])) patch = URIRef("#patch-{}".format(row["id"])) g.add((patch, FOAF.page, PERIODO[identifier.prefix(url_for("patch", id=row["id"]))])) g.add((change, PROV.startedAtTime, Literal(utils.isoformat(row["created_at"]), datatype=XSD.dateTime))) g.add((change, PROV.endedAtTime, Literal(utils.isoformat(row["merged_at"]), datatype=XSD.dateTime))) dataset = PERIODO[identifier.prefix(url_for("abstract_dataset"))] version_in = PERIODO[identifier.prefix(url_for("abstract_dataset", version=row["applied_to"]))] g.add((version_in, PROV.specializationOf, dataset)) version_out = PERIODO[identifier.prefix(url_for("abstract_dataset", version=row["resulted_in"]))] g.add((version_out, PROV.specializationOf, dataset)) g.add((change, PROV.used, version_in)) g.add((change, PROV.used, patch)) g.add((change, PROV.generated, version_out)) def add_entity_version(entity_id): entity = PERIODO[entity_id] entity_version = PERIODO[entity_id + "?version={}".format(row["resulted_in"])] g.add((entity_version, PROV.specializationOf, entity)) g.add((change, PROV.generated, entity_version)) return entity_version for entity_id in json.loads(row["created_entities"]): add_entity_version(entity_id) for entity_id in json.loads(row["updated_entities"]): entity_version = add_entity_version(entity_id) prev_entity_version = PERIODO[entity_id + "?version={}".format(row["applied_to"])] g.add((entity_version, PROV.wasRevisionOf, prev_entity_version)) for entity_id in json.loads(row["removed_entities"]): g.add((change, PROV.invalidated, PERIODO[entity_id])) for field, term in (("created_by", "submitted"), ("updated_by", "updated"), ("merged_by", "merged")): if row[field] == "initial-data-loader": continue agent = URIRef(row[field]) association = URIRef("#patch-{}-{}".format(row["id"], term)) g.add((change, PROV.wasAssociatedWith, agent)) g.add((change, PROV.qualifiedAssociation, association)) g.add((association, PROV.agent, agent)) g.add((association, PROV.hadRole, PERIODO[identifier.prefix(url_for("vocab") + "#" + term)])) changelog.append(change) def ordering(o): if o["@id"] == "#changelog": # sort first return " " return o["@id"] jsonld = json.loads(g.serialize(format="json-ld", context=CONTEXT).decode("utf-8")) jsonld["history"] = sorted(jsonld["history"], key=ordering) return json.dumps(jsonld, sort_keys=True)