From 4f3e463975052bac99c43dd54aac3eb95700a70a Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Sun, 10 Jul 2016 17:25:39 +0200 Subject: [PATCH] [ADD] document_sftp --- .travis.yml | 1 + document_sftp/README.rst | 91 ++++++++++++++ document_sftp/__init__.py | 5 + document_sftp/__openerp__.py | 26 ++++ document_sftp/data/ir_config_parameter.xml | 15 +++ document_sftp/demo/demo.key | 51 ++++++++ document_sftp/demo/res_users.xml | 8 ++ document_sftp/document_sftp_handle.py | 20 +++ document_sftp/document_sftp_server.py | 49 ++++++++ document_sftp/document_sftp_sftp_server.py | 52 ++++++++ document_sftp/document_sftp_transport.py | 29 +++++ document_sftp/hooks.py | 33 +++++ document_sftp/models/__init__.py | 7 ++ document_sftp/models/document_sftp.py | 118 ++++++++++++++++++ document_sftp/models/document_sftp_root.py | 64 ++++++++++ .../models/document_sftp_root_by_model.py | 96 ++++++++++++++ document_sftp/models/res_users.py | 18 +++ document_sftp/static/description/icon.png | Bin 0 -> 9455 bytes document_sftp/tests/__init__.py | 4 + document_sftp/tests/test_document_sftp.py | 43 +++++++ document_sftp/views/res_users.xml | 29 +++++ 21 files changed, 759 insertions(+) create mode 100644 document_sftp/README.rst create mode 100644 document_sftp/__init__.py create mode 100644 document_sftp/__openerp__.py create mode 100644 document_sftp/data/ir_config_parameter.xml create mode 100644 document_sftp/demo/demo.key create mode 100644 document_sftp/demo/res_users.xml create mode 100644 document_sftp/document_sftp_handle.py create mode 100644 document_sftp/document_sftp_server.py create mode 100644 document_sftp/document_sftp_sftp_server.py create mode 100644 document_sftp/document_sftp_transport.py create mode 100644 document_sftp/hooks.py create mode 100644 document_sftp/models/__init__.py create mode 100644 document_sftp/models/document_sftp.py create mode 100644 document_sftp/models/document_sftp_root.py create mode 100644 document_sftp/models/document_sftp_root_by_model.py create mode 100644 document_sftp/models/res_users.py create mode 100644 document_sftp/static/description/icon.png create mode 100644 document_sftp/tests/__init__.py create mode 100644 document_sftp/tests/test_document_sftp.py create mode 100644 document_sftp/views/res_users.xml diff --git a/.travis.yml b/.travis.yml index da6f1dbc..835e2427 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ virtualenv: install: - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} + - pip install --upgrade paramiko - travis_install_nightly script: diff --git a/document_sftp/README.rst b/document_sftp/README.rst new file mode 100644 index 00000000..5a637b99 --- /dev/null +++ b/document_sftp/README.rst @@ -0,0 +1,91 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==== +SFTP +==== + +This module allows you to access your data (= documents attached to records) +via SFTP. There will be different virtual roots to get your browsing started, +for the time being, there's only one map `By model` that lets you browse ids +of the records of the different models. + +Installation +============ + +To install this module, you need to: + +#. install paramiko. You need version 2.0 or higher +#. install the module. It will generate a new host key during installation + +Configuration +============= + +To configure this module, you need to: + +#. be sure there's a proper hostkey in config parameter ``document_sftp.hostkey`` +#. add some authorized key on the users' form who should be allowed to login via SFTP + +Usage +===== + +To use this module, you need to: + +#. add some keys to your user's authorized key field +#. say ``sftp -p 2222 $yourodoohost`` and browse through the possibilities +#. when you found what you want, say ``sshfs -p 2222 "admin@localhost:By model/res.company/1" $yourmountpoint`` to see all attachments of your main company in ``$yourmountpoint`` +#. pay someone to implement locking (see below) in order to be able to write on this + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/118/8.0 + +Known issues / Roadmap +====================== + +* implement writing. This is not trivial, because we need some concept of locking, UI for that and take care of some corner cases +* allow to restrict actions on SFTP that are allowed normally (because ``rf -rf $yourmountpoint/`` can become a serious problem) +* should support directories +* implement some other useful virtual roots (Mails, By Directory, By name, ...) + This should look like symlinks to the rigid designator (model+id) from the point of view of the user. +* with this, `auth_ssh` should be simple to implement enabling passwordless logins for your scripts +* if you want to see error messages from the SFTP server thread, use ``--log-handler=paramiko:DEBUG`` + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Holger Brunn + +Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list `_ or the `appropriate specialized mailinglist `_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/document_sftp/__init__.py b/document_sftp/__init__.py new file mode 100644 index 00000000..f58eeba9 --- /dev/null +++ b/document_sftp/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models +from .hooks import post_init_hook diff --git a/document_sftp/__openerp__.py b/document_sftp/__openerp__.py new file mode 100644 index 00000000..43b2c1c7 --- /dev/null +++ b/document_sftp/__openerp__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "SFTP", + "version": "8.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Knowledge Management", + "summary": "Access your documents via SFTP", + "depends": [ + 'base', + ], + "demo": [ + "demo/res_users.xml", + ], + "data": [ + "demo/res_users.xml", + "views/res_users.xml", + "data/ir_config_parameter.xml", + ], + "post_init_hook": 'post_init_hook', + "external_dependencies": { + 'python': ['paramiko'], + }, +} diff --git a/document_sftp/data/ir_config_parameter.xml b/document_sftp/data/ir_config_parameter.xml new file mode 100644 index 00000000..38c3928a --- /dev/null +++ b/document_sftp/data/ir_config_parameter.xml @@ -0,0 +1,15 @@ + + + + + document_sftp.hostkey + If you see this text, initialization went wrong + + + + document_sftp.bind + localhost:2222 + + + + diff --git a/document_sftp/demo/demo.key b/document_sftp/demo/demo.key new file mode 100644 index 00000000..0dd6133d --- /dev/null +++ b/document_sftp/demo/demo.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/document_sftp/demo/res_users.xml b/document_sftp/demo/res_users.xml new file mode 100644 index 00000000..b400539e --- /dev/null +++ b/document_sftp/demo/res_users.xml @@ -0,0 +1,8 @@ + + + + + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCuJbpX5OB8tnXzn+T72D+uIHTvCnpuDMD2So5lfkJsU3M4TvidERP0MtZykNNjs2RmIV97CLTdfJXxWpjYm92KWhd9X52lFBKVIJO2FHayhWw4VQGBeNXd9iGs3h9g5/B5LPvdPeZ4OtgpuTv06fTJTdBmzjmOCSa+5j/FWWbEr3F5TTRcba7v3W2a+qvzh1PODpeBFpXEGQqK2DnAA5XwmZG17fo0hHdLhQD1vb//vL1E8sMHWc/zDn8tef8JAmWpkK7Cuib7oB55/+7PkgcsRqQx3FRhg0hcmSwPWLBT+GfU0fUfAILUPVukkOiCl8GSqN+St0fYhPtbFPwLkAscmZCTWbI0HViLXR5nsWHd1eziimpDJXeo7d2wo7cvQpSdZFKlERws5gErEgewPYFYW7SqPFVf0hZ9AqHiX01Nu3tcVUdxQvw7+92ShAcar5CVcrWdD2T5YdxDNapUJ0uL6t/vvLYz8OpcsoyfXbMFG/KPxs9UTKx7UqaPOK6pyc12MxJ+EksEMhqGY+dyPwBfwce4R9FybRhz+BhYP5ABl54dxrF894xrrkHeqS1avgslx9fUU9mXb6LyAbuyykCI6JUsKeR+O3SnLv3PdAVYmqqa/7MhMQWz+jeX3nrs+9OtETpB8P0EtcyzDNwS0keLu4pHhXH9hPmLBRDiTQ4F0Q== + + + diff --git a/document_sftp/document_sftp_handle.py b/document_sftp/document_sftp_handle.py new file mode 100644 index 00000000..a5e0b53c --- /dev/null +++ b/document_sftp/document_sftp_handle.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from paramiko import SFTP_EOF, SFTPHandle +from base64 import b64decode + + +class DocumentSFTPHandle(SFTPHandle): + def __init__(self, attachment, flags=0): + self.attachment = attachment + super(DocumentSFTPHandle, self).__init__(flags) + + def stat(self): + return self.attachment.env['document.sftp.root']._file(self.attachment) + + def read(self, offset, length): + data = b64decode(self.attachment.datas) + if offset > len(data): + return SFTP_EOF + return data[offset:offset + length] diff --git a/document_sftp/document_sftp_server.py b/document_sftp/document_sftp_server.py new file mode 100644 index 00000000..383ac6e8 --- /dev/null +++ b/document_sftp/document_sftp_server.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from paramiko import AUTH_SUCCESSFUL, AUTH_FAILED,\ + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_SUCCEEDED,\ + RSAKey, ServerInterface +from paramiko.py3compat import decodebytes +from openerp.exceptions import AccessDenied + + +class DocumentSFTPServer(ServerInterface): + def __init__(self, env): + self.env = env + super(DocumentSFTPServer, self).__init__() + + def check_auth_password(self, username, password): + try: + user = self.env['res.users'].search([('login', '=', username)]) + if not user: + return AUTH_FAILED + user.sudo(user.id).check_credentials(password) + return AUTH_SUCCESSFUL + except AccessDenied: + pass + return AUTH_FAILED + + def check_auth_publickey(self, username, key): + user = self.env['res.users'].search([('login', '=', username)]) + if not user: + return AUTH_FAILED + for line in (user.authorized_keys or '').split('\n'): + if not line or line.startswith('#'): + continue + key_type, key_data = line.split(' ', 2)[:2] + if key_type != 'ssh-rsa': + self.logger.info( + 'Ignoring key of unknown type for line %s', line) + continue + if RSAKey(data=decodebytes(key_data)) == key: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def get_allowed_auths(self, username): + return 'password,publickey' + + def check_channel_request(self, kind, chanid): + if kind in ('session',): + return OPEN_SUCCEEDED + return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED diff --git a/document_sftp/document_sftp_sftp_server.py b/document_sftp/document_sftp_sftp_server.py new file mode 100644 index 00000000..ed4e7597 --- /dev/null +++ b/document_sftp/document_sftp_sftp_server.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from paramiko import SFTP_PERMISSION_DENIED, SFTPServerInterface, SFTPServer +from openerp import api + + +class DocumentSFTPSftpServerInterface(SFTPServerInterface): + def __init__(self, server, env): + self.env = env + + def list_folder(self, path): + if not path or path in ('/', '.'): + return self.env['document.sftp']._get_root_entries() + handler = self.env['document.sftp']._get_handler_for(path) + if handler is None: + return SFTP_PERMISSION_DENIED + return handler._list_folder(path) + + def lstat(self, path): + if path == '.': + return self.env['document.sftp.root']._directory('/') + handler = self.env['document.sftp']._get_handler_for(path) + if handler is None: + return SFTP_PERMISSION_DENIED + return handler._lstat(path) + + def stat(self, path): + handler = self.env['document.sftp']._get_handler_for(path) + if handler is None: + return SFTP_PERMISSION_DENIED + return handler._stat(path) + + def open(self, path, flags, attr): + handler = self.env['document.sftp']._get_handler_for(path) + if handler is None: + return SFTP_PERMISSION_DENIED + return handler._open(path, flags, attr) + + def session_ended(self): + self.env.cr.close() + return super(DocumentSFTPSftpServerInterface, self).session_ended() + + def session_started(self): + self.env = self.env(cr=self.env.registry.cursor()) + + +class DocumentSFTPSftpServer(SFTPServer): + def start_subsystem(self, name, transport, channel): + with api.Environment.manage(): + return super(DocumentSFTPSftpServer, self).start_subsystem( + name, transport, channel) diff --git a/document_sftp/document_sftp_transport.py b/document_sftp/document_sftp_transport.py new file mode 100644 index 00000000..8e5f0b8f --- /dev/null +++ b/document_sftp/document_sftp_transport.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from paramiko import Transport +from paramiko.transport import DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE +from openerp import api, SUPERUSER_ID +from openerp.modules.registry import RegistryManager + + +class DocumentSFTPTransport(Transport): + def __init__( + self, dbname, sock, default_window_size=DEFAULT_WINDOW_SIZE, + default_max_packet_size=DEFAULT_MAX_PACKET_SIZE, gss_kex=False, + gss_deleg_creds=True + ): + self.dbname = dbname + super(DocumentSFTPTransport, self).__init__( + sock, default_window_size=default_window_size, + default_max_packet_size=default_max_packet_size, gss_kex=gss_kex, + gss_deleg_creds=gss_deleg_creds + ) + + def run(self): + with api.Environment.manage(): + self.env = api.Environment( + RegistryManager.get(self.dbname).cursor(), SUPERUSER_ID, {}) + result = super(DocumentSFTPTransport, self).run() + self.env.cr.close() + return result diff --git a/document_sftp/hooks.py b/document_sftp/hooks.py new file mode 100644 index 00000000..3cbd932c --- /dev/null +++ b/document_sftp/hooks.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging +import StringIO +import socket +from lxml import etree +from paramiko.ecdsakey import ECDSAKey +from openerp import SUPERUSER_ID, tools +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, pool): + if socket.getfqdn().endswith('odoo-community.org'): # pragma: no cover + # we need a different default listeing address on runbot + pool['ir.config_parameter'].set_param( + cr, SUPERUSER_ID, 'document_sftp.bind', '%s:0' % socket.getfqdn()) + hostkey = pool['ir.config_parameter'].get_param( + cr, SUPERUSER_ID, 'document_sftp.hostkey') + parameters = etree.parse( + tools.file_open('document_sftp/data/ir_config_parameter.xml')) + default_value = None + for node in parameters.xpath( + "//record[@id='param_hostkey']//field[@name='value']" + ): + default_value = node.text + if not hostkey or hostkey == default_value: + _logger.info('Generating host key for database %s', cr.dbname) + key = StringIO.StringIO() + ECDSAKey.generate().write_private_key(key) + pool['ir.config_parameter'].set_param( + cr, SUPERUSER_ID, 'document_sftp.hostkey', key.getvalue()) + key.close() diff --git a/document_sftp/models/__init__.py b/document_sftp/models/__init__.py new file mode 100644 index 00000000..89087772 --- /dev/null +++ b/document_sftp/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import document_sftp +from . import res_users +from . import document_sftp_root +from . import document_sftp_root_by_model diff --git a/document_sftp/models/document_sftp.py b/document_sftp/models/document_sftp.py new file mode 100644 index 00000000..fcebd03e --- /dev/null +++ b/document_sftp/models/document_sftp.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import paramiko +import logging +import socket +import StringIO +import threading +from openerp import SUPERUSER_ID, api, models +from openerp.modules.registry import RegistryManager +try: + from ..document_sftp_transport import DocumentSFTPTransport + from ..document_sftp_server import DocumentSFTPServer + from ..document_sftp_sftp_server import DocumentSFTPSftpServerInterface,\ + DocumentSFTPSftpServer +except ImportError: # pragma: no cover + pass +_db2thread = {} +_channels = [] +_logger = logging.getLogger(__name__) + + +class DocumentSFTP(models.AbstractModel): + _name = 'document.sftp' + _description = 'SFTP server' + + def _run_server(self, dbname, stop): + with api.Environment.manage(): + with RegistryManager.get(dbname).cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + env[self._name].__run_server(stop) + + @api.model + def __run_server(self, stop): + # this is heavily inspired by + # https://github.com/rspivak/sftpserver/blob/master/src/sftpserver + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + host, port = self.env['ir.config_parameter'].get_param( + 'document_sftp.bind', 'localhost:0' + ).split(':') + _logger.info('Binding to %s:%s', host, port) + server_socket.bind((host, int(port))) + host_real, port_real = server_socket.getsockname() + _logger.info( + 'Listening to SFTP connections on %s:%s', host_real, port_real) + if host_real != host or port_real != port: + self.env['ir.config_parameter'].set_param( + 'document_sftp.bind', '%s:%s' % (host_real, port_real)) + server_socket.listen(5) + server_socket.settimeout(2) + + while not stop.is_set(): + try: + conn, addr = server_socket.accept() + except socket.timeout: + while _channels and\ + not _channels[0].get_transport().is_active(): + _channels.pop(0) + continue + + _logger.debug('Accepted connection from %s', addr) + + key = StringIO.StringIO(self.env['ir.config_parameter'].get_param( + 'document_sftp.hostkey')) + host_key = paramiko.ecdsakey.ECDSAKey.from_private_key(key) + key.close() + transport = DocumentSFTPTransport(self.env.cr.dbname, conn) + transport.add_server_key(host_key) + transport.set_subsystem_handler( + 'sftp', DocumentSFTPSftpServer, + DocumentSFTPSftpServerInterface, self.env) + + server = DocumentSFTPServer(self.env) + try: + transport.start_server(server=server) + channel = transport.accept() + if channel: + _channels.append(channel) + except (paramiko.SSHException, EOFError): + continue + + @api.model + def _get_root_handlers(self): + return [ + self.env['document.sftp.root.by_model'], + ] + + @api.model + def _get_root_entries(self): + entries = [] + for model in self._get_root_handlers(): + entries.append(model._get_root_attributes()) + return entries + + @api.model + def _get_handler_for(self, path): + # TODO: this can be smarter + return self.env['document.sftp.root.by_model'] + + def _register_hook(self, cr): + if cr.dbname not in _db2thread: + stop = threading.Event() + _db2thread[cr.dbname] = ( + threading.Thread( + target=self._run_server, args=(cr.dbname, stop)), + stop, + ) + _db2thread[cr.dbname][0].start() + from openerp.service.server import server + old_stop = server.stop + + def new_stop(): + stop.set() + old_stop() + + server.stop = new_stop + return super(DocumentSFTP, self)._register_hook(cr) diff --git a/document_sftp/models/document_sftp_root.py b/document_sftp/models/document_sftp_root.py new file mode 100644 index 00000000..093a2814 --- /dev/null +++ b/document_sftp/models/document_sftp_root.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import stat +try: + from paramiko import SFTPAttributes + from ..document_sftp_handle import DocumentSFTPHandle +except ImportError: # pragma: no cover + pass +from openerp import api, models + + +class DocumentSFTPRoot(models.AbstractModel): + _name = 'document.sftp.root' + _description = 'Base class for virtual roots in SFTP' + + @api.model + def _directory(self, name): + """Return SFTPAttributes for a directory with given name""" + result = SFTPAttributes() + result.filename = name + result.st_uid = 0 + result.st_group = 0 + result.st_size = 0 + result.st_mode = stat.S_IFDIR | stat.S_IRUSR | stat.S_IXUSR + return result + + @api.model + def _file(self, attachment): + """Return SFTPAttributes for a given attachment""" + if not hasattr(attachment, '_ids'): + attachment = self.env['ir.attachment'].browse(attachment) + result = SFTPAttributes() + result.filename = attachment.datas_fname or attachment.name + result.st_uid = 0 + result.st_group = 0 + result.st_size = attachment.file_size + result.st_mode = stat.S_IFREG | stat.S_IRUSR + return result + + @api.model + def _file_handle(self, attachment): + """Return a DocumentSFTPHandle for a given attachment""" + return DocumentSFTPHandle(attachment) + + @api.model + def _get_root_attributes(self): + """Return the entry in the root folder as SFTPAttributes""" + raise NotImplementedError() + + @api.model + def _stat(self, path): + """Return file attributes""" + raise NotImplementedError() + + @api.model + def _open(self, path, flags, attr): + """Return file attributes""" + raise NotImplementedError() + + @api.model + def _lstat(self, path): + """Return attributes about a link""" + return self._stat(path) diff --git a/document_sftp/models/document_sftp_root_by_model.py b/document_sftp/models/document_sftp_root_by_model.py new file mode 100644 index 00000000..76d0c818 --- /dev/null +++ b/document_sftp/models/document_sftp_root_by_model.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import os +from openerp import api, models +try: + from paramiko import SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED +except ImportError: # pragma: no cover + pass + + +class DocumentSFTPRootByModel(models.Model): + _inherit = 'document.sftp.root' + _name = 'document.sftp.root.by_model' + _virtual_root = 'By model' + _virtual_root_by_id = 'By id' + + @api.model + def _get_root_attributes(self): + return self._directory(self._virtual_root) + + @api.model + def _stat(self, path): + path = path.strip('/') + if not path.startswith(self._virtual_root): + return SFTP_NO_SUCH_FILE + components = path.split('/') + if len(components) == 1: + return self._get_root_attributes() + elif len(components) in (2, 3): + return self._directory(components[-1]) + elif len(components) == 4: + return self._file(self.env['ir.attachment'].search([ + ('res_model', '=', components[-3]), + ('res_id', '=', components[-2]), + '|', + ('datas_fname', '=', components[-1]), + ('name', '=', components[-1]), + ], limit=1)) + return SFTP_NO_SUCH_FILE + + @api.model + def _list_folder(self, path): + path = path.strip('/') + components = path.split('/') + result = [] + if len(components) == 1: + for model in self.env['ir.model'].search([ + ('osv_memory', '=', False), + ]): + if not self.env['ir.model.access'].check( + model.model, raise_exception=False + ): + continue + result.append(self._directory(model.model)) + elif len(components) == 2: + model = components[-1] + seen = set([]) + if model not in self.env.registry: + return SFTP_NO_SUCH_FILE + for attachment in self.env['ir.attachment'].search([ + ('res_model', '=', model), + ('res_id', '!=', False), + ], order='res_id asc'): + # TODO: better lump ids together in steps of 100 or something? + if attachment.res_id not in seen: + seen.add(attachment.res_id) + result.append(self._directory(str(attachment.res_id))) + elif len(components) == 3: + model = components[-2] + res_id = int(components[-1]) + for attachment in self.env['ir.attachment'].search([ + ('res_model', '=', model), + ('res_id', '=', res_id), + ]): + result.append(self._file(attachment)) + else: + return SFTP_NO_SUCH_FILE + return result + + @api.model + def _open(self, path, flags, attr): + if flags & os.O_WRONLY or flags & os.O_RDWR: + # TODO: do something more sensible here + return SFTP_PERMISSION_DENIED + path = path.strip('/') + components = path.split('/') + if len(components) == 4: + return self._file_handle(self.env['ir.attachment'].search([ + ('res_model', '=', components[-3]), + ('res_id', '=', components[-2]), + '|', + ('datas_fname', '=', components[-1]), + ('name', '=', components[-1]), + ], limit=1)) + return SFTP_PERMISSION_DENIED diff --git a/document_sftp/models/res_users.py b/document_sftp/models/res_users.py new file mode 100644 index 00000000..a4ffd72a --- /dev/null +++ b/document_sftp/models/res_users.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + authorized_keys = fields.Text( + 'Authorized keys', + help='An authorized key file as in ~/.ssh/authorized_keys') + + def _register_hook(self, cr): + if 'authorized_keys' not in self.SELF_WRITEABLE_FIELDS: + self.SELF_WRITEABLE_FIELDS.append('authorized_keys') + self.SELF_READABLE_FIELDS.append('authorized_keys') + return super(ResUsers, self)._register_hook(cr) diff --git a/document_sftp/static/description/icon.png b/document_sftp/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/document_sftp/tests/__init__.py b/document_sftp/tests/__init__.py new file mode 100644 index 00000000..a1e95acb --- /dev/null +++ b/document_sftp/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_document_sftp diff --git a/document_sftp/tests/test_document_sftp.py b/document_sftp/tests/test_document_sftp.py new file mode 100644 index 00000000..0dfedb27 --- /dev/null +++ b/document_sftp/tests/test_document_sftp.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import time +import paramiko +from openerp import tools +from openerp.modules.registry import RegistryManager +from openerp.tests.common import TransactionCase +from openerp.addons.document_sftp.models.document_sftp import _db2thread +from ..hooks import post_init_hook + + +class TestDocumentSftp(TransactionCase): + def test_document_sftp(self): + # without this, your ssh thread gets a real registry with blocks + RegistryManager.enter_test_mode() + # be sure to set a hostkey + post_init_hook(self.env.cr, self.registry) + self.assertTrue( + 'PRIVATE KEY' in + self.env['ir.config_parameter'].get_param('document_sftp.hostkey') + ) + # be sure to start our server + self.env['document.sftp']._register_hook() + # give it some time + time.sleep(5) + # use this to bind to our server + bind = self.env['ir.config_parameter'].get_param('document_sftp.bind') + host, port = bind.split(':') + transport = paramiko.Transport((host, int(port))) + demo_key = paramiko.rsakey.RSAKey( + file_obj=tools.file_open('document_sftp/demo/demo.key')) + transport.connect(username='demo', pkey=demo_key) + + sftp = paramiko.SFTPClient.from_transport(transport) + self.assertTrue('By model' in sftp.listdir('.')) + self.assertTrue('res.company' in sftp.listdir('/By model')) + sftp.close() + # we need to stop our thread before leaving test mode, otherwise: lock + thread = _db2thread[self.env.cr.dbname][0] + _db2thread[self.env.cr.dbname][1].set() + thread.join() + RegistryManager.leave_test_mode() diff --git a/document_sftp/views/res_users.xml b/document_sftp/views/res_users.xml new file mode 100644 index 00000000..aacc1c51 --- /dev/null +++ b/document_sftp/views/res_users.xml @@ -0,0 +1,29 @@ + + + + + res.users + + + + + + + + + + + + + res.users + + + + + + + + + + +