[ADD] document_sftp

This commit is contained in:
Holger Brunn 2016-07-10 17:25:39 +02:00
parent 46811e3e5a
commit 4f3e463975
No known key found for this signature in database
GPG Key ID: 01C9760FECA3AE18
21 changed files with 759 additions and 0 deletions

View File

@ -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
View 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.

View 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

View 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'],
},
}

View 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>

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View 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>

View 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]

View 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

View 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)

View 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
View 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()

View 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

View 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)

View 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)

View 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

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View 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

View 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()

View 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>