Imported Debian patch 4.7.2-3
This commit is contained in:
committed by
Mario Fetka
parent
27edeba051
commit
8bc559c5a1
@@ -1,7 +1,7 @@
|
||||
# Makefile.in generated by automake 1.16.2 from Makefile.am.
|
||||
# Makefile.in generated by automake 1.16.1 from Makefile.am.
|
||||
# @configure_input@
|
||||
|
||||
# Copyright (C) 1994-2020 Free Software Foundation, Inc.
|
||||
# Copyright (C) 1994-2018 Free Software Foundation, Inc.
|
||||
|
||||
# This Makefile.in is free software; the Free Software Foundation
|
||||
# gives unlimited permission to copy and/or distribute it,
|
||||
@@ -185,8 +185,6 @@ JSLINT = @JSLINT@
|
||||
KRAD_LIBS = @KRAD_LIBS@
|
||||
KRB5KDC_SERVICE = @KRB5KDC_SERVICE@
|
||||
KRB5_CFLAGS = @KRB5_CFLAGS@
|
||||
KRB5_GSSAPI_CFLAGS = @KRB5_GSSAPI_CFLAGS@
|
||||
KRB5_GSSAPI_LIBS = @KRB5_GSSAPI_LIBS@
|
||||
KRB5_LIBS = @KRB5_LIBS@
|
||||
LD = @LD@
|
||||
LDAP_CFLAGS = @LDAP_CFLAGS@
|
||||
@@ -229,10 +227,11 @@ NM = @NM@
|
||||
NMEDIT = @NMEDIT@
|
||||
NSPR_CFLAGS = @NSPR_CFLAGS@
|
||||
NSPR_LIBS = @NSPR_LIBS@
|
||||
NSS_CFLAGS = @NSS_CFLAGS@
|
||||
NSS_LIBS = @NSS_LIBS@
|
||||
NUM_VERSION = @NUM_VERSION@
|
||||
OBJDUMP = @OBJDUMP@
|
||||
OBJEXT = @OBJEXT@
|
||||
ODS_GROUP = @ODS_GROUP@
|
||||
ODS_USER = @ODS_USER@
|
||||
OTOOL = @OTOOL@
|
||||
OTOOL64 = @OTOOL64@
|
||||
@@ -253,6 +252,8 @@ POPT_LIBS = @POPT_LIBS@
|
||||
POSUB = @POSUB@
|
||||
PYLINT = @PYLINT@
|
||||
PYTHON = @PYTHON@
|
||||
PYTHON2 = @PYTHON2@
|
||||
PYTHON3 = @PYTHON3@
|
||||
PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
|
||||
PYTHON_INSTALL_EXTRA_OPTIONS = @PYTHON_INSTALL_EXTRA_OPTIONS@
|
||||
PYTHON_PLATFORM = @PYTHON_PLATFORM@
|
||||
@@ -340,9 +341,7 @@ program_transform_name = @program_transform_name@
|
||||
psdir = @psdir@
|
||||
pyexecdir = @pyexecdir@
|
||||
pythondir = @pythondir@
|
||||
runstatedir = @runstatedir@
|
||||
sbindir = @sbindir@
|
||||
selinux_makefile = @selinux_makefile@
|
||||
sharedstatedir = @sharedstatedir@
|
||||
srcdir = @srcdir@
|
||||
sysconfdir = @sysconfdir@
|
||||
|
||||
@@ -722,7 +722,7 @@ Key Example value Description
|
||||
============= ============================= =======================
|
||||
bin '/usr/bin' Dir. containing script
|
||||
dot_ipa '/home/jderose/.ipa' User config directory
|
||||
home os.path.expanduser('~') User home dir.
|
||||
home os.environ['HOME'] User home dir.
|
||||
ipalib '.../site-packages/ipalib' Dir. of ipalib package
|
||||
mode 'unit_test' The mode ipalib is in
|
||||
script sys.argv[0] Path of script
|
||||
|
||||
@@ -41,7 +41,7 @@ PERMISSIONS = ["read", "write", "add", "delete", "search", "compare",
|
||||
"selfwrite", "proxy", "all"]
|
||||
|
||||
|
||||
class ACI:
|
||||
class ACI(object):
|
||||
"""
|
||||
Holds the basic data for an ACI entry, as stored in the cn=accounts
|
||||
entry in LDAP. Has methods to parse an ACI string and export to an
|
||||
@@ -83,7 +83,7 @@ class ACI:
|
||||
op = v['operator']
|
||||
if type(v['expression']) in (tuple, list):
|
||||
target = ""
|
||||
for l in self._unique_list(v['expression']):
|
||||
for l in v['expression']:
|
||||
target = target + l + " || "
|
||||
target = target[:-4]
|
||||
aci = aci + "(%s %s \"%s\")" % (t, op, target)
|
||||
@@ -92,20 +92,6 @@ class ACI:
|
||||
aci = aci + "(version 3.0;acl \"%s\";%s (%s) %s %s \"%s\"" % (self.name, self.action, ",".join(self.permissions), self.bindrule['keyword'], self.bindrule['operator'], self.bindrule['expression']) + ";)"
|
||||
return aci
|
||||
|
||||
def _unique_list(self, l):
|
||||
"""
|
||||
A set() doesn't maintain order so make a list unique ourselves.
|
||||
|
||||
The number of entries in our lists are always going to be
|
||||
relatively low and this code will be called infrequently
|
||||
anyway so the overhead will be small.
|
||||
"""
|
||||
unique = []
|
||||
for item in l:
|
||||
if item not in unique:
|
||||
unique.append(item)
|
||||
return unique
|
||||
|
||||
def _remove_quotes(self, s):
|
||||
# Remove leading and trailing quotes
|
||||
if s.startswith('"'):
|
||||
@@ -163,9 +149,7 @@ class ACI:
|
||||
if not bindperms or len(bindperms.groups()) < 3:
|
||||
raise SyntaxError("malformed ACI, permissions match failed %s" % acistr)
|
||||
self.action = bindperms.group(1)
|
||||
self.permissions = self._unique_list(
|
||||
bindperms.group(2).replace(' ','').split(',')
|
||||
)
|
||||
self.permissions = bindperms.group(2).replace(' ','').split(',')
|
||||
self.set_bindrule(bindperms.group(3))
|
||||
|
||||
def validate(self):
|
||||
@@ -181,7 +165,7 @@ class ACI:
|
||||
raise SyntaxError("invalid permission: '%s'" % p)
|
||||
if not self.name:
|
||||
raise SyntaxError("name must be set")
|
||||
if not isinstance(self.name, str):
|
||||
if not isinstance(self.name, six.string_types):
|
||||
raise SyntaxError("name must be a string")
|
||||
if not isinstance(self.target, dict) or len(self.target) == 0:
|
||||
raise SyntaxError("target must be a non-empty dictionary")
|
||||
@@ -191,11 +175,6 @@ class ACI:
|
||||
raise SyntaxError("bindrule is missing a component")
|
||||
return True
|
||||
|
||||
def set_permissions(self, permissions):
|
||||
if type(permissions) not in (tuple, list):
|
||||
permissions = [permissions]
|
||||
self.permissions = self._unique_list(permissions)
|
||||
|
||||
def set_target_filter(self, filter, operator="="):
|
||||
self.target['targetfilter'] = {}
|
||||
if not filter.startswith("("):
|
||||
@@ -211,7 +190,7 @@ class ACI:
|
||||
if type(attr) not in (tuple, list):
|
||||
attr = [attr]
|
||||
self.target['targetattr'] = {}
|
||||
self.target['targetattr']['expression'] = self._unique_list(attr)
|
||||
self.target['targetattr']['expression'] = attr
|
||||
self.target['targetattr']['operator'] = operator
|
||||
|
||||
def set_target(self, target, operator="="):
|
||||
|
||||
@@ -23,11 +23,13 @@ Foundational classes and functions.
|
||||
|
||||
import re
|
||||
|
||||
import six
|
||||
|
||||
from ipalib.constants import NAME_REGEX, NAME_ERROR
|
||||
from ipalib.constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
|
||||
|
||||
|
||||
class ReadOnly:
|
||||
class ReadOnly(object):
|
||||
"""
|
||||
Base class for classes that can be locked into a read-only state.
|
||||
|
||||
@@ -267,7 +269,7 @@ class NameSpace(ReadOnly):
|
||||
through a dictionary interface. For example, say we create a `NameSpace`
|
||||
instance from a list containing a single member, like this:
|
||||
|
||||
>>> class my_member:
|
||||
>>> class my_member(object):
|
||||
... name = 'my_name'
|
||||
...
|
||||
>>> namespace = NameSpace([my_member])
|
||||
@@ -285,7 +287,7 @@ class NameSpace(ReadOnly):
|
||||
For a more detailed example, say we create a `NameSpace` instance from a
|
||||
generator like this:
|
||||
|
||||
>>> class Member:
|
||||
>>> class Member(object):
|
||||
... def __init__(self, i):
|
||||
... self.i = i
|
||||
... self.name = self.__name__ = 'member%d' % i
|
||||
@@ -466,7 +468,7 @@ class NameSpace(ReadOnly):
|
||||
:param key: The name or index of a member, or a slice object.
|
||||
"""
|
||||
key = getattr(key, '__name__', key)
|
||||
if isinstance(key, str):
|
||||
if isinstance(key, six.string_types):
|
||||
return self.__map[key]
|
||||
if type(key) in (int, slice):
|
||||
return self.__members[key]
|
||||
|
||||
@@ -23,7 +23,6 @@ Functionality for Command Line Interface.
|
||||
from __future__ import print_function
|
||||
|
||||
import atexit
|
||||
import builtins
|
||||
import importlib
|
||||
import logging
|
||||
import textwrap
|
||||
@@ -49,11 +48,14 @@ import six
|
||||
from six.moves import input
|
||||
|
||||
from ipalib.util import (
|
||||
check_client_configuration, get_pager, get_terminal_height, open_in_pager
|
||||
check_client_configuration, get_terminal_height, open_in_pager
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
import builtins # pylint: disable=import-error
|
||||
else:
|
||||
import __builtin__ as builtins # pylint: disable=import-error
|
||||
|
||||
if six.PY2:
|
||||
reload(sys) # pylint: disable=reload-builtin, undefined-variable
|
||||
@@ -309,7 +311,7 @@ class textui(backend.Backend):
|
||||
objectClass: top
|
||||
objectClass: someClass
|
||||
"""
|
||||
assert isinstance(attr, str)
|
||||
assert isinstance(attr, six.string_types)
|
||||
if not isinstance(value, (list, tuple)):
|
||||
# single-value attribute
|
||||
self.print_indented(format % (attr, self.encode_binary(value)), indent)
|
||||
@@ -448,7 +450,7 @@ class textui(backend.Backend):
|
||||
------------------
|
||||
Only dashed above.
|
||||
"""
|
||||
assert isinstance(dash, str)
|
||||
assert isinstance(dash, six.string_types)
|
||||
assert len(dash) == 1
|
||||
dashes = dash * len(string)
|
||||
if above:
|
||||
@@ -702,7 +704,7 @@ class help(frontend.Local):
|
||||
"""
|
||||
Display help for a command or topic.
|
||||
"""
|
||||
class Writer:
|
||||
class Writer(object):
|
||||
"""
|
||||
Writer abstraction
|
||||
"""
|
||||
@@ -721,11 +723,9 @@ class help(frontend.Local):
|
||||
self.buffer.append(unicode(string))
|
||||
|
||||
def write(self):
|
||||
pager = get_pager()
|
||||
|
||||
if pager and self.buffer_length > get_terminal_height():
|
||||
if self.buffer_length > get_terminal_height():
|
||||
data = "\n".join(self.buffer).encode("utf-8")
|
||||
open_in_pager(data, pager)
|
||||
open_in_pager(data)
|
||||
else:
|
||||
try:
|
||||
for line in self.buffer:
|
||||
@@ -949,14 +949,6 @@ class show_mappings(frontend.Command):
|
||||
print(to_cli(item[0]).ljust(mcl)+' : '+item[1])
|
||||
|
||||
|
||||
class IPACompleter(rlcompleter.Completer):
|
||||
def _callable_postfix(self, val, word):
|
||||
# Don't add '(' postfix for callable API objects
|
||||
if isinstance(val, (plugable.APINameSpace, plugable.API)):
|
||||
return word
|
||||
return super()._callable_postfix(val, word)
|
||||
|
||||
|
||||
class console(frontend.Command):
|
||||
"""Start the IPA interactive Python console, or run a script.
|
||||
|
||||
@@ -972,7 +964,7 @@ class console(frontend.Command):
|
||||
def _setup_tab_completion(self, local):
|
||||
readline.parse_and_bind("tab: complete")
|
||||
# completer with custom locals
|
||||
readline.set_completer(IPACompleter(local).complete)
|
||||
readline.set_completer(rlcompleter.Completer(local).complete)
|
||||
# load history
|
||||
history = os.path.join(api.env.dot_ipa, "console.history")
|
||||
try:
|
||||
@@ -1090,7 +1082,7 @@ cli_application_commands = (
|
||||
)
|
||||
|
||||
|
||||
class Collector:
|
||||
class Collector(object):
|
||||
def __init__(self):
|
||||
object.__setattr__(self, '_Collector__options', {})
|
||||
|
||||
@@ -1462,14 +1454,14 @@ def run(api):
|
||||
(_options, argv) = api.bootstrap_with_global_options(context='cli')
|
||||
|
||||
try:
|
||||
check_client_configuration(env=api.env)
|
||||
check_client_configuration()
|
||||
except ScriptError as e:
|
||||
sys.exit(e)
|
||||
|
||||
for klass in cli_plugins:
|
||||
api.add_plugin(klass)
|
||||
api.finalize()
|
||||
if 'config_loaded' not in api.env and 'help' not in argv:
|
||||
if not 'config_loaded' in api.env and not 'help' in argv:
|
||||
raise NotConfiguredError()
|
||||
sys.exit(api.Backend.cli.run(argv))
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -33,10 +33,12 @@ from __future__ import absolute_import
|
||||
import os
|
||||
from os import path
|
||||
import sys
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from configparser import RawConfigParser, ParsingError
|
||||
|
||||
import six
|
||||
# pylint: disable=import-error
|
||||
from six.moves.urllib.parse import urlparse, urlunparse
|
||||
from six.moves.configparser import RawConfigParser, ParsingError
|
||||
# pylint: enable=import-error
|
||||
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipapython.dn import DN
|
||||
@@ -44,15 +46,14 @@ from ipalib.base import check_name
|
||||
from ipalib.constants import (
|
||||
CONFIG_SECTION,
|
||||
OVERRIDE_ERROR, SET_ERROR, DEL_ERROR,
|
||||
TLS_VERSIONS, TLS_VERSION_DEFAULT_MIN, TLS_VERSION_DEFAULT_MAX,
|
||||
TLS_VERSIONS
|
||||
)
|
||||
from ipalib import errors
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
|
||||
class Env:
|
||||
class Env(object):
|
||||
"""
|
||||
Store and retrieve environment variables.
|
||||
|
||||
@@ -251,7 +252,7 @@ class Env:
|
||||
)
|
||||
# pylint: enable=no-member
|
||||
assert not hasattr(self, key)
|
||||
if isinstance(value, str):
|
||||
if isinstance(value, six.string_types):
|
||||
value = value.strip()
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
@@ -450,18 +451,17 @@ class Env:
|
||||
self.site_packages = path.dirname(self.ipalib)
|
||||
self.script = path.abspath(sys.argv[0])
|
||||
self.bin = path.dirname(self.script)
|
||||
home = os.path.expanduser('~')
|
||||
self.home = home if not home.startswith('~') else None
|
||||
self.home = os.environ.get('HOME', None)
|
||||
self.fips_mode = tasks.is_fips_enabled()
|
||||
|
||||
# Merge in overrides:
|
||||
self._merge(**overrides)
|
||||
|
||||
# Determine if running in source tree. The root directory of
|
||||
# IPA source directory contains ipasetup.py.in.
|
||||
# Determine if running in source tree:
|
||||
if 'in_tree' not in self:
|
||||
self.in_tree = os.path.isfile(
|
||||
os.path.join(self.site_packages, "ipasetup.py.in")
|
||||
self.in_tree = (
|
||||
self.bin == self.site_packages
|
||||
and path.isfile(path.join(self.bin, 'setup.py'))
|
||||
)
|
||||
if self.in_tree and 'mode' not in self:
|
||||
self.mode = 'developer'
|
||||
@@ -561,8 +561,6 @@ class Env:
|
||||
|
||||
# Merge in context config file and then default config file:
|
||||
mode = self.__d.get('mode') # pylint: disable=no-member
|
||||
# documented public modes: production, developer
|
||||
# internal modes: dummy, unit_test
|
||||
if mode != 'dummy':
|
||||
self._merge_from_file(self.conf)
|
||||
self._merge_from_file(self.conf_default)
|
||||
@@ -634,30 +632,20 @@ class Env:
|
||||
|
||||
# set the best known TLS version if min/max versions are not set
|
||||
if 'tls_version_min' not in self:
|
||||
self.tls_version_min = TLS_VERSION_DEFAULT_MIN
|
||||
if (
|
||||
self.tls_version_min is not None and
|
||||
self.tls_version_min not in TLS_VERSIONS
|
||||
):
|
||||
self.tls_version_min = TLS_VERSIONS[-1]
|
||||
elif self.tls_version_min not in TLS_VERSIONS:
|
||||
raise errors.EnvironmentError(
|
||||
"Unknown TLS version '{ver}' set in tls_version_min."
|
||||
.format(ver=self.tls_version_min))
|
||||
|
||||
if 'tls_version_max' not in self:
|
||||
self.tls_version_max = TLS_VERSION_DEFAULT_MAX
|
||||
if (
|
||||
self.tls_version_max is not None and
|
||||
self.tls_version_max not in TLS_VERSIONS
|
||||
):
|
||||
self.tls_version_max = TLS_VERSIONS[-1]
|
||||
elif self.tls_version_max not in TLS_VERSIONS:
|
||||
raise errors.EnvironmentError(
|
||||
"Unknown TLS version '{ver}' set in tls_version_max."
|
||||
.format(ver=self.tls_version_max))
|
||||
|
||||
if (
|
||||
self.tls_version_min is not None and
|
||||
self.tls_version_max is not None and
|
||||
self.tls_version_max < self.tls_version_min
|
||||
):
|
||||
if self.tls_version_max < self.tls_version_min:
|
||||
raise errors.EnvironmentError(
|
||||
"tls_version_min is set to a higher TLS version than "
|
||||
"tls_version_max.")
|
||||
|
||||
@@ -35,26 +35,6 @@ except Exception:
|
||||
except Exception:
|
||||
FQDN = None
|
||||
|
||||
# TLS related constants
|
||||
# * SSL2 and SSL3 are broken.
|
||||
# * TLS1.0 and TLS1.1 are no longer state of the art.
|
||||
# * TLS1.2 and 1.3 are secure and working properly
|
||||
# * Crypto policies restrict TLS range to 1.2 and 1.3. Python 3.6 cannot
|
||||
# override the crypto policy.
|
||||
|
||||
TLS_VERSIONS = [
|
||||
"ssl2",
|
||||
"ssl3",
|
||||
"tls1.0",
|
||||
"tls1.1",
|
||||
"tls1.2",
|
||||
"tls1.3",
|
||||
]
|
||||
TLS_VERSION_MINIMAL = "tls1.2"
|
||||
TLS_VERSION_MAXIMAL = "tls1.3"
|
||||
TLS_VERSION_DEFAULT_MIN = None
|
||||
TLS_VERSION_DEFAULT_MAX = None
|
||||
|
||||
# regular expression NameSpace member names must match:
|
||||
NAME_REGEX = r'^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'
|
||||
|
||||
@@ -149,8 +129,6 @@ DEFAULT_CONFIG = (
|
||||
('container_sysaccounts', DN(('cn', 'sysaccounts'), ('cn', 'etc'))),
|
||||
('container_certmap', DN(('cn', 'certmap'))),
|
||||
('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'))),
|
||||
('container_ca_renewal',
|
||||
DN(('cn', 'ca_renewal'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
|
||||
# Ports, hosts, and URIs:
|
||||
# Following values do not have any reasonable default.
|
||||
@@ -164,19 +142,15 @@ DEFAULT_CONFIG = (
|
||||
('rpc_protocol', 'jsonrpc'),
|
||||
|
||||
# Define an inclusive range of SSL/TLS version support
|
||||
('tls_version_min', TLS_VERSION_DEFAULT_MIN),
|
||||
('tls_version_max', TLS_VERSION_DEFAULT_MAX),
|
||||
('tls_version_min', 'tls1.0'),
|
||||
('tls_version_max', 'tls1.2'),
|
||||
|
||||
# Time to wait for a service to start, in seconds.
|
||||
# Note that systemd has a DefaultTimeoutStartSec of 90 seconds. Higher
|
||||
# values are not effective unless systemd is reconfigured, too.
|
||||
('startup_timeout', 120),
|
||||
# Time to wait for a service to start, in seconds
|
||||
('startup_timeout', 300),
|
||||
# How long http connection should wait for reply [seconds].
|
||||
('http_timeout', 30),
|
||||
# How long to wait for an entry to appear on a replica
|
||||
('replication_wait_timeout', 300),
|
||||
# How long to wait for a certmonger request to finish
|
||||
('certmonger_wait_timeout', 300),
|
||||
|
||||
# Web Application mount points
|
||||
('mount_ipa', '/ipa/'),
|
||||
@@ -251,7 +225,7 @@ DEFAULT_CONFIG = (
|
||||
('site_packages', object), # The directory contaning ipalib
|
||||
('script', object), # sys.argv[0]
|
||||
('bin', object), # The directory containing the script
|
||||
('home', object), # os.path.expanduser('~')
|
||||
('home', object), # $HOME
|
||||
|
||||
# Vars set in Env._bootstrap():
|
||||
('in_tree', object), # Whether or not running in-tree (bool)
|
||||
@@ -310,17 +284,9 @@ IPA_CA_RECORD = "ipa-ca"
|
||||
IPA_CA_NICKNAME = 'caSigningCert cert-pki-ca'
|
||||
RENEWAL_CA_NAME = 'dogtag-ipa-ca-renew-agent'
|
||||
RENEWAL_REUSE_CA_NAME = 'dogtag-ipa-ca-renew-agent-reuse'
|
||||
RENEWAL_SELFSIGNED_CA_NAME = 'dogtag-ipa-ca-renew-agent-selfsigned'
|
||||
# The RA agent cert is used for client cert authentication. In the past IPA
|
||||
# used caServerCert profile, which adds clientAuth and serverAuth EKU. The
|
||||
# serverAuth EKU caused trouble with NamedConstraints, see RHBZ#1670239.
|
||||
RA_AGENT_PROFILE = 'caSubsystemCert'
|
||||
# How long dbus clients should wait for CA certificate RPCs [seconds]
|
||||
CA_DBUS_TIMEOUT = 120
|
||||
|
||||
# Maximum hostname length in Linux
|
||||
MAXHOSTNAMELEN = 64
|
||||
|
||||
# regexp definitions
|
||||
PATTERN_GROUPUSER_NAME = (
|
||||
'(?!^[0-9]+$)^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$'
|
||||
@@ -333,10 +299,27 @@ ANON_USER = 'WELLKNOWN/ANONYMOUS'
|
||||
IPAAPI_USER = 'ipaapi'
|
||||
IPAAPI_GROUP = 'ipaapi'
|
||||
|
||||
# TLS related constants
|
||||
TLS_VERSIONS = [
|
||||
"ssl2",
|
||||
"ssl3",
|
||||
"tls1.0",
|
||||
"tls1.1",
|
||||
"tls1.2"
|
||||
]
|
||||
TLS_VERSION_MINIMAL = "tls1.0"
|
||||
|
||||
|
||||
# Use cache path
|
||||
USER_CACHE_PATH = (
|
||||
os.environ.get('XDG_CACHE_HOME') or
|
||||
os.path.expanduser('~/.cache')
|
||||
os.path.join(
|
||||
os.environ.get(
|
||||
'HOME',
|
||||
os.path.expanduser('~')
|
||||
),
|
||||
'.cache'
|
||||
)
|
||||
)
|
||||
|
||||
SOFTHSM_DNSSEC_TOKEN_LABEL = u'ipaDNSSEC'
|
||||
|
||||
@@ -55,7 +55,7 @@ def get_extra_rrtype(name):
|
||||
|
||||
|
||||
def has_cli_options(cmd, options, no_option_msg, allow_empty_attrs=False):
|
||||
sufficient = ('setattr', 'addattr', 'delattr', 'rename', 'dnsttl')
|
||||
sufficient = ('setattr', 'addattr', 'delattr', 'rename')
|
||||
if any(k in options for k in sufficient):
|
||||
return
|
||||
|
||||
|
||||
@@ -1462,15 +1462,6 @@ class CSRTemplateError(ExecutionError):
|
||||
format = _('%(reason)s')
|
||||
|
||||
|
||||
class AlreadyContainsValueError(ExecutionError):
|
||||
"""
|
||||
**4038** Raised when BaseLDAPAddAttribute operation fails because one
|
||||
or more new values are already present.
|
||||
"""
|
||||
errno = 4038
|
||||
format = _("'%(attr)s' already contains one or more values")
|
||||
|
||||
|
||||
class BuiltinError(ExecutionError):
|
||||
"""
|
||||
**4100** Base class for builtin execution errors (*4100 - 4199*).
|
||||
@@ -2016,7 +2007,5 @@ class GenericError(PublicError):
|
||||
public_errors = tuple(sorted(
|
||||
messages.iter_messages(globals(), PublicError), key=lambda E: E.errno))
|
||||
|
||||
errors_by_code = dict((e.errno, e) for e in public_errors)
|
||||
|
||||
if __name__ == '__main__':
|
||||
messages.print_report('public errors', public_errors)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Facts about the installation
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from . import sysrestore
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Used to determine install status
|
||||
IPA_MODULES = [
|
||||
'httpd', 'kadmin', 'dirsrv', 'pki-tomcatd', 'install', 'krb5kdc', 'named']
|
||||
|
||||
|
||||
def is_ipa_configured():
|
||||
"""
|
||||
Use the state to determine if IPA has been configured.
|
||||
"""
|
||||
sstore = sysrestore.StateFile(paths.SYSRESTORE)
|
||||
if sstore.has_state('installation'):
|
||||
return sstore.get_state('installation', 'complete')
|
||||
|
||||
# Fall back to older method in case this is an existing installation
|
||||
|
||||
installed = False
|
||||
|
||||
fstore = sysrestore.FileStore(paths.SYSRESTORE)
|
||||
|
||||
for module in IPA_MODULES:
|
||||
if sstore.has_state(module):
|
||||
logger.debug('%s is configured', module)
|
||||
installed = True
|
||||
else:
|
||||
logger.debug('%s is not configured', module)
|
||||
|
||||
if fstore.has_files():
|
||||
logger.debug('filestore has files')
|
||||
installed = True
|
||||
else:
|
||||
logger.debug('filestore is tracking no files')
|
||||
|
||||
return installed
|
||||
|
||||
|
||||
def is_ipa_client_configured(on_master=False):
|
||||
"""
|
||||
Consider IPA client not installed if nothing is backed up
|
||||
and default.conf file does not exist. If on_master is set to True,
|
||||
the existence of default.conf file is not taken into consideration,
|
||||
since it has been already created by ipa-server-install.
|
||||
"""
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
installed = statestore.get_state('installation', 'complete')
|
||||
if installed is not None:
|
||||
return installed
|
||||
|
||||
# Fall back to the old detection
|
||||
|
||||
installed = (
|
||||
fstore.has_files() or (
|
||||
not on_master and os.path.exists(paths.IPA_DEFAULT_CONF)
|
||||
)
|
||||
)
|
||||
|
||||
return installed
|
||||
@@ -29,7 +29,6 @@ from ipapython.ipautil import APIVersion
|
||||
from ipalib.base import NameSpace
|
||||
from ipalib.plugable import Plugin, APINameSpace
|
||||
from ipalib.parameters import create_param, Param, Str, Flag
|
||||
from ipalib.parameters import create_signature
|
||||
from ipalib.parameters import Password # pylint: disable=unused-import
|
||||
from ipalib.output import Output, Entry, ListOfEntries
|
||||
from ipalib.text import _
|
||||
@@ -38,7 +37,7 @@ from ipalib.errors import (ZeroArgumentError, MaxArgumentError, OverlapError,
|
||||
ValidationError, ConversionError)
|
||||
from ipalib import errors, messages
|
||||
from ipalib.request import context, context_frame
|
||||
from ipalib.util import classproperty, classobjectproperty, json_serialize
|
||||
from ipalib.util import classproperty, json_serialize
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
@@ -434,26 +433,6 @@ class Command(HasParam):
|
||||
|
||||
topic = classproperty(__topic_getter)
|
||||
|
||||
@classobjectproperty
|
||||
@classmethod
|
||||
def __signature__(cls, obj):
|
||||
# signature is cached on the class object
|
||||
if hasattr(cls, "_signature"):
|
||||
return cls._signature
|
||||
# can only create signature for 'final' classes
|
||||
# help(api.Command.user_show) breaks because pydoc inspects parent
|
||||
# classes and baseuser plugin is not a registered object.
|
||||
if cls.__subclasses__():
|
||||
cls._signature = None
|
||||
return None
|
||||
# special, rare case: user calls help() on a plugin class instead of
|
||||
# an instance
|
||||
if obj is None:
|
||||
from ipalib import api
|
||||
obj = cls(api=api)
|
||||
cls._signature = signature = create_signature(obj)
|
||||
return signature
|
||||
|
||||
@property
|
||||
def forwarded_name(self):
|
||||
return self.full_name
|
||||
@@ -596,7 +575,7 @@ class Command(HasParam):
|
||||
if self.params[name].attribute and name in kw:
|
||||
value = kw[name]
|
||||
if isinstance(value, tuple):
|
||||
yield (name, list(value))
|
||||
yield (name, [v for v in value])
|
||||
else:
|
||||
yield (name, kw[name])
|
||||
|
||||
@@ -1090,14 +1069,15 @@ class Command(HasParam):
|
||||
|
||||
if o == 'value':
|
||||
continue
|
||||
if o.lower() == 'count' and result == 0:
|
||||
elif o.lower() == 'count' and result == 0:
|
||||
rv = 1
|
||||
elif o.lower() == 'failed':
|
||||
if entry_count(result) == 0:
|
||||
# Don't display an empty failed list
|
||||
continue
|
||||
# Return an error to the shell
|
||||
rv = 1
|
||||
else:
|
||||
# Return an error to the shell
|
||||
rv = 1
|
||||
if isinstance(outp, ListOfEntries):
|
||||
textui.print_entries(result, order, labels, flags, print_all)
|
||||
elif isinstance(result, (tuple, list)):
|
||||
@@ -1319,7 +1299,7 @@ class Object(HasParam):
|
||||
)
|
||||
if self.primary_key:
|
||||
json_dict['primary_key'] = self.primary_key.name
|
||||
json_dict['methods'] = list(self.methods)
|
||||
json_dict['methods'] = [m for m in self.methods]
|
||||
return json_dict
|
||||
|
||||
|
||||
|
||||
@@ -46,39 +46,8 @@ DBUS_CM_REQUEST_IF = 'org.fedorahosted.certmonger.request'
|
||||
DBUS_CM_CA_IF = 'org.fedorahosted.certmonger.ca'
|
||||
DBUS_PROPERTY_IF = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
# These properties, if encountered in search criteria, result in a
|
||||
# subset test instead of equality test.
|
||||
ARRAY_PROPERTIES = ['template-hostname']
|
||||
|
||||
|
||||
"""
|
||||
Certmonger helper routines.
|
||||
|
||||
Search criteria
|
||||
---------------
|
||||
|
||||
Functions that look up requests take a ``dict`` of search criteria.
|
||||
In general, the key is a name of a property in the request property
|
||||
interface. But there are some special cases with different
|
||||
behaviour:
|
||||
|
||||
``nickname``
|
||||
a.k.a. "request ID". If given, only the specified request is
|
||||
retrieved (if it exists), and it is still tested against other
|
||||
criteria.
|
||||
|
||||
``ca-name``
|
||||
Test equality against the nickname of the CA (a.k.a. request
|
||||
helper) object for the request.
|
||||
|
||||
``template-hostname``
|
||||
Must be an iterable of DNS names. Tests that the given values
|
||||
are a subset of the values defined on the Certmonger request.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class _cm_dbus_object:
|
||||
class _cm_dbus_object(object):
|
||||
"""
|
||||
Auxiliary class for convenient DBus object handling.
|
||||
"""
|
||||
@@ -187,12 +156,9 @@ class _certmonger(_cm_dbus_object):
|
||||
DBUS_CM_IF)
|
||||
|
||||
|
||||
def _get_requests(criteria):
|
||||
def _get_requests(criteria=dict()):
|
||||
"""
|
||||
Get all requests that matches the provided criteria.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
|
||||
"""
|
||||
if not isinstance(criteria, dict):
|
||||
raise TypeError('"criteria" must be dict.')
|
||||
@@ -213,23 +179,13 @@ def _get_requests(criteria):
|
||||
for criterion in criteria:
|
||||
if criterion == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
if ca_path is None:
|
||||
raise RuntimeError("certmonger CA '%s' is not defined" %
|
||||
criteria.get('ca-name'))
|
||||
ca = _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
if criteria[criterion] != ca.obj_if.get_nickname():
|
||||
break
|
||||
elif criterion in ARRAY_PROPERTIES:
|
||||
# perform subset test
|
||||
expect = set(criteria[criterion])
|
||||
got = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if not expect.issubset(got):
|
||||
break
|
||||
value = ca.obj_if.get_nickname()
|
||||
else:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if criteria[criterion] != value:
|
||||
break
|
||||
if value != criteria[criterion]:
|
||||
break
|
||||
else:
|
||||
requests.append(request)
|
||||
|
||||
@@ -238,11 +194,11 @@ def _get_requests(criteria):
|
||||
|
||||
def _get_request(criteria):
|
||||
"""
|
||||
Find request that matches criteria. Return ``None`` if no match.
|
||||
Raise ``RuntimeError`` if there is more than one matching request.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
|
||||
Find request that matches criteria.
|
||||
If 'nickname' is specified other criteria are ignored because 'nickname'
|
||||
uniquely identify single request.
|
||||
When multiple or none request matches specified criteria RuntimeError is
|
||||
raised.
|
||||
"""
|
||||
requests = _get_requests(criteria)
|
||||
if len(requests) == 0:
|
||||
@@ -280,11 +236,11 @@ def get_request_id(criteria):
|
||||
If you don't know the certmonger request_id then try to find it by looking
|
||||
through all the requests.
|
||||
|
||||
Return ``None`` if no match. Raise ``RuntimeError`` if there is
|
||||
more than one matching request.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
criteria is a tuple of key/value to search for. The more specific
|
||||
the better. An error is raised if multiple request_ids are returned for
|
||||
the same criteria.
|
||||
|
||||
None is returned if none of the criteria match.
|
||||
"""
|
||||
try:
|
||||
request = _get_request(criteria)
|
||||
@@ -350,7 +306,7 @@ def request_and_wait_for_cert(
|
||||
certpath, subject, principal, nickname=None, passwd_fname=None,
|
||||
dns=None, ca='IPA', profile=None,
|
||||
pre_command=None, post_command=None, storage='NSSDB', perms=None,
|
||||
resubmit_timeout=0, stop_tracking_on_error=False):
|
||||
resubmit_timeout=0):
|
||||
"""Request certificate, wait and possibly resubmit failing requests
|
||||
|
||||
Submit a cert request to certmonger and wait until the request has
|
||||
@@ -370,11 +326,7 @@ def request_and_wait_for_cert(
|
||||
|
||||
deadline = time.time() + resubmit_timeout
|
||||
while True: # until success, timeout, or error
|
||||
try:
|
||||
state = wait_for_request(req_id, api.env.http_timeout)
|
||||
except RuntimeError as e:
|
||||
logger.debug("wait_for_request raised %s", e)
|
||||
state = 'TIMEOUT'
|
||||
state = wait_for_request(req_id, api.env.replication_wait_timeout)
|
||||
ca_error = get_request_value(req_id, 'ca-error')
|
||||
if state == 'MONITORING' and ca_error is None:
|
||||
# we got a winner, exiting
|
||||
@@ -384,28 +336,22 @@ def request_and_wait_for_cert(
|
||||
logger.debug(
|
||||
"Cert request %s failed: %s (%s)", req_id, state, ca_error
|
||||
)
|
||||
if state in {'CA_REJECTED', 'CA_UNREACHABLE'}:
|
||||
if state not in {'CA_REJECTED', 'CA_UNREACHABLE'}:
|
||||
# probably unrecoverable error
|
||||
logger.debug("Giving up on cert request %s", req_id)
|
||||
break
|
||||
if not resubmit_timeout:
|
||||
elif not resubmit_timeout:
|
||||
# no resubmit
|
||||
break
|
||||
if time.time() > deadline:
|
||||
logger.debug("Request %s reached resubmit deadline", req_id)
|
||||
elif time.time() > deadline:
|
||||
logger.debug("Request %s reached resubmit dead line", req_id)
|
||||
break
|
||||
if state == 'TIMEOUT':
|
||||
logger.debug("%s not in final state, continue waiting", req_id)
|
||||
time.sleep(10)
|
||||
else:
|
||||
# sleep and resubmit
|
||||
logger.debug("Sleep and resubmit cert request %s", req_id)
|
||||
time.sleep(10)
|
||||
resubmit_request(req_id)
|
||||
|
||||
if stop_tracking_on_error:
|
||||
stop_tracking(request_id=req_id)
|
||||
|
||||
raise RuntimeError(
|
||||
"Certificate issuance failed ({}: {})".format(state, ca_error)
|
||||
)
|
||||
@@ -481,8 +427,7 @@ def request_cert(
|
||||
|
||||
def start_tracking(
|
||||
certpath, ca='IPA', nickname=None, pin=None, pinfile=None,
|
||||
pre_command=None, post_command=None, profile=None, storage="NSSDB",
|
||||
token_name=None, dns=None):
|
||||
pre_command=None, post_command=None, profile=None, storage="NSSDB"):
|
||||
"""
|
||||
Tell certmonger to track the given certificate in either a file or an NSS
|
||||
database. The certificate access can be protected by a password_file.
|
||||
@@ -515,10 +460,6 @@ def start_tracking(
|
||||
NSS or OpenSSL backend to track the certificate in ``certpath``
|
||||
:param profile:
|
||||
Which certificate profile should be used.
|
||||
:param token_name:
|
||||
Hardware token name for HSM support
|
||||
:param dns:
|
||||
List of DNS names
|
||||
:returns: certificate tracking nickname.
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
@@ -559,12 +500,6 @@ def start_tracking(
|
||||
params['cert-postsave-command'] = post_command
|
||||
if profile:
|
||||
params['ca-profile'] = profile
|
||||
if token_name not in {None, "internal"}:
|
||||
# only pass token names for external tokens (e.g. HSM)
|
||||
params['key-token'] = token_name
|
||||
params['cert-token'] = token_name
|
||||
if dns is not None and len(dns) > 0:
|
||||
params['DNS'] = dns
|
||||
|
||||
result = cm.obj_if.add_request(params)
|
||||
try:
|
||||
@@ -655,18 +590,6 @@ def resubmit_request(
|
||||
update['template-is-ca'] = True
|
||||
update['template-ca-path-length'] = -1 # no path length
|
||||
|
||||
# TODO: certmonger assumes some hard-coded defaults like RSA 2048.
|
||||
# Try to fetch values from current cert rather.
|
||||
for key, convert in [('key-size', int), ('key-type', str)]:
|
||||
try:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, key)
|
||||
except dbus.DBusException:
|
||||
continue
|
||||
else:
|
||||
if value:
|
||||
# convert dbus.Int64() to int, dbus.String() to str
|
||||
update[key] = convert(value)
|
||||
|
||||
if len(update) > 0:
|
||||
request.obj_if.modify(update)
|
||||
request.obj_if.resubmit()
|
||||
@@ -740,7 +663,7 @@ def modify_ca_helper(ca_name, helper):
|
||||
return old_helper
|
||||
|
||||
|
||||
def get_pin(token="internal"):
|
||||
def get_pin(token):
|
||||
"""
|
||||
Dogtag stores its NSS pin in a file formatted as token:PIN.
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
"""DNS forwarder and systemd-resolve1 helpers
|
||||
"""
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
import dbus
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dnsutil import get_ipa_resolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_SYSTEMD_RESOLV_CONF = {
|
||||
"/run/systemd/resolve/stub-resolv.conf",
|
||||
"/run/systemd/resolve/resolv.conf",
|
||||
"/lib/systemd/resolv.conf",
|
||||
"/usr/lib/systemd/resolv.conf",
|
||||
}
|
||||
|
||||
_DBUS_RESOLVE1_NAME = "org.freedesktop.resolve1"
|
||||
_DBUS_RESOLVE1_PATH = "/org/freedesktop/resolve1"
|
||||
_DBUS_RESOLVE1_MANAGER_IF = "org.freedesktop.resolve1.Manager"
|
||||
_DBUS_PROPERTY_IF = "org.freedesktop.DBus.Properties"
|
||||
|
||||
# netlink interface index for resolve1 global settings and loopback
|
||||
IFINDEX_GLOBAL = 0
|
||||
IFINDEX_LOOPBACK = 1
|
||||
|
||||
|
||||
def detect_resolve1_resolv_conf():
|
||||
"""Detect if /etc/resolv.conf is managed by systemd-resolved
|
||||
|
||||
See man(5) NetworkManager.conf
|
||||
"""
|
||||
try:
|
||||
dest = os.readlink(paths.RESOLV_CONF)
|
||||
except OSError:
|
||||
# not a link
|
||||
return False
|
||||
# convert path relative to /etc/resolv.conf to abs path
|
||||
dest = os.path.normpath(
|
||||
os.path.join(os.path.dirname(paths.RESOLV_CONF), dest)
|
||||
)
|
||||
return dest in _SYSTEMD_RESOLV_CONF
|
||||
|
||||
|
||||
def get_resolve1_nameservers(*, with_ifindex=False):
|
||||
"""Get list of DNS nameservers from systemd-resolved
|
||||
|
||||
:return: list of tuples (ifindex, ipaddress_obj)
|
||||
"""
|
||||
bus = dbus.SystemBus()
|
||||
try:
|
||||
resolve1 = bus.get_object(_DBUS_RESOLVE1_NAME, _DBUS_RESOLVE1_PATH)
|
||||
prop_if = dbus.Interface(resolve1, _DBUS_PROPERTY_IF)
|
||||
dns_prop = prop_if.Get(_DBUS_RESOLVE1_MANAGER_IF, "DNSEx")
|
||||
finally:
|
||||
bus.close()
|
||||
|
||||
results = []
|
||||
for ifindex, af, dns_arr, port, sniname in dns_prop:
|
||||
if port not in {0, 53} or sniname:
|
||||
# non-default port, non-standard port, or SNI name configuration
|
||||
# for DNS over TLS, e.g. 1.2.3.4:9953#example.com
|
||||
continue
|
||||
# convert packed format to IPAddress object (like inet_ntop)
|
||||
if af == socket.AF_INET:
|
||||
dnsip = ipaddress.IPv4Address(bytes(dns_arr))
|
||||
elif af == socket.AF_INET6:
|
||||
dnsip = ipaddress.IPv6Address(bytes(dns_arr))
|
||||
else:
|
||||
# neither IPv4 nor IPv6
|
||||
continue
|
||||
if with_ifindex:
|
||||
# netlink interface index, see socket.if_nameindex()
|
||||
ifindex = int(ifindex)
|
||||
results.append((ifindex, dnsip))
|
||||
else:
|
||||
results.append(dnsip)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_dnspython_nameservers(*, with_ifindex=False):
|
||||
"""Get list of DNS nameservers from dnspython
|
||||
|
||||
On Linux dnspython parses /etc/resolv.conf for us
|
||||
|
||||
:return: list of tuples (ifindex, ipaddress_obj)
|
||||
"""
|
||||
results = []
|
||||
for nameserver in get_ipa_resolver().nameservers:
|
||||
nameserver = ipaddress.ip_address(nameserver)
|
||||
if with_ifindex:
|
||||
results.append((IFINDEX_GLOBAL, nameserver))
|
||||
else:
|
||||
results.append(nameserver)
|
||||
return results
|
||||
|
||||
|
||||
def get_nameservers():
|
||||
"""Get list of unique, non-loopback DNS nameservers
|
||||
|
||||
:return: list of strings
|
||||
"""
|
||||
if detect_resolve1_resolv_conf():
|
||||
logger.debug(
|
||||
"systemd-resolved detected, fetching nameservers from D-Bus"
|
||||
)
|
||||
nameservers = get_resolve1_nameservers(with_ifindex=True)
|
||||
else:
|
||||
logger.debug(
|
||||
"systemd-resolved not detected, parsing %s", paths.RESOLV_CONF
|
||||
)
|
||||
nameservers = get_dnspython_nameservers(with_ifindex=True)
|
||||
|
||||
logger.debug("Detected nameservers: %r", nameservers)
|
||||
|
||||
result = []
|
||||
seen = set()
|
||||
for ifindex, ip in nameservers:
|
||||
# unique entries
|
||||
if ip in seen:
|
||||
continue
|
||||
seen.add(ip)
|
||||
# skip loopback
|
||||
if ifindex == IFINDEX_LOOPBACK or ip.is_loopback:
|
||||
continue
|
||||
result.append(str(ip))
|
||||
|
||||
logger.debug("Use nameservers %r", result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pprint import pprint
|
||||
|
||||
print("systemd-resolved detected:", detect_resolve1_resolv_conf())
|
||||
print("Interfaces:", socket.if_nameindex())
|
||||
print("dnspython nameservers:")
|
||||
pprint(get_dnspython_nameservers(with_ifindex=True))
|
||||
print("resolve1 nameservers:")
|
||||
try:
|
||||
pprint(get_resolve1_nameservers(with_ifindex=True))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("nameservers:", get_nameservers())
|
||||
@@ -1,21 +1,457 @@
|
||||
# Authors: Mark McLoughlin <markmc@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
# Copyright (C) 2007 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Facade for ipalib.sysrestore for backwards compatibility
|
||||
"""
|
||||
#
|
||||
# This module provides a very simple API which allows
|
||||
# ipa-xxx-install --uninstall to restore certain
|
||||
# parts of the system configuration to the way it was
|
||||
# before ipa-server-install was first run
|
||||
|
||||
from ipalib import sysrestore as real_sysrestore
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
import six
|
||||
# pylint: disable=import-error
|
||||
if six.PY3:
|
||||
# The SafeConfigParser class has been renamed to ConfigParser in Py3
|
||||
from configparser import ConfigParser as SafeConfigParser
|
||||
else:
|
||||
from ConfigParser import SafeConfigParser
|
||||
# pylint: enable=import-error
|
||||
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSRESTORE_PATH = paths.TMP
|
||||
SYSRESTORE_INDEXFILE = "sysrestore.index"
|
||||
SYSRESTORE_STATEFILE = "sysrestore.state"
|
||||
|
||||
|
||||
class FileStore(real_sysrestore.FileStore):
|
||||
def __init__(self, path=real_sysrestore.SYSRESTORE_PATH,
|
||||
index_file=real_sysrestore.SYSRESTORE_INDEXFILE):
|
||||
super(FileStore, self).__init__(path, index_file)
|
||||
class FileStore(object):
|
||||
"""Class for handling backup and restore of files"""
|
||||
|
||||
def __init__(self, path = SYSRESTORE_PATH, index_file = SYSRESTORE_INDEXFILE):
|
||||
"""Create a _StoreFiles object, that uses @path as the
|
||||
base directory.
|
||||
|
||||
The file @path/sysrestore.index is used to store information
|
||||
about the original location of the saved files.
|
||||
"""
|
||||
self._path = path
|
||||
self._index = os.path.join(self._path, index_file)
|
||||
|
||||
self.random = random.Random()
|
||||
|
||||
self.files = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the file list from the index file. @files will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
|
||||
logger.debug("Loading Index file from '%s'", self._index)
|
||||
|
||||
self.files = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._index)
|
||||
|
||||
for section in p.sections():
|
||||
if section == "files":
|
||||
for (key, value) in p.items(section):
|
||||
self.files[key] = value
|
||||
|
||||
|
||||
class StateFile(real_sysrestore.StateFile):
|
||||
def __init__(self, path=real_sysrestore.SYSRESTORE_PATH,
|
||||
state_file=real_sysrestore.SYSRESTORE_STATEFILE):
|
||||
super(StateFile, self).__init__(path, state_file)
|
||||
def save(self):
|
||||
"""Save the file list to @_index. If @files is an empty
|
||||
dict, then @_index should be removed.
|
||||
"""
|
||||
logger.debug("Saving Index File to '%s'", self._index)
|
||||
|
||||
if len(self.files) == 0:
|
||||
logger.debug(" -> no files, removing file")
|
||||
if os.path.exists(self._index):
|
||||
os.remove(self._index)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
p.add_section('files')
|
||||
for (key, value) in self.files.items():
|
||||
p.set('files', key, str(value))
|
||||
|
||||
with open(self._index, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_file(self, path):
|
||||
"""Create a copy of the file at @path - as long as an exact copy
|
||||
does not already exist - which will be restored to its
|
||||
original location by restore_files().
|
||||
"""
|
||||
logger.debug("Backing up system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
if not os.path.isfile(path):
|
||||
logger.debug(" -> Not backing up - '%s' doesn't exist", path)
|
||||
return
|
||||
|
||||
_reldir, backupfile = os.path.split(path)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
cont_hash = sha256(f.read()).hexdigest()
|
||||
|
||||
filename = "{hexhash}-{bcppath}".format(
|
||||
hexhash=cont_hash, bcppath=backupfile)
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if os.path.exists(backup_path):
|
||||
logger.debug(" -> Not backing up - already have a copy of '%s'",
|
||||
path)
|
||||
return
|
||||
|
||||
shutil.copy2(path, backup_path)
|
||||
|
||||
stat = os.stat(path)
|
||||
|
||||
template = '{stat.st_mode},{stat.st_uid},{stat.st_gid},{path}'
|
||||
self.files[filename] = template.format(stat=stat, path=path)
|
||||
self.save()
|
||||
|
||||
def has_file(self, path):
|
||||
"""Checks whether file at @path was added to the file store
|
||||
|
||||
Returns #True if the file exists in the file store, #False otherwise
|
||||
"""
|
||||
result = False
|
||||
for _key, value in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
|
||||
def restore_file(self, path, new_path = None):
|
||||
"""Restore the copy of a file at @path to its original
|
||||
location and delete the copy.
|
||||
|
||||
Takes optional parameter @new_path which specifies the
|
||||
location where the file is to be restored.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if new_path is None:
|
||||
logger.debug("Restoring system configuration file '%s'",
|
||||
path)
|
||||
else:
|
||||
logger.debug("Restoring system configuration file '%s' to '%s'",
|
||||
path, new_path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
if new_path is not None and not os.path.isabs(new_path):
|
||||
raise ValueError("Absolute new path required")
|
||||
|
||||
mode = None
|
||||
uid = None
|
||||
gid = None
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
(mode,uid,gid,filepath) = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
if new_path is not None:
|
||||
path = new_path
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def restore_all_files(self):
|
||||
"""Restore the files in the inbdex to their original
|
||||
location and delete the copy.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if len(self.files) == 0:
|
||||
return False
|
||||
|
||||
for (filename, value) in self.files.items():
|
||||
|
||||
(mode,uid,gid,path) = value.split(',', 3)
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
continue
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
# force file to be deleted
|
||||
self.files = {}
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def has_files(self):
|
||||
"""Return True or False if there are any files in the index
|
||||
|
||||
Can be used to determine if a program is configured.
|
||||
"""
|
||||
|
||||
return len(self.files) > 0
|
||||
|
||||
def untrack_file(self, path):
|
||||
"""Remove file at path @path from list of backed up files.
|
||||
|
||||
Does not remove any files from the filesystem.
|
||||
|
||||
Returns #True if the file was untracked, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
logger.debug("Untracking system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
os.unlink(backup_path)
|
||||
except Exception as e:
|
||||
logger.error('Error removing %s: %s', backup_path, str(e))
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class StateFile(object):
|
||||
"""A metadata file for recording system state which can
|
||||
be backed up and later restored.
|
||||
StateFile gets reloaded every time to prevent loss of information
|
||||
recorded by child processes. But we do not solve concurrency
|
||||
because there is no need for it right now.
|
||||
The format is something like:
|
||||
|
||||
[httpd]
|
||||
running=True
|
||||
enabled=False
|
||||
"""
|
||||
|
||||
def __init__(self, path = SYSRESTORE_PATH, state_file = SYSRESTORE_STATEFILE):
|
||||
"""Create a StateFile object, loading from @path.
|
||||
|
||||
The dictionary @modules, a member of the returned object,
|
||||
is where the state can be modified. @modules is indexed
|
||||
using a module name to return another dictionary containing
|
||||
key/value pairs with the saved state of that module.
|
||||
|
||||
The keys in these latter dictionaries are arbitrary strings
|
||||
and the values may either be strings or booleans.
|
||||
"""
|
||||
self._path = os.path.join(path, state_file)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the modules from the file @_path. @modules will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
logger.debug("Loading StateFile from '%s'", self._path)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._path)
|
||||
|
||||
for module in p.sections():
|
||||
self.modules[module] = {}
|
||||
for (key, value) in p.items(module):
|
||||
if value == str(True):
|
||||
value = True
|
||||
elif value == str(False):
|
||||
value = False
|
||||
self.modules[module][key] = value
|
||||
|
||||
def save(self):
|
||||
"""Save the modules to @_path. If @modules is an empty
|
||||
dict, then @_path should be removed.
|
||||
"""
|
||||
logger.debug("Saving StateFile to '%s'", self._path)
|
||||
|
||||
for module in list(self.modules):
|
||||
if len(self.modules[module]) == 0:
|
||||
del self.modules[module]
|
||||
|
||||
if len(self.modules) == 0:
|
||||
logger.debug(" -> no modules, removing file")
|
||||
if os.path.exists(self._path):
|
||||
os.remove(self._path)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
for module in self.modules:
|
||||
p.add_section(module)
|
||||
for (key, value) in self.modules[module].items():
|
||||
p.set(module, key, str(value))
|
||||
|
||||
with open(self._path, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_state(self, module, key, value):
|
||||
"""Backup an item of system state from @module, identified
|
||||
by the string @key and with the value @value. @value may be
|
||||
a string or boolean.
|
||||
"""
|
||||
if not isinstance(value, (str, bool, unicode)):
|
||||
raise ValueError("Only strings, booleans or unicode strings are supported")
|
||||
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
self.modules[module] = {}
|
||||
|
||||
if key not in self.modules:
|
||||
self.modules[module][key] = value
|
||||
|
||||
self.save()
|
||||
|
||||
def get_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
return None
|
||||
|
||||
return self.modules[module].get(key, None)
|
||||
|
||||
def delete_state(self, module, key):
|
||||
"""Delete system state from @module, identified by the string
|
||||
@key.
|
||||
|
||||
If the item doesn't exist, no change is done.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
try:
|
||||
del self.modules[module][key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.save()
|
||||
|
||||
def restore_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key, and remove it from the backed
|
||||
up system state.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
|
||||
value = self.get_state(module, key)
|
||||
|
||||
if value is not None:
|
||||
self.delete_state(module, key)
|
||||
|
||||
return value
|
||||
|
||||
def has_state(self, module):
|
||||
"""Return True or False if there is any state stored for @module.
|
||||
|
||||
Can be used to determine if a service is configured.
|
||||
"""
|
||||
|
||||
return module in self.modules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: ipalib
|
||||
Version: 4.8.10
|
||||
Version: 4.7.2
|
||||
Summary: FreeIPA common python library
|
||||
Home-page: https://www.freeipa.org/
|
||||
Author: FreeIPA Developers
|
||||
@@ -16,9 +16,10 @@ Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: System Administrators
|
||||
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Classifier: Programming Language :: C
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
@@ -26,5 +27,5 @@ Classifier: Operating System :: Unix
|
||||
Classifier: Topic :: Internet :: Name Service (DNS)
|
||||
Classifier: Topic :: Security
|
||||
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
||||
Requires-Python: >=3.6.0
|
||||
Requires-Python: >=2.7.5,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*
|
||||
Provides-Extra: install
|
||||
|
||||
@@ -9,7 +9,6 @@ constants.py
|
||||
crud.py
|
||||
dns.py
|
||||
errors.py
|
||||
facts.py
|
||||
frontend.py
|
||||
krb_utils.py
|
||||
messages.py
|
||||
@@ -22,15 +21,12 @@ request.py
|
||||
rpc.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
sysrestore.py
|
||||
text.py
|
||||
util.py
|
||||
x509.py
|
||||
../COPYING
|
||||
install/__init__.py
|
||||
install/certmonger.py
|
||||
install/certstore.py
|
||||
install/dnsforwarders.py
|
||||
install/hostname.py
|
||||
install/kinit.py
|
||||
install/service.py
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
ipaplatform==4.8.10
|
||||
ipapython==4.8.10
|
||||
ipaplatform==4.7.2
|
||||
ipapython==4.7.2
|
||||
netaddr
|
||||
pyasn1
|
||||
pyasn1-modules
|
||||
six
|
||||
|
||||
[install]
|
||||
dbus-python
|
||||
ipaplatform
|
||||
|
||||
@@ -50,7 +50,7 @@ def add_message(version, result, message):
|
||||
|
||||
def process_message_arguments(obj, format=None, message=None, **kw):
|
||||
for key, value in kw.items():
|
||||
if not isinstance(value, int):
|
||||
if not isinstance(value, six.integer_types):
|
||||
try:
|
||||
kw[key] = unicode(value)
|
||||
except UnicodeError:
|
||||
@@ -71,7 +71,7 @@ def process_message_arguments(obj, format=None, message=None, **kw):
|
||||
obj.format = format
|
||||
obj.forwarded = False
|
||||
obj.msg = obj.format % kw
|
||||
if isinstance(obj.format, str):
|
||||
if isinstance(obj.format, six.string_types):
|
||||
obj.strerror = ugettext(obj.format) % kw
|
||||
else:
|
||||
obj.strerror = obj.format % kw
|
||||
@@ -487,15 +487,6 @@ class FailedToAddHostDNSRecords(PublicMessage):
|
||||
"%(reason)s")
|
||||
|
||||
|
||||
class LightweightCACertificateNotAvailable(PublicMessage):
|
||||
"""
|
||||
**13031** Certificate is not available
|
||||
"""
|
||||
errno = 13031
|
||||
type = "error"
|
||||
format = _("The certificate for %(ca)s is not available on this server.")
|
||||
|
||||
|
||||
def iter_messages(variables, base):
|
||||
"""Return a tuple with all subclasses
|
||||
"""
|
||||
|
||||
@@ -103,13 +103,12 @@ import re
|
||||
import decimal
|
||||
import base64
|
||||
import datetime
|
||||
import inspect
|
||||
import typing
|
||||
from xmlrpc.client import MAXINT, MININT
|
||||
|
||||
import six
|
||||
# pylint: disable=import-error
|
||||
from six.moves.xmlrpc_client import MAXINT, MININT
|
||||
# pylint: enable=import-error
|
||||
from cryptography import x509 as crypto_x509
|
||||
import dns.name
|
||||
|
||||
from ipalib.text import _ as ugettext
|
||||
from ipalib.base import check_name
|
||||
@@ -133,7 +132,7 @@ from ipapython.dnsutil import DNSName
|
||||
def _is_null(value):
|
||||
if value:
|
||||
return False
|
||||
elif isinstance(value, (int, float, decimal.Decimal)):
|
||||
elif isinstance(value, six.integer_types + (float, decimal.Decimal)):
|
||||
# 0 is not NULL
|
||||
return False
|
||||
else:
|
||||
@@ -416,8 +415,8 @@ class Param(ReadOnly):
|
||||
('cli_name', str, None),
|
||||
('cli_short_name', str, None),
|
||||
('deprecated_cli_aliases', frozenset, frozenset()),
|
||||
('label', (str, Gettext), None),
|
||||
('doc', (str, Gettext), None),
|
||||
('label', (six.string_types, Gettext), None),
|
||||
('doc', (six.string_types, Gettext), None),
|
||||
('required', bool, True),
|
||||
('multivalue', bool, False),
|
||||
('primary_key', bool, False),
|
||||
@@ -560,7 +559,7 @@ class Param(ReadOnly):
|
||||
# Check that all the rules are callable
|
||||
self.class_rules = tuple(class_rules)
|
||||
self.rules = rules
|
||||
if self.query: # pylint: disable=using-constant-test
|
||||
if self.query:
|
||||
# by definition a query enforces no class or parameter rules
|
||||
self.all_rules = ()
|
||||
else:
|
||||
@@ -598,7 +597,7 @@ class Param(ReadOnly):
|
||||
value = self.__kw[key]
|
||||
if callable(value) and hasattr(value, '__name__'):
|
||||
value = value.__name__
|
||||
elif isinstance(value, int):
|
||||
elif isinstance(value, six.integer_types):
|
||||
value = str(value)
|
||||
elif isinstance(value, (tuple, set, frozenset)):
|
||||
value = apirepr(list(value))
|
||||
@@ -762,10 +761,10 @@ class Param(ReadOnly):
|
||||
|
||||
:param value: A proposed value for this parameter.
|
||||
"""
|
||||
if self.multivalue: # pylint: disable=using-constant-test
|
||||
if self.multivalue:
|
||||
if type(value) not in (tuple, list):
|
||||
value = (value,)
|
||||
if self.multivalue: # pylint: disable=using-constant-test
|
||||
if self.multivalue:
|
||||
return tuple(
|
||||
self._normalize_scalar(v) for v in value
|
||||
)
|
||||
@@ -841,7 +840,7 @@ class Param(ReadOnly):
|
||||
|
||||
if _is_null(value):
|
||||
return
|
||||
if self.multivalue: # pylint: disable=using-constant-test
|
||||
if self.multivalue:
|
||||
if type(value) not in (tuple, list):
|
||||
value = (value,)
|
||||
values = tuple(
|
||||
@@ -873,10 +872,10 @@ class Param(ReadOnly):
|
||||
if self.required or (supplied and 'nonempty' in self.flags):
|
||||
raise RequirementError(name=self.name)
|
||||
return
|
||||
if self.deprecated: # pylint: disable=using-constant-test
|
||||
if self.deprecated:
|
||||
raise ValidationError(name=self.get_param_name(),
|
||||
error=_('this option is deprecated'))
|
||||
if self.multivalue: # pylint: disable=using-constant-test
|
||||
if self.multivalue:
|
||||
if type(value) is not tuple:
|
||||
raise TypeError(
|
||||
TYPE_ERROR % ('value', tuple, value, type(value))
|
||||
@@ -970,8 +969,8 @@ class Param(ReadOnly):
|
||||
for a, k, _d in self.kwargs:
|
||||
if k in (callable, DefaultFrom):
|
||||
continue
|
||||
if isinstance(getattr(self, a), frozenset):
|
||||
json_dict[a] = list(getattr(self, a, []))
|
||||
elif isinstance(getattr(self, a), frozenset):
|
||||
json_dict[a] = [k for k in getattr(self, a, [])]
|
||||
else:
|
||||
val = getattr(self, a, '')
|
||||
if val is None:
|
||||
@@ -1008,7 +1007,7 @@ class Bool(Param):
|
||||
"""
|
||||
if type(value) in self.allowed_types:
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
if isinstance(value, six.string_types):
|
||||
value = value.lower()
|
||||
if value in self.truths:
|
||||
return True
|
||||
@@ -1073,7 +1072,7 @@ class Number(Param):
|
||||
"""
|
||||
if type(value) in self.allowed_types:
|
||||
return value
|
||||
if type(value) in (unicode, float, int):
|
||||
if type(value) in (unicode, float) + six.integer_types:
|
||||
try:
|
||||
return self.type(value)
|
||||
except ValueError:
|
||||
@@ -1090,12 +1089,12 @@ class Int(Number):
|
||||
"""
|
||||
|
||||
type = int
|
||||
allowed_types = (int,)
|
||||
allowed_types = six.integer_types
|
||||
type_error = _('must be an integer')
|
||||
|
||||
kwargs = Param.kwargs + (
|
||||
('minvalue', int, int(MININT)),
|
||||
('maxvalue', int, int(MAXINT)),
|
||||
('minvalue', six.integer_types, int(MININT)),
|
||||
('maxvalue', six.integer_types, int(MAXINT)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -1139,7 +1138,7 @@ class Int(Number):
|
||||
"""
|
||||
Check min constraint.
|
||||
"""
|
||||
assert isinstance(value, int)
|
||||
assert type(value) in six.integer_types
|
||||
if value < self.minvalue:
|
||||
return _('must be at least %(minvalue)d') % dict(
|
||||
minvalue=self.minvalue,
|
||||
@@ -1151,7 +1150,7 @@ class Int(Number):
|
||||
"""
|
||||
Check max constraint.
|
||||
"""
|
||||
assert isinstance(value, int)
|
||||
assert type(value) in six.integer_types
|
||||
if value > self.maxvalue:
|
||||
return _('can be at most %(maxvalue)d') % dict(
|
||||
maxvalue=self.maxvalue,
|
||||
@@ -1189,7 +1188,7 @@ class Decimal(Number):
|
||||
value = kw.get(kwparam)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (str, float)):
|
||||
if isinstance(value, (six.string_types, float)):
|
||||
try:
|
||||
value = decimal.Decimal(value)
|
||||
except Exception as e:
|
||||
@@ -1283,7 +1282,7 @@ class Decimal(Number):
|
||||
return value
|
||||
|
||||
def _convert_scalar(self, value, index=None):
|
||||
if isinstance(value, (str, float)):
|
||||
if isinstance(value, (six.string_types, float)):
|
||||
try:
|
||||
value = decimal.Decimal(value)
|
||||
except decimal.DecimalException as e:
|
||||
@@ -1314,7 +1313,7 @@ class Data(Param):
|
||||
('minlength', int, None),
|
||||
('maxlength', int, None),
|
||||
('length', int, None),
|
||||
('pattern_errmsg', (str,), None),
|
||||
('pattern_errmsg', (six.string_types,), None),
|
||||
)
|
||||
|
||||
re = None
|
||||
@@ -1543,7 +1542,7 @@ class Str(Data):
|
||||
"""
|
||||
|
||||
kwargs = Data.kwargs + (
|
||||
('pattern', (str,), None),
|
||||
('pattern', (six.string_types,), None),
|
||||
('noextrawhitespace', bool, True),
|
||||
)
|
||||
|
||||
@@ -1564,7 +1563,7 @@ class Str(Data):
|
||||
"""
|
||||
if type(value) in self.allowed_types:
|
||||
return value
|
||||
if type(value) in (int, float, decimal.Decimal):
|
||||
if type(value) in (float, decimal.Decimal) + six.integer_types:
|
||||
return self.type(value)
|
||||
if type(value) in (tuple, list):
|
||||
raise ConversionError(name=self.name,
|
||||
@@ -1631,7 +1630,7 @@ class IA5Str(Str):
|
||||
super(IA5Str, self).__init__(name, *rules, **kw)
|
||||
|
||||
def _convert_scalar(self, value, index=None):
|
||||
if isinstance(value, str):
|
||||
if isinstance(value, six.string_types):
|
||||
for char in value:
|
||||
if ord(char) > 127:
|
||||
raise ConversionError(name=self.get_param_name(),
|
||||
@@ -1722,7 +1721,7 @@ class IntEnum(Enum):
|
||||
"""
|
||||
|
||||
type = int
|
||||
allowed_types = (int,)
|
||||
allowed_types = six.integer_types
|
||||
type_error = Int.type_error
|
||||
|
||||
def _convert_scalar(self, value, index=None):
|
||||
@@ -1806,7 +1805,7 @@ class DateTime(Param):
|
||||
type_error = _('must be datetime value')
|
||||
|
||||
def _convert_scalar(self, value, index=None):
|
||||
if isinstance(value, str):
|
||||
if isinstance(value, six.string_types):
|
||||
if value == u'now':
|
||||
time = datetime.datetime.utcnow()
|
||||
return time
|
||||
@@ -2158,67 +2157,3 @@ class Principal(Param):
|
||||
name=self.get_param_name(),
|
||||
error=_("Service principal is required")
|
||||
)
|
||||
|
||||
|
||||
_map_types = {
|
||||
# map internal certificate subclass to generic cryptography class
|
||||
IPACertificate: crypto_x509.Certificate,
|
||||
# map internal DNS name class to generic dnspython class
|
||||
DNSName: dns.name.Name,
|
||||
# DN, Principal have their names mangled in ipaapi.__init__
|
||||
}
|
||||
|
||||
|
||||
def create_signature(command):
|
||||
"""Create an inspect.Signature for a command
|
||||
|
||||
:param command: ipa plugin instance (server or client)
|
||||
:return: inspect.Signature instance
|
||||
"""
|
||||
|
||||
signature_params = []
|
||||
seen = set()
|
||||
args_options = [
|
||||
(command.get_args(), inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
||||
(command.get_options(), inspect.Parameter.KEYWORD_ONLY)
|
||||
]
|
||||
for ipaparams, kind in args_options:
|
||||
for ipaparam in ipaparams:
|
||||
# filter out duplicates, for example user_del has a preserve flag
|
||||
# and preserve bool.
|
||||
if ipaparam.name in seen:
|
||||
continue
|
||||
seen.add(ipaparam.name)
|
||||
# ipalib.plugins.misc.env has wrong type
|
||||
if not isinstance(ipaparam, Param):
|
||||
continue
|
||||
|
||||
if ipaparam.required:
|
||||
default = inspect.Parameter.empty
|
||||
else:
|
||||
default = ipaparam.default
|
||||
|
||||
allowed_types = tuple(
|
||||
_map_types.get(t, t) for t in ipaparam.allowed_types
|
||||
)
|
||||
# ipalib.parameters.DNSNameParam also handles text
|
||||
if isinstance(ipaparam, DNSNameParam):
|
||||
allowed_types += (six.text_type,)
|
||||
ann = typing.Union[allowed_types]
|
||||
if ipaparam.multivalue:
|
||||
ann = typing.List[ann]
|
||||
|
||||
signature_params.append(
|
||||
inspect.Parameter(
|
||||
ipaparam.name, kind, default=default, annotation=ann
|
||||
)
|
||||
)
|
||||
|
||||
# cannot describe return parameter with typing yet. TypedDict
|
||||
# is only available with mypy_extension.
|
||||
signature = inspect.Signature(
|
||||
signature_params,
|
||||
return_annotation=typing.Dict[typing.Text, typing.Any]
|
||||
)
|
||||
|
||||
return signature
|
||||
|
||||
@@ -24,7 +24,6 @@ The classes in this module make heavy use of Python container emulation. If
|
||||
you are unfamiliar with this Python feature, see
|
||||
http://docs.python.org/ref/sequence-types.html
|
||||
"""
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import re
|
||||
@@ -47,6 +46,7 @@ from ipalib.base import ReadOnly, lock, islocked
|
||||
from ipalib.constants import DEFAULT_CONFIG
|
||||
from ipapython import ipa_log_manager, ipautil
|
||||
from ipapython.ipa_log_manager import (
|
||||
log_mgr,
|
||||
LOGGING_FORMAT_FILE,
|
||||
LOGGING_FORMAT_STDERR)
|
||||
from ipapython.version import VERSION, API_VERSION, DEFAULT_PLUGINS
|
||||
@@ -89,7 +89,7 @@ def find_modules_in_dir(src_dir):
|
||||
yield module
|
||||
|
||||
|
||||
class Registry:
|
||||
class Registry(object):
|
||||
"""A decorator that makes plugins available to the API
|
||||
|
||||
Usage::
|
||||
@@ -144,6 +144,7 @@ class Plugin(ReadOnly):
|
||||
self.__finalize_called = False
|
||||
self.__finalized = False
|
||||
self.__finalize_lock = threading.RLock()
|
||||
log_mgr.get_logger(self, True)
|
||||
|
||||
@classmethod
|
||||
def __name_getter(cls):
|
||||
@@ -174,7 +175,7 @@ class Plugin(ReadOnly):
|
||||
def __summary_getter(cls):
|
||||
doc = cls.doc
|
||||
if not _(doc).msg:
|
||||
return '<%s.%s>' % (cls.__module__, cls.__name__)
|
||||
return u'<%s.%s>' % (cls.__module__, cls.__name__)
|
||||
else:
|
||||
return unicode(doc).split('\n\n', 1)[0].strip()
|
||||
|
||||
@@ -238,7 +239,7 @@ class Plugin(ReadOnly):
|
||||
if not self.__finalized:
|
||||
self.finalize()
|
||||
|
||||
class finalize_attr:
|
||||
class finalize_attr(object):
|
||||
"""
|
||||
Create a stub object for plugin attribute that isn't set until the
|
||||
finalization of the plugin initialization.
|
||||
@@ -326,14 +327,6 @@ class APINameSpace(Mapping):
|
||||
self.__enumerate()
|
||||
return iter(self.__plugins)
|
||||
|
||||
def __dir__(self):
|
||||
# include plugins for readline tab completion and in dir()
|
||||
self.__enumerate()
|
||||
names = super().__dir__()
|
||||
names.extend(p.name for p in self)
|
||||
names.sort()
|
||||
return names
|
||||
|
||||
def get_plugin(self, key):
|
||||
self.__enumerate()
|
||||
return self.__plugins_by_key[key]
|
||||
@@ -445,6 +438,7 @@ class API(ReadOnly):
|
||||
Initialize environment variables and logging.
|
||||
"""
|
||||
self.__doing('bootstrap')
|
||||
self.log = log_mgr.get_logger(self)
|
||||
self.env._bootstrap(**overrides)
|
||||
self.env._finalize_core(**dict(DEFAULT_CONFIG))
|
||||
|
||||
@@ -460,7 +454,7 @@ class API(ReadOnly):
|
||||
if root_logger.handlers or self.env.validate_api:
|
||||
return
|
||||
|
||||
if self.env.debug: # pylint: disable=using-constant-test
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
@@ -476,7 +470,7 @@ class API(ReadOnly):
|
||||
level = ipa_log_manager.convert_log_level(match.group(1))
|
||||
|
||||
value = getattr(self.env, attr)
|
||||
regexps = re.split(r'\s*,\s*', value)
|
||||
regexps = re.split('\s*,\s*', value)
|
||||
|
||||
# Add the regexp, it maps to the configured level
|
||||
for regexp in regexps:
|
||||
@@ -484,7 +478,7 @@ class API(ReadOnly):
|
||||
|
||||
# Add stderr handler:
|
||||
level = logging.INFO
|
||||
if self.env.debug: # pylint: disable=using-constant-test
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
if self.env.context == 'cli':
|
||||
@@ -516,7 +510,7 @@ class API(ReadOnly):
|
||||
return
|
||||
|
||||
level = logging.INFO
|
||||
if self.env.debug: # pylint: disable=using-constant-test
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
try:
|
||||
handler = logging.FileHandler(self.env.log)
|
||||
@@ -597,18 +591,13 @@ class API(ReadOnly):
|
||||
assert type(options.env) is list
|
||||
for item in options.env:
|
||||
try:
|
||||
values = item.split('=', 1)
|
||||
(key, value) = item.split('=', 1)
|
||||
except ValueError:
|
||||
# FIXME: this should raise an IPA exception with an
|
||||
# error code.
|
||||
# --Jason, 2008-10-31
|
||||
pass
|
||||
if len(values) == 2:
|
||||
(key, value) = values
|
||||
overrides[str(key.strip())] = value.strip()
|
||||
else:
|
||||
raise errors.OptionError(_('Unable to parse option {item}'
|
||||
.format(item=item)))
|
||||
overrides[str(key.strip())] = value.strip()
|
||||
for key in ('conf', 'debug', 'verbose', 'prompt_all', 'interactive',
|
||||
'fallback', 'delegate'):
|
||||
value = getattr(options, key, None)
|
||||
@@ -665,8 +654,7 @@ class API(ReadOnly):
|
||||
logger.debug("skipping plugin module %s: %s", name, e.reason)
|
||||
continue
|
||||
except Exception as e:
|
||||
tb = self.env.startup_traceback
|
||||
if tb: # pylint: disable=using-constant-test
|
||||
if self.env.startup_traceback:
|
||||
logger.exception("could not load plugin module %s", name)
|
||||
raise
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from ipalib.constants import CALLABLE_ERROR
|
||||
context = threading.local()
|
||||
|
||||
|
||||
class _FrameContext:
|
||||
class _FrameContext(object):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -42,17 +42,17 @@ import json
|
||||
import re
|
||||
import socket
|
||||
import gzip
|
||||
import urllib
|
||||
from ssl import SSLError
|
||||
|
||||
from cryptography import x509 as crypto_x509
|
||||
|
||||
import gssapi
|
||||
from dns.exception import DNSException
|
||||
from ssl import SSLError
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
from ipalib.backend import Connectible
|
||||
from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT
|
||||
from ipalib.errors import (errors_by_code, UnknownError, NetworkError,
|
||||
from ipalib.errors import (public_errors, UnknownError, NetworkError,
|
||||
XMLRPCMarshallError, JSONError)
|
||||
from ipalib import errors, capabilities
|
||||
from ipalib.request import context, Connection
|
||||
@@ -97,6 +97,8 @@ logger = logging.getLogger(__name__)
|
||||
COOKIE_NAME = 'ipa_session'
|
||||
CCACHE_COOKIE_KEY = 'X-IPA-Session-Cookie'
|
||||
|
||||
errors_by_code = dict((e.errno, e) for e in public_errors)
|
||||
|
||||
|
||||
def update_persistent_client_session_data(principal, data):
|
||||
'''
|
||||
@@ -169,7 +171,7 @@ def xml_wrap(value, version):
|
||||
if type(value) is Decimal:
|
||||
# transfer Decimal as a string
|
||||
return unicode(value)
|
||||
if isinstance(value, int) and (value < MININT or value > MAXINT):
|
||||
if isinstance(value, six.integer_types) and (value < MININT or value > MAXINT):
|
||||
return unicode(value)
|
||||
if isinstance(value, DN):
|
||||
return str(value)
|
||||
@@ -198,7 +200,7 @@ def xml_wrap(value, version):
|
||||
return base64.b64encode(
|
||||
value.public_bytes(x509_Encoding.DER)).decode('ascii')
|
||||
|
||||
assert type(value) in (unicode, float, int, bool, type(None))
|
||||
assert type(value) in (unicode, float, bool, type(None)) + six.integer_types
|
||||
return value
|
||||
|
||||
|
||||
@@ -318,7 +320,6 @@ class _JSONPrimer(dict):
|
||||
self.update({
|
||||
unicode: _identity,
|
||||
bool: _identity,
|
||||
int: _identity,
|
||||
type(None): _identity,
|
||||
float: _identity,
|
||||
Decimal: unicode,
|
||||
@@ -333,6 +334,9 @@ class _JSONPrimer(dict):
|
||||
crypto_x509.Certificate: self._enc_certificate,
|
||||
crypto_x509.CertificateSigningRequest: self._enc_certificate,
|
||||
})
|
||||
# int, long
|
||||
for t in six.integer_types:
|
||||
self[t] = _identity
|
||||
|
||||
def __missing__(self, typ):
|
||||
# walk MRO to find best match
|
||||
@@ -487,7 +491,7 @@ def xml_loads(data, encoding='UTF-8'):
|
||||
raise decode_fault(e)
|
||||
|
||||
|
||||
class DummyParser:
|
||||
class DummyParser(object):
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
|
||||
@@ -1168,11 +1172,11 @@ class RPCClient(Connectible):
|
||||
try:
|
||||
principal = getattr(context, 'principal', None)
|
||||
delete_persistent_client_session_data(principal)
|
||||
except Exception as e2:
|
||||
except Exception as e:
|
||||
# This shouldn't happen if we have a session
|
||||
# but it isn't fatal.
|
||||
logger.debug("Error trying to remove persisent "
|
||||
"session data: %s", e2)
|
||||
"session data: %s", e)
|
||||
|
||||
# Create a new serverproxy with the non-session URI
|
||||
serverproxy = self.create_connection(
|
||||
@@ -1206,7 +1210,7 @@ class xmlclient(RPCClient):
|
||||
return xml_unwrap(result)
|
||||
|
||||
|
||||
class JSONServerProxy:
|
||||
class JSONServerProxy(object):
|
||||
def __init__(self, uri, transport, encoding, verbose, allow_none):
|
||||
split_uri = urllib.parse.urlsplit(uri)
|
||||
if split_uri.scheme not in ("http", "https"):
|
||||
|
||||
@@ -44,6 +44,6 @@ if __name__ == '__main__':
|
||||
"six",
|
||||
],
|
||||
extras_require={
|
||||
"install": ["dbus-python"], # for certmonger and resolve1
|
||||
"install": ["ipaplatform"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
# Authors: Mark McLoughlin <markmc@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2007 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/>.
|
||||
#
|
||||
|
||||
#
|
||||
# This module provides a very simple API which allows
|
||||
# ipa-xxx-install --uninstall to restore certain
|
||||
# parts of the system configuration to the way it was
|
||||
# before ipa-server-install was first run
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
import six
|
||||
# pylint: disable=import-error
|
||||
if six.PY3:
|
||||
# The SafeConfigParser class has been renamed to ConfigParser in Py3
|
||||
from configparser import ConfigParser as SafeConfigParser
|
||||
else:
|
||||
from ConfigParser import SafeConfigParser
|
||||
# pylint: enable=import-error
|
||||
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSRESTORE_PATH = paths.TMP
|
||||
SYSRESTORE_INDEXFILE = "sysrestore.index"
|
||||
SYSRESTORE_STATEFILE = "sysrestore.state"
|
||||
|
||||
|
||||
class FileStore:
|
||||
"""Class for handling backup and restore of files"""
|
||||
|
||||
def __init__(self, path=SYSRESTORE_PATH, index_file=SYSRESTORE_INDEXFILE):
|
||||
"""Create a _StoreFiles object, that uses @path as the
|
||||
base directory.
|
||||
|
||||
The file @path/sysrestore.index is used to store information
|
||||
about the original location of the saved files.
|
||||
"""
|
||||
self._path = path
|
||||
self._index = os.path.join(self._path, index_file)
|
||||
|
||||
self.random = random.Random()
|
||||
|
||||
self.files = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the file list from the index file. @files will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
|
||||
logger.debug("Loading Index file from '%s'", self._index)
|
||||
|
||||
self.files = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._index)
|
||||
|
||||
for section in p.sections():
|
||||
if section == "files":
|
||||
for (key, value) in p.items(section):
|
||||
self.files[key] = value
|
||||
|
||||
def save(self):
|
||||
"""Save the file list to @_index. If @files is an empty
|
||||
dict, then @_index should be removed.
|
||||
"""
|
||||
logger.debug("Saving Index File to '%s'", self._index)
|
||||
|
||||
if len(self.files) == 0:
|
||||
logger.debug(" -> no files, removing file")
|
||||
if os.path.exists(self._index):
|
||||
os.remove(self._index)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
p.add_section('files')
|
||||
for (key, value) in self.files.items():
|
||||
p.set('files', key, str(value))
|
||||
|
||||
with open(self._index, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_file(self, path):
|
||||
"""Create a copy of the file at @path - as long as an exact copy
|
||||
does not already exist - which will be restored to its
|
||||
original location by restore_files().
|
||||
"""
|
||||
logger.debug("Backing up system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
if not os.path.isfile(path):
|
||||
logger.debug(" -> Not backing up - '%s' doesn't exist", path)
|
||||
return
|
||||
|
||||
_reldir, backupfile = os.path.split(path)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
cont_hash = sha256(f.read()).hexdigest()
|
||||
|
||||
filename = "{hexhash}-{bcppath}".format(hexhash=cont_hash,
|
||||
bcppath=backupfile)
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if os.path.exists(backup_path):
|
||||
logger.debug(" -> Not backing up - already have a copy of '%s'",
|
||||
path)
|
||||
return
|
||||
|
||||
shutil.copy2(path, backup_path)
|
||||
|
||||
stat = os.stat(path)
|
||||
|
||||
template = '{stat.st_mode},{stat.st_uid},{stat.st_gid},{path}'
|
||||
self.files[filename] = template.format(stat=stat, path=path)
|
||||
self.save()
|
||||
|
||||
def has_file(self, path):
|
||||
"""Checks whether file at @path was added to the file store
|
||||
|
||||
Returns #True if the file exists in the file store, #False otherwise
|
||||
"""
|
||||
result = False
|
||||
for _key, value in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
|
||||
def restore_file(self, path, new_path=None):
|
||||
"""Restore the copy of a file at @path to its original
|
||||
location and delete the copy.
|
||||
|
||||
Takes optional parameter @new_path which specifies the
|
||||
location where the file is to be restored.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if new_path is None:
|
||||
logger.debug("Restoring system configuration file '%s'",
|
||||
path)
|
||||
else:
|
||||
logger.debug("Restoring system configuration file '%s' to '%s'",
|
||||
path, new_path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
if new_path is not None and not os.path.isabs(new_path):
|
||||
raise ValueError("Absolute new path required")
|
||||
|
||||
mode = None
|
||||
uid = None
|
||||
gid = None
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
(mode,uid,gid,filepath) = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
if new_path is not None:
|
||||
path = new_path
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def restore_all_files(self):
|
||||
"""Restore the files in the inbdex to their original
|
||||
location and delete the copy.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if len(self.files) == 0:
|
||||
return False
|
||||
|
||||
for (filename, value) in self.files.items():
|
||||
|
||||
(mode,uid,gid,path) = value.split(',', 3)
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
continue
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
# force file to be deleted
|
||||
self.files = {}
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def has_files(self):
|
||||
"""Return True or False if there are any files in the index
|
||||
|
||||
Can be used to determine if a program is configured.
|
||||
"""
|
||||
|
||||
return len(self.files) > 0
|
||||
|
||||
def untrack_file(self, path):
|
||||
"""Remove file at path @path from list of backed up files.
|
||||
|
||||
Does not remove any files from the filesystem.
|
||||
|
||||
Returns #True if the file was untracked, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
logger.debug("Untracking system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
os.unlink(backup_path)
|
||||
except Exception as e:
|
||||
logger.error('Error removing %s: %s', backup_path, str(e))
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class StateFile:
|
||||
"""A metadata file for recording system state which can
|
||||
be backed up and later restored.
|
||||
StateFile gets reloaded every time to prevent loss of information
|
||||
recorded by child processes. But we do not solve concurrency
|
||||
because there is no need for it right now.
|
||||
The format is something like:
|
||||
|
||||
[httpd]
|
||||
running=True
|
||||
enabled=False
|
||||
"""
|
||||
|
||||
def __init__(self, path=SYSRESTORE_PATH, state_file=SYSRESTORE_STATEFILE):
|
||||
"""Create a StateFile object, loading from @path.
|
||||
|
||||
The dictionary @modules, a member of the returned object,
|
||||
is where the state can be modified. @modules is indexed
|
||||
using a module name to return another dictionary containing
|
||||
key/value pairs with the saved state of that module.
|
||||
|
||||
The keys in these latter dictionaries are arbitrary strings
|
||||
and the values may either be strings or booleans.
|
||||
"""
|
||||
self._path = os.path.join(path, state_file)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the modules from the file @_path. @modules will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
logger.debug("Loading StateFile from '%s'", self._path)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._path)
|
||||
|
||||
for module in p.sections():
|
||||
self.modules[module] = {}
|
||||
for (key, value) in p.items(module):
|
||||
if value == str(True):
|
||||
value = True
|
||||
elif value == str(False):
|
||||
value = False
|
||||
self.modules[module][key] = value
|
||||
|
||||
def save(self):
|
||||
"""Save the modules to @_path. If @modules is an empty
|
||||
dict, then @_path should be removed.
|
||||
"""
|
||||
logger.debug("Saving StateFile to '%s'", self._path)
|
||||
|
||||
for module in list(self.modules):
|
||||
if len(self.modules[module]) == 0:
|
||||
del self.modules[module]
|
||||
|
||||
if len(self.modules) == 0:
|
||||
logger.debug(" -> no modules, removing file")
|
||||
if os.path.exists(self._path):
|
||||
os.remove(self._path)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
for module in self.modules:
|
||||
p.add_section(module)
|
||||
for (key, value) in self.modules[module].items():
|
||||
p.set(module, key, str(value))
|
||||
|
||||
with open(self._path, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_state(self, module, key, value):
|
||||
"""Backup an item of system state from @module, identified
|
||||
by the string @key and with the value @value. @value may be
|
||||
a string or boolean.
|
||||
"""
|
||||
if not isinstance(value, (str, bool, unicode)):
|
||||
raise ValueError("Only strings, booleans or unicode strings "
|
||||
"are supported")
|
||||
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
self.modules[module] = {}
|
||||
|
||||
if key not in self.modules:
|
||||
self.modules[module][key] = value
|
||||
|
||||
self.save()
|
||||
|
||||
def get_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
return None
|
||||
|
||||
return self.modules[module].get(key, None)
|
||||
|
||||
def delete_state(self, module, key):
|
||||
"""Delete system state from @module, identified by the string
|
||||
@key.
|
||||
|
||||
If the item doesn't exist, no change is done.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
try:
|
||||
del self.modules[module][key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.save()
|
||||
|
||||
def restore_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key, and remove it from the backed
|
||||
up system state.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
|
||||
value = self.get_state(module, key)
|
||||
|
||||
if value is not None:
|
||||
self.delete_state(module, key)
|
||||
|
||||
return value
|
||||
|
||||
def has_state(self, module):
|
||||
"""Return True or False if there is any state stored for @module.
|
||||
|
||||
Can be used to determine if a service is configured.
|
||||
"""
|
||||
|
||||
return module in self.modules
|
||||
@@ -154,7 +154,7 @@ def create_translation(key):
|
||||
return translation
|
||||
|
||||
|
||||
class LazyText:
|
||||
class LazyText(object):
|
||||
"""
|
||||
Base class for deferred translation.
|
||||
|
||||
@@ -319,7 +319,7 @@ class FixMe(Gettext):
|
||||
creates conspicuous looking UI labels that remind the programmer to
|
||||
"fix-me!". For example, the typical usage would be something like this:
|
||||
|
||||
>>> class Plugin:
|
||||
>>> class Plugin(object):
|
||||
... label = None
|
||||
... def __init__(self):
|
||||
... self.name = self.__class__.__name__
|
||||
@@ -485,7 +485,7 @@ class NGettext(LazyText):
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class ConcatenatedLazyText:
|
||||
class ConcatenatedLazyText(object):
|
||||
"""Concatenation of multiple strings, or any objects convertible to unicode
|
||||
|
||||
Used to concatenate several LazyTexts together.
|
||||
@@ -525,7 +525,7 @@ class ConcatenatedLazyText:
|
||||
return ConcatenatedLazyText(*[other] + self.components)
|
||||
|
||||
|
||||
class GettextFactory:
|
||||
class GettextFactory(object):
|
||||
"""
|
||||
Factory for creating ``_()`` functions.
|
||||
|
||||
|
||||
172
ipalib/util.py
172
ipalib/util.py
@@ -37,12 +37,11 @@ import sys
|
||||
import ssl
|
||||
import termios
|
||||
import fcntl
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
import netaddr
|
||||
from dns import rdatatype
|
||||
from dns import resolver, rdatatype
|
||||
from dns.exception import DNSException
|
||||
from dns.resolver import NXDOMAIN
|
||||
from netaddr.core import AddrFormatError
|
||||
@@ -57,22 +56,15 @@ except ImportError:
|
||||
from ipalib import errors, messages
|
||||
from ipalib.constants import (
|
||||
DOMAIN_LEVEL_0,
|
||||
TLS_VERSIONS, TLS_VERSION_MINIMAL, TLS_VERSION_MAXIMAL,
|
||||
TLS_VERSION_DEFAULT_MIN, TLS_VERSION_DEFAULT_MAX,
|
||||
TLS_VERSIONS, TLS_VERSION_MINIMAL
|
||||
)
|
||||
# pylint: disable=ipa-forbidden-import
|
||||
from ipalib.facts import is_ipa_client_configured
|
||||
from ipalib.text import _
|
||||
from ipaplatform.constants import constants
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.ssh import SSHPublicKey
|
||||
from ipapython.dn import DN, RDN
|
||||
from ipapython.dnsutil import (
|
||||
DNSName,
|
||||
DNSResolver,
|
||||
resolve,
|
||||
resolve_ip_addresses,
|
||||
)
|
||||
from ipapython.dnsutil import DNSName
|
||||
from ipapython.dnsutil import resolve_ip_addresses
|
||||
from ipapython.admintool import ScriptError
|
||||
|
||||
if sys.version_info >= (3, 2):
|
||||
@@ -94,7 +86,7 @@ def json_serialize(obj):
|
||||
return [json_serialize(o) for o in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: json_serialize(v) for (k, v) in obj.items()}
|
||||
if isinstance(obj, (int, bool, float, unicode, type(None))):
|
||||
if isinstance(obj, (bool, float, unicode, type(None), six.integer_types)):
|
||||
return obj
|
||||
if isinstance(obj, str):
|
||||
return obj.decode('utf-8')
|
||||
@@ -121,13 +113,13 @@ def has_soa_or_ns_record(domain):
|
||||
Returns True or False.
|
||||
"""
|
||||
try:
|
||||
resolve(domain, rdatatype.SOA)
|
||||
resolver.query(domain, rdatatype.SOA)
|
||||
soa_record_found = True
|
||||
except DNSException:
|
||||
soa_record_found = False
|
||||
|
||||
try:
|
||||
resolve(domain, rdatatype.NS)
|
||||
resolver.query(domain, rdatatype.NS)
|
||||
ns_record_found = True
|
||||
except DNSException:
|
||||
ns_record_found = False
|
||||
@@ -222,7 +214,7 @@ def check_writable_file(filename):
|
||||
raise errors.FileError(reason=str(e))
|
||||
|
||||
def normalize_zonemgr(zonemgr):
|
||||
if not zonemgr or not isinstance(zonemgr, str):
|
||||
if not zonemgr or not isinstance(zonemgr, six.string_types):
|
||||
return zonemgr
|
||||
if '@' in zonemgr:
|
||||
# local-part needs to be normalized
|
||||
@@ -254,13 +246,6 @@ def get_proper_tls_version_span(tls_version_min, tls_version_max):
|
||||
if lower than TLS_VERSION_MINIMAL
|
||||
:raises: ValueError
|
||||
"""
|
||||
if tls_version_min is None and tls_version_max is None:
|
||||
# no defaults, use system's default TLS version range
|
||||
return None
|
||||
if tls_version_min is None:
|
||||
tls_version_min = TLS_VERSION_MINIMAL
|
||||
if tls_version_max is None:
|
||||
tls_version_max = TLS_VERSION_MAXIMAL
|
||||
min_allowed_idx = TLS_VERSIONS.index(TLS_VERSION_MINIMAL)
|
||||
|
||||
try:
|
||||
@@ -296,8 +281,8 @@ def create_https_connection(
|
||||
cafile=None,
|
||||
client_certfile=None, client_keyfile=None,
|
||||
keyfile_passwd=None,
|
||||
tls_version_min=TLS_VERSION_DEFAULT_MIN,
|
||||
tls_version_max=TLS_VERSION_DEFAULT_MAX,
|
||||
tls_version_min="tls1.1",
|
||||
tls_version_max="tls1.2",
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
@@ -327,7 +312,6 @@ def create_https_connection(
|
||||
"tls1.0": ssl.OP_NO_TLSv1,
|
||||
"tls1.1": ssl.OP_NO_TLSv1_1,
|
||||
"tls1.2": ssl.OP_NO_TLSv1_2,
|
||||
"tls1.3": getattr(ssl, "OP_NO_TLSv1_3", 0),
|
||||
}
|
||||
# pylint: enable=no-member
|
||||
|
||||
@@ -339,6 +323,9 @@ def create_https_connection(
|
||||
raise RuntimeError("cafile \'{file}\' doesn't exist or is unreadable".
|
||||
format(file=cafile))
|
||||
|
||||
# remove the slice of negating protocol options according to options
|
||||
tls_span = get_proper_tls_version_span(tls_version_min, tls_version_max)
|
||||
|
||||
# official Python documentation states that the best option to get
|
||||
# TLSv1 and later is to setup SSLContext with PROTOCOL_SSLv23
|
||||
# and then negate the insecure SSLv2 and SSLv3
|
||||
@@ -349,28 +336,20 @@ def create_https_connection(
|
||||
ssl.OP_SINGLE_ECDH_USE
|
||||
)
|
||||
|
||||
if constants.TLS_HIGH_CIPHERS is not None:
|
||||
# configure ciphers, uses system crypto policies on RH platforms.
|
||||
ctx.set_ciphers(constants.TLS_HIGH_CIPHERS)
|
||||
|
||||
# remove the slice of negating protocol options according to options
|
||||
tls_span = get_proper_tls_version_span(tls_version_min, tls_version_max)
|
||||
# high ciphers without RC4, MD5, TripleDES, pre-shared key and secure
|
||||
# remote password. Uses system crypto policies on some platforms.
|
||||
ctx.set_ciphers(constants.TLS_HIGH_CIPHERS)
|
||||
|
||||
# pylint: enable=no-member
|
||||
# set up the correct TLS version flags for the SSL context
|
||||
if tls_span is not None:
|
||||
for version in TLS_VERSIONS:
|
||||
if version in tls_span:
|
||||
# make sure the required TLS versions are available if Python
|
||||
# decides to modify the default TLS flags
|
||||
ctx.options &= ~tls_cutoff_map[version]
|
||||
else:
|
||||
# disable all TLS versions not in tls_span
|
||||
ctx.options |= tls_cutoff_map[version]
|
||||
|
||||
# Enable TLS 1.3 post-handshake auth
|
||||
if getattr(ctx, "post_handshake_auth", None) is not None:
|
||||
ctx.post_handshake_auth = True
|
||||
for version in TLS_VERSIONS:
|
||||
if version in tls_span:
|
||||
# make sure the required TLS versions are available if Python
|
||||
# decides to modify the default TLS flags
|
||||
ctx.options &= ~tls_cutoff_map[version]
|
||||
else:
|
||||
# disable all TLS versions not in tls_span
|
||||
ctx.options |= tls_cutoff_map[version]
|
||||
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
ctx.check_hostname = True
|
||||
@@ -454,28 +433,14 @@ def validate_zonemgr_str(zonemgr):
|
||||
zonemgr = DNSName(zonemgr)
|
||||
return validate_zonemgr(zonemgr)
|
||||
|
||||
|
||||
def validate_hostname(hostname, check_fqdn=True, allow_underscore=False,
|
||||
allow_slash=False, maxlen=255):
|
||||
def validate_hostname(hostname, check_fqdn=True, allow_underscore=False, allow_slash=False):
|
||||
""" See RFC 952, 1123
|
||||
|
||||
Length limit of 64 imposed by MAXHOSTNAMELEN on Linux.
|
||||
|
||||
DNS and other operating systems has a max length of 255. Default to
|
||||
the theoretical max unless explicitly told to limit. The cases
|
||||
where a limit would be set might include:
|
||||
* *-install --hostname
|
||||
* ipa host-add
|
||||
|
||||
The *-install commands by definition are executed on Linux hosts so
|
||||
the maximum length needs to be limited.
|
||||
|
||||
:param hostname Checked value
|
||||
:param check_fqdn Check if hostname is fully qualified
|
||||
"""
|
||||
if len(hostname) > maxlen:
|
||||
raise ValueError(_('cannot be longer that {} characters'.format(
|
||||
maxlen)))
|
||||
if len(hostname) > 255:
|
||||
raise ValueError(_('cannot be longer that 255 characters'))
|
||||
|
||||
if hostname.endswith('.'):
|
||||
hostname = hostname[:-1]
|
||||
@@ -798,10 +763,10 @@ def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
|
||||
:param flag_cd: requires dnssec=True, adds flag CD
|
||||
:raise DNSException: if error occurs
|
||||
"""
|
||||
assert isinstance(nameserver_ip, str) or nameserver_ip is None
|
||||
assert isinstance(rtype, str)
|
||||
assert isinstance(nameserver_ip, six.string_types) or nameserver_ip is None
|
||||
assert isinstance(rtype, six.string_types)
|
||||
|
||||
res = DNSResolver()
|
||||
res = dns.resolver.Resolver()
|
||||
if nameserver_ip:
|
||||
res.nameservers = [nameserver_ip]
|
||||
res.lifetime = timeout
|
||||
@@ -819,7 +784,7 @@ def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
|
||||
elif edns0:
|
||||
res.use_edns(0, 0, 4096)
|
||||
|
||||
return res.resolve(owner, rtype)
|
||||
return res.query(owner, rtype)
|
||||
|
||||
|
||||
def _validate_edns0_forwarder(owner, rtype, ip_addr, timeout=10):
|
||||
@@ -989,7 +954,7 @@ def detect_dns_zone_realm_type(api, domain):
|
||||
kerberos_record_name = kerberos_prefix + domain_suffix
|
||||
|
||||
try:
|
||||
result = resolve(kerberos_record_name, rdatatype.TXT)
|
||||
result = resolver.query(kerberos_record_name, rdatatype.TXT)
|
||||
answer = result.response.answer
|
||||
|
||||
# IPA domain will have only one _kerberos TXT record
|
||||
@@ -1016,7 +981,7 @@ def detect_dns_zone_realm_type(api, domain):
|
||||
|
||||
try:
|
||||
# The presence of this record is enough, return foreign in such case
|
||||
resolve(ad_specific_record_name, rdatatype.SRV)
|
||||
resolver.query(ad_specific_record_name, rdatatype.SRV)
|
||||
except DNSException:
|
||||
# If we could not detect type with certainty, return unknown
|
||||
return 'unknown'
|
||||
@@ -1029,11 +994,10 @@ def has_managed_topology(api):
|
||||
return domainlevel > DOMAIN_LEVEL_0
|
||||
|
||||
|
||||
class classproperty:
|
||||
class classproperty(object):
|
||||
__slots__ = ('__doc__', 'fget')
|
||||
|
||||
def __init__(self, fget=None, doc=None):
|
||||
assert isinstance(fget, classmethod)
|
||||
if doc is None and fget is not None:
|
||||
doc = fget.__doc__
|
||||
|
||||
@@ -1056,17 +1020,6 @@ class classproperty:
|
||||
return self
|
||||
|
||||
|
||||
class classobjectproperty(classproperty):
|
||||
# A class property that also passes the object to the getter
|
||||
# obj is None for class objects and 'self' for instance objects.
|
||||
__slots__ = ('__doc__',)
|
||||
|
||||
def __get__(self, obj, obj_type):
|
||||
if self.fget is not None:
|
||||
return self.fget.__get__(obj, obj_type)(obj)
|
||||
raise AttributeError("unreadable attribute")
|
||||
|
||||
|
||||
def normalize_hostname(hostname):
|
||||
"""Use common fqdn form without the trailing dot"""
|
||||
if hostname.endswith(u'.'):
|
||||
@@ -1075,16 +1028,9 @@ def normalize_hostname(hostname):
|
||||
return hostname
|
||||
|
||||
|
||||
def hostname_validator(ugettext, value, maxlen=255):
|
||||
"""Validator used by plugins to ensure hostname compliance.
|
||||
|
||||
In Linux the maximum hostname length is 64. In DNS and
|
||||
other operaring systems (Solaris) it is 255. If not explicitly
|
||||
checking a Linux hostname (e.g. the server) use the DNS
|
||||
default.
|
||||
"""
|
||||
def hostname_validator(ugettext, value):
|
||||
try:
|
||||
validate_hostname(value, maxlen=maxlen)
|
||||
validate_hostname(value)
|
||||
except ValueError as e:
|
||||
return _('invalid domain-name: %s') % unicode(e)
|
||||
|
||||
@@ -1176,37 +1122,17 @@ def ensure_krbcanonicalname_set(ldap, entry_attrs):
|
||||
entry_attrs.update(old_entry)
|
||||
|
||||
|
||||
def check_client_configuration(env=None):
|
||||
def check_client_configuration():
|
||||
"""
|
||||
Check if IPA client is configured on the system.
|
||||
|
||||
This is a convenience wrapper that also supports using
|
||||
a custom configuration via IPA_CONFDIR.
|
||||
|
||||
Raises a ScriptError exception if the client is not
|
||||
configured.
|
||||
|
||||
Hardcode return code to avoid recursive imports
|
||||
"""
|
||||
CLIENT_NOT_CONFIGURED = 2
|
||||
if env is not None and env.confdir != paths.ETC_IPA:
|
||||
# custom IPA conf dir, check for custom conf_default
|
||||
if os.path.isfile(env.conf_default):
|
||||
return True
|
||||
else:
|
||||
raise ScriptError(
|
||||
f'IPA client is not configured on this system (confdir '
|
||||
f'{env.confdir} is missing {env.conf_default})',
|
||||
CLIENT_NOT_CONFIGURED
|
||||
)
|
||||
|
||||
if is_ipa_client_configured():
|
||||
return True
|
||||
else:
|
||||
raise ScriptError(
|
||||
'IPA client is not configured on this system',
|
||||
CLIENT_NOT_CONFIGURED
|
||||
)
|
||||
if (not os.path.isfile(paths.IPA_DEFAULT_CONF) or
|
||||
not os.path.isdir(paths.IPA_CLIENT_SYSRESTORE) or
|
||||
not os.listdir(paths.IPA_CLIENT_SYSRESTORE)):
|
||||
raise ScriptError('IPA client is not configured on this system',
|
||||
2) # CLIENT_NOT_CONFIGURED
|
||||
|
||||
|
||||
def check_principal_realm_in_trust_namespace(api_instance, *keys):
|
||||
@@ -1277,27 +1203,17 @@ def get_terminal_height(fd=1):
|
||||
return os.environ.get("LINES", 25)
|
||||
|
||||
|
||||
def get_pager():
|
||||
""" Get path to a pager
|
||||
|
||||
:return: path to the file if it exists otherwise None
|
||||
:rtype: str or None
|
||||
"""
|
||||
pager = os.environ.get('PAGER', 'less')
|
||||
return shutil.which(pager)
|
||||
|
||||
|
||||
def open_in_pager(data, pager):
|
||||
def open_in_pager(data):
|
||||
"""
|
||||
Open text data in pager
|
||||
|
||||
Args:
|
||||
data (bytes): data to view in pager
|
||||
pager (str): path to the pager
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
pager = os.environ.get("PAGER", "less")
|
||||
pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE)
|
||||
|
||||
try:
|
||||
|
||||
175
ipalib/x509.py
175
ipalib/x509.py
@@ -34,7 +34,6 @@ from __future__ import print_function
|
||||
import os
|
||||
import binascii
|
||||
import datetime
|
||||
import enum
|
||||
import ipaddress
|
||||
import ssl
|
||||
import base64
|
||||
@@ -48,7 +47,6 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding, PublicFormat, PrivateFormat, load_pem_private_key
|
||||
)
|
||||
import pyasn1
|
||||
import pyasn1.error
|
||||
from pyasn1.type import univ, char, namedtype, tag
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from pyasn1_modules import rfc2315, rfc2459
|
||||
@@ -85,7 +83,7 @@ SAN_KRB5PRINCIPALNAME = '1.3.6.1.5.2.2'
|
||||
|
||||
|
||||
@crypto_utils.register_interface(crypto_x509.Certificate)
|
||||
class IPACertificate:
|
||||
class IPACertificate(object):
|
||||
"""
|
||||
A proxy class wrapping a python-cryptography certificate representation for
|
||||
FreeIPA purposes
|
||||
@@ -598,7 +596,7 @@ def write_pem_private_key(priv_key, filename, passwd=None):
|
||||
os.fchmod(fp.fileno(), 0o600)
|
||||
fp.write(priv_key.private_bytes(
|
||||
Encoding.PEM,
|
||||
PrivateFormat.PKCS8,
|
||||
PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=enc_alg))
|
||||
except (IOError, OSError) as e:
|
||||
raise errors.FileError(reason=str(e))
|
||||
@@ -747,172 +745,3 @@ def format_datetime(t):
|
||||
if t.tzinfo is None:
|
||||
t = t.replace(tzinfo=UTC())
|
||||
return unicode(t.strftime("%a %b %d %H:%M:%S %Y %Z"))
|
||||
|
||||
|
||||
class ExternalCAType(enum.Enum):
|
||||
GENERIC = 'generic'
|
||||
MS_CS = 'ms-cs'
|
||||
|
||||
|
||||
class ExternalCAProfile:
|
||||
"""
|
||||
An external CA profile configuration. Currently the only
|
||||
subclasses are for Microsoft CAs, for providing data in the
|
||||
"Certificate Template" extension.
|
||||
|
||||
Constructing this class will actually return an instance of a
|
||||
subclass.
|
||||
|
||||
Subclasses MUST set ``valid_for``.
|
||||
|
||||
"""
|
||||
def __init__(self, s=None):
|
||||
self.unparsed_input = s
|
||||
|
||||
# Which external CA types is the data valid for?
|
||||
# A set of VALUES of the ExternalCAType enum.
|
||||
valid_for = set()
|
||||
|
||||
def __new__(cls, s=None):
|
||||
"""Construct the ExternalCAProfile value.
|
||||
|
||||
Return an instance of a subclass determined by
|
||||
the format of the argument.
|
||||
|
||||
"""
|
||||
# we are directly constructing a subclass; instantiate
|
||||
# it and be done
|
||||
if cls is not ExternalCAProfile:
|
||||
return super(ExternalCAProfile, cls).__new__(cls)
|
||||
|
||||
# construction via the base class; therefore the string
|
||||
# argument is required, and is used to determine which
|
||||
# subclass to construct
|
||||
if s is None:
|
||||
raise ValueError('string argument is required')
|
||||
|
||||
parts = s.split(':')
|
||||
|
||||
try:
|
||||
# Is the first part on OID?
|
||||
_oid = univ.ObjectIdentifier(parts[0])
|
||||
|
||||
# It is; construct a V2 template
|
||||
# pylint: disable=too-many-function-args
|
||||
return MSCSTemplateV2.__new__(MSCSTemplateV2, s)
|
||||
|
||||
except pyasn1.error.PyAsn1Error:
|
||||
# It is not an OID; treat as a template name
|
||||
# pylint: disable=too-many-function-args
|
||||
return MSCSTemplateV1.__new__(MSCSTemplateV1, s)
|
||||
|
||||
def __getstate__(self):
|
||||
return self.unparsed_input
|
||||
|
||||
def __setstate__(self, state):
|
||||
# explicitly call __init__ method to initialise object
|
||||
self.__init__(state)
|
||||
|
||||
|
||||
class MSCSTemplate(ExternalCAProfile):
|
||||
"""
|
||||
An Microsoft AD-CS Template specifier.
|
||||
|
||||
Subclasses MUST set ext_oid.
|
||||
|
||||
Subclass constructors MUST set asn1obj.
|
||||
|
||||
"""
|
||||
valid_for = set([ExternalCAType.MS_CS.value])
|
||||
|
||||
ext_oid = None # extension OID, as a Python str
|
||||
asn1obj = None # unencoded extension data
|
||||
|
||||
def get_ext_data(self):
|
||||
"""Return DER-encoded extension data."""
|
||||
return encoder.encode(self.asn1obj)
|
||||
|
||||
|
||||
class MSCSTemplateV1(MSCSTemplate):
|
||||
"""
|
||||
A v1 template specifier, per
|
||||
https://msdn.microsoft.com/en-us/library/cc250011.aspx.
|
||||
|
||||
::
|
||||
|
||||
CertificateTemplateName ::= SEQUENCE {
|
||||
Name UTF8String
|
||||
}
|
||||
|
||||
But note that a bare BMPString is used in practice.
|
||||
|
||||
"""
|
||||
ext_oid = "1.3.6.1.4.1.311.20.2"
|
||||
|
||||
def __init__(self, s):
|
||||
super(MSCSTemplateV1, self).__init__(s)
|
||||
parts = s.split(':')
|
||||
if len(parts) > 1:
|
||||
raise ValueError(
|
||||
"Cannot specify certificate template version when using name.")
|
||||
self.asn1obj = char.BMPString(str(parts[0]))
|
||||
|
||||
|
||||
class MSCSTemplateV2(MSCSTemplate):
|
||||
"""
|
||||
A v2 template specifier, per
|
||||
https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx
|
||||
|
||||
::
|
||||
|
||||
CertificateTemplate ::= SEQUENCE {
|
||||
templateID EncodedObjectID,
|
||||
templateMajorVersion TemplateVersion,
|
||||
templateMinorVersion TemplateVersion OPTIONAL
|
||||
}
|
||||
|
||||
TemplateVersion ::= INTEGER (0..4294967295)
|
||||
|
||||
"""
|
||||
ext_oid = "1.3.6.1.4.1.311.21.7"
|
||||
|
||||
@staticmethod
|
||||
def check_version_in_range(desc, n):
|
||||
if n < 0 or n >= 2**32:
|
||||
raise ValueError(
|
||||
"Template {} version must be in range 0..4294967295"
|
||||
.format(desc))
|
||||
|
||||
def __init__(self, s):
|
||||
super(MSCSTemplateV2, self).__init__(s)
|
||||
|
||||
parts = s.split(':')
|
||||
|
||||
obj = CertificateTemplateV2()
|
||||
if len(parts) < 2 or len(parts) > 3:
|
||||
raise ValueError(
|
||||
"Incorrect template specification; required format is: "
|
||||
"<oid>:<majorVersion>[:<minorVersion>]")
|
||||
try:
|
||||
obj['templateID'] = univ.ObjectIdentifier(parts[0])
|
||||
|
||||
major = int(parts[1])
|
||||
self.check_version_in_range("major", major)
|
||||
obj['templateMajorVersion'] = major
|
||||
|
||||
if len(parts) > 2:
|
||||
minor = int(parts[2])
|
||||
self.check_version_in_range("minor", minor)
|
||||
obj['templateMinorVersion'] = int(parts[2])
|
||||
|
||||
except pyasn1.error.PyAsn1Error:
|
||||
raise ValueError("Could not parse certificate template specifier.")
|
||||
self.asn1obj = obj
|
||||
|
||||
|
||||
class CertificateTemplateV2(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('templateID', univ.ObjectIdentifier()),
|
||||
namedtype.NamedType('templateMajorVersion', univ.Integer()),
|
||||
namedtype.OptionalNamedType('templateMinorVersion', univ.Integer())
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user