mirror of
https://github.com/OCA/knowledge.git
synced 2025-07-23 04:22:04 -06:00
[ADD] document_sftp
This commit is contained in:
parent
46811e3e5a
commit
4f3e463975
@ -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:
|
||||
|
91
document_sftp/README.rst
Normal file
91
document_sftp/README.rst
Normal file
@ -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
|
||||
<https://github.com/OCA/knowledge/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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Holger Brunn <hbrunn@therp.nl>
|
||||
|
||||
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ 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.
|
5
document_sftp/__init__.py
Normal file
5
document_sftp/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import models
|
||||
from .hooks import post_init_hook
|
26
document_sftp/__openerp__.py
Normal file
26
document_sftp/__openerp__.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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'],
|
||||
},
|
||||
}
|
15
document_sftp/data/ir_config_parameter.xml
Normal file
15
document_sftp/data/ir_config_parameter.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="param_hostkey" model="ir.config_parameter">
|
||||
<field name="key">document_sftp.hostkey</field>
|
||||
<field name="value">If you see this text, initialization went wrong</field>
|
||||
<field name="group_ids" eval="[(4, ref('base.group_system'))]" />
|
||||
</record>
|
||||
<record id="param_bind" model="ir.config_parameter">
|
||||
<field name="key">document_sftp.bind</field>
|
||||
<field name="value">localhost:2222</field>
|
||||
<field name="group_ids" eval="[(4, ref('base.group_system'))]" />
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
51
document_sftp/demo/demo.key
Normal file
51
document_sftp/demo/demo.key
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
8
document_sftp/demo/res_users.xml
Normal file
8
document_sftp/demo/res_users.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="authorized_keys">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCuJbpX5OB8tnXzn+T72D+uIHTvCnpuDMD2So5lfkJsU3M4TvidERP0MtZykNNjs2RmIV97CLTdfJXxWpjYm92KWhd9X52lFBKVIJO2FHayhWw4VQGBeNXd9iGs3h9g5/B5LPvdPeZ4OtgpuTv06fTJTdBmzjmOCSa+5j/FWWbEr3F5TTRcba7v3W2a+qvzh1PODpeBFpXEGQqK2DnAA5XwmZG17fo0hHdLhQD1vb//vL1E8sMHWc/zDn8tef8JAmWpkK7Cuib7oB55/+7PkgcsRqQx3FRhg0hcmSwPWLBT+GfU0fUfAILUPVukkOiCl8GSqN+St0fYhPtbFPwLkAscmZCTWbI0HViLXR5nsWHd1eziimpDJXeo7d2wo7cvQpSdZFKlERws5gErEgewPYFYW7SqPFVf0hZ9AqHiX01Nu3tcVUdxQvw7+92ShAcar5CVcrWdD2T5YdxDNapUJ0uL6t/vvLYz8OpcsoyfXbMFG/KPxs9UTKx7UqaPOK6pyc12MxJ+EksEMhqGY+dyPwBfwce4R9FybRhz+BhYP5ABl54dxrF894xrrkHeqS1avgslx9fUU9mXb6LyAbuyykCI6JUsKeR+O3SnLv3PdAVYmqqa/7MhMQWz+jeX3nrs+9OtETpB8P0EtcyzDNwS0keLu4pHhXH9hPmLBRDiTQ4F0Q==</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
20
document_sftp/document_sftp_handle.py
Normal file
20
document_sftp/document_sftp_handle.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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]
|
49
document_sftp/document_sftp_server.py
Normal file
49
document_sftp/document_sftp_server.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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
|
52
document_sftp/document_sftp_sftp_server.py
Normal file
52
document_sftp/document_sftp_sftp_server.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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)
|
29
document_sftp/document_sftp_transport.py
Normal file
29
document_sftp/document_sftp_transport.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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
|
33
document_sftp/hooks.py
Normal file
33
document_sftp/hooks.py
Normal file
@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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()
|
7
document_sftp/models/__init__.py
Normal file
7
document_sftp/models/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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
|
118
document_sftp/models/document_sftp.py
Normal file
118
document_sftp/models/document_sftp.py
Normal file
@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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)
|
64
document_sftp/models/document_sftp_root.py
Normal file
64
document_sftp/models/document_sftp_root.py
Normal file
@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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)
|
96
document_sftp/models/document_sftp_root_by_model.py
Normal file
96
document_sftp/models/document_sftp_root_by_model.py
Normal file
@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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
|
18
document_sftp/models/res_users.py
Normal file
18
document_sftp/models/res_users.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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)
|
BIN
document_sftp/static/description/icon.png
Normal file
BIN
document_sftp/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
4
document_sftp/tests/__init__.py
Normal file
4
document_sftp/tests/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import test_document_sftp
|
43
document_sftp/tests/test_document_sftp.py
Normal file
43
document_sftp/tests/test_document_sftp.py
Normal file
@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Therp BV <http://therp.nl>
|
||||
# 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()
|
29
document_sftp/views/res_users.xml
Normal file
29
document_sftp/views/res_users.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_users_form" model="ir.ui.view">
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="document_sftp" string="SSH">
|
||||
<group>
|
||||
<field name="authorized_keys" />
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//footer" position="before">
|
||||
<group name="document_sftp" string="SSH">
|
||||
<field name="authorized_keys" />
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
Loading…
Reference in New Issue
Block a user