mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
ipalib: move client-side plugins to ipaclient
Move the rpcclient backend and commands which are executed on the client to ipaclient.plugins. https://fedorahosted.org/freeipa/ticket/4739 Reviewed-By: David Kupka <dkupka@redhat.com>
This commit is contained in:
227
ipaclient/plugins/automount.py
Normal file
227
ipaclient/plugins/automount.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# Authors:
|
||||
# Rob Crittenden <rcritten@redhat.com>
|
||||
# Pavel Zuna <pzuna@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from ipalib import api, errors
|
||||
from ipalib import Flag, Str
|
||||
from ipalib.frontend import Command
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib import _
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
register = Registry()
|
||||
|
||||
DEFAULT_MAPS = (u'auto.direct', )
|
||||
DEFAULT_KEYS = (u'/-', )
|
||||
|
||||
|
||||
@register()
|
||||
class automountlocation_import(Command):
|
||||
__doc__ = _('Import automount files for a specific location.')
|
||||
|
||||
takes_args = (
|
||||
Str('masterfile',
|
||||
label=_('Master file'),
|
||||
doc=_('Automount master file.'),
|
||||
),
|
||||
)
|
||||
|
||||
takes_options = (
|
||||
Flag('continue?',
|
||||
cli_name='continue',
|
||||
doc=_('Continuous operation mode. Errors are reported but the process continues.'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.automountlocation_show.args():
|
||||
yield arg
|
||||
for arg in super(automountlocation_import, self).get_args():
|
||||
yield arg
|
||||
|
||||
def __read_mapfile(self, filename):
|
||||
try:
|
||||
fp = open(filename, 'r')
|
||||
map = fp.readlines()
|
||||
fp.close()
|
||||
except IOError as e:
|
||||
if e.errno == 2:
|
||||
raise errors.NotFound(
|
||||
reason=_('File %(file)s not found') % {'file': filename}
|
||||
)
|
||||
else:
|
||||
raise
|
||||
return map
|
||||
|
||||
def forward(self, *args, **options):
|
||||
"""
|
||||
The basic idea is to read the master file and create all the maps
|
||||
we need, then read each map file and add all the keys for the map.
|
||||
"""
|
||||
location = self.api.Command['automountlocation_show'](args[0])
|
||||
|
||||
result = {'maps':[], 'keys':[], 'skipped':[], 'duplicatekeys':[], 'duplicatemaps':[]}
|
||||
maps = {}
|
||||
master = self.__read_mapfile(args[1])
|
||||
for m in master:
|
||||
if m.startswith('#'):
|
||||
continue
|
||||
m = m.rstrip()
|
||||
if m.startswith('+'):
|
||||
result['skipped'].append([m,args[1]])
|
||||
continue
|
||||
if len(m) == 0:
|
||||
continue
|
||||
am = m.split(None)
|
||||
if len(am) < 2:
|
||||
continue
|
||||
|
||||
if am[1].startswith('/'):
|
||||
mapfile = am[1].replace('"','')
|
||||
am[1] = os.path.basename(am[1])
|
||||
maps[am[1]] = mapfile
|
||||
|
||||
# Add a new key to the auto.master map for the new map file
|
||||
try:
|
||||
api.Command['automountkey_add'](
|
||||
args[0],
|
||||
u'auto.master',
|
||||
automountkey=unicode(am[0]),
|
||||
automountinformation=unicode(' '.join(am[1:])))
|
||||
result['keys'].append([am[0], u'auto.master'])
|
||||
except errors.DuplicateEntry as e:
|
||||
if unicode(am[0]) in DEFAULT_KEYS:
|
||||
# ignore conflict when the key was pre-created by the framework
|
||||
pass
|
||||
elif options.get('continue', False):
|
||||
result['duplicatekeys'].append(am[0])
|
||||
else:
|
||||
raise errors.DuplicateEntry(
|
||||
message=_('key %(key)s already exists') % dict(
|
||||
key=am[0]))
|
||||
# Add the new map
|
||||
if not am[1].startswith('-'):
|
||||
try:
|
||||
api.Command['automountmap_add'](args[0], unicode(am[1]))
|
||||
result['maps'].append(am[1])
|
||||
except errors.DuplicateEntry as e:
|
||||
if unicode(am[1]) in DEFAULT_MAPS:
|
||||
# ignore conflict when the map was pre-created by the framework
|
||||
pass
|
||||
elif options.get('continue', False):
|
||||
result['duplicatemaps'].append(am[0])
|
||||
else:
|
||||
raise errors.DuplicateEntry(
|
||||
message=_('map %(map)s already exists') % dict(
|
||||
map=am[1]))
|
||||
|
||||
# Now iterate over the map files and add the keys. To handle
|
||||
# continuation lines I'll make a pass through it to skip comments
|
||||
# etc and also to combine lines.
|
||||
for m in maps:
|
||||
map = self.__read_mapfile(maps[m])
|
||||
lines = []
|
||||
cont = ''
|
||||
for x in map:
|
||||
if x.startswith('#'):
|
||||
continue
|
||||
x = x.rstrip()
|
||||
if x.startswith('+'):
|
||||
result['skipped'].append([m, maps[m]])
|
||||
continue
|
||||
if len(x) == 0:
|
||||
continue
|
||||
if x.endswith("\\"):
|
||||
cont = cont + x[:-1] + ' '
|
||||
else:
|
||||
lines.append(cont + x)
|
||||
cont=''
|
||||
for x in lines:
|
||||
am = x.split(None)
|
||||
key = unicode(am[0].replace('"',''))
|
||||
try:
|
||||
api.Command['automountkey_add'](
|
||||
args[0],
|
||||
unicode(m),
|
||||
automountkey=key,
|
||||
automountinformation=unicode(' '.join(am[1:])))
|
||||
result['keys'].append([key,m])
|
||||
except errors.DuplicateEntry as e:
|
||||
if options.get('continue', False):
|
||||
result['duplicatekeys'].append(am[0])
|
||||
else:
|
||||
raise e
|
||||
|
||||
return dict(result=result)
|
||||
|
||||
def output_for_cli(self, textui, result, *keys, **options):
|
||||
maps = result['result']['maps']
|
||||
keys = result['result']['keys']
|
||||
duplicatemaps = result['result']['duplicatemaps']
|
||||
duplicatekeys = result['result']['duplicatekeys']
|
||||
skipped = result['result']['skipped']
|
||||
|
||||
textui.print_plain('Imported maps:')
|
||||
for m in maps:
|
||||
textui.print_plain(
|
||||
'Added %s' % m
|
||||
)
|
||||
textui.print_plain('')
|
||||
|
||||
textui.print_plain('Imported keys:')
|
||||
for k in keys:
|
||||
textui.print_plain(
|
||||
'Added %s to %s' % (
|
||||
k[0], k[1]
|
||||
)
|
||||
)
|
||||
textui.print_plain('')
|
||||
|
||||
if len(skipped) > 0:
|
||||
textui.print_plain('Ignored keys:')
|
||||
for k in skipped:
|
||||
textui.print_plain(
|
||||
'Ignored %s to %s' % (
|
||||
k[0], k[1]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if options.get('continue', False) and len(duplicatemaps) > 0:
|
||||
textui.print_plain('')
|
||||
textui.print_plain('Duplicate maps skipped:')
|
||||
for m in duplicatemaps:
|
||||
textui.print_plain(
|
||||
'Skipped %s' % m
|
||||
)
|
||||
|
||||
|
||||
if options.get('continue', False) and len(duplicatekeys) > 0:
|
||||
textui.print_plain('')
|
||||
textui.print_plain('Duplicate keys skipped:')
|
||||
for k in duplicatekeys:
|
||||
textui.print_plain(
|
||||
'Skipped %s' % k
|
||||
)
|
||||
110
ipaclient/plugins/otptoken.py
Normal file
110
ipaclient/plugins/otptoken.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Authors:
|
||||
# Nathaniel McCallum <npmccallum@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2013 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ipalib import api, Str, Password, _
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib.frontend import Local
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
from ipapython.nsslib import NSSConnection
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
register = Registry()
|
||||
|
||||
|
||||
class HTTPSHandler(urllib.request.HTTPSHandler):
|
||||
"Opens SSL HTTPS connections that perform hostname validation."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__kwargs = kwargs
|
||||
|
||||
# Can't use super() because the parent is an old-style class.
|
||||
urllib.request.HTTPSHandler.__init__(self)
|
||||
|
||||
def __inner(self, host, **kwargs):
|
||||
tmp = self.__kwargs.copy()
|
||||
tmp.update(kwargs)
|
||||
# NSSConnection doesn't support timeout argument
|
||||
tmp.pop('timeout', None)
|
||||
return NSSConnection(host, **tmp)
|
||||
|
||||
def https_open(self, req):
|
||||
# pylint: disable=no-member
|
||||
return self.do_open(self.__inner, req)
|
||||
|
||||
@register()
|
||||
class otptoken_sync(Local):
|
||||
__doc__ = _('Synchronize an OTP token.')
|
||||
|
||||
header = 'X-IPA-TokenSync-Result'
|
||||
|
||||
takes_options = (
|
||||
Str('user', label=_('User ID')),
|
||||
Password('password', label=_('Password'), confirm=False),
|
||||
Password('first_code', label=_('First Code'), confirm=False),
|
||||
Password('second_code', label=_('Second Code'), confirm=False),
|
||||
)
|
||||
|
||||
takes_args = (
|
||||
Str('token?', label=_('Token ID')),
|
||||
)
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
status = {'result': {self.header: 'unknown'}}
|
||||
|
||||
# Get the sync URI.
|
||||
segments = list(urllib.parse.urlparse(self.api.env.xmlrpc_uri))
|
||||
assert segments[0] == 'https' # Ensure encryption.
|
||||
segments[2] = segments[2].replace('/xml', '/session/sync_token')
|
||||
# urlunparse *can* take one argument
|
||||
# pylint: disable=too-many-function-args
|
||||
sync_uri = urllib.parse.urlunparse(segments)
|
||||
|
||||
# Prepare the query.
|
||||
query = {k: v for k, v in kwargs.items()
|
||||
if k in {x.name for x in self.takes_options}}
|
||||
if args and args[0] is not None:
|
||||
obj = self.api.Object.otptoken
|
||||
query['token'] = DN((obj.primary_key.name, args[0]),
|
||||
obj.container_dn, self.api.env.basedn)
|
||||
query = urllib.parse.urlencode(query)
|
||||
|
||||
# Sync the token.
|
||||
# pylint: disable=E1101
|
||||
handler = HTTPSHandler(dbdir=paths.IPA_NSSDB_DIR,
|
||||
tls_version_min=api.env.tls_version_min,
|
||||
tls_version_max=api.env.tls_version_max)
|
||||
rsp = urllib.request.build_opener(handler).open(sync_uri, query)
|
||||
if rsp.getcode() == 200:
|
||||
status['result'][self.header] = rsp.info().get(self.header, 'unknown')
|
||||
rsp.close()
|
||||
|
||||
return status
|
||||
|
||||
def output_for_cli(self, textui, result, *keys, **options):
|
||||
textui.print_plain({
|
||||
'ok': 'Token synchronized.',
|
||||
'error': 'Error contacting server!',
|
||||
'invalid-credentials': 'Invalid Credentials!',
|
||||
}.get(result['result'][self.header], 'Unknown Error!'))
|
||||
936
ipaclient/plugins/vault.py
Normal file
936
ipaclient/plugins/vault.py
Normal file
@@ -0,0 +1,936 @@
|
||||
# Authors:
|
||||
# Endi S. Dewata <edewata@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2015 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import base64
|
||||
import getpass
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
|
||||
load_pem_private_key
|
||||
|
||||
import nss.nss as nss
|
||||
|
||||
from ipalib.frontend import Local
|
||||
from ipalib import errors
|
||||
from ipalib import Bytes, Flag, Str
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib import _
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
|
||||
def validated_read(argname, filename, mode='r', encoding=None):
|
||||
"""Read file and catch errors
|
||||
|
||||
IOError and UnicodeError (for text files) are turned into a
|
||||
ValidationError
|
||||
"""
|
||||
try:
|
||||
with io.open(filename, mode=mode, encoding=encoding) as f:
|
||||
data = f.read()
|
||||
except IOError as exc:
|
||||
raise errors.ValidationError(
|
||||
name=argname,
|
||||
error=_("Cannot read file '%(filename)s': %(exc)s") % {
|
||||
'filename': filename, 'exc': exc.args[1]
|
||||
}
|
||||
)
|
||||
except UnicodeError as exc:
|
||||
raise errors.ValidationError(
|
||||
name=argname,
|
||||
error=_("Cannot decode file '%(filename)s': %(exc)s") % {
|
||||
'filename': filename, 'exc': exc
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
register = Registry()
|
||||
|
||||
MAX_VAULT_DATA_SIZE = 2**20 # = 1 MB
|
||||
|
||||
|
||||
def get_new_password():
|
||||
"""
|
||||
Gets new password from user and verify it.
|
||||
"""
|
||||
while True:
|
||||
password = getpass.getpass('New password: ').decode(
|
||||
sys.stdin.encoding)
|
||||
password2 = getpass.getpass('Verify password: ').decode(
|
||||
sys.stdin.encoding)
|
||||
|
||||
if password == password2:
|
||||
return password
|
||||
|
||||
print(' ** Passwords do not match! **')
|
||||
|
||||
|
||||
def get_existing_password():
|
||||
"""
|
||||
Gets existing password from user.
|
||||
"""
|
||||
return getpass.getpass('Password: ').decode(sys.stdin.encoding)
|
||||
|
||||
|
||||
def generate_symmetric_key(password, salt):
|
||||
"""
|
||||
Generates symmetric key from password and salt.
|
||||
"""
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
return base64.b64encode(kdf.derive(password.encode('utf-8')))
|
||||
|
||||
|
||||
def encrypt(data, symmetric_key=None, public_key=None):
|
||||
"""
|
||||
Encrypts data with symmetric key or public key.
|
||||
"""
|
||||
if symmetric_key:
|
||||
fernet = Fernet(symmetric_key)
|
||||
return fernet.encrypt(data)
|
||||
|
||||
elif public_key:
|
||||
public_key_obj = load_pem_public_key(
|
||||
data=public_key,
|
||||
backend=default_backend()
|
||||
)
|
||||
return public_key_obj.encrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def decrypt(data, symmetric_key=None, private_key=None):
|
||||
"""
|
||||
Decrypts data with symmetric key or public key.
|
||||
"""
|
||||
if symmetric_key:
|
||||
try:
|
||||
fernet = Fernet(symmetric_key)
|
||||
return fernet.decrypt(data)
|
||||
except InvalidToken:
|
||||
raise errors.AuthenticationError(
|
||||
message=_('Invalid credentials'))
|
||||
|
||||
elif private_key:
|
||||
try:
|
||||
private_key_obj = load_pem_private_key(
|
||||
data=private_key,
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
return private_key_obj.decrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
except AssertionError:
|
||||
raise errors.AuthenticationError(
|
||||
message=_('Invalid credentials'))
|
||||
|
||||
|
||||
@register()
|
||||
class vault_add(Local):
|
||||
__doc__ = _('Create a new vault.')
|
||||
|
||||
takes_options = (
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'public_key_file?',
|
||||
cli_name='public_key_file',
|
||||
doc=_('File containing the vault public key'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_add_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_add, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_add_internal.options():
|
||||
if option.name not in ('ipavaultsalt', 'version'):
|
||||
yield option
|
||||
for option in super(vault_add, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_add_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
vault_type = options.get('ipavaulttype')
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
public_key = options.get('ipavaultpublickey')
|
||||
public_key_file = options.get('public_key_file')
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
if 'public_key_file' in options:
|
||||
del options['public_key_file']
|
||||
|
||||
if vault_type != u'symmetric' and (password or password_file):
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password can be specified only for '
|
||||
'symmetric vault')
|
||||
)
|
||||
|
||||
if vault_type != u'asymmetric' and (public_key or public_key_file):
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Public key can be specified only for '
|
||||
'asymmetric vault')
|
||||
)
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
pass
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
# get password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
password = get_new_password()
|
||||
|
||||
# generate vault salt
|
||||
options['ipavaultsalt'] = os.urandom(16)
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get new vault public key
|
||||
if public_key and public_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Public key specified multiple times'))
|
||||
|
||||
elif public_key:
|
||||
pass
|
||||
|
||||
elif public_key_file:
|
||||
public_key = validated_read('public-key-file',
|
||||
public_key_file,
|
||||
mode='rb')
|
||||
|
||||
# store vault public key
|
||||
options['ipavaultpublickey'] = public_key
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Missing vault public key'))
|
||||
|
||||
# validate public key and prevent users from accidentally
|
||||
# sending a private key to the server.
|
||||
try:
|
||||
load_pem_public_key(
|
||||
data=public_key,
|
||||
backend=default_backend()
|
||||
)
|
||||
except ValueError as e:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Invalid or unsupported vault public key: %s') % e,
|
||||
)
|
||||
|
||||
# create vault
|
||||
response = self.api.Command.vault_add_internal(*args, **options)
|
||||
|
||||
# prepare parameters for archival
|
||||
opts = options.copy()
|
||||
if 'description' in opts:
|
||||
del opts['description']
|
||||
if 'ipavaulttype' in opts:
|
||||
del opts['ipavaulttype']
|
||||
|
||||
if vault_type == u'symmetric':
|
||||
opts['password'] = password
|
||||
del opts['ipavaultsalt']
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
del opts['ipavaultpublickey']
|
||||
|
||||
# archive blank data
|
||||
self.api.Command.vault_archive(*args, **opts)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_mod(Local):
|
||||
__doc__ = _('Modify a vault.')
|
||||
|
||||
takes_options = (
|
||||
Flag(
|
||||
'change_password?',
|
||||
doc=_('Change password'),
|
||||
),
|
||||
Str(
|
||||
'old_password?',
|
||||
cli_name='old_password',
|
||||
doc=_('Old vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'old_password_file?',
|
||||
cli_name='old_password_file',
|
||||
doc=_('File containing the old vault password'),
|
||||
),
|
||||
Str(
|
||||
'new_password?',
|
||||
cli_name='new_password',
|
||||
doc=_('New vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'new_password_file?',
|
||||
cli_name='new_password_file',
|
||||
doc=_('File containing the new vault password'),
|
||||
),
|
||||
Bytes(
|
||||
'private_key?',
|
||||
cli_name='private_key',
|
||||
doc=_('Old vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'private_key_file?',
|
||||
cli_name='private_key_file',
|
||||
doc=_('File containing the old vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'public_key_file?',
|
||||
cli_name='public_key_file',
|
||||
doc=_('File containing the new vault public key'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_mod_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_mod, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_mod_internal.options():
|
||||
if option.name not in ('ipavaultsalt', 'version'):
|
||||
yield option
|
||||
for option in super(vault_mod, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_mod_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
vault_type = options.pop('ipavaulttype', False)
|
||||
salt = options.pop('ipavaultsalt', False)
|
||||
change_password = options.pop('change_password', False)
|
||||
|
||||
old_password = options.pop('old_password', None)
|
||||
old_password_file = options.pop('old_password_file', None)
|
||||
new_password = options.pop('new_password', None)
|
||||
new_password_file = options.pop('new_password_file', None)
|
||||
|
||||
old_private_key = options.pop('private_key', None)
|
||||
old_private_key_file = options.pop('private_key_file', None)
|
||||
new_public_key = options.pop('ipavaultpublickey', None)
|
||||
new_public_key_file = options.pop('public_key_file', None)
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# determine the vault type based on parameters specified
|
||||
if vault_type:
|
||||
pass
|
||||
|
||||
elif change_password or new_password or new_password_file or salt:
|
||||
vault_type = u'symmetric'
|
||||
|
||||
elif new_public_key or new_public_key_file:
|
||||
vault_type = u'asymmetric'
|
||||
|
||||
# if vault type is specified, retrieve existing secret
|
||||
if vault_type:
|
||||
opts = options.copy()
|
||||
opts.pop('description', None)
|
||||
|
||||
opts['password'] = old_password
|
||||
opts['password_file'] = old_password_file
|
||||
opts['private_key'] = old_private_key
|
||||
opts['private_key_file'] = old_private_key_file
|
||||
|
||||
response = self.api.Command.vault_retrieve(*args, **opts)
|
||||
data = response['result']['data']
|
||||
|
||||
opts = options.copy()
|
||||
|
||||
# if vault type is specified, update crypto attributes
|
||||
if vault_type:
|
||||
opts['ipavaulttype'] = vault_type
|
||||
|
||||
if vault_type == u'standard':
|
||||
opts['ipavaultsalt'] = None
|
||||
opts['ipavaultpublickey'] = None
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
if salt:
|
||||
opts['ipavaultsalt'] = salt
|
||||
else:
|
||||
opts['ipavaultsalt'] = os.urandom(16)
|
||||
|
||||
opts['ipavaultpublickey'] = None
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get new vault public key
|
||||
if new_public_key and new_public_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('New public key specified multiple times'))
|
||||
|
||||
elif new_public_key:
|
||||
pass
|
||||
|
||||
elif new_public_key_file:
|
||||
new_public_key = validated_read('public_key_file',
|
||||
new_public_key_file,
|
||||
mode='rb')
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Missing new vault public key'))
|
||||
|
||||
opts['ipavaultsalt'] = None
|
||||
opts['ipavaultpublickey'] = new_public_key
|
||||
|
||||
response = self.api.Command.vault_mod_internal(*args, **opts)
|
||||
|
||||
# if vault type is specified, rearchive existing secret
|
||||
if vault_type:
|
||||
opts = options.copy()
|
||||
opts.pop('description', None)
|
||||
|
||||
opts['data'] = data
|
||||
opts['password'] = new_password
|
||||
opts['password_file'] = new_password_file
|
||||
opts['override_password'] = True
|
||||
|
||||
self.api.Command.vault_archive(*args, **opts)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_archive(Local):
|
||||
__doc__ = _('Archive data into a vault.')
|
||||
|
||||
takes_options = (
|
||||
Bytes(
|
||||
'data?',
|
||||
doc=_('Binary data to archive'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'in?',
|
||||
doc=_('File containing data to archive'),
|
||||
),
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Flag(
|
||||
'override_password?',
|
||||
doc=_('Override existing password'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_archive_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_archive, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_archive_internal.options():
|
||||
if option.name not in ('nonce',
|
||||
'session_key',
|
||||
'vault_data',
|
||||
'version'):
|
||||
yield option
|
||||
for option in super(vault_archive, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_archive_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
name = args[-1]
|
||||
|
||||
data = options.get('data')
|
||||
input_file = options.get('in')
|
||||
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
|
||||
override_password = options.pop('override_password', False)
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'data' in options:
|
||||
del options['data']
|
||||
if 'in' in options:
|
||||
del options['in']
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
|
||||
# get data
|
||||
if data and input_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Input data specified multiple times'))
|
||||
|
||||
elif data:
|
||||
if len(data) > MAX_VAULT_DATA_SIZE:
|
||||
raise errors.ValidationError(name="data", error=_(
|
||||
"Size of data exceeds the limit. Current vault data size "
|
||||
"limit is %(limit)d B")
|
||||
% {'limit': MAX_VAULT_DATA_SIZE})
|
||||
|
||||
elif input_file:
|
||||
try:
|
||||
stat = os.stat(input_file)
|
||||
except OSError as exc:
|
||||
raise errors.ValidationError(name="in", error=_(
|
||||
"Cannot read file '%(filename)s': %(exc)s")
|
||||
% {'filename': input_file, 'exc': exc.args[1]})
|
||||
if stat.st_size > MAX_VAULT_DATA_SIZE:
|
||||
raise errors.ValidationError(name="in", error=_(
|
||||
"Size of data exceeds the limit. Current vault data size "
|
||||
"limit is %(limit)d B")
|
||||
% {'limit': MAX_VAULT_DATA_SIZE})
|
||||
data = validated_read('in', input_file, mode='rb')
|
||||
|
||||
else:
|
||||
data = ''
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# retrieve vault info
|
||||
vault = self.api.Command.vault_show(*args, **options)['result']
|
||||
|
||||
vault_type = vault['ipavaulttype'][0]
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
# get password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
if override_password:
|
||||
password = get_new_password()
|
||||
else:
|
||||
password = get_existing_password()
|
||||
|
||||
if not override_password:
|
||||
# verify password by retrieving existing data
|
||||
opts = options.copy()
|
||||
opts['password'] = password
|
||||
try:
|
||||
self.api.Command.vault_retrieve(*args, **opts)
|
||||
except errors.NotFound:
|
||||
pass
|
||||
|
||||
salt = vault['ipavaultsalt'][0]
|
||||
|
||||
# generate encryption key from vault password
|
||||
encryption_key = generate_symmetric_key(password, salt)
|
||||
|
||||
# encrypt data with encryption key
|
||||
data = encrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
public_key = vault['ipavaultpublickey'][0].encode('utf-8')
|
||||
|
||||
# generate encryption key
|
||||
encryption_key = base64.b64encode(os.urandom(32))
|
||||
|
||||
# encrypt data with encryption key
|
||||
data = encrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
# encrypt encryption key with public key
|
||||
encrypted_key = encrypt(encryption_key, public_key=public_key)
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='vault_type',
|
||||
error=_('Invalid vault type'))
|
||||
|
||||
# initialize NSS database
|
||||
current_dbdir = paths.IPA_NSSDB_DIR
|
||||
nss.nss_init(current_dbdir)
|
||||
|
||||
# retrieve transport certificate
|
||||
config = self.api.Command.vaultconfig_show()['result']
|
||||
transport_cert_der = config['transport_cert']
|
||||
nss_transport_cert = nss.Certificate(transport_cert_der)
|
||||
|
||||
# generate session key
|
||||
mechanism = nss.CKM_DES3_CBC_PAD
|
||||
slot = nss.get_best_slot(mechanism)
|
||||
key_length = slot.get_best_key_length(mechanism)
|
||||
session_key = slot.key_gen(mechanism, None, key_length)
|
||||
|
||||
# wrap session key with transport certificate
|
||||
# pylint: disable=no-member
|
||||
public_key = nss_transport_cert.subject_public_key_info.public_key
|
||||
# pylint: enable=no-member
|
||||
wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
|
||||
public_key,
|
||||
session_key)
|
||||
|
||||
options['session_key'] = wrapped_session_key.data
|
||||
|
||||
nonce_length = nss.get_iv_length(mechanism)
|
||||
nonce = nss.generate_random(nonce_length)
|
||||
options['nonce'] = nonce
|
||||
|
||||
vault_data = {}
|
||||
vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
|
||||
|
||||
if encrypted_key:
|
||||
vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\
|
||||
.decode('utf-8')
|
||||
|
||||
json_vault_data = json.dumps(vault_data)
|
||||
|
||||
# wrap vault_data with session key
|
||||
iv_si = nss.SecItem(nonce)
|
||||
iv_param = nss.param_from_iv(mechanism, iv_si)
|
||||
|
||||
encoding_ctx = nss.create_context_by_sym_key(mechanism,
|
||||
nss.CKA_ENCRYPT,
|
||||
session_key,
|
||||
iv_param)
|
||||
|
||||
wrapped_vault_data = encoding_ctx.cipher_op(json_vault_data)\
|
||||
+ encoding_ctx.digest_final()
|
||||
|
||||
options['vault_data'] = wrapped_vault_data
|
||||
|
||||
return self.api.Command.vault_archive_internal(*args, **options)
|
||||
|
||||
|
||||
@register()
|
||||
class vault_retrieve(Local):
|
||||
__doc__ = _('Retrieve a data from a vault.')
|
||||
|
||||
takes_options = (
|
||||
Str(
|
||||
'out?',
|
||||
doc=_('File to store retrieved data'),
|
||||
),
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Bytes(
|
||||
'private_key?',
|
||||
cli_name='private_key',
|
||||
doc=_('Vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'private_key_file?',
|
||||
cli_name='private_key_file',
|
||||
doc=_('File containing the vault private key'),
|
||||
),
|
||||
)
|
||||
|
||||
has_output_params = (
|
||||
Bytes(
|
||||
'data',
|
||||
label=_('Data'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_retrieve_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_retrieve, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_retrieve_internal.options():
|
||||
if option.name not in ('session_key', 'version'):
|
||||
yield option
|
||||
for option in super(vault_retrieve, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_retrieve_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
name = args[-1]
|
||||
|
||||
output_file = options.get('out')
|
||||
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
private_key = options.get('private_key')
|
||||
private_key_file = options.get('private_key_file')
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'out' in options:
|
||||
del options['out']
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
if 'private_key' in options:
|
||||
del options['private_key']
|
||||
if 'private_key_file' in options:
|
||||
del options['private_key_file']
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# retrieve vault info
|
||||
vault = self.api.Command.vault_show(*args, **options)['result']
|
||||
|
||||
vault_type = vault['ipavaulttype'][0]
|
||||
|
||||
# initialize NSS database
|
||||
current_dbdir = paths.IPA_NSSDB_DIR
|
||||
nss.nss_init(current_dbdir)
|
||||
|
||||
# retrieve transport certificate
|
||||
config = self.api.Command.vaultconfig_show()['result']
|
||||
transport_cert_der = config['transport_cert']
|
||||
nss_transport_cert = nss.Certificate(transport_cert_der)
|
||||
|
||||
# generate session key
|
||||
mechanism = nss.CKM_DES3_CBC_PAD
|
||||
slot = nss.get_best_slot(mechanism)
|
||||
key_length = slot.get_best_key_length(mechanism)
|
||||
session_key = slot.key_gen(mechanism, None, key_length)
|
||||
|
||||
# wrap session key with transport certificate
|
||||
# pylint: disable=no-member
|
||||
public_key = nss_transport_cert.subject_public_key_info.public_key
|
||||
# pylint: enable=no-member
|
||||
wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
|
||||
public_key,
|
||||
session_key)
|
||||
|
||||
# send retrieval request to server
|
||||
options['session_key'] = wrapped_session_key.data
|
||||
|
||||
response = self.api.Command.vault_retrieve_internal(*args, **options)
|
||||
|
||||
result = response['result']
|
||||
nonce = result['nonce']
|
||||
|
||||
# unwrap data with session key
|
||||
wrapped_vault_data = result['vault_data']
|
||||
|
||||
iv_si = nss.SecItem(nonce)
|
||||
iv_param = nss.param_from_iv(mechanism, iv_si)
|
||||
|
||||
decoding_ctx = nss.create_context_by_sym_key(mechanism,
|
||||
nss.CKA_DECRYPT,
|
||||
session_key,
|
||||
iv_param)
|
||||
|
||||
json_vault_data = decoding_ctx.cipher_op(wrapped_vault_data)\
|
||||
+ decoding_ctx.digest_final()
|
||||
|
||||
vault_data = json.loads(json_vault_data)
|
||||
data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
if 'encrypted_key' in vault_data:
|
||||
encrypted_key = base64.b64decode(vault_data[u'encrypted_key']
|
||||
.encode('utf-8'))
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
pass
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
salt = vault['ipavaultsalt'][0]
|
||||
|
||||
# get encryption key from vault password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
password = get_existing_password()
|
||||
|
||||
# generate encryption key from password
|
||||
encryption_key = generate_symmetric_key(password, salt)
|
||||
|
||||
# decrypt data with encryption key
|
||||
data = decrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get encryption key with vault private key
|
||||
if private_key and private_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Private key specified multiple times'))
|
||||
|
||||
elif private_key:
|
||||
pass
|
||||
|
||||
elif private_key_file:
|
||||
private_key = validated_read('private-key-file',
|
||||
private_key_file,
|
||||
mode='rb')
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='private_key',
|
||||
error=_('Missing vault private key'))
|
||||
|
||||
# decrypt encryption key with private key
|
||||
encryption_key = decrypt(encrypted_key, private_key=private_key)
|
||||
|
||||
# decrypt data with encryption key
|
||||
data = decrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='vault_type',
|
||||
error=_('Invalid vault type'))
|
||||
|
||||
if output_file:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(data)
|
||||
|
||||
else:
|
||||
response['result'] = {'data': data}
|
||||
|
||||
return response
|
||||
@@ -18,13 +18,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from ipalib import api, errors
|
||||
from ipalib import Flag, Str, IA5Str
|
||||
from ipalib.frontend import Command
|
||||
from ipalib import Str, IA5Str
|
||||
from ipalib.plugable import Registry
|
||||
from .baseldap import (
|
||||
pkey_to_value,
|
||||
@@ -211,8 +208,6 @@ automountInformation: -ro,soft,rsize=8192,wsize=8192 nfs.example.com:/vol/arch
|
||||
register = Registry()
|
||||
|
||||
DIRECT_MAP_KEY = u'/-'
|
||||
DEFAULT_MAPS = (u'auto.direct', )
|
||||
DEFAULT_KEYS = (u'/-', )
|
||||
|
||||
@register()
|
||||
class automountlocation(LDAPObject):
|
||||
@@ -392,196 +387,6 @@ class automountlocation_tofiles(LDAPQuery):
|
||||
)
|
||||
|
||||
|
||||
@register()
|
||||
class automountlocation_import(Command):
|
||||
__doc__ = _('Import automount files for a specific location.')
|
||||
|
||||
takes_args = (
|
||||
Str('masterfile',
|
||||
label=_('Master file'),
|
||||
doc=_('Automount master file.'),
|
||||
),
|
||||
)
|
||||
|
||||
takes_options = (
|
||||
Flag('continue?',
|
||||
cli_name='continue',
|
||||
doc=_('Continuous operation mode. Errors are reported but the process continues.'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.automountlocation_show.args():
|
||||
yield arg
|
||||
for arg in super(automountlocation_import, self).get_args():
|
||||
yield arg
|
||||
|
||||
def __read_mapfile(self, filename):
|
||||
try:
|
||||
fp = open(filename, 'r')
|
||||
map = fp.readlines()
|
||||
fp.close()
|
||||
except IOError as e:
|
||||
if e.errno == 2:
|
||||
raise errors.NotFound(
|
||||
reason=_('File %(file)s not found') % {'file': filename}
|
||||
)
|
||||
else:
|
||||
raise
|
||||
return map
|
||||
|
||||
def forward(self, *args, **options):
|
||||
"""
|
||||
The basic idea is to read the master file and create all the maps
|
||||
we need, then read each map file and add all the keys for the map.
|
||||
"""
|
||||
location = self.api.Command['automountlocation_show'](args[0])
|
||||
|
||||
result = {'maps':[], 'keys':[], 'skipped':[], 'duplicatekeys':[], 'duplicatemaps':[]}
|
||||
maps = {}
|
||||
master = self.__read_mapfile(args[1])
|
||||
for m in master:
|
||||
if m.startswith('#'):
|
||||
continue
|
||||
m = m.rstrip()
|
||||
if m.startswith('+'):
|
||||
result['skipped'].append([m,args[1]])
|
||||
continue
|
||||
if len(m) == 0:
|
||||
continue
|
||||
am = m.split(None)
|
||||
if len(am) < 2:
|
||||
continue
|
||||
|
||||
if am[1].startswith('/'):
|
||||
mapfile = am[1].replace('"','')
|
||||
am[1] = os.path.basename(am[1])
|
||||
maps[am[1]] = mapfile
|
||||
|
||||
# Add a new key to the auto.master map for the new map file
|
||||
try:
|
||||
api.Command['automountkey_add'](
|
||||
args[0],
|
||||
u'auto.master',
|
||||
automountkey=unicode(am[0]),
|
||||
automountinformation=unicode(' '.join(am[1:])))
|
||||
result['keys'].append([am[0], u'auto.master'])
|
||||
except errors.DuplicateEntry as e:
|
||||
if unicode(am[0]) in DEFAULT_KEYS:
|
||||
# ignore conflict when the key was pre-created by the framework
|
||||
pass
|
||||
elif options.get('continue', False):
|
||||
result['duplicatekeys'].append(am[0])
|
||||
else:
|
||||
raise errors.DuplicateEntry(
|
||||
message=_('key %(key)s already exists') % dict(
|
||||
key=am[0]))
|
||||
# Add the new map
|
||||
if not am[1].startswith('-'):
|
||||
try:
|
||||
api.Command['automountmap_add'](args[0], unicode(am[1]))
|
||||
result['maps'].append(am[1])
|
||||
except errors.DuplicateEntry as e:
|
||||
if unicode(am[1]) in DEFAULT_MAPS:
|
||||
# ignore conflict when the map was pre-created by the framework
|
||||
pass
|
||||
elif options.get('continue', False):
|
||||
result['duplicatemaps'].append(am[0])
|
||||
else:
|
||||
raise errors.DuplicateEntry(
|
||||
message=_('map %(map)s already exists') % dict(
|
||||
map=am[1]))
|
||||
|
||||
# Now iterate over the map files and add the keys. To handle
|
||||
# continuation lines I'll make a pass through it to skip comments
|
||||
# etc and also to combine lines.
|
||||
for m in maps:
|
||||
map = self.__read_mapfile(maps[m])
|
||||
lines = []
|
||||
cont = ''
|
||||
for x in map:
|
||||
if x.startswith('#'):
|
||||
continue
|
||||
x = x.rstrip()
|
||||
if x.startswith('+'):
|
||||
result['skipped'].append([m, maps[m]])
|
||||
continue
|
||||
if len(x) == 0:
|
||||
continue
|
||||
if x.endswith("\\"):
|
||||
cont = cont + x[:-1] + ' '
|
||||
else:
|
||||
lines.append(cont + x)
|
||||
cont=''
|
||||
for x in lines:
|
||||
am = x.split(None)
|
||||
key = unicode(am[0].replace('"',''))
|
||||
try:
|
||||
api.Command['automountkey_add'](
|
||||
args[0],
|
||||
unicode(m),
|
||||
automountkey=key,
|
||||
automountinformation=unicode(' '.join(am[1:])))
|
||||
result['keys'].append([key,m])
|
||||
except errors.DuplicateEntry as e:
|
||||
if options.get('continue', False):
|
||||
result['duplicatekeys'].append(am[0])
|
||||
else:
|
||||
raise e
|
||||
|
||||
return dict(result=result)
|
||||
|
||||
def output_for_cli(self, textui, result, *keys, **options):
|
||||
maps = result['result']['maps']
|
||||
keys = result['result']['keys']
|
||||
duplicatemaps = result['result']['duplicatemaps']
|
||||
duplicatekeys = result['result']['duplicatekeys']
|
||||
skipped = result['result']['skipped']
|
||||
|
||||
textui.print_plain('Imported maps:')
|
||||
for m in maps:
|
||||
textui.print_plain(
|
||||
'Added %s' % m
|
||||
)
|
||||
textui.print_plain('')
|
||||
|
||||
textui.print_plain('Imported keys:')
|
||||
for k in keys:
|
||||
textui.print_plain(
|
||||
'Added %s to %s' % (
|
||||
k[0], k[1]
|
||||
)
|
||||
)
|
||||
textui.print_plain('')
|
||||
|
||||
if len(skipped) > 0:
|
||||
textui.print_plain('Ignored keys:')
|
||||
for k in skipped:
|
||||
textui.print_plain(
|
||||
'Ignored %s to %s' % (
|
||||
k[0], k[1]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if options.get('continue', False) and len(duplicatemaps) > 0:
|
||||
textui.print_plain('')
|
||||
textui.print_plain('Duplicate maps skipped:')
|
||||
for m in duplicatemaps:
|
||||
textui.print_plain(
|
||||
'Skipped %s' % m
|
||||
)
|
||||
|
||||
|
||||
if options.get('continue', False) and len(duplicatekeys) > 0:
|
||||
textui.print_plain('')
|
||||
textui.print_plain('Duplicate keys skipped:')
|
||||
for k in duplicatekeys:
|
||||
textui.print_plain(
|
||||
'Skipped %s' % k
|
||||
)
|
||||
|
||||
|
||||
@register()
|
||||
class automountmap(LDAPObject):
|
||||
"""
|
||||
|
||||
@@ -22,7 +22,7 @@ import sys
|
||||
|
||||
from .baseldap import LDAPObject, LDAPAddMember, LDAPRemoveMember
|
||||
from .baseldap import LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
|
||||
from ipalib import api, Int, Str, Bool, DateTime, Flag, Bytes, IntEnum, StrEnum, Password, _, ngettext
|
||||
from ipalib import api, Int, Str, Bool, DateTime, Flag, Bytes, IntEnum, StrEnum, _, ngettext
|
||||
from ipalib.messages import add_message, ResultFormattingError
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib.errors import (
|
||||
@@ -31,10 +31,7 @@ from ipalib.errors import (
|
||||
NotFound,
|
||||
ValidationError)
|
||||
from ipalib.request import context
|
||||
from ipalib.frontend import Local
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
from ipapython.nsslib import NSSConnection
|
||||
from ipapython.version import API_VERSION
|
||||
|
||||
import base64
|
||||
@@ -542,80 +539,3 @@ class otptoken_remove_managedby(LDAPRemoveMember):
|
||||
__doc__ = _('Remove users that can manage this token.')
|
||||
|
||||
member_attributes = ['managedby']
|
||||
|
||||
|
||||
class HTTPSHandler(urllib.request.HTTPSHandler):
|
||||
"Opens SSL HTTPS connections that perform hostname validation."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__kwargs = kwargs
|
||||
|
||||
# Can't use super() because the parent is an old-style class.
|
||||
urllib.request.HTTPSHandler.__init__(self)
|
||||
|
||||
def __inner(self, host, **kwargs):
|
||||
tmp = self.__kwargs.copy()
|
||||
tmp.update(kwargs)
|
||||
# NSSConnection doesn't support timeout argument
|
||||
tmp.pop('timeout', None)
|
||||
return NSSConnection(host, **tmp)
|
||||
|
||||
def https_open(self, req):
|
||||
# pylint: disable=no-member
|
||||
return self.do_open(self.__inner, req)
|
||||
|
||||
@register()
|
||||
class otptoken_sync(Local):
|
||||
__doc__ = _('Synchronize an OTP token.')
|
||||
|
||||
header = 'X-IPA-TokenSync-Result'
|
||||
|
||||
takes_options = (
|
||||
Str('user', label=_('User ID')),
|
||||
Password('password', label=_('Password'), confirm=False),
|
||||
Password('first_code', label=_('First Code'), confirm=False),
|
||||
Password('second_code', label=_('Second Code'), confirm=False),
|
||||
)
|
||||
|
||||
takes_args = (
|
||||
Str('token?', label=_('Token ID')),
|
||||
)
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
status = {'result': {self.header: 'unknown'}}
|
||||
|
||||
# Get the sync URI.
|
||||
segments = list(urllib.parse.urlparse(self.api.env.xmlrpc_uri))
|
||||
assert segments[0] == 'https' # Ensure encryption.
|
||||
segments[2] = segments[2].replace('/xml', '/session/sync_token')
|
||||
# urlunparse *can* take one argument
|
||||
# pylint: disable=too-many-function-args
|
||||
sync_uri = urllib.parse.urlunparse(segments)
|
||||
|
||||
# Prepare the query.
|
||||
query = {k: v for k, v in kwargs.items()
|
||||
if k in {x.name for x in self.takes_options}}
|
||||
if args and args[0] is not None:
|
||||
obj = self.api.Object.otptoken
|
||||
query['token'] = DN((obj.primary_key.name, args[0]),
|
||||
obj.container_dn, self.api.env.basedn)
|
||||
query = urllib.parse.urlencode(query)
|
||||
|
||||
# Sync the token.
|
||||
# pylint: disable=E1101
|
||||
handler = HTTPSHandler(dbdir=paths.IPA_NSSDB_DIR,
|
||||
tls_version_min=api.env.tls_version_min,
|
||||
tls_version_max=api.env.tls_version_max)
|
||||
rsp = urllib.request.build_opener(handler).open(sync_uri, query)
|
||||
if rsp.getcode() == 200:
|
||||
status['result'][self.header] = rsp.info().get(self.header, 'unknown')
|
||||
rsp.close()
|
||||
|
||||
return status
|
||||
|
||||
def output_for_cli(self, textui, result, *keys, **options):
|
||||
textui.print_plain({
|
||||
'ok': 'Token synchronized.',
|
||||
'error': 'Error contacting server!',
|
||||
'invalid-credentials': 'Invalid Credentials!',
|
||||
}.get(result['result'][self.header], 'Unknown Error!'))
|
||||
|
||||
@@ -19,24 +19,7 @@
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import base64
|
||||
import getpass
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
|
||||
load_pem_private_key
|
||||
|
||||
import nss.nss as nss
|
||||
|
||||
from ipalib.frontend import Command, Object, Local
|
||||
from ipalib.frontend import Command, Object
|
||||
from ipalib import api, errors
|
||||
from ipalib import Bytes, Flag, Str, StrEnum
|
||||
from ipalib import output
|
||||
@@ -49,7 +32,6 @@ from ipalib.request import context
|
||||
from .baseuser import split_principal
|
||||
from .service import normalize_principal
|
||||
from ipalib import _, ngettext
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
|
||||
if api.env.in_server:
|
||||
@@ -208,36 +190,8 @@ EXAMPLES:
|
||||
""")
|
||||
|
||||
|
||||
def validated_read(argname, filename, mode='r', encoding=None):
|
||||
"""Read file and catch errors
|
||||
|
||||
IOError and UnicodeError (for text files) are turned into a
|
||||
ValidationError
|
||||
"""
|
||||
try:
|
||||
with io.open(filename, mode=mode, encoding=encoding) as f:
|
||||
data = f.read()
|
||||
except IOError as exc:
|
||||
raise errors.ValidationError(
|
||||
name=argname,
|
||||
error=_("Cannot read file '%(filename)s': %(exc)s") % {
|
||||
'filename': filename, 'exc': exc.args[1]
|
||||
}
|
||||
)
|
||||
except UnicodeError as exc:
|
||||
raise errors.ValidationError(
|
||||
name=argname,
|
||||
error=_("Cannot decode file '%(filename)s': %(exc)s") % {
|
||||
'filename': filename, 'exc': exc
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
register = Registry()
|
||||
|
||||
MAX_VAULT_DATA_SIZE = 2**20 # = 1 MB
|
||||
|
||||
vault_options = (
|
||||
Str(
|
||||
'service?',
|
||||
@@ -801,257 +755,6 @@ class vault(LDAPObject):
|
||||
entry['username'] = entry.dn[1]['cn']
|
||||
|
||||
|
||||
def get_new_password():
|
||||
"""
|
||||
Gets new password from user and verify it.
|
||||
"""
|
||||
while True:
|
||||
password = getpass.getpass('New password: ').decode(
|
||||
sys.stdin.encoding)
|
||||
password2 = getpass.getpass('Verify password: ').decode(
|
||||
sys.stdin.encoding)
|
||||
|
||||
if password == password2:
|
||||
return password
|
||||
|
||||
print(' ** Passwords do not match! **')
|
||||
|
||||
|
||||
def get_existing_password():
|
||||
"""
|
||||
Gets existing password from user.
|
||||
"""
|
||||
return getpass.getpass('Password: ').decode(sys.stdin.encoding)
|
||||
|
||||
|
||||
def generate_symmetric_key(password, salt):
|
||||
"""
|
||||
Generates symmetric key from password and salt.
|
||||
"""
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
return base64.b64encode(kdf.derive(password.encode('utf-8')))
|
||||
|
||||
|
||||
def encrypt(data, symmetric_key=None, public_key=None):
|
||||
"""
|
||||
Encrypts data with symmetric key or public key.
|
||||
"""
|
||||
if symmetric_key:
|
||||
fernet = Fernet(symmetric_key)
|
||||
return fernet.encrypt(data)
|
||||
|
||||
elif public_key:
|
||||
public_key_obj = load_pem_public_key(
|
||||
data=public_key,
|
||||
backend=default_backend()
|
||||
)
|
||||
return public_key_obj.encrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def decrypt(data, symmetric_key=None, private_key=None):
|
||||
"""
|
||||
Decrypts data with symmetric key or public key.
|
||||
"""
|
||||
if symmetric_key:
|
||||
try:
|
||||
fernet = Fernet(symmetric_key)
|
||||
return fernet.decrypt(data)
|
||||
except InvalidToken:
|
||||
raise errors.AuthenticationError(
|
||||
message=_('Invalid credentials'))
|
||||
|
||||
elif private_key:
|
||||
try:
|
||||
private_key_obj = load_pem_private_key(
|
||||
data=private_key,
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
return private_key_obj.decrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
except AssertionError:
|
||||
raise errors.AuthenticationError(
|
||||
message=_('Invalid credentials'))
|
||||
|
||||
|
||||
@register()
|
||||
class vault_add(Local):
|
||||
__doc__ = _('Create a new vault.')
|
||||
|
||||
takes_options = (
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'public_key_file?',
|
||||
cli_name='public_key_file',
|
||||
doc=_('File containing the vault public key'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_add_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_add, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_add_internal.options():
|
||||
if option.name not in ('ipavaultsalt', 'version'):
|
||||
yield option
|
||||
for option in super(vault_add, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_add_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
vault_type = options.get('ipavaulttype')
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
public_key = options.get('ipavaultpublickey')
|
||||
public_key_file = options.get('public_key_file')
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
if 'public_key_file' in options:
|
||||
del options['public_key_file']
|
||||
|
||||
if vault_type != u'symmetric' and (password or password_file):
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password can be specified only for '
|
||||
'symmetric vault')
|
||||
)
|
||||
|
||||
if vault_type != u'asymmetric' and (public_key or public_key_file):
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Public key can be specified only for '
|
||||
'asymmetric vault')
|
||||
)
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
pass
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
# get password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
password = get_new_password()
|
||||
|
||||
# generate vault salt
|
||||
options['ipavaultsalt'] = os.urandom(16)
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get new vault public key
|
||||
if public_key and public_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Public key specified multiple times'))
|
||||
|
||||
elif public_key:
|
||||
pass
|
||||
|
||||
elif public_key_file:
|
||||
public_key = validated_read('public-key-file',
|
||||
public_key_file,
|
||||
mode='rb')
|
||||
|
||||
# store vault public key
|
||||
options['ipavaultpublickey'] = public_key
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Missing vault public key'))
|
||||
|
||||
# validate public key and prevent users from accidentally
|
||||
# sending a private key to the server.
|
||||
try:
|
||||
load_pem_public_key(
|
||||
data=public_key,
|
||||
backend=default_backend()
|
||||
)
|
||||
except ValueError as e:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Invalid or unsupported vault public key: %s') % e,
|
||||
)
|
||||
|
||||
# create vault
|
||||
response = self.api.Command.vault_add_internal(*args, **options)
|
||||
|
||||
# prepare parameters for archival
|
||||
opts = options.copy()
|
||||
if 'description' in opts:
|
||||
del opts['description']
|
||||
if 'ipavaulttype' in opts:
|
||||
del opts['ipavaulttype']
|
||||
|
||||
if vault_type == u'symmetric':
|
||||
opts['password'] = password
|
||||
del opts['ipavaultsalt']
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
del opts['ipavaultpublickey']
|
||||
|
||||
# archive blank data
|
||||
self.api.Command.vault_archive(*args, **opts)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_add_internal(LDAPCreate):
|
||||
|
||||
@@ -1202,172 +905,6 @@ class vault_find(LDAPSearch):
|
||||
raise exc
|
||||
|
||||
|
||||
@register()
|
||||
class vault_mod(Local):
|
||||
__doc__ = _('Modify a vault.')
|
||||
|
||||
takes_options = (
|
||||
Flag(
|
||||
'change_password?',
|
||||
doc=_('Change password'),
|
||||
),
|
||||
Str(
|
||||
'old_password?',
|
||||
cli_name='old_password',
|
||||
doc=_('Old vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'old_password_file?',
|
||||
cli_name='old_password_file',
|
||||
doc=_('File containing the old vault password'),
|
||||
),
|
||||
Str(
|
||||
'new_password?',
|
||||
cli_name='new_password',
|
||||
doc=_('New vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'new_password_file?',
|
||||
cli_name='new_password_file',
|
||||
doc=_('File containing the new vault password'),
|
||||
),
|
||||
Bytes(
|
||||
'private_key?',
|
||||
cli_name='private_key',
|
||||
doc=_('Old vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'private_key_file?',
|
||||
cli_name='private_key_file',
|
||||
doc=_('File containing the old vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'public_key_file?',
|
||||
cli_name='public_key_file',
|
||||
doc=_('File containing the new vault public key'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_mod_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_mod, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_mod_internal.options():
|
||||
if option.name not in ('ipavaultsalt', 'version'):
|
||||
yield option
|
||||
for option in super(vault_mod, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_mod_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
vault_type = options.pop('ipavaulttype', False)
|
||||
salt = options.pop('ipavaultsalt', False)
|
||||
change_password = options.pop('change_password', False)
|
||||
|
||||
old_password = options.pop('old_password', None)
|
||||
old_password_file = options.pop('old_password_file', None)
|
||||
new_password = options.pop('new_password', None)
|
||||
new_password_file = options.pop('new_password_file', None)
|
||||
|
||||
old_private_key = options.pop('private_key', None)
|
||||
old_private_key_file = options.pop('private_key_file', None)
|
||||
new_public_key = options.pop('ipavaultpublickey', None)
|
||||
new_public_key_file = options.pop('public_key_file', None)
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# determine the vault type based on parameters specified
|
||||
if vault_type:
|
||||
pass
|
||||
|
||||
elif change_password or new_password or new_password_file or salt:
|
||||
vault_type = u'symmetric'
|
||||
|
||||
elif new_public_key or new_public_key_file:
|
||||
vault_type = u'asymmetric'
|
||||
|
||||
# if vault type is specified, retrieve existing secret
|
||||
if vault_type:
|
||||
opts = options.copy()
|
||||
opts.pop('description', None)
|
||||
|
||||
opts['password'] = old_password
|
||||
opts['password_file'] = old_password_file
|
||||
opts['private_key'] = old_private_key
|
||||
opts['private_key_file'] = old_private_key_file
|
||||
|
||||
response = self.api.Command.vault_retrieve(*args, **opts)
|
||||
data = response['result']['data']
|
||||
|
||||
opts = options.copy()
|
||||
|
||||
# if vault type is specified, update crypto attributes
|
||||
if vault_type:
|
||||
opts['ipavaulttype'] = vault_type
|
||||
|
||||
if vault_type == u'standard':
|
||||
opts['ipavaultsalt'] = None
|
||||
opts['ipavaultpublickey'] = None
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
if salt:
|
||||
opts['ipavaultsalt'] = salt
|
||||
else:
|
||||
opts['ipavaultsalt'] = os.urandom(16)
|
||||
|
||||
opts['ipavaultpublickey'] = None
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get new vault public key
|
||||
if new_public_key and new_public_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('New public key specified multiple times'))
|
||||
|
||||
elif new_public_key:
|
||||
pass
|
||||
|
||||
elif new_public_key_file:
|
||||
new_public_key = validated_read('public_key_file',
|
||||
new_public_key_file,
|
||||
mode='rb')
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='ipavaultpublickey',
|
||||
error=_('Missing new vault public key'))
|
||||
|
||||
opts['ipavaultsalt'] = None
|
||||
opts['ipavaultpublickey'] = new_public_key
|
||||
|
||||
response = self.api.Command.vault_mod_internal(*args, **opts)
|
||||
|
||||
# if vault type is specified, rearchive existing secret
|
||||
if vault_type:
|
||||
opts = options.copy()
|
||||
opts.pop('description', None)
|
||||
|
||||
opts['data'] = data
|
||||
opts['password'] = new_password
|
||||
opts['password_file'] = new_password_file
|
||||
opts['override_password'] = True
|
||||
|
||||
self.api.Command.vault_archive(*args, **opts)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_mod_internal(LDAPUpdate):
|
||||
|
||||
@@ -1470,235 +1007,6 @@ class vaultconfig_show(Retrieve):
|
||||
}
|
||||
|
||||
|
||||
@register()
|
||||
class vault_archive(Local):
|
||||
__doc__ = _('Archive data into a vault.')
|
||||
|
||||
takes_options = (
|
||||
Bytes(
|
||||
'data?',
|
||||
doc=_('Binary data to archive'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'in?',
|
||||
doc=_('File containing data to archive'),
|
||||
),
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Flag(
|
||||
'override_password?',
|
||||
doc=_('Override existing password'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_archive_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_archive, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_archive_internal.options():
|
||||
if option.name not in ('nonce',
|
||||
'session_key',
|
||||
'vault_data',
|
||||
'version'):
|
||||
yield option
|
||||
for option in super(vault_archive, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_archive_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
name = args[-1]
|
||||
|
||||
data = options.get('data')
|
||||
input_file = options.get('in')
|
||||
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
|
||||
override_password = options.pop('override_password', False)
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'data' in options:
|
||||
del options['data']
|
||||
if 'in' in options:
|
||||
del options['in']
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
|
||||
# get data
|
||||
if data and input_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Input data specified multiple times'))
|
||||
|
||||
elif data:
|
||||
if len(data) > MAX_VAULT_DATA_SIZE:
|
||||
raise errors.ValidationError(name="data", error=_(
|
||||
"Size of data exceeds the limit. Current vault data size "
|
||||
"limit is %(limit)d B")
|
||||
% {'limit': MAX_VAULT_DATA_SIZE})
|
||||
|
||||
elif input_file:
|
||||
try:
|
||||
stat = os.stat(input_file)
|
||||
except OSError as exc:
|
||||
raise errors.ValidationError(name="in", error=_(
|
||||
"Cannot read file '%(filename)s': %(exc)s")
|
||||
% {'filename': input_file, 'exc': exc.args[1]})
|
||||
if stat.st_size > MAX_VAULT_DATA_SIZE:
|
||||
raise errors.ValidationError(name="in", error=_(
|
||||
"Size of data exceeds the limit. Current vault data size "
|
||||
"limit is %(limit)d B")
|
||||
% {'limit': MAX_VAULT_DATA_SIZE})
|
||||
data = validated_read('in', input_file, mode='rb')
|
||||
|
||||
else:
|
||||
data = ''
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# retrieve vault info
|
||||
vault = self.api.Command.vault_show(*args, **options)['result']
|
||||
|
||||
vault_type = vault['ipavaulttype'][0]
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
# get password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
if override_password:
|
||||
password = get_new_password()
|
||||
else:
|
||||
password = get_existing_password()
|
||||
|
||||
if not override_password:
|
||||
# verify password by retrieving existing data
|
||||
opts = options.copy()
|
||||
opts['password'] = password
|
||||
try:
|
||||
self.api.Command.vault_retrieve(*args, **opts)
|
||||
except errors.NotFound:
|
||||
pass
|
||||
|
||||
salt = vault['ipavaultsalt'][0]
|
||||
|
||||
# generate encryption key from vault password
|
||||
encryption_key = generate_symmetric_key(password, salt)
|
||||
|
||||
# encrypt data with encryption key
|
||||
data = encrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
public_key = vault['ipavaultpublickey'][0].encode('utf-8')
|
||||
|
||||
# generate encryption key
|
||||
encryption_key = base64.b64encode(os.urandom(32))
|
||||
|
||||
# encrypt data with encryption key
|
||||
data = encrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
# encrypt encryption key with public key
|
||||
encrypted_key = encrypt(encryption_key, public_key=public_key)
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='vault_type',
|
||||
error=_('Invalid vault type'))
|
||||
|
||||
# initialize NSS database
|
||||
current_dbdir = paths.IPA_NSSDB_DIR
|
||||
nss.nss_init(current_dbdir)
|
||||
|
||||
# retrieve transport certificate
|
||||
config = self.api.Command.vaultconfig_show()['result']
|
||||
transport_cert_der = config['transport_cert']
|
||||
nss_transport_cert = nss.Certificate(transport_cert_der)
|
||||
|
||||
# generate session key
|
||||
mechanism = nss.CKM_DES3_CBC_PAD
|
||||
slot = nss.get_best_slot(mechanism)
|
||||
key_length = slot.get_best_key_length(mechanism)
|
||||
session_key = slot.key_gen(mechanism, None, key_length)
|
||||
|
||||
# wrap session key with transport certificate
|
||||
# pylint: disable=no-member
|
||||
public_key = nss_transport_cert.subject_public_key_info.public_key
|
||||
# pylint: enable=no-member
|
||||
wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
|
||||
public_key,
|
||||
session_key)
|
||||
|
||||
options['session_key'] = wrapped_session_key.data
|
||||
|
||||
nonce_length = nss.get_iv_length(mechanism)
|
||||
nonce = nss.generate_random(nonce_length)
|
||||
options['nonce'] = nonce
|
||||
|
||||
vault_data = {}
|
||||
vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
|
||||
|
||||
if encrypted_key:
|
||||
vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\
|
||||
.decode('utf-8')
|
||||
|
||||
json_vault_data = json.dumps(vault_data)
|
||||
|
||||
# wrap vault_data with session key
|
||||
iv_si = nss.SecItem(nonce)
|
||||
iv_param = nss.param_from_iv(mechanism, iv_si)
|
||||
|
||||
encoding_ctx = nss.create_context_by_sym_key(mechanism,
|
||||
nss.CKA_ENCRYPT,
|
||||
session_key,
|
||||
iv_param)
|
||||
|
||||
wrapped_vault_data = encoding_ctx.cipher_op(json_vault_data)\
|
||||
+ encoding_ctx.digest_final()
|
||||
|
||||
options['vault_data'] = wrapped_vault_data
|
||||
|
||||
return self.api.Command.vault_archive_internal(*args, **options)
|
||||
|
||||
|
||||
@register()
|
||||
class vault_archive_internal(PKQuery):
|
||||
|
||||
@@ -1776,221 +1084,6 @@ class vault_archive_internal(PKQuery):
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_retrieve(Local):
|
||||
__doc__ = _('Retrieve a data from a vault.')
|
||||
|
||||
takes_options = (
|
||||
Str(
|
||||
'out?',
|
||||
doc=_('File to store retrieved data'),
|
||||
),
|
||||
Str(
|
||||
'password?',
|
||||
cli_name='password',
|
||||
doc=_('Vault password'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'password_file?',
|
||||
cli_name='password_file',
|
||||
doc=_('File containing the vault password'),
|
||||
),
|
||||
Bytes(
|
||||
'private_key?',
|
||||
cli_name='private_key',
|
||||
doc=_('Vault private key'),
|
||||
),
|
||||
Str( # TODO: use File parameter
|
||||
'private_key_file?',
|
||||
cli_name='private_key_file',
|
||||
doc=_('File containing the vault private key'),
|
||||
),
|
||||
)
|
||||
|
||||
has_output_params = (
|
||||
Bytes(
|
||||
'data',
|
||||
label=_('Data'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in self.api.Command.vault_retrieve_internal.args():
|
||||
yield arg
|
||||
for arg in super(vault_retrieve, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_retrieve_internal.options():
|
||||
if option.name not in ('session_key', 'version'):
|
||||
yield option
|
||||
for option in super(vault_retrieve, self).get_options():
|
||||
yield option
|
||||
|
||||
def _iter_output(self):
|
||||
return self.api.Command.vault_retrieve_internal.output()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
|
||||
name = args[-1]
|
||||
|
||||
output_file = options.get('out')
|
||||
|
||||
password = options.get('password')
|
||||
password_file = options.get('password_file')
|
||||
private_key = options.get('private_key')
|
||||
private_key_file = options.get('private_key_file')
|
||||
|
||||
# don't send these parameters to server
|
||||
if 'out' in options:
|
||||
del options['out']
|
||||
if 'password' in options:
|
||||
del options['password']
|
||||
if 'password_file' in options:
|
||||
del options['password_file']
|
||||
if 'private_key' in options:
|
||||
del options['private_key']
|
||||
if 'private_key_file' in options:
|
||||
del options['private_key_file']
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
# retrieve vault info
|
||||
vault = self.api.Command.vault_show(*args, **options)['result']
|
||||
|
||||
vault_type = vault['ipavaulttype'][0]
|
||||
|
||||
# initialize NSS database
|
||||
current_dbdir = paths.IPA_NSSDB_DIR
|
||||
nss.nss_init(current_dbdir)
|
||||
|
||||
# retrieve transport certificate
|
||||
config = self.api.Command.vaultconfig_show()['result']
|
||||
transport_cert_der = config['transport_cert']
|
||||
nss_transport_cert = nss.Certificate(transport_cert_der)
|
||||
|
||||
# generate session key
|
||||
mechanism = nss.CKM_DES3_CBC_PAD
|
||||
slot = nss.get_best_slot(mechanism)
|
||||
key_length = slot.get_best_key_length(mechanism)
|
||||
session_key = slot.key_gen(mechanism, None, key_length)
|
||||
|
||||
# wrap session key with transport certificate
|
||||
# pylint: disable=no-member
|
||||
public_key = nss_transport_cert.subject_public_key_info.public_key
|
||||
# pylint: enable=no-member
|
||||
wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
|
||||
public_key,
|
||||
session_key)
|
||||
|
||||
# send retrieval request to server
|
||||
options['session_key'] = wrapped_session_key.data
|
||||
|
||||
response = self.api.Command.vault_retrieve_internal(*args, **options)
|
||||
|
||||
result = response['result']
|
||||
nonce = result['nonce']
|
||||
|
||||
# unwrap data with session key
|
||||
wrapped_vault_data = result['vault_data']
|
||||
|
||||
iv_si = nss.SecItem(nonce)
|
||||
iv_param = nss.param_from_iv(mechanism, iv_si)
|
||||
|
||||
decoding_ctx = nss.create_context_by_sym_key(mechanism,
|
||||
nss.CKA_DECRYPT,
|
||||
session_key,
|
||||
iv_param)
|
||||
|
||||
json_vault_data = decoding_ctx.cipher_op(wrapped_vault_data)\
|
||||
+ decoding_ctx.digest_final()
|
||||
|
||||
vault_data = json.loads(json_vault_data)
|
||||
data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
|
||||
|
||||
encrypted_key = None
|
||||
|
||||
if 'encrypted_key' in vault_data:
|
||||
encrypted_key = base64.b64decode(vault_data[u'encrypted_key']
|
||||
.encode('utf-8'))
|
||||
|
||||
if vault_type == u'standard':
|
||||
|
||||
pass
|
||||
|
||||
elif vault_type == u'symmetric':
|
||||
|
||||
salt = vault['ipavaultsalt'][0]
|
||||
|
||||
# get encryption key from vault password
|
||||
if password and password_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Password specified multiple times'))
|
||||
|
||||
elif password:
|
||||
pass
|
||||
|
||||
elif password_file:
|
||||
password = validated_read('password-file',
|
||||
password_file,
|
||||
encoding='utf-8')
|
||||
password = password.rstrip('\n')
|
||||
|
||||
else:
|
||||
password = get_existing_password()
|
||||
|
||||
# generate encryption key from password
|
||||
encryption_key = generate_symmetric_key(password, salt)
|
||||
|
||||
# decrypt data with encryption key
|
||||
data = decrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
elif vault_type == u'asymmetric':
|
||||
|
||||
# get encryption key with vault private key
|
||||
if private_key and private_key_file:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_('Private key specified multiple times'))
|
||||
|
||||
elif private_key:
|
||||
pass
|
||||
|
||||
elif private_key_file:
|
||||
private_key = validated_read('private-key-file',
|
||||
private_key_file,
|
||||
mode='rb')
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='private_key',
|
||||
error=_('Missing vault private key'))
|
||||
|
||||
# decrypt encryption key with private key
|
||||
encryption_key = decrypt(encrypted_key, private_key=private_key)
|
||||
|
||||
# decrypt data with encryption key
|
||||
data = decrypt(data, symmetric_key=encryption_key)
|
||||
|
||||
else:
|
||||
raise errors.ValidationError(
|
||||
name='vault_type',
|
||||
error=_('Invalid vault type'))
|
||||
|
||||
if output_file:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(data)
|
||||
|
||||
else:
|
||||
response['result'] = {'data': data}
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@register()
|
||||
class vault_retrieve_internal(PKQuery):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user