trigger 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trigger/__init__.py +7 -0
- trigger/acl/__init__.py +32 -0
- trigger/acl/autoacl.py +70 -0
- trigger/acl/db.py +324 -0
- trigger/acl/dicts.py +357 -0
- trigger/acl/grammar.py +112 -0
- trigger/acl/ios.py +222 -0
- trigger/acl/junos.py +422 -0
- trigger/acl/models.py +118 -0
- trigger/acl/parser.py +168 -0
- trigger/acl/queue.py +296 -0
- trigger/acl/support.py +1431 -0
- trigger/acl/tools.py +746 -0
- trigger/bin/__init__.py +0 -0
- trigger/bin/acl.py +233 -0
- trigger/bin/acl_script.py +574 -0
- trigger/bin/aclconv.py +82 -0
- trigger/bin/check_access.py +93 -0
- trigger/bin/check_syntax.py +66 -0
- trigger/bin/fe.py +197 -0
- trigger/bin/find_access.py +191 -0
- trigger/bin/gnng.py +434 -0
- trigger/bin/gong.py +86 -0
- trigger/bin/load_acl.py +841 -0
- trigger/bin/load_config.py +18 -0
- trigger/bin/netdev.py +317 -0
- trigger/bin/optimizer.py +638 -0
- trigger/bin/run_cmds.py +18 -0
- trigger/changemgmt/__init__.py +352 -0
- trigger/changemgmt/bounce.py +57 -0
- trigger/cmds.py +1217 -0
- trigger/conf/__init__.py +94 -0
- trigger/conf/global_settings.py +674 -0
- trigger/contrib/__init__.py +7 -0
- trigger/exceptions.py +307 -0
- trigger/gorc.py +172 -0
- trigger/netdevices/__init__.py +1288 -0
- trigger/netdevices/loader.py +174 -0
- trigger/netscreen.py +1030 -0
- trigger/packages/__init__.py +6 -0
- trigger/packages/peewee.py +8084 -0
- trigger/rancid.py +463 -0
- trigger/tacacsrc.py +584 -0
- trigger/twister.py +2203 -0
- trigger/twister2.py +745 -0
- trigger/utils/__init__.py +88 -0
- trigger/utils/cli.py +349 -0
- trigger/utils/importlib.py +77 -0
- trigger/utils/network.py +157 -0
- trigger/utils/rcs.py +178 -0
- trigger/utils/templates.py +81 -0
- trigger/utils/url.py +78 -0
- trigger/utils/xmltodict.py +298 -0
- trigger-2.0.0.dist-info/METADATA +146 -0
- trigger-2.0.0.dist-info/RECORD +61 -0
- trigger-2.0.0.dist-info/WHEEL +5 -0
- trigger-2.0.0.dist-info/entry_points.txt +15 -0
- trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
- trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
- trigger-2.0.0.dist-info/top_level.txt +2 -0
- twisted/plugins/trigger_xmlrpc.py +124 -0
trigger/tacacsrc.py
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""Abstract interface to .tacacsrc credentials file.
|
|
2
|
+
|
|
3
|
+
Designed to interoperate with the legacy DeviceV2 implementation, but
|
|
4
|
+
provide a reasonable API on top of that. The name and format of the
|
|
5
|
+
.tacacsrc file are not ideal, but compatibility matters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__author__ = "Jathan McCollum, Mark Thomas, Michael Shields"
|
|
9
|
+
__maintainer__ = "Jathan McCollum"
|
|
10
|
+
__email__ = "jmccollum@salesforce.com"
|
|
11
|
+
__copyright__ = "Copyright 2006-2012, AOL Inc.; 2013 Salesforce.com"
|
|
12
|
+
|
|
13
|
+
import getpass
|
|
14
|
+
import os
|
|
15
|
+
import pwd
|
|
16
|
+
import sys
|
|
17
|
+
from base64 import decodebytes as decodestring
|
|
18
|
+
from base64 import encodebytes as encodestring
|
|
19
|
+
from collections import namedtuple
|
|
20
|
+
from time import localtime, strftime
|
|
21
|
+
|
|
22
|
+
from cryptography.hazmat.backends.openssl import backend as openssl_backend
|
|
23
|
+
from cryptography.hazmat.primitives import ciphers
|
|
24
|
+
|
|
25
|
+
# Python 3: distutils deprecated, use packaging instead
|
|
26
|
+
from packaging.version import Version
|
|
27
|
+
from twisted.python import log
|
|
28
|
+
|
|
29
|
+
from trigger.conf import settings
|
|
30
|
+
|
|
31
|
+
# Python 3 / cryptography 48+: TripleDES moved to decrepit module
|
|
32
|
+
try:
|
|
33
|
+
from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES
|
|
34
|
+
except ImportError:
|
|
35
|
+
from cryptography.hazmat.primitives.ciphers.algorithms import TripleDES
|
|
36
|
+
|
|
37
|
+
# Exports
|
|
38
|
+
__all__ = (
|
|
39
|
+
"get_device_password",
|
|
40
|
+
"prompt_credentials",
|
|
41
|
+
"convert_tacacsrc",
|
|
42
|
+
"update_credentials",
|
|
43
|
+
"validate_credentials",
|
|
44
|
+
"Credentials",
|
|
45
|
+
"Tacacsrc",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Credential object stored in Tacacsrc.creds
|
|
49
|
+
Credentials = namedtuple("Credentials", "username password realm")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Exceptions
|
|
53
|
+
class TacacsrcError(Exception):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CouldNotParse(TacacsrcError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MissingPassword(TacacsrcError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MissingRealmName(TacacsrcError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class VersionMismatch(TacacsrcError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Functions
|
|
74
|
+
def get_device_password(device=None, tcrc=None):
|
|
75
|
+
"""
|
|
76
|
+
Fetch the password for a device/realm or create a new entry for it.
|
|
77
|
+
If device is not passed, ``settings.DEFAULT_REALM`` is used, which is default
|
|
78
|
+
realm for most devices.
|
|
79
|
+
|
|
80
|
+
:param device:
|
|
81
|
+
Realm or device name to updated
|
|
82
|
+
|
|
83
|
+
:param device:
|
|
84
|
+
Optional `~trigger.tacacsrc.Tacacsrc` instance
|
|
85
|
+
"""
|
|
86
|
+
if tcrc is None:
|
|
87
|
+
tcrc = Tacacsrc()
|
|
88
|
+
|
|
89
|
+
# If device isn't passed, assume we are initializing the .tacacsrc.
|
|
90
|
+
try:
|
|
91
|
+
creds = tcrc.creds[device]
|
|
92
|
+
except KeyError:
|
|
93
|
+
print(f"\nCredentials not found for device/realm {device!r}, prompting...")
|
|
94
|
+
creds = prompt_credentials(device)
|
|
95
|
+
tcrc.creds[device] = creds
|
|
96
|
+
tcrc.write()
|
|
97
|
+
|
|
98
|
+
return creds
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def prompt_credentials(device, user=None):
|
|
102
|
+
"""
|
|
103
|
+
Prompt for username, password and return them as Credentials namedtuple.
|
|
104
|
+
|
|
105
|
+
:param device: Device or realm name to store
|
|
106
|
+
:param user: (Optional) If set, use as default username
|
|
107
|
+
"""
|
|
108
|
+
if not device:
|
|
109
|
+
raise MissingRealmName("You must specify a device/realm name.")
|
|
110
|
+
|
|
111
|
+
creds = ()
|
|
112
|
+
# Make sure we can even get tty i/o!
|
|
113
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
114
|
+
print(f"\nUpdating credentials for device/realm {device!r}")
|
|
115
|
+
|
|
116
|
+
user_default = ""
|
|
117
|
+
if user:
|
|
118
|
+
user_default = f" [{user}]"
|
|
119
|
+
|
|
120
|
+
username = input(f"Username{user_default}: ") or user
|
|
121
|
+
if username == "":
|
|
122
|
+
print("\nYou must specify a username, try again!")
|
|
123
|
+
return prompt_credentials(device, user=user)
|
|
124
|
+
|
|
125
|
+
passwd = getpass.getpass("Password: ")
|
|
126
|
+
passwd2 = getpass.getpass("Password (again): ")
|
|
127
|
+
if not passwd:
|
|
128
|
+
print("\nPassword cannot be blank, try again!")
|
|
129
|
+
return prompt_credentials(device, user=username)
|
|
130
|
+
|
|
131
|
+
if passwd != passwd2:
|
|
132
|
+
print("\nPasswords did not match, try again!")
|
|
133
|
+
return prompt_credentials(device, user=username)
|
|
134
|
+
|
|
135
|
+
creds = Credentials(username, passwd, device)
|
|
136
|
+
|
|
137
|
+
return creds
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def update_credentials(device, username=None):
|
|
141
|
+
"""
|
|
142
|
+
Update the credentials for a given device/realm. Assumes the same username
|
|
143
|
+
that is already cached unless it is passed.
|
|
144
|
+
|
|
145
|
+
This may seem redundant at first compared to Tacacsrc.update_creds() but we
|
|
146
|
+
need this factored out so that we don't end up with a race condition when
|
|
147
|
+
credentials are messed up.
|
|
148
|
+
|
|
149
|
+
Returns True if it actually updated something or None if it didn't.
|
|
150
|
+
|
|
151
|
+
:param device: Device or realm name to update
|
|
152
|
+
:param username: Username for credentials
|
|
153
|
+
"""
|
|
154
|
+
tcrc = Tacacsrc()
|
|
155
|
+
if tcrc.creds_updated:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
mycreds = tcrc.creds.get(device, tcrc.creds[settings.DEFAULT_REALM])
|
|
159
|
+
if username is None:
|
|
160
|
+
username = mycreds.username
|
|
161
|
+
|
|
162
|
+
tcrc.update_creds(tcrc.creds, mycreds.realm, username)
|
|
163
|
+
tcrc.write()
|
|
164
|
+
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def validate_credentials(creds=None):
|
|
169
|
+
"""
|
|
170
|
+
Given a set of credentials, try to return a `~trigger.tacacsrc.Credentials`
|
|
171
|
+
object.
|
|
172
|
+
|
|
173
|
+
If ``creds`` is unset it will fetch from ``.tacacsrc``.
|
|
174
|
+
|
|
175
|
+
Expects either a 2-tuple of (username, password) or a 3-tuple of (username,
|
|
176
|
+
password, realm). If only (username, password) are provided, realm will be populated from
|
|
177
|
+
:setting:`DEFAULT_REALM`.
|
|
178
|
+
|
|
179
|
+
:param creds:
|
|
180
|
+
A tuple of credentials.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
realm = settings.DEFAULT_REALM
|
|
184
|
+
|
|
185
|
+
# If it isn't set or it's a string, or less than 1 or more than 3 items,
|
|
186
|
+
# get from .tacacsrc
|
|
187
|
+
if (not creds) or (type(creds) == str) or (len(creds) not in (2, 3)):
|
|
188
|
+
log.msg("Creds not valid, fetching from .tacacsrc...")
|
|
189
|
+
tcrc = Tacacsrc()
|
|
190
|
+
return tcrc.creds.get(realm, get_device_password(realm, tcrc))
|
|
191
|
+
|
|
192
|
+
# If it's a dict, get the values
|
|
193
|
+
if hasattr(creds, "values"):
|
|
194
|
+
log.msg("Creds is a dict, converting to values...")
|
|
195
|
+
creds = creds.values()
|
|
196
|
+
|
|
197
|
+
# If it's missing realm, add it.
|
|
198
|
+
if len(creds) == 2:
|
|
199
|
+
log.msg("Creds is a 2-tuple, making into namedtuple...")
|
|
200
|
+
username, password = creds
|
|
201
|
+
return Credentials(username, password, realm)
|
|
202
|
+
|
|
203
|
+
# Or just make it go...
|
|
204
|
+
elif len(creds) == 3:
|
|
205
|
+
log.msg("Creds is a 3-tuple, making into namedtuple...")
|
|
206
|
+
return Credentials(*creds)
|
|
207
|
+
|
|
208
|
+
raise RuntimeError("THIS SHOULD NOT HAVE HAPPENED!!")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def convert_tacacsrc():
|
|
212
|
+
"""Converts old .tacacsrc to new .tacacsrc.gpg."""
|
|
213
|
+
print("Converting old tacacsrc to new kind :)")
|
|
214
|
+
tco = Tacacsrc(old=True)
|
|
215
|
+
tcn = Tacacsrc(old=False, gen=True)
|
|
216
|
+
tcn.creds = tco.creds
|
|
217
|
+
tcn.write()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _perl_unhex_old(c):
|
|
221
|
+
"""
|
|
222
|
+
Emulate Crypt::TripleDES's bizarre handling of keys, which relies on
|
|
223
|
+
the fact that you can pass Perl's pack('H*') a string that contains
|
|
224
|
+
anything, not just hex digits. "The result for bytes "g".."z" and
|
|
225
|
+
"G".."Z" is not well-defined", says perlfunc(1). Smash!
|
|
226
|
+
|
|
227
|
+
This function can be safely removed once GPG is fully supported.
|
|
228
|
+
"""
|
|
229
|
+
if "a" <= c <= "z":
|
|
230
|
+
return (ord(c) - ord("a") + 10) & 0xF
|
|
231
|
+
if "A" <= c <= "Z":
|
|
232
|
+
return (ord(c) - ord("A") + 10) & 0xF
|
|
233
|
+
return ord(c) & 0xF
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _perl_pack_Hstar_old(s):
|
|
237
|
+
"""
|
|
238
|
+
Used with _perl_unhex_old(). Ghetto hack.
|
|
239
|
+
|
|
240
|
+
This function can be safely removed once GPG is fully supported.
|
|
241
|
+
"""
|
|
242
|
+
r = ""
|
|
243
|
+
while len(s) > 1:
|
|
244
|
+
r += chr((_perl_unhex_old(s[0]) << 4) | _perl_unhex_old(s[1]))
|
|
245
|
+
s = s[2:]
|
|
246
|
+
if len(s) == 1:
|
|
247
|
+
r += _perl_unhex_old(s[0])
|
|
248
|
+
return r
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Classes
|
|
252
|
+
class Tacacsrc:
|
|
253
|
+
"""
|
|
254
|
+
Encrypts, decrypts and returns credentials for use by network devices and
|
|
255
|
+
other tools.
|
|
256
|
+
|
|
257
|
+
Pass use_gpg=True to force GPG, otherwise it relies on
|
|
258
|
+
settings.USE_GPG_AUTH
|
|
259
|
+
|
|
260
|
+
`*_old` functions should be removed after everyone is moved to the new
|
|
261
|
+
system.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(
|
|
265
|
+
self, tacacsrc_file=None, use_gpg=settings.USE_GPG_AUTH, generate_new=False
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
Open .tacacsrc (tacacsrc_file or $TACACSRC or ~/.tacacsrc), or create
|
|
269
|
+
a new file if one cannot be found on disk.
|
|
270
|
+
|
|
271
|
+
If settings.USE_GPG_AUTH is enabled, tries to use GPG (.tacacsrc.gpg).
|
|
272
|
+
"""
|
|
273
|
+
self.file_name = tacacsrc_file
|
|
274
|
+
self.use_gpg = use_gpg
|
|
275
|
+
self.generate_new = generate_new
|
|
276
|
+
self.userinfo = pwd.getpwuid(os.getuid())
|
|
277
|
+
self.username = self.userinfo.pw_name
|
|
278
|
+
self.user_home = self.userinfo.pw_dir
|
|
279
|
+
self.data = []
|
|
280
|
+
self.creds = {}
|
|
281
|
+
self.creds_updated = False
|
|
282
|
+
self.version = Version("2.0")
|
|
283
|
+
|
|
284
|
+
# If we're not generating a new file and gpg is enabled, turn it off if
|
|
285
|
+
# the right files can't be found.
|
|
286
|
+
if not self.generate_new:
|
|
287
|
+
if self.use_gpg and not self.user_has_gpg():
|
|
288
|
+
log.msg(".tacacsrc.gpg not setup, disabling GPG", debug=True)
|
|
289
|
+
self.use_gpg = False
|
|
290
|
+
|
|
291
|
+
log.msg(f"Using GPG method: {self.use_gpg!r}", debug=True)
|
|
292
|
+
log.msg(f"Got username: {self.username!r}", debug=True)
|
|
293
|
+
|
|
294
|
+
# Set the .tacacsrc file location
|
|
295
|
+
if self.file_name is None:
|
|
296
|
+
self.file_name = settings.TACACSRC
|
|
297
|
+
|
|
298
|
+
# GPG uses '.tacacsrc.gpg'
|
|
299
|
+
if self.use_gpg:
|
|
300
|
+
self.file_name += ".gpg"
|
|
301
|
+
|
|
302
|
+
# Check if the file exists
|
|
303
|
+
if not os.path.exists(self.file_name):
|
|
304
|
+
print(f"{self.file_name} not found, generating a new one!")
|
|
305
|
+
self.generate_new = True
|
|
306
|
+
|
|
307
|
+
if self.use_gpg:
|
|
308
|
+
if not self.generate_new:
|
|
309
|
+
self.rawdata = self._decrypt_and_read()
|
|
310
|
+
self.creds = self._parse()
|
|
311
|
+
else:
|
|
312
|
+
self.creds[settings.DEFAULT_REALM] = prompt_credentials(
|
|
313
|
+
device="tacacsrc"
|
|
314
|
+
)
|
|
315
|
+
self.write()
|
|
316
|
+
else:
|
|
317
|
+
# If passphrase is enable, use that
|
|
318
|
+
if settings.TACACSRC_USE_PASSPHRASE:
|
|
319
|
+
passphrase = settings.TACACSRC_PASSPHRASE
|
|
320
|
+
import hashlib
|
|
321
|
+
|
|
322
|
+
# Python 3 requires encoding string to bytes before hashing
|
|
323
|
+
if isinstance(passphrase, str):
|
|
324
|
+
passphrase = passphrase.encode("utf-8")
|
|
325
|
+
key = hashlib.md5(passphrase).hexdigest()[:24] # 24 bytes
|
|
326
|
+
self.key = key
|
|
327
|
+
# Otherwise read from keyfile.
|
|
328
|
+
else:
|
|
329
|
+
self.key = self._get_key_old(settings.TACACSRC_KEYFILE)
|
|
330
|
+
|
|
331
|
+
if not self.generate_new:
|
|
332
|
+
self.rawdata = self._read_file_old()
|
|
333
|
+
self.creds = self._parse_old()
|
|
334
|
+
if self.creds_updated: # _parse_old() might set this flag
|
|
335
|
+
log.msg("creds updated, writing to file", debug=True)
|
|
336
|
+
self.write()
|
|
337
|
+
else:
|
|
338
|
+
self.creds[settings.DEFAULT_REALM] = prompt_credentials(
|
|
339
|
+
device="tacacsrc"
|
|
340
|
+
)
|
|
341
|
+
self.write()
|
|
342
|
+
|
|
343
|
+
def _get_key_nonce_old(self):
|
|
344
|
+
"""Yes, the key nonce is the userid. Awesome, right?"""
|
|
345
|
+
return pwd.getpwuid(os.getuid())[0] + "\n"
|
|
346
|
+
|
|
347
|
+
def _get_key_old(self, keyfile):
|
|
348
|
+
"""Of course, encrypting something in the filesystem using a key
|
|
349
|
+
in the filesystem really doesn't buy much. This is best referred
|
|
350
|
+
to as obfuscation of the .tacacsrc."""
|
|
351
|
+
try:
|
|
352
|
+
with open(keyfile) as kf:
|
|
353
|
+
key = kf.readline().strip()
|
|
354
|
+
except OSError:
|
|
355
|
+
msg = f"Keyfile at {keyfile} not found. Please create it."
|
|
356
|
+
raise CouldNotParse(msg)
|
|
357
|
+
|
|
358
|
+
if not key:
|
|
359
|
+
msg = f"Keyfile at {keyfile} must contain a passphrase."
|
|
360
|
+
raise CouldNotParse(msg)
|
|
361
|
+
|
|
362
|
+
key += self._get_key_nonce_old()
|
|
363
|
+
key = _perl_pack_Hstar_old((key + (" " * 48))[:48])
|
|
364
|
+
assert len(key) == 24
|
|
365
|
+
|
|
366
|
+
return key
|
|
367
|
+
|
|
368
|
+
def _parse_old(self):
|
|
369
|
+
"""Parses .tacacsrc and returns dictionary of credentials."""
|
|
370
|
+
data = {}
|
|
371
|
+
creds = {}
|
|
372
|
+
|
|
373
|
+
# Cleanup the rawdata
|
|
374
|
+
for idx, line in enumerate(self.rawdata):
|
|
375
|
+
line = line.strip() # eat \n
|
|
376
|
+
lineno = idx + 1 # increment index for actual lineno
|
|
377
|
+
|
|
378
|
+
# Skip blank lines and comments
|
|
379
|
+
if any((line.startswith("#"), line == "")):
|
|
380
|
+
log.msg(f"skipping {line!r}", debug=True)
|
|
381
|
+
continue
|
|
382
|
+
# log.msg('parsing %r' % line, debug=True)
|
|
383
|
+
|
|
384
|
+
if line.count(" = ") > 1:
|
|
385
|
+
raise CouldNotParse(f"Malformed line {line!r} at line {lineno}")
|
|
386
|
+
|
|
387
|
+
key, sep, val = line.partition(" = ")
|
|
388
|
+
if val == "":
|
|
389
|
+
continue # Don't add a key with a missing value
|
|
390
|
+
raise CouldNotParse(f"Missing value for key {key!r} at line {lineno}")
|
|
391
|
+
|
|
392
|
+
# Check for version
|
|
393
|
+
if key == "version":
|
|
394
|
+
if val != self.version:
|
|
395
|
+
raise VersionMismatch(f"Bad .tacacsrc version ({v})")
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# Make sure tokens can be parsed
|
|
399
|
+
realm, token, end = key.split("_")
|
|
400
|
+
if end != "" or (realm, token) in data:
|
|
401
|
+
raise CouldNotParse(f"Could not parse {line!r} at line {lineno}")
|
|
402
|
+
|
|
403
|
+
data[(realm, token)] = self._decrypt_old(val)
|
|
404
|
+
del key, val, line
|
|
405
|
+
|
|
406
|
+
# Store the creds, if a password is empty, try to prompt for it.
|
|
407
|
+
for (realm, key), val in data.items():
|
|
408
|
+
if key == "uname":
|
|
409
|
+
try:
|
|
410
|
+
# creds[realm] = Credentials(val, data[(realm, 'pwd')])
|
|
411
|
+
creds[realm] = Credentials(val, data[(realm, "pwd")], realm)
|
|
412
|
+
except KeyError:
|
|
413
|
+
print(f"\nMissing password for {realm!r}, initializing...")
|
|
414
|
+
self.update_creds(creds=creds, realm=realm, user=val)
|
|
415
|
+
# raise MissingPassword('Missing password for %r' % realm)
|
|
416
|
+
elif key == "pwd":
|
|
417
|
+
pass
|
|
418
|
+
else:
|
|
419
|
+
raise CouldNotParse(f"Unknown .tacacsrc entry ({realm}_{val})")
|
|
420
|
+
|
|
421
|
+
self.data = data
|
|
422
|
+
return creds
|
|
423
|
+
|
|
424
|
+
def update_creds(self, creds, realm, user=None):
|
|
425
|
+
"""
|
|
426
|
+
Update username/password for a realm/device and set self.creds_updated
|
|
427
|
+
bit to trigger .write().
|
|
428
|
+
|
|
429
|
+
:param creds: Dictionary of credentials keyed by realm
|
|
430
|
+
:param realm: The realm to update within the creds dict
|
|
431
|
+
:param user: (Optional) Username passed to prompt_credentials()
|
|
432
|
+
"""
|
|
433
|
+
creds[realm] = prompt_credentials(realm, user)
|
|
434
|
+
log.msg("setting self.creds_updated flag", debug=True)
|
|
435
|
+
self.creds_updated = True
|
|
436
|
+
new_user = creds[realm].username
|
|
437
|
+
print(f"\nCredentials updated for user: {new_user!r}, device/realm: {realm!r}.")
|
|
438
|
+
|
|
439
|
+
def _encrypt_old(self, s):
|
|
440
|
+
"""Encodes using the old method. Adds a newline for you."""
|
|
441
|
+
# Ensure key and plaintext are bytes for cryptography library
|
|
442
|
+
key = self.key if isinstance(self.key, bytes) else self.key.encode("latin-1")
|
|
443
|
+
plaintext = s if isinstance(s, bytes) else s.encode("latin-1")
|
|
444
|
+
|
|
445
|
+
des = TripleDES(key)
|
|
446
|
+
cipher = ciphers.Cipher(des, ciphers.modes.ECB(), backend=openssl_backend)
|
|
447
|
+
encryptor = cipher.encryptor()
|
|
448
|
+
|
|
449
|
+
# Crypt::TripleDES pads with *spaces*! How 1960. Pad it so the
|
|
450
|
+
# length is a multiple of 8.
|
|
451
|
+
padding_len = (8 - len(plaintext) % 8) % 8
|
|
452
|
+
padding = b" " * padding_len
|
|
453
|
+
|
|
454
|
+
cipher_text = encryptor.update(plaintext + padding) + encryptor.finalize()
|
|
455
|
+
|
|
456
|
+
# We need to return a newline if a field is empty so as not to break
|
|
457
|
+
# .tacacsrc parsing (trust me, this is easier)
|
|
458
|
+
return (
|
|
459
|
+
encodestring(cipher_text).decode("ascii").replace("\n", "") or ""
|
|
460
|
+
) + "\n"
|
|
461
|
+
|
|
462
|
+
def _decrypt_old(self, s):
|
|
463
|
+
"""Decodes using the old method. Strips newline for you."""
|
|
464
|
+
# Ensure key is bytes for cryptography library
|
|
465
|
+
key = self.key if isinstance(self.key, bytes) else self.key.encode("latin-1")
|
|
466
|
+
des = TripleDES(key)
|
|
467
|
+
cipher = ciphers.Cipher(des, ciphers.modes.ECB(), backend=openssl_backend)
|
|
468
|
+
decryptor = cipher.decryptor()
|
|
469
|
+
# Ensure s is bytes for base64.decodebytes
|
|
470
|
+
s_bytes = s if isinstance(s, bytes) else s.encode("ascii")
|
|
471
|
+
# rstrip() to undo space-padding; unfortunately this means that
|
|
472
|
+
# passwords cannot end in spaces.
|
|
473
|
+
plaintext = decryptor.update(decodestring(s_bytes)) + decryptor.finalize()
|
|
474
|
+
return plaintext.rstrip(b" ").decode("latin-1")
|
|
475
|
+
|
|
476
|
+
def _read_file_old(self):
|
|
477
|
+
"""Read old style file and return the raw data."""
|
|
478
|
+
self._update_perms()
|
|
479
|
+
with open(self.file_name) as f:
|
|
480
|
+
return f.readlines()
|
|
481
|
+
|
|
482
|
+
def _write_old(self):
|
|
483
|
+
"""Write old style to disk. Newlines provided by _encrypt_old(), so don't fret!"""
|
|
484
|
+
out = [
|
|
485
|
+
"# Saved by {} at {}\n\n".format(
|
|
486
|
+
self.__module__, strftime("%Y-%m-%d %H:%M:%S %Z", localtime())
|
|
487
|
+
)
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
for realm, (uname, pwd, _) in self.creds.items():
|
|
491
|
+
# log.msg('encrypting %r' % ((uname, pwd),), debug=True)
|
|
492
|
+
out.append(f"{realm}_uname_ = {self._encrypt_old(uname)}")
|
|
493
|
+
out.append(f"{realm}_pwd_ = {self._encrypt_old(pwd)}")
|
|
494
|
+
|
|
495
|
+
with open(self.file_name, "w+") as fd:
|
|
496
|
+
fd.writelines(out)
|
|
497
|
+
|
|
498
|
+
self._update_perms()
|
|
499
|
+
|
|
500
|
+
def _decrypt_and_read(self):
|
|
501
|
+
"""Decrypt file using GPG and return the raw data."""
|
|
502
|
+
ret = []
|
|
503
|
+
for x in os.popen(f"gpg2 --no-tty --quiet -d {self.file_name}"):
|
|
504
|
+
x = x.rstrip()
|
|
505
|
+
ret.append(x)
|
|
506
|
+
|
|
507
|
+
return ret
|
|
508
|
+
|
|
509
|
+
def _encrypt_and_write(self):
|
|
510
|
+
"""Encrypt using GPG and dump password data to disk."""
|
|
511
|
+
fin, fout = os.popen2(
|
|
512
|
+
f"gpg2 --yes --quiet -r {self.username} -e -o {self.file_name}"
|
|
513
|
+
)
|
|
514
|
+
for line in self.rawdata:
|
|
515
|
+
print(line, file=fin)
|
|
516
|
+
|
|
517
|
+
def _write_new(self):
|
|
518
|
+
"""Replace self.rawdata with current password details."""
|
|
519
|
+
out = [
|
|
520
|
+
"# Saved by {} at {}\n\n".format(
|
|
521
|
+
self.__module__, strftime("%Y-%m-%d %H:%M:%S %Z", localtime())
|
|
522
|
+
)
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
for realm, (uname, pwd, _) in self.creds.items():
|
|
526
|
+
out.append(f"{realm}_uname_ = {uname}")
|
|
527
|
+
out.append(f"{realm}_pwd_ = {pwd}")
|
|
528
|
+
|
|
529
|
+
self.rawdata = out
|
|
530
|
+
self._encrypt_and_write()
|
|
531
|
+
self._update_perms()
|
|
532
|
+
|
|
533
|
+
def write(self):
|
|
534
|
+
"""Writes .tacacsrc(.gpg) using the accurate method (old vs. new)."""
|
|
535
|
+
if self.use_gpg:
|
|
536
|
+
return self._write_new()
|
|
537
|
+
|
|
538
|
+
return self._write_old()
|
|
539
|
+
|
|
540
|
+
def _update_perms(self):
|
|
541
|
+
"""Enforce -rw------- on the creds file"""
|
|
542
|
+
os.chmod(self.file_name, 0o600)
|
|
543
|
+
|
|
544
|
+
def _parse(self):
|
|
545
|
+
"""Parses .tacacsrc.gpg and returns dictionary of credentials."""
|
|
546
|
+
data = {}
|
|
547
|
+
creds = {}
|
|
548
|
+
for line in self.rawdata:
|
|
549
|
+
if line.find("#") != -1:
|
|
550
|
+
line = line[: line.find("#")]
|
|
551
|
+
line = line.strip()
|
|
552
|
+
if len(line):
|
|
553
|
+
k, v = line.split(" = ")
|
|
554
|
+
if k == "version":
|
|
555
|
+
if v != self.version:
|
|
556
|
+
raise VersionMismatch(f"Bad .tacacsrc version ({v})")
|
|
557
|
+
else:
|
|
558
|
+
realm, s, junk = k.split("_")
|
|
559
|
+
# assert(junk == '')
|
|
560
|
+
assert (realm, s) not in data
|
|
561
|
+
data[(realm, s)] = v # self._decrypt(v)
|
|
562
|
+
|
|
563
|
+
for (realm, k), v in data.items():
|
|
564
|
+
if k == "uname":
|
|
565
|
+
# creds[realm] = (v, data[(realm, 'pwd')])
|
|
566
|
+
# creds[realm] = Credentials(v, data[(realm, 'pwd')])
|
|
567
|
+
creds[realm] = Credentials(v, data[(realm, "pwd")], realm)
|
|
568
|
+
elif k == "pwd":
|
|
569
|
+
pass
|
|
570
|
+
else:
|
|
571
|
+
raise CouldNotParse(f"Unknown .tacacsrc entry ({realm}_{v})")
|
|
572
|
+
|
|
573
|
+
return creds
|
|
574
|
+
|
|
575
|
+
def user_has_gpg(self):
|
|
576
|
+
"""Checks if user has .gnupg directory and .tacacsrc.gpg file."""
|
|
577
|
+
gpg_dir = os.path.join(self.user_home, ".gnupg")
|
|
578
|
+
tacacsrc_gpg = os.path.join(self.user_home, ".tacacsrc.gpg")
|
|
579
|
+
|
|
580
|
+
# If not generating new .tacacsrc.gpg, we want both to be True
|
|
581
|
+
if os.path.isdir(gpg_dir) and os.path.isfile(tacacsrc_gpg):
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
return False
|