Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LIBFCREPO-1257. Implementation of Publish/Unpublish HTTP Api. #275

Merged
6 changes: 6 additions & 0 deletions docker-plastron.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ COMMANDS:
IMPORT:
SSH_PRIVATE_KEY: /etc/plastron/auth/archelon_id
JOBS_DIR: /var/opt/plastron/jobs
PUBLICATION_WORKFLOW:
HANDLE_ENDPOINT: http://docker.for.mac.localhost:3000/api/v1
HANDLE_JWT_TOKEN: <Replace with value from running `bundle exec rails 'jwt:create_token[publication_workflow]'>
HANDLE_PREFIX: 1903.1
HANDLE_REPO: fcrepo
PUBLIC_URL_PATTERN: http://digital-local/{uuid}
6 changes: 6 additions & 0 deletions plastron-utils/src/plastron/namespaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
acl = Namespace('http://www.w3.org/ns/auth/acl#')
"""[Web Access Controls (WebAC)](https://solidproject.org/TR/wac)"""

activitystreams = Namespace('https://www.w3.org/ns/activitystreams#')
"""[Activity Streams 2.0](https://www.w3.org/TR/activitystreams-core/)"""

bibo = Namespace('http://purl.org/ontology/bibo/')
"""[Bibliographic Ontology](https://www.dublincore.org/specifications/bibo/bibo/)"""

Expand Down Expand Up @@ -105,6 +108,9 @@
umdtype = Namespace('http://vocab.lib.umd.edu/datatype#')
"""[UMD Datatypes Vocabulary](http://vocab.lib.umd.edu/datatype)"""

umdact = Namespace('http://vocab.lib.umd.edu/activity#')
"""[UMD Activity Types Vocabulary](http://vocab.lib.umd.edu/activity)"""

webac = Namespace('http://fedora.info/definitions/v4/webac#')
"""[Fedora Commons WebAC Ontology](https://fedora.info/definitions/v4/2015/09/03/webac)"""

Expand Down
8 changes: 6 additions & 2 deletions plastron-web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ COPY plastron-rdf /opt/plastron/plastron-rdf
COPY plastron-repo /opt/plastron/plastron-repo
COPY plastron-utils /opt/plastron/plastron-utils
COPY plastron-web /opt/plastron/plastron-web
COPY plastron-cli /opt/plastron/plastron-cli
COPY plastron-stomp /opt/plastron/plastron-stomp

WORKDIR /opt/plastron
RUN pip install \
Expand All @@ -24,12 +26,14 @@ RUN pip install \
'./plastron-rdf' \
'./plastron-models' \
'./plastron-repo' \
'./plastron-web'
'./plastron-web' \
'./plastron-cli' \
'./plastron-stomp'

ENV PYTHONUNBUFFERED=1
VOLUME /var/opt/plastron/msg
VOLUME /var/opt/plastron/jobs

EXPOSE 5000

ENTRYPOINT ["plastrond-http"]
ENTRYPOINT ["plastrond-http", "-c", "/etc/plastron-config.yml"]
8 changes: 6 additions & 2 deletions plastron-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ HTTP server for synchronous remote operations
As a Flask application:

```bash
flask --app plastron.web:create_app run
flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
```

To enable debugging, for hot code reloading, set `FLASK_DEBUG=1` either on
the command line or in a `.env` file:

```bash
FLASK_DEBUG=1 flask --app plastron.web:create_app run
FLASK_DEBUG=1 flask --app plastron.web:create_app("/path/to/docker-plastron.yml") run
```

Using the console script entrypoint, which runs the application with the
Expand Down Expand Up @@ -55,6 +55,10 @@ docker swarm init
docker build -t docker.lib.umd.edu/plastrond-http:latest \
-f plastron-web/Dockerfile .

# Copy the docker-plastron-template.yml and edit the configuration
cp docker-plastron.template.yml docker-plastron.yml
vim docker-plastron.yml

# deploy the stack to run the HTTP webapp
docker stack deploy -c plastron-web/compose.yml plastrond
```
Expand Down
6 changes: 6 additions & 0 deletions plastron-web/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ version: "3.7"
services:
http:
image: docker.lib.umd.edu/plastrond-http
configs:
- source: plastron-config
target: /etc/plastron-config.yml
volumes:
- plastrond-jobs:/var/opt/plastron/jobs
environment:
Expand All @@ -11,6 +14,9 @@ services:
volumes:
plastrond-jobs:
plastrond-messages:
configs:
plastron-config:
file: ../docker-plastron.yml
networks:
default:
external: true
Expand Down
68 changes: 68 additions & 0 deletions plastron-web/integration-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Manual Integration Tests for STOMP Commands

## Prerequisites

* a running fcrepo Docker stack
* a running [umd-handle] application

### Required Configuration

_fcrepo-local.yml_

```yaml
REPOSITORY:
REST_ENDPOINT: http://fcrepo-local:8080/fcrepo/rest
RELPATH: /
AUTH_TOKEN: ... # JWT token generated from http://fcrepo-local:8080/fcrepo/user
LOG_DIR: logs
STRUCTURE: hierarchical
PUBLICATION_WORKFLOW:
HANDLE_ENDPOINT: http://handle-local:3000/api/v1
HANDLE_JWT_TOKEN: ... # JWT token generated by running "rails 'jwt:create_token[plastrond-http]'" in the umd-handle application directory
HANDLE_PREFIX: 1903.1
HANDLE_REPO: fcrepo
PUBLIC_URL_PATTERN: http://digital-local/{uuid}
```

## Steps

1. Create a new resource in the repository:
```bash
plastron -c fcrepo-local.yml create -T pcdm:Object
```
2. Export the URI value in bash:
```bash
export URI={value copied from create command output}
```
3. Start up the Plastron HTTP daemon:
```bash
python -m plastron.web.server -c fcrepo-local.yml
```
4. Change into the integration-tests directory from the main Plastron
project directory:
```bash
cd plastron-web/integration-tests
```
5. Enable the integration tests:
```bash
export INTEGRATION_TESTS=1
```
6. Submit the "Publish" message:
```bash
pytest test_publish_http.py
```
7. Confirm that the item now has an `rdf:type` of `umdaccess:Published`.
8. Submit the "Unpublish" message:
```bash
pytest test_unpublish_http.py
```
9. Confirm that the item now does *not* have an `rdf:type` of
`umdaccess:Published`.
10. Submit the "PublishHidden" message:
```bash
pytest test_publish_hidden_http.py
```
11. Confirm that the item now has both the `rdf:type` of
`umdaccess:Published` and `umdaccess:Hidden`.

[umd-handle]: https://github.com/umd-lib/umd-handle
19 changes: 19 additions & 0 deletions plastron-web/integration-tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest


@pytest.fixture
def inbox_url():
return 'http://localhost:5000/inbox'


@pytest.fixture
def jsonld_context():
return [
"https://www.w3.org/ns/activitystreams",
{
"umdact": "http://vocab.lib.umd.edu/activity#",
"Publish": "umdact:Publish",
"PublishHidden": "umdact:PublishHidden",
"Unpublish": "umdact:Unpublish"
}
]
21 changes: 21 additions & 0 deletions plastron-web/integration-tests/test_publish_hidden_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

import pytest
import requests


@pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS', False), reason='integration test')
def test_publish_hidden_http(jsonld_context, inbox_url):
target_uri = os.environ['URI']
response = requests.post(
url=inbox_url,
json={
'@context': jsonld_context,
"type": "PublishHidden",
"object": [target_uri]
},
headers={
'Content-Type': 'application/json',
}
)
assert response.ok
21 changes: 21 additions & 0 deletions plastron-web/integration-tests/test_publish_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

import pytest
import requests


@pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS', False), reason='integration test')
def test_publish_http(jsonld_context, inbox_url):
target_uri = os.environ['URI']
response = requests.post(
url=inbox_url,
json={
'@context': jsonld_context,
"type": "Publish",
"object": [target_uri]
},
headers={
'Content-Type': 'application/json',
}
)
assert response.ok
21 changes: 21 additions & 0 deletions plastron-web/integration-tests/test_unpublish_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

import pytest
import requests


@pytest.mark.skipif(not os.environ.get('INTEGRATION_TESTS', False), reason='integration test')
def test_unpublish_http(jsonld_context, inbox_url):
target_uri = os.environ['URI']
response = requests.post(
url=inbox_url,
json={
'@context': jsonld_context,
"type": "Unpublish",
"object": [target_uri]
},
headers={
'Content-Type': 'application/json',
}
)
assert response.ok
12 changes: 10 additions & 2 deletions plastron-web/src/plastron/web/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging
import os
import yaml
import urllib.parse
from pathlib import Path

from argparse import Namespace
from flask import Flask, url_for
from werkzeug.exceptions import NotFound

from plastron.cli.context import PlastronContext
from plastron.jobs.imports import ImportJob, ImportJobs
from plastron.jobs import JobError, JobConfigError, JobNotFoundError
from plastron.web.activitystream import activitystream_bp
from plastron.utils import envsubst

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -35,10 +39,14 @@ def latest_dropped_items(job: ImportJob):
}


def create_app():
def create_app(config_file: str):
app = Flask(__name__)
with open(config_file, "r") as stream:
config = envsubst(yaml.safe_load(stream))
app.config['CONTEXT'] = Namespace(obj=PlastronContext(config=config, args=Namespace(delegated_user=None)))
jobs_dir = Path(os.environ.get('JOBS_DIR', 'jobs'))
jobs = ImportJobs(directory=jobs_dir)
app.register_blueprint(activitystream_bp)

def get_job(job_id: str):
return jobs.get_job(urllib.parse.unquote(job_id))
Expand Down
92 changes: 92 additions & 0 deletions plastron-web/src/plastron/web/activitystream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import json
import logging
from typing import List
from uuid import uuid4

from flask import Blueprint, Response, current_app, jsonify, request
from rdflib import Graph

from plastron.cli.commands.publish import publish
from plastron.cli.commands.unpublish import unpublish
from plastron.namespaces import activitystreams, rdf, umdact

logger = logging.getLogger(__name__)

activitystream_bp = Blueprint('activitystream', __name__, template_folder='templates')


@activitystream_bp.route('/inbox', methods=['POST'])
def new_activity():
try:
activity = Activity(from_json=request.get_json())
ctx = current_app.config['CONTEXT']
cmd = get_command(activity)
cmd(ctx, uris=activity.objects, force_hidden=activity.force_hidden, force_visible=False)
return Response(status=201)
except ValidationError as e:
logger.error(f'Exception: {e}')
return jsonify({'error': str(e)}), 400
except Exception as e:
logger.error(f'Exception: {e}')
return jsonify({'error': str(e)}), 500


def get_command(activity):
if activity.publish:
return publish
elif activity.unpublish:
return unpublish
else:
raise ValidationError(f'Invalid JSON-LD provided: unsupported activity type.')


class Activity:
mohideen marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, from_json: str):
self.id = uuid4()
self._objects = []
self._publish = False
self._unpublish = False
self._force_hidden = False
g = Graph()
g.parse(data=json.dumps(from_json), format='json-ld')

for s, p, o in g:
if activitystreams.object == p:
self._objects.append(str(o))
elif rdf.type == p:
if o == umdact.Publish:
self._publish = True
elif o == umdact.Unpublish:
self._unpublish = True
elif o == umdact.PublishHidden:
self._publish = True
self._force_hidden = True
else:
raise ValidationError(f'Invalid Activity type: {str(o)}')
if not self.publish and not self.unpublish:
raise ValidationError(f'Invalid JSON-LD provided: Type not specified.')
if not self.objects:
raise ValidationError(f'Invalid JSON-LD provided: Object(s) not specified.')

def __str__(self):
return self.id

@property
def publish(self) -> bool:
return self._publish

@property
def unpublish(self) -> bool:
return self._unpublish

@property
def force_hidden(self) -> bool:
return self._force_hidden

@property
def objects(self) -> List[str]:
return self._objects


class ValidationError(Exception):
pass
Loading