0007-add-utilities-to-access-SFTP-servers-31595.patch
passerelle/base/templates/passerelle/widgets/sftp.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<div class="sftp-widget-url"> |
|
3 |
<label for="{{ widget.subwidgets.0.attrs.id }}">{% trans "URL" %}</label> |
|
4 |
{% include widget.subwidgets.0.template_name with widget=widget.subwidgets.0 %} |
|
5 |
</div> |
|
6 |
<div class="sftp-widget-private-key"> |
|
7 |
<label for="{{ widget.subwidgets.1.attrs.id }}">{% trans "SSH private key" %}</label> |
|
8 |
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} |
|
9 |
</div> |
|
10 |
<div class="sftp-widget-private-key-text"> |
|
11 |
{% include widget.subwidgets.2.template_name with widget=widget.subwidgets.2 %} |
|
12 |
</div> |
|
13 |
<div class="sftp-widget-private-key-password"> |
|
14 |
<label for="{{ widget.subwidgets.3.attrs.id }}">{% trans "SSH private key password" %}</label> |
|
15 |
{% include widget.subwidgets.3.template_name with widget=widget.subwidgets.3 %} |
|
16 |
</div> |
passerelle/utils/sftp.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import os |
|
18 |
import re |
|
19 |
import io |
|
20 |
import json |
|
21 |
import contextlib |
|
22 | ||
23 |
from django import forms |
|
24 |
from django.core import validators |
|
25 |
from django.db import models |
|
26 |
from django.utils import six |
|
27 |
from django.utils.translation import ugettext_lazy as _ |
|
28 |
from django.utils.six.moves.urllib import parse as urlparse |
|
29 |
from django.utils.encoding import force_bytes |
|
30 | ||
31 |
import paramiko |
|
32 |
from paramiko.dsskey import DSSKey |
|
33 |
from paramiko.ecdsakey import ECDSAKey |
|
34 |
from paramiko.ed25519key import Ed25519Key |
|
35 |
from paramiko.rsakey import RSAKey |
|
36 | ||
37 | ||
38 |
def _load_private_key(content_or_file, password=None): |
|
39 |
if not hasattr(content_or_file, 'read'): |
|
40 |
content_or_file = io.BytesIO(force_bytes(content_or_file)) |
|
41 |
for pkey_class in RSAKey, DSSKey, Ed25519Key, ECDSAKey: |
|
42 |
try: |
|
43 |
return pkey_class.from_private_key( |
|
44 |
content_or_file, |
|
45 |
password=password) |
|
46 |
except paramiko.PasswordRequiredException: |
|
47 |
raise |
|
48 |
except paramiko.SSHException: |
|
49 |
pass |
|
50 | ||
51 | ||
52 |
@six.python_2_unicode_compatible |
|
53 |
class SFTP(object): |
|
54 |
def __init__(self, url, private_key_content=None, private_key_password=None): |
|
55 |
self.url = url |
|
56 |
parsed = urlparse.urlparse(url) |
|
57 |
if not parsed.scheme == 'sftp': |
|
58 |
raise ValueError('invalid scheme %s' % parsed.scheme) |
|
59 |
if not parsed.hostname: |
|
60 |
raise ValueError('missing hostname') |
|
61 |
self.username = parsed.username or None |
|
62 |
self.password = parsed.password or None |
|
63 |
self.hostname = parsed.hostname |
|
64 |
self.port = parsed.port or 22 |
|
65 |
self.path = parsed.path.strip('/') |
|
66 |
self.private_key_content = private_key_content |
|
67 |
self.private_key_password = private_key_password |
|
68 |
if private_key_content: |
|
69 |
self.private_key = _load_private_key(private_key_content, private_key_password) |
|
70 |
else: |
|
71 |
self.private_key = None |
|
72 |
self._client = None |
|
73 |
self._transport = None |
|
74 | ||
75 |
def __json__(self): |
|
76 |
return { |
|
77 |
'url': self.url, |
|
78 |
'private_key_content': self.private_key_content, |
|
79 |
'private_key_password': self.private_key_password, |
|
80 |
} |
|
81 | ||
82 |
def __str__(self): |
|
83 |
return re.sub(r'://([^/]*:[^/]*?)@', '://***:***@', self.url) |
|
84 | ||
85 |
# Paramiko can hang processes if not closed, it's important to use it as a |
|
86 |
# contextmanager |
|
87 |
@contextlib.contextmanager |
|
88 |
def client(self): |
|
89 |
ssh = paramiko.SSHClient() |
|
90 |
try: |
|
91 |
ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy) |
|
92 |
ssh.connect( |
|
93 |
hostname=self.hostname, |
|
94 |
port=self.port, |
|
95 |
timeout=5, |
|
96 |
pkey=self.private_key, |
|
97 |
look_for_keys=False, |
|
98 |
allow_agent=False, |
|
99 |
username=self.username, |
|
100 |
password=self.password) |
|
101 |
client = ssh.open_sftp() |
|
102 |
try: |
|
103 |
if self.path: |
|
104 |
client.chdir(self.path) |
|
105 |
base_cwd = str(client._cwd) |
|
106 |
old_adjust_cwd = client._adjust_cwd |
|
107 | ||
108 |
def _adjust_cwd(path): |
|
109 |
path = old_adjust_cwd(path) |
|
110 |
if not os.path.normpath(path).startswith(base_cwd): |
|
111 |
raise ValueError('all paths must be under base path %s: %s' % (base_cwd, path)) |
|
112 |
return path |
|
113 |
client._adjust_cwd = _adjust_cwd |
|
114 |
yield client |
|
115 |
finally: |
|
116 |
client.close() |
|
117 |
finally: |
|
118 |
ssh.close() |
|
119 | ||
120 | ||
121 |
class SFTPURLField(forms.URLField): |
|
122 |
default_validators = [validators.URLValidator(schemes=['sftp'])] |
|
123 | ||
124 | ||
125 |
class SFTPWidget(forms.MultiWidget): |
|
126 |
template_name = 'passerelle/widgets/sftp.html' |
|
127 | ||
128 |
def __init__(self, **kwargs): |
|
129 |
widgets = [ |
|
130 |
forms.TextInput, |
|
131 |
forms.FileInput, |
|
132 |
forms.Textarea, |
|
133 |
forms.TextInput, |
|
134 |
] |
|
135 |
super(SFTPWidget, self).__init__(widgets=widgets, **kwargs) |
|
136 | ||
137 |
def decompress(self, value): |
|
138 |
if not value: |
|
139 |
return [None, None, None, None] |
|
140 |
if hasattr(value, '__json__'): |
|
141 |
value = value.__json__() |
|
142 |
return [ |
|
143 |
value['url'], |
|
144 |
None, |
|
145 |
value.get('private_key_content'), |
|
146 |
value.get('private_key_password'), |
|
147 |
] |
|
148 | ||
149 |
# XXX: bug in Django https://code.djangoproject.com/ticket/29205 |
|
150 |
# required_attribute is initialized from the parent.field required |
|
151 |
# attribute and not from each sub-field attribute |
|
152 |
def use_required_attribute(self, initial): |
|
153 |
return False |
|
154 | ||
155 | ||
156 |
class SFTPFormField(forms.MultiValueField): |
|
157 |
widget = SFTPWidget |
|
158 | ||
159 |
def __init__(self, **kwargs): |
|
160 |
fields = [ |
|
161 |
SFTPURLField(), |
|
162 |
forms.FileField(required=False), |
|
163 |
forms.CharField(required=False), |
|
164 |
forms.CharField(required=False), |
|
165 |
] |
|
166 |
super(SFTPFormField, self).__init__( |
|
167 |
fields=fields, |
|
168 |
require_all_fields=False, **kwargs) |
|
169 | ||
170 |
# def clean(self, value): |
|
171 |
# import pdb |
|
172 |
# pdb.set_trace() |
|
173 |
# return super(SFTPFormField, self).clean(value) |
|
174 | ||
175 |
def compress(self, data_list): |
|
176 |
url, private_key_file, private_key_content, private_key_password = data_list |
|
177 |
if private_key_file: |
|
178 |
private_key_content = private_key_file.read().decode('ascii') |
|
179 |
if private_key_content: |
|
180 |
try: |
|
181 |
pkey = _load_private_key(private_key_content, private_key_password) |
|
182 |
except paramiko.PasswordRequiredException: |
|
183 |
raise forms.ValidationError(_('SSH private key needs a password')) |
|
184 |
if not pkey: |
|
185 |
raise forms.ValidationError(_('SSH private key invalid')) |
|
186 |
return SFTP( |
|
187 |
url=url, |
|
188 |
private_key_content=private_key_content, |
|
189 |
private_key_password=private_key_password) |
|
190 | ||
191 | ||
192 |
class SFTPField(models.Field): |
|
193 |
description = 'A SFTP connection' |
|
194 | ||
195 |
def __init__(self, **kwargs): |
|
196 |
kwargs.setdefault('default', None) |
|
197 |
super(SFTPField, self).__init__(**kwargs) |
|
198 | ||
199 |
def get_internal_type(self): |
|
200 |
return 'TextField' |
|
201 | ||
202 |
def from_db_value(self, value, *args, **kwargs): |
|
203 |
return self.to_python(value) |
|
204 | ||
205 |
def to_python(self, value): |
|
206 |
if not value: |
|
207 |
return None |
|
208 |
if isinstance(value, SFTP): |
|
209 |
return value |
|
210 |
if isinstance(value, six.string_types): |
|
211 |
return SFTP(url=value) |
|
212 |
return SFTP(**json.loads(value)) |
|
213 | ||
214 |
def get_prep_value(self, value): |
|
215 |
if not value: |
|
216 |
return '' |
|
217 |
return json.dumps(value.__json__()) |
|
218 | ||
219 |
def formfield(self, **kwargs): |
|
220 |
defaults = { |
|
221 |
'form_class': SFTPFormField, |
|
222 |
} |
|
223 |
defaults.update(**kwargs) |
|
224 |
return super(SFTPField, self).formfield(**defaults) |
|
225 |
setup.py | ||
---|---|---|
108 | 108 |
'zeep < 3.0', |
109 | 109 |
'pycrypto', |
110 | 110 |
'unidecode', |
111 |
'paramiko', |
|
111 | 112 |
], |
112 | 113 |
cmdclass={ |
113 | 114 |
'build': build, |
tests/ssh_key | ||
---|---|---|
1 |
-----BEGIN RSA PRIVATE KEY----- |
|
2 |
MIIEpAIBAAKCAQEAqH/MTAXMKqz3uQDcliK9udOXlGNUlPN/gVMdxsw0kwmrKdrW |
|
3 |
svuIa3UhrHI/6bPnTTniKvXmccAIm8aAdEg8rAgcksAada6qUYbc0aZbR0WyC+3/ |
|
4 |
fvaLbD9WutD05frrIEwjIUkcXavWSlaHrEzV2T3mGGI8amqR//bjwksRkClx2vvW |
|
5 |
apc5FkzGtxKsc0tA6ZwyA3riqCeEVwOx+ezV8B/631q62h6AAt0j4cb+THzD1t3w |
|
6 |
6KAsxt72lNw6Bp55uR5Uubin3OpijEVL4Gruy1sSJO6pQ5YHpDFTS9ZAHLLldUea |
|
7 |
w+hNha+fGM/EvQJEGZsZSA/urEAuC6Xly8T/JwIDAQABAoIBAGjMe4s4+9/7DmQB |
|
8 |
VjEG0IvYP2mqUfwGamJMCLQRZA2jsNJqaqiNay6yfkwcDwZSv2S3wKRJppdPAcup |
|
9 |
LVGlcB7rOKJJWuugxAvK3mKCnjj47yEeWI9l1hdwWYf92KOFaWIAGMVmDH9yFejM |
|
10 |
YrvWWhcwuYCm8L6bI81YiBXazMSlD+UXB9WOityCc5DayXhTvKiP6mzVQHJJ4p4o |
|
11 |
q/LtBI4jOI7ZEnlI+lYNBvCfZLaPIi7vvLpOXG263ZRlwYr+IjED+7Ex6zjL3YEd |
|
12 |
VFfp2MX5uY3IsXmjyhHokm2f2JRH1JtigjkRysT4AYHk76XktrsczfY6Hgs6Lwz8 |
|
13 |
n6/o2sECgYEA09ErhKlObtWfWdp0HXd/HRubvBQBQ+oYPxNcb6QdWOaXzG8jDBuN |
|
14 |
bxOFXdqknkWzG6pNlurjVW4njeoovts4Wmza5U4Ju9IL2lrUNXqRkopbdcPCw0v6 |
|
15 |
kdJqbui3DwOA/gmphO802jCXu+xASSdoVK8/sCC/itOcAzi8E0YUHCECgYEAy6V+ |
|
16 |
3ivJczRT9xM7hOJA1pJDpLbbuE6XTQBn9QfoP90zAC6Tbr5k7dh0Uyc4JcTOPK7C |
|
17 |
aMmr8ScRL9UxZKsDwxFQPJwz4/R3VCcmJt0j0vgilwm2ujiqWmgqgvM4YBg+dzlU |
|
18 |
J05PenMgzYAp9HD9a8B6d6lOvioF0jx1nbZa8kcCgYEAqJhhDyLDryyRva9HpPys |
|
19 |
TLrg5n710tzNl8cNWD9ErLI+ORZsywJTPQpIqT+Sr/fCbE7Nm0Yy1JjtGuQ6sk9D |
|
20 |
N5ZVVRccYEb78D1Dk52PqRg/XCkJKPGc69yTotvQeT7MuWdvasQLSXBMFeQh9xhK |
|
21 |
zrz+8G3gh9uO3nGWIbEx6IECgYBUW73uMp1Eh8ywcNsa9M5/FB/JP6ZM9uFeGGj3 |
|
22 |
68qdiffyf1i7a0tL63pkZ76uhpQYNxx5Y/FB+Dj6Y4oOdXkdeTKPqPUl3MMBrSX0 |
|
23 |
u253mipZ/sAe7BJFWRkjHbWguOpHYQwnLB1oUACqoAjBJX0VAaq5nvzrcWTv7fOa |
|
24 |
3UtXSQKBgQC8un5szEXQth2viUe5P7FyMsz+34XRKL7qCaCv1/4kGgPhlSbkYHao |
|
25 |
PZ8u5hue/hnmUfpbXNJfc+zazTIojuZ047Z5rJYpKCfs5JozuGOGVOVIf5hunWfJ |
|
26 |
FU3c4umawKvh5tUUeyrXilzEWnVECGYDyzj9BumGT+YkG94yll7iZw== |
|
27 |
-----END RSA PRIVATE KEY----- |
tests/ssh_key_with_password | ||
---|---|---|
1 |
-----BEGIN RSA PRIVATE KEY----- |
|
2 |
Proc-Type: 4,ENCRYPTED |
|
3 |
DEK-Info: AES-128-CBC,AE8F07BCBA597172BBC8D034B3C24C30 |
|
4 | ||
5 |
eg/EgNvQzSeH+W7GP23U2XIbzqg3UQgnSqi+3GbZcZUnqYmsmChiMZy+Wb0b1BIY |
|
6 |
QOFLvOkaiP3hSnfFvx8oSD7/J2h633uSvhtGrq9wIMVUCXLLgBL2k2pzbWHPz1WG |
|
7 |
6eJRAdWQ8c7jCCVkLdFf077ebJXL3tCU/1gP7RJ2wLhkfRXtWARgYPrmw2+ZY6l1 |
|
8 |
tre1O0AS+yjI6ZAsVjPCPqq9WrP4CggtQ3W33/AERGrAWi9PJZFjQUE0cMj6J+R0 |
|
9 |
uciudN+PgO3SRKc+uWX3YrUc5zb+4SMvRO2uMiULyVMahJnnpHSO5qB+dcn6vjcU |
|
10 |
F6ogpfyJpNgm4dCvEwtEiuva1HKlzCQRpWVlHhsRM6l7/faxON8QNND2ZKgO5XS6 |
|
11 |
QFvBbshOLssM7XQQIfdk4yNjx7lWA63W0E+t6sQqTMTeLbWePIq5IlvazFlSqkIS |
|
12 |
I2B3e0Dd3emStwhZhEDncPhysh0P0WOCqnXiql3uGZGkL6bl47qRD2Ft0DnWoNoJ |
|
13 |
0LUTClhInvgi9K50iMup/pq4HN8bo3fAgAcMePF3AqktFTzyIzKK7/pt0jjdX+7N |
|
14 |
MBU9YkrDA3BzwupT71M/hQJIoqGBD68RGFR2Wmxc73xy6Pmv5DDv0e/fGpo0dnHC |
|
15 |
IuhKMs24uRn85OWIGUi+GAbwp9OlJ6Vlh59ZAl4KVSySAnaGCZiAHfWpkWrGMlkd |
|
16 |
wMi1V/L/NGXyzaereK4PUT3IKPFglRr2bTfwrhX83zO5fU4vMuMy22KNQpoX9agG |
|
17 |
J2IkyhMsd56x2JmAhQKLi3rLagzEdx2a4pRJyoU8n95/NhLYnPBBl73weJKtZj/t |
|
18 |
8KNhqjgerPPXdgYua2TE+4e81XrYWhbqdepG0GiYZ23XBIAIor2jgGjswENUw2TZ |
|
19 |
TEFrs9MAx/0r1isz4UurXvyA4IX8TANfJzEqZwimNcn9Eehj6drnvO5GRKCbVRhe |
|
20 |
p0mqMqXnTZGdJyT4EXSNnbRxW5jkaLbDSaiJ+6U0fLawqGRR0VazT4JlOAVz8i3i |
|
21 |
BJxRpYZ/PvRXevh1QrUpgS/wds7+b6nyf4XgV1RsvRS2uuyqx6smE5d+sW9qVZsU |
|
22 |
kuTgI99zr7JSXUO1IDqfBXldnfMVJyGvYMGXdd3UGOis23wDlAd61H6YJAwp75oR |
|
23 |
pOv45Jn8JVQ0w1yFZXTZz2PyDztVpH46RQLxHMPjKber7qN8Lfhc97+01tisV0UB |
|
24 |
k6cWuAXZo61uBr35jr2BRHS1xnrK8Ul4sJPq9LUUaJU2+oZeKNmytTbMRel6w156 |
|
25 |
d64QU75OsT5tZHKKV/NQDLuAWY1ZB+useZNHSkqz860zNzRA+gyb6pLL88Z/Q2YS |
|
26 |
z0OJMXNHxBZhNKk6SPZQTIo6Q0bn8dxY8BwCLSwJGi0NMEugm5f+9+GTjYNplGTm |
|
27 |
z0itYZ8dyhk+rc/48eUg/ctCGM7FbvBaLGkBGqSUrPDFzZjvqASLtKD2IqUyU1bW |
|
28 |
EDlz9PHSX+SGP1A3Y5IKd1L3dbq7lO0SlJnkAOReaagjwNJ+f3dxkDikkBRO+/SB |
|
29 |
V7zuK8XIc1D0Hy13AGdGsiiBF8BXCW7hrhnY+7hKHvtw4nljQ5CGKWhPBauohMdG |
|
30 |
-----END RSA PRIVATE KEY----- |
tests/test_utils_sftp.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import os |
|
18 | ||
19 |
import pytest |
|
20 | ||
21 |
from django.core.files.uploadedfile import SimpleUploadedFile |
|
22 |
from django.db import models |
|
23 | ||
24 |
from passerelle.utils.sftp import SFTP, SFTPFormField, SFTPField |
|
25 | ||
26 | ||
27 |
@pytest.fixture |
|
28 |
def ssh_key(): |
|
29 |
with open( |
|
30 |
os.path.join( |
|
31 |
os.path.dirname(__file__), |
|
32 |
'ssh_key'), 'rb') as fd: |
|
33 |
yield fd.read() |
|
34 | ||
35 | ||
36 |
@pytest.fixture |
|
37 |
def ssh_key_with_password(): |
|
38 |
with open( |
|
39 |
os.path.join( |
|
40 |
os.path.dirname(__file__), |
|
41 |
'ssh_key_with_password'), 'rb') as fd: |
|
42 |
yield fd.read() |
|
43 | ||
44 | ||
45 |
def test_http_url(sftpserver): |
|
46 |
with pytest.raises(ValueError): |
|
47 |
SFTP('https://coin.org/') |
|
48 | ||
49 | ||
50 |
def test_missing_hostname(sftpserver): |
|
51 |
with pytest.raises(ValueError): |
|
52 |
SFTP('sftp://a:x@/hop/') |
|
53 | ||
54 | ||
55 |
def test_sftp_ok(sftpserver): |
|
56 |
with sftpserver.serve_content({'DILA': {'a.zip': 'a'}}): |
|
57 |
with SFTP('sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver)).client() as sftp: |
|
58 |
assert sftp.listdir() == ['a.zip'] |
|
59 | ||
60 | ||
61 |
def test_sftp_bad_paths(sftpserver): |
|
62 |
with sftpserver.serve_content({'DILA': {'a.zip': 'a'}}): |
|
63 |
with SFTP('sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver)).client() as sftp: |
|
64 |
with pytest.raises(ValueError): |
|
65 |
sftp.chdir('..') |
|
66 |
with pytest.raises(ValueError): |
|
67 |
sftp.chdir('/') |
|
68 |
with pytest.raises(ValueError): |
|
69 |
sftp.chdir('/coin') |
|
70 | ||
71 | ||
72 | ||
73 |
def test_form_field(sftpserver, ssh_key, ssh_key_with_password): |
|
74 |
from django import forms |
|
75 | ||
76 |
class Form(forms.Form): |
|
77 |
sftp = SFTPFormField(label='sftp') |
|
78 | ||
79 |
with sftpserver.serve_content({'DILA': {'a.zip': 'a'}}): |
|
80 |
url = 'sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver) |
|
81 | ||
82 |
form = Form(data={'sftp_0': 'http://coin.org'}) |
|
83 |
assert not form.is_valid() |
|
84 |
assert 'Enter a valid URL.' in str(form.errors) |
|
85 | ||
86 |
form = Form(data={'sftp_0': url}) |
|
87 |
assert form.is_valid() |
|
88 |
sftp = form.cleaned_data['sftp'] |
|
89 |
assert isinstance(sftp, SFTP) |
|
90 |
assert sftp.url == url |
|
91 |
assert sftp.username == 'john' |
|
92 |
assert sftp.password == 'doe' |
|
93 |
assert sftp.hostname == sftpserver.host |
|
94 |
assert sftp.port == sftpserver.port |
|
95 |
assert sftp.path == 'DILA' |
|
96 |
assert not sftp.private_key |
|
97 |
with form.cleaned_data['sftp'].client() as sftp: |
|
98 |
assert sftp.listdir() == ['a.zip'] |
|
99 | ||
100 |
form = Form(data={'sftp_0': url, 'sftp_2': ssh_key.decode('ascii')}) |
|
101 |
assert form.is_valid() |
|
102 |
sftp = form.cleaned_data['sftp'] |
|
103 |
assert isinstance(sftp, SFTP) |
|
104 |
assert sftp.url == url |
|
105 |
assert sftp.username == 'john' |
|
106 |
assert sftp.password == 'doe' |
|
107 |
assert sftp.hostname == sftpserver.host |
|
108 |
assert sftp.port == sftpserver.port |
|
109 |
assert sftp.path == 'DILA' |
|
110 |
assert sftp.private_key |
|
111 |
with form.cleaned_data['sftp'].client() as sftp: |
|
112 |
assert sftp.listdir() == ['a.zip'] |
|
113 | ||
114 |
form = Form( |
|
115 |
data={'sftp_0': url}, |
|
116 |
files={'sftp_1': SimpleUploadedFile('ssh_key', ssh_key, 'application/octet-stream')}) |
|
117 |
assert form.is_valid() |
|
118 |
sftp = form.cleaned_data['sftp'] |
|
119 |
assert isinstance(sftp, SFTP) |
|
120 |
assert sftp.url == url |
|
121 |
assert sftp.username == 'john' |
|
122 |
assert sftp.password == 'doe' |
|
123 |
assert sftp.hostname == sftpserver.host |
|
124 |
assert sftp.port == sftpserver.port |
|
125 |
assert sftp.path == 'DILA' |
|
126 |
assert sftp.private_key |
|
127 |
with form.cleaned_data['sftp'].client() as sftp: |
|
128 |
assert sftp.listdir() == ['a.zip'] |
|
129 | ||
130 |
form = Form(data={'sftp_0': url, 'sftp_2': ssh_key_with_password.decode('ascii')}) |
|
131 |
assert not form.is_valid() |
|
132 |
assert 'key invalid' in str(form.errors) |
|
133 | ||
134 |
form = Form(data={ |
|
135 |
'sftp_0': url, |
|
136 |
'sftp_2': ssh_key_with_password.decode('ascii'), |
|
137 |
'sftp_3': 'coucou', |
|
138 |
}) |
|
139 |
assert form.is_valid() |
|
140 |
with form.cleaned_data['sftp'].client() as sftp: |
|
141 |
assert sftp.listdir() == ['a.zip'] |
|
142 | ||
143 | ||
144 |
@pytest.fixture |
|
145 |
def temp_model(db): |
|
146 |
from django.db import connection |
|
147 | ||
148 |
class Model(models.Model): |
|
149 |
sftp = SFTPField(blank=True) |
|
150 | ||
151 |
class Meta: |
|
152 |
app_label = 'test' |
|
153 | ||
154 |
with connection.schema_editor() as editor: |
|
155 |
editor.create_model(Model) |
|
156 |
yield Model |
|
157 |
editor.delete_model(Model) |
|
158 | ||
159 | ||
160 |
def test_model_field(temp_model, ssh_key_with_password): |
|
161 |
instance = temp_model.objects.create() |
|
162 |
assert instance.sftp is None |
|
163 | ||
164 |
url = 'sftp://john:doe@example.com:45/a/b' |
|
165 |
sftp = SFTP(url=url, private_key_content=ssh_key_with_password, private_key_password='coucou') |
|
166 |
instance.sftp = sftp |
|
167 |
instance.save() |
|
168 | ||
169 |
instance = temp_model.objects.get() |
|
170 |
assert instance.sftp is not None |
|
171 |
assert instance.sftp.url == url |
|
172 |
assert instance.sftp.private_key is not None |
|
173 |
instance.sftp = None |
|
174 |
instance.save() |
|
175 | ||
176 |
instance = temp_model.objects.get() |
|
177 |
assert instance.sftp is None |
|
178 |
instance.delete() |
|
179 | ||
180 |
temp_model.objects.create(sftp=sftp) |
|
181 | ||
182 |
instance = temp_model.objects.get() |
|
183 |
assert instance.sftp is not None |
|
184 |
assert instance.sftp.url == url |
|
185 |
assert instance.sftp.private_key is not None |
tox.ini | ||
---|---|---|
31 | 31 |
pytest-freezegun |
32 | 32 |
pytest-httpbin |
33 | 33 |
pytest-localserver |
34 |
pytest-sftpserver |
|
34 | 35 |
commands = |
35 | 36 |
django18: py.test {posargs: {env:FAST:} --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=passerelle/ --cov-config .coveragerc tests/} |
36 | 37 |
django18: ./pylint.sh passerelle/ |
37 |
- |