diff --git a/invenio_theme_tugraz/config.py b/invenio_theme_tugraz/config.py index 576f6e8..e87fcb2 100644 --- a/invenio_theme_tugraz/config.py +++ b/invenio_theme_tugraz/config.py @@ -105,22 +105,10 @@ DEPOSITS_HEADER_TEMPLATE = "invenio_theme_tugraz/header.html" TUG_ROUTES = { "index": "/", "comingsoon": "/comingsoon", + "deposit_create": "/uploads/new", } -# Invenio-app-rdm -# ============= -# See https://invenio-app-rdm.readthedocs.io/en/latest/configuration.html -# """override the default search page""" -# Keep this in sync -APP_RDM_ROUTES = { - "index": "/notvalid/notvalid/notvalid", - "help_search": "/help/search", - "record_search": "/search2", - "record_detail": "/records/", - "record_export": "/records//export/", - "record_file_preview": "/records//preview/", - "record_file_download": "/records//files/", - "deposit_search": "/uploads", - "deposit_create": "/uploads/new", - "deposit_edit": "/uploads/", -} +INVENIO_DATACITE_URL = "" +INVENIO_DATACITE_UNAME = "" +INVENIO_DATACITE_PASS = "password" +INVENIO_DATACITE_PREFIX = "" diff --git a/invenio_theme_tugraz/crypto.py b/invenio_theme_tugraz/crypto.py new file mode 100644 index 0000000..c778326 --- /dev/null +++ b/invenio_theme_tugraz/crypto.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +"""crypto helper module. see https://gist.github.com/marcoslin/8026990.""" + +import binascii + +from Crypto import Random +from Crypto.Cipher import AES + + +# ------------------------------ +# DEFINE Encryption Class +class Cryptor(object): + """Crypto class implementation. + + Provide encryption and decryption function that works with crypto-js. + https://code.google.com/p/crypto-js/ + + Padding implemented as per RFC 2315: PKCS#7 page 21 + http://www.ietf.org/rfc/rfc2315.txt + + The key to make pycrypto work with crypto-js are: + 1. Use MODE_CFB. For some reason, crypto-js decrypted result from MODE_CBC + gets truncated + 2. Use Pkcs7 padding as per RFC 2315, the default padding used by CryptoJS + 3. On the JS side, make sure to wrap ciphertext with CryptoJS.lib.CipherParams.create() + """ + + # AES-256 key (32 bytes) + KEY = "01ab38d5e05c92aa098921d9d4626107133c7e2ab0e4849558921ebcc242bcb0" + BLOCK_SIZE = 16 + + @classmethod + def _pad_string(cls, in_string): + """Pad an input string according to PKCS#7.""" + in_len = len(in_string) + pad_size = cls.BLOCK_SIZE - (in_len % cls.BLOCK_SIZE) + return in_string.ljust(in_len + pad_size, chr(pad_size)) + + @classmethod + def _unpad_string(cls, in_string): + """Remove the PKCS#7 padding from a text string.""" + in_len = len(in_string) + pad_size = ord(in_string[-1]) + if pad_size > cls.BLOCK_SIZE: + raise ValueError("Input is not padded or padding is corrupt") + return in_string[: in_len - pad_size] + + @classmethod + def generate_iv(cls, size=16): + """Generate initialization vector.""" + return Random.get_random_bytes(size) + + @classmethod + def encrypt(cls, in_string, in_key, in_iv=None): + """Return encrypted string. + + @in_string: Simple str to be encrypted + @key: hexified key + @iv: hexified iv + """ + key = binascii.a2b_hex(in_key) + + if in_iv is None: + iv = cls.generate_iv() + in_iv = binascii.b2a_hex(iv) + else: + iv = binascii.a2b_hex(in_iv) + + aes = AES.new(key, AES.MODE_CFB, iv, segment_size=128) + padded = cls._pad_string(in_string).encode("utf-8") + encrypted = aes.encrypt(padded) + return in_iv, encrypted + + @classmethod + def decrypt(cls, in_encrypted, in_key, in_iv): + """Return encrypted string. + + @in_encrypted: Base64 encoded + @key: hexified key + @iv: hexified iv + """ + key = binascii.a2b_hex(in_key) + iv = binascii.a2b_hex(in_iv) + aes = AES.new(key, AES.MODE_CFB, iv, segment_size=128) + + decrypted = aes.decrypt(binascii.a2b_base64(in_encrypted).rstrip()) + return cls._unpad_string(decrypted) diff --git a/invenio_theme_tugraz/ext.py b/invenio_theme_tugraz/ext.py index ae12527..0a609b3 100644 --- a/invenio_theme_tugraz/ext.py +++ b/invenio_theme_tugraz/ext.py @@ -9,6 +9,7 @@ """invenio module for TUGRAZ theme.""" from . import config +from .views import deposit_create, index class InvenioThemeTugraz(object): @@ -21,6 +22,10 @@ class InvenioThemeTugraz(object): def init_app(self, app): """Flask application initialization.""" + # add index route rule + # https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.add_url_rule + app.add_url_rule("/", "index", index) + app.add_url_rule("/uploads/new", "deposit_create", deposit_create) self.init_config(app) app.extensions["invenio-theme-tugraz"] = self diff --git a/invenio_theme_tugraz/views.py b/invenio_theme_tugraz/views.py index 920cd58..e79f1c6 100644 --- a/invenio_theme_tugraz/views.py +++ b/invenio_theme_tugraz/views.py @@ -8,12 +8,20 @@ """invenio module for TUGRAZ theme.""" +import binascii from typing import Dict from elasticsearch_dsl.utils import AttrDict -from flask import Blueprint, render_template +from flask import Blueprint, current_app, render_template +from flask_login import login_required from flask_menu import current_menu +from invenio_app_rdm.records_ui.views.deposits import ( + get_form_config, + get_search_url, + new_record, +) +from .crypto import Cryptor from .search import FrontpageRecordsSearch @@ -30,6 +38,7 @@ def ui_blueprint(app): blueprint.add_url_rule(routes["index"], view_func=index) blueprint.add_url_rule(routes["comingsoon"], view_func=comingsoon) + blueprint.add_url_rule(routes["deposit_create"], view_func=deposit_create) @blueprint.app_template_filter("make_dict_like") def make_dict_like(value: str, key: str) -> Dict[str, str]: @@ -51,9 +60,42 @@ def index(): """Frontpage.""" return render_template( "invenio_theme_tugraz/index.html", - records=FrontpageRecordsSearch()[:5].sort("-created").execute()) + records=FrontpageRecordsSearch()[:5].sort("-created").execute(), + ) def comingsoon(): """Frontpage.""" return render_template("invenio_theme_tugraz/comingsoon.html") + + +def get_application_details(): + """Application credentials for DOI.""" + url = current_app.config.get("invenio_datacite_url") or "" + username = current_app.config.get("INVENIO_DATACITE_UNAME") or "" + password = current_app.config.get("INVENIO_DATACITE_PASS") or "" + prefix = current_app.config.get("INVENIO_DATACITE_PREFIX") or "" + + password_iv, encrypted_password = Cryptor.encrypt(password, Cryptor.KEY) + + details = { + "datacite_url": url, + "datacite_uname": username, + "datacite_pass": binascii.b2a_base64(encrypted_password).rstrip(), + "datacite_prefix": prefix, + "datacite_password_iv": password_iv, + } + return details + + +@login_required +def deposit_create(): + """Create a new deposit.""" + return render_template( + "invenio_app_rdm/records/deposit.html", + forms_config=get_form_config(createUrl=("/api/records")), + datacite_config=get_application_details(), + searchbar_config=dict(searchUrl=get_search_url()), + record=new_record(), + files=dict(default_preview=None, enabled=True, entries=[], links={}), + ) diff --git a/setup.py b/setup.py index f561b4a..8b400ab 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,9 @@ install_requires = [ "elasticsearch_dsl>=7.2.1", "invenio_search>=1.4.0,<2.0.0", # keep this package updated. - "invenio_app_rdm>=0.18.8", + "invenio_app_rdm<=1.0.0", + # needed for DOI credential encryption + "pycryptodome==3.10.1", ] packages = find_packages()