Imported Upstream version 4.6.2
This commit is contained in:
1
ipalib/Makefile.am
Normal file
1
ipalib/Makefile.am
Normal file
@@ -0,0 +1 @@
|
||||
include $(top_srcdir)/Makefile.python.am
|
||||
618
ipalib/Makefile.in
Normal file
618
ipalib/Makefile.in
Normal file
@@ -0,0 +1,618 @@
|
||||
# Makefile.in generated by automake 1.15.1 from Makefile.am.
|
||||
# @configure_input@
|
||||
|
||||
# Copyright (C) 1994-2017 Free Software Foundation, Inc.
|
||||
|
||||
# This Makefile.in is free software; the Free Software Foundation
|
||||
# gives unlimited permission to copy and/or distribute it,
|
||||
# with or without modifications, as long as this notice is preserved.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
|
||||
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
# PARTICULAR PURPOSE.
|
||||
|
||||
@SET_MAKE@
|
||||
VPATH = @srcdir@
|
||||
am__is_gnu_make = { \
|
||||
if test -z '$(MAKELEVEL)'; then \
|
||||
false; \
|
||||
elif test -n '$(MAKE_HOST)'; then \
|
||||
true; \
|
||||
elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
|
||||
true; \
|
||||
else \
|
||||
false; \
|
||||
fi; \
|
||||
}
|
||||
am__make_running_with_option = \
|
||||
case $${target_option-} in \
|
||||
?) ;; \
|
||||
*) echo "am__make_running_with_option: internal error: invalid" \
|
||||
"target option '$${target_option-}' specified" >&2; \
|
||||
exit 1;; \
|
||||
esac; \
|
||||
has_opt=no; \
|
||||
sane_makeflags=$$MAKEFLAGS; \
|
||||
if $(am__is_gnu_make); then \
|
||||
sane_makeflags=$$MFLAGS; \
|
||||
else \
|
||||
case $$MAKEFLAGS in \
|
||||
*\\[\ \ ]*) \
|
||||
bs=\\; \
|
||||
sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
|
||||
| sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
|
||||
esac; \
|
||||
fi; \
|
||||
skip_next=no; \
|
||||
strip_trailopt () \
|
||||
{ \
|
||||
flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
|
||||
}; \
|
||||
for flg in $$sane_makeflags; do \
|
||||
test $$skip_next = yes && { skip_next=no; continue; }; \
|
||||
case $$flg in \
|
||||
*=*|--*) continue;; \
|
||||
-*I) strip_trailopt 'I'; skip_next=yes;; \
|
||||
-*I?*) strip_trailopt 'I';; \
|
||||
-*O) strip_trailopt 'O'; skip_next=yes;; \
|
||||
-*O?*) strip_trailopt 'O';; \
|
||||
-*l) strip_trailopt 'l'; skip_next=yes;; \
|
||||
-*l?*) strip_trailopt 'l';; \
|
||||
-[dEDm]) skip_next=yes;; \
|
||||
-[JT]) skip_next=yes;; \
|
||||
esac; \
|
||||
case $$flg in \
|
||||
*$$target_option*) has_opt=yes; break;; \
|
||||
esac; \
|
||||
done; \
|
||||
test $$has_opt = yes
|
||||
am__make_dryrun = (target_option=n; $(am__make_running_with_option))
|
||||
am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
|
||||
pkgdatadir = $(datadir)/@PACKAGE@
|
||||
pkgincludedir = $(includedir)/@PACKAGE@
|
||||
pkglibdir = $(libdir)/@PACKAGE@
|
||||
pkglibexecdir = $(libexecdir)/@PACKAGE@
|
||||
am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
|
||||
install_sh_DATA = $(install_sh) -c -m 644
|
||||
install_sh_PROGRAM = $(install_sh) -c
|
||||
install_sh_SCRIPT = $(install_sh) -c
|
||||
INSTALL_HEADER = $(INSTALL_DATA)
|
||||
transform = $(program_transform_name)
|
||||
NORMAL_INSTALL = :
|
||||
PRE_INSTALL = :
|
||||
POST_INSTALL = :
|
||||
NORMAL_UNINSTALL = :
|
||||
PRE_UNINSTALL = :
|
||||
POST_UNINSTALL = :
|
||||
build_triplet = @build@
|
||||
host_triplet = @host@
|
||||
subdir = ipalib
|
||||
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
|
||||
am__aclocal_m4_deps = $(top_srcdir)/m4/gettext.m4 \
|
||||
$(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \
|
||||
$(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \
|
||||
$(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/libtool.m4 \
|
||||
$(top_srcdir)/m4/ltoptions.m4 $(top_srcdir)/m4/ltsugar.m4 \
|
||||
$(top_srcdir)/m4/ltversion.m4 $(top_srcdir)/m4/lt~obsolete.m4 \
|
||||
$(top_srcdir)/m4/nls.m4 $(top_srcdir)/m4/po.m4 \
|
||||
$(top_srcdir)/m4/progtest.m4 $(top_srcdir)/VERSION.m4 \
|
||||
$(top_srcdir)/server.m4 $(top_srcdir)/configure.ac
|
||||
am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
|
||||
$(ACLOCAL_M4)
|
||||
DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
|
||||
mkinstalldirs = $(install_sh) -d
|
||||
CONFIG_HEADER = $(top_builddir)/config.h
|
||||
CONFIG_CLEAN_FILES =
|
||||
CONFIG_CLEAN_VPATH_FILES =
|
||||
AM_V_P = $(am__v_P_@AM_V@)
|
||||
am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
|
||||
am__v_P_0 = false
|
||||
am__v_P_1 = :
|
||||
AM_V_GEN = $(am__v_GEN_@AM_V@)
|
||||
am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
|
||||
am__v_GEN_0 = @echo " GEN " $@;
|
||||
am__v_GEN_1 =
|
||||
AM_V_at = $(am__v_at_@AM_V@)
|
||||
am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
|
||||
am__v_at_0 = @
|
||||
am__v_at_1 =
|
||||
SOURCES =
|
||||
DIST_SOURCES =
|
||||
am__can_run_installinfo = \
|
||||
case $$AM_UPDATE_INFO_DIR in \
|
||||
n|no|NO) false;; \
|
||||
*) (install-info --version) >/dev/null 2>&1;; \
|
||||
esac
|
||||
am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
|
||||
am__DIST_COMMON = $(srcdir)/Makefile.in \
|
||||
$(top_srcdir)/Makefile.python.am
|
||||
DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
|
||||
ACLOCAL = @ACLOCAL@
|
||||
AMTAR = @AMTAR@
|
||||
AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
|
||||
API_VERSION = @API_VERSION@
|
||||
AR = @AR@
|
||||
AUTOCONF = @AUTOCONF@
|
||||
AUTOHEADER = @AUTOHEADER@
|
||||
AUTOMAKE = @AUTOMAKE@
|
||||
AWK = @AWK@
|
||||
CC = @CC@
|
||||
CCDEPMODE = @CCDEPMODE@
|
||||
CFLAGS = @CFLAGS@
|
||||
CMOCKA_CFLAGS = @CMOCKA_CFLAGS@
|
||||
CMOCKA_LIBS = @CMOCKA_LIBS@
|
||||
CONFIG_STATUS = @CONFIG_STATUS@
|
||||
CPP = @CPP@
|
||||
CPPFLAGS = @CPPFLAGS@
|
||||
CRYPTO_CFLAGS = @CRYPTO_CFLAGS@
|
||||
CRYPTO_LIBS = @CRYPTO_LIBS@
|
||||
CYGPATH_W = @CYGPATH_W@
|
||||
DATA_VERSION = @DATA_VERSION@
|
||||
DEFS = @DEFS@
|
||||
DEPDIR = @DEPDIR@
|
||||
DIRSRV_CFLAGS = @DIRSRV_CFLAGS@
|
||||
DIRSRV_LIBS = @DIRSRV_LIBS@
|
||||
DLLTOOL = @DLLTOOL@
|
||||
DSYMUTIL = @DSYMUTIL@
|
||||
DUMPBIN = @DUMPBIN@
|
||||
ECHO_C = @ECHO_C@
|
||||
ECHO_N = @ECHO_N@
|
||||
ECHO_T = @ECHO_T@
|
||||
EGREP = @EGREP@
|
||||
EXEEXT = @EXEEXT@
|
||||
FGREP = @FGREP@
|
||||
GETTEXT_DOMAIN = @GETTEXT_DOMAIN@
|
||||
GETTEXT_MACRO_VERSION = @GETTEXT_MACRO_VERSION@
|
||||
GIT_BRANCH = @GIT_BRANCH@
|
||||
GIT_VERSION = @GIT_VERSION@
|
||||
GMSGFMT = @GMSGFMT@
|
||||
GMSGFMT_015 = @GMSGFMT_015@
|
||||
GREP = @GREP@
|
||||
INI_CFLAGS = @INI_CFLAGS@
|
||||
INI_LIBS = @INI_LIBS@
|
||||
INSTALL = @INSTALL@
|
||||
INSTALL_DATA = @INSTALL_DATA@
|
||||
INSTALL_PROGRAM = @INSTALL_PROGRAM@
|
||||
INSTALL_SCRIPT = @INSTALL_SCRIPT@
|
||||
INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
|
||||
INTLLIBS = @INTLLIBS@
|
||||
INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@
|
||||
IPAPLATFORM = @IPAPLATFORM@
|
||||
IPA_DATA_DIR = @IPA_DATA_DIR@
|
||||
IPA_SYSCONF_DIR = @IPA_SYSCONF_DIR@
|
||||
JSLINT = @JSLINT@
|
||||
KRAD_LIBS = @KRAD_LIBS@
|
||||
KRB5KDC_SERVICE = @KRB5KDC_SERVICE@
|
||||
KRB5_CFLAGS = @KRB5_CFLAGS@
|
||||
KRB5_LIBS = @KRB5_LIBS@
|
||||
LD = @LD@
|
||||
LDAP_CFLAGS = @LDAP_CFLAGS@
|
||||
LDAP_LIBS = @LDAP_LIBS@
|
||||
LDFLAGS = @LDFLAGS@
|
||||
LIBICONV = @LIBICONV@
|
||||
LIBINTL = @LIBINTL@
|
||||
LIBINTL_LIBS = @LIBINTL_LIBS@
|
||||
LIBOBJS = @LIBOBJS@
|
||||
LIBPDB_NAME = @LIBPDB_NAME@
|
||||
LIBS = @LIBS@
|
||||
LIBTOOL = @LIBTOOL@
|
||||
LIBVERTO_CFLAGS = @LIBVERTO_CFLAGS@
|
||||
LIBVERTO_LIBS = @LIBVERTO_LIBS@
|
||||
LIPO = @LIPO@
|
||||
LN_S = @LN_S@
|
||||
LTLIBICONV = @LTLIBICONV@
|
||||
LTLIBINTL = @LTLIBINTL@
|
||||
LTLIBOBJS = @LTLIBOBJS@
|
||||
LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
|
||||
MAKEINFO = @MAKEINFO@
|
||||
MANIFEST_TOOL = @MANIFEST_TOOL@
|
||||
MKDIR_P = @MKDIR_P@
|
||||
MK_ASSIGN = @MK_ASSIGN@
|
||||
MK_ELSE = @MK_ELSE@
|
||||
MK_ENDIF = @MK_ENDIF@
|
||||
MK_IFEQ = @MK_IFEQ@
|
||||
MSGATTRIB = @MSGATTRIB@
|
||||
MSGFMT = @MSGFMT@
|
||||
MSGFMT_015 = @MSGFMT_015@
|
||||
MSGMERGE = @MSGMERGE@
|
||||
NAMED_GROUP = @NAMED_GROUP@
|
||||
NDRNBT_CFLAGS = @NDRNBT_CFLAGS@
|
||||
NDRNBT_LIBS = @NDRNBT_LIBS@
|
||||
NDRPAC_CFLAGS = @NDRPAC_CFLAGS@
|
||||
NDRPAC_LIBS = @NDRPAC_LIBS@
|
||||
NDR_CFLAGS = @NDR_CFLAGS@
|
||||
NDR_LIBS = @NDR_LIBS@
|
||||
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_USER = @ODS_USER@
|
||||
OTOOL = @OTOOL@
|
||||
OTOOL64 = @OTOOL64@
|
||||
PACKAGE = @PACKAGE@
|
||||
PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
|
||||
PACKAGE_NAME = @PACKAGE_NAME@
|
||||
PACKAGE_STRING = @PACKAGE_STRING@
|
||||
PACKAGE_TARNAME = @PACKAGE_TARNAME@
|
||||
PACKAGE_URL = @PACKAGE_URL@
|
||||
PACKAGE_VERSION = @PACKAGE_VERSION@
|
||||
PATH_SEPARATOR = @PATH_SEPARATOR@
|
||||
PKG_CONFIG = @PKG_CONFIG@
|
||||
PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
|
||||
PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
|
||||
POPT_CFLAGS = @POPT_CFLAGS@
|
||||
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@
|
||||
PYTHON_PREFIX = @PYTHON_PREFIX@
|
||||
PYTHON_VERSION = @PYTHON_VERSION@
|
||||
RANLIB = @RANLIB@
|
||||
SAMBA40EXTRA_LIBPATH = @SAMBA40EXTRA_LIBPATH@
|
||||
SAMBAUTIL_CFLAGS = @SAMBAUTIL_CFLAGS@
|
||||
SAMBAUTIL_LIBS = @SAMBAUTIL_LIBS@
|
||||
SASL_CFLAGS = @SASL_CFLAGS@
|
||||
SASL_LIBS = @SASL_LIBS@
|
||||
SED = @SED@
|
||||
SET_MAKE = @SET_MAKE@
|
||||
SHELL = @SHELL@
|
||||
SSSCERTMAP_CFLAGS = @SSSCERTMAP_CFLAGS@
|
||||
SSSCERTMAP_LIBS = @SSSCERTMAP_LIBS@
|
||||
SSSIDMAP_CFLAGS = @SSSIDMAP_CFLAGS@
|
||||
SSSIDMAP_LIBS = @SSSIDMAP_LIBS@
|
||||
SSSNSSIDMAP_CFLAGS = @SSSNSSIDMAP_CFLAGS@
|
||||
SSSNSSIDMAP_LIBS = @SSSNSSIDMAP_LIBS@
|
||||
STRIP = @STRIP@
|
||||
TALLOC_CFLAGS = @TALLOC_CFLAGS@
|
||||
TALLOC_LIBS = @TALLOC_LIBS@
|
||||
TEVENT_CFLAGS = @TEVENT_CFLAGS@
|
||||
TEVENT_LIBS = @TEVENT_LIBS@
|
||||
UNISTRING_LIBS = @UNISTRING_LIBS@
|
||||
UNLINK = @UNLINK@
|
||||
USE_NLS = @USE_NLS@
|
||||
UUID_CFLAGS = @UUID_CFLAGS@
|
||||
UUID_LIBS = @UUID_LIBS@
|
||||
VENDOR_SUFFIX = @VENDOR_SUFFIX@
|
||||
VERSION = @VERSION@
|
||||
XGETTEXT = @XGETTEXT@
|
||||
XGETTEXT_015 = @XGETTEXT_015@
|
||||
XGETTEXT_EXTRA_OPTIONS = @XGETTEXT_EXTRA_OPTIONS@
|
||||
XMLRPC_CFLAGS = @XMLRPC_CFLAGS@
|
||||
XMLRPC_LIBS = @XMLRPC_LIBS@
|
||||
abs_builddir = @abs_builddir@
|
||||
abs_srcdir = @abs_srcdir@
|
||||
abs_top_builddir = @abs_top_builddir@
|
||||
abs_top_srcdir = @abs_top_srcdir@
|
||||
ac_ct_AR = @ac_ct_AR@
|
||||
ac_ct_CC = @ac_ct_CC@
|
||||
ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
|
||||
am__include = @am__include@
|
||||
am__leading_dot = @am__leading_dot@
|
||||
am__quote = @am__quote@
|
||||
am__tar = @am__tar@
|
||||
am__untar = @am__untar@
|
||||
bindir = @bindir@
|
||||
build = @build@
|
||||
build_alias = @build_alias@
|
||||
build_cpu = @build_cpu@
|
||||
build_os = @build_os@
|
||||
build_vendor = @build_vendor@
|
||||
builddir = @builddir@
|
||||
datadir = @datadir@
|
||||
datarootdir = @datarootdir@
|
||||
docdir = @docdir@
|
||||
dvidir = @dvidir@
|
||||
exec_prefix = @exec_prefix@
|
||||
host = @host@
|
||||
host_alias = @host_alias@
|
||||
host_cpu = @host_cpu@
|
||||
host_os = @host_os@
|
||||
host_vendor = @host_vendor@
|
||||
htmldir = @htmldir@
|
||||
i18ntests = @i18ntests@
|
||||
includedir = @includedir@
|
||||
infodir = @infodir@
|
||||
install_sh = @install_sh@
|
||||
krb5rundir = @krb5rundir@
|
||||
libdir = @libdir@
|
||||
libexecdir = @libexecdir@
|
||||
localedir = @localedir@
|
||||
localstatedir = @localstatedir@
|
||||
mandir = @mandir@
|
||||
mkdir_p = @mkdir_p@
|
||||
oldincludedir = @oldincludedir@
|
||||
pdfdir = @pdfdir@
|
||||
pkgpyexecdir = @pkgpyexecdir@
|
||||
pkgpythondir = $(pythondir)/$(pkgname)
|
||||
prefix = @prefix@
|
||||
program_transform_name = @program_transform_name@
|
||||
psdir = @psdir@
|
||||
pyexecdir = @pyexecdir@
|
||||
pythondir = @pythondir@
|
||||
sbindir = @sbindir@
|
||||
sharedstatedir = @sharedstatedir@
|
||||
srcdir = @srcdir@
|
||||
sysconfdir = @sysconfdir@
|
||||
sysconfenvdir = @sysconfenvdir@
|
||||
systemdsystemunitdir = @systemdsystemunitdir@
|
||||
systemdtmpfilesdir = @systemdtmpfilesdir@
|
||||
target_alias = @target_alias@
|
||||
top_build_prefix = @top_build_prefix@
|
||||
top_builddir = @top_builddir@
|
||||
top_srcdir = @top_srcdir@
|
||||
pkgname = $(shell basename "$(abs_srcdir)")
|
||||
@VERBOSE_MAKE_FALSE@VERBOSITY = "--quiet"
|
||||
@VERBOSE_MAKE_TRUE@VERBOSITY = "--verbose"
|
||||
WHEELDISTDIR = $(top_builddir)/dist/wheels
|
||||
all: all-am
|
||||
|
||||
.SUFFIXES:
|
||||
$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(top_srcdir)/Makefile.python.am $(am__configure_deps)
|
||||
@for dep in $?; do \
|
||||
case '$(am__configure_deps)' in \
|
||||
*$$dep*) \
|
||||
( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
|
||||
&& { if test -f $@; then exit 0; else break; fi; }; \
|
||||
exit 1;; \
|
||||
esac; \
|
||||
done; \
|
||||
echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign ipalib/Makefile'; \
|
||||
$(am__cd) $(top_srcdir) && \
|
||||
$(AUTOMAKE) --foreign ipalib/Makefile
|
||||
Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
|
||||
@case '$?' in \
|
||||
*config.status*) \
|
||||
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
|
||||
*) \
|
||||
echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \
|
||||
cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \
|
||||
esac;
|
||||
$(top_srcdir)/Makefile.python.am $(am__empty):
|
||||
|
||||
$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
|
||||
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
|
||||
|
||||
$(top_srcdir)/configure: $(am__configure_deps)
|
||||
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
|
||||
$(ACLOCAL_M4): $(am__aclocal_m4_deps)
|
||||
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
|
||||
$(am__aclocal_m4_deps):
|
||||
|
||||
mostlyclean-libtool:
|
||||
-rm -f *.lo
|
||||
|
||||
clean-libtool:
|
||||
-rm -rf .libs _libs
|
||||
tags TAGS:
|
||||
|
||||
ctags CTAGS:
|
||||
|
||||
cscope cscopelist:
|
||||
|
||||
|
||||
distdir: $(DISTFILES)
|
||||
@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
|
||||
topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
|
||||
list='$(DISTFILES)'; \
|
||||
dist_files=`for file in $$list; do echo $$file; done | \
|
||||
sed -e "s|^$$srcdirstrip/||;t" \
|
||||
-e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
|
||||
case $$dist_files in \
|
||||
*/*) $(MKDIR_P) `echo "$$dist_files" | \
|
||||
sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
|
||||
sort -u` ;; \
|
||||
esac; \
|
||||
for file in $$dist_files; do \
|
||||
if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
|
||||
if test -d $$d/$$file; then \
|
||||
dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
|
||||
if test -d "$(distdir)/$$file"; then \
|
||||
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
|
||||
fi; \
|
||||
if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
|
||||
cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
|
||||
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
|
||||
fi; \
|
||||
cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
|
||||
else \
|
||||
test -f "$(distdir)/$$file" \
|
||||
|| cp -p $$d/$$file "$(distdir)/$$file" \
|
||||
|| exit 1; \
|
||||
fi; \
|
||||
done
|
||||
$(MAKE) $(AM_MAKEFLAGS) \
|
||||
top_distdir="$(top_distdir)" distdir="$(distdir)" \
|
||||
dist-hook
|
||||
check-am: all-am
|
||||
check: check-am
|
||||
all-am: Makefile all-local
|
||||
installdirs:
|
||||
install: install-am
|
||||
install-exec: install-exec-am
|
||||
install-data: install-data-am
|
||||
uninstall: uninstall-am
|
||||
|
||||
install-am: all-am
|
||||
@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
|
||||
|
||||
installcheck: installcheck-am
|
||||
install-strip:
|
||||
if test -z '$(STRIP)'; then \
|
||||
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
|
||||
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
|
||||
install; \
|
||||
else \
|
||||
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
|
||||
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
|
||||
"INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
|
||||
fi
|
||||
mostlyclean-generic:
|
||||
|
||||
clean-generic:
|
||||
|
||||
distclean-generic:
|
||||
-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
|
||||
-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
|
||||
|
||||
maintainer-clean-generic:
|
||||
@echo "This command is intended for maintainers to use"
|
||||
@echo "it deletes files that may require special tools to rebuild."
|
||||
clean: clean-am
|
||||
|
||||
clean-am: clean-generic clean-libtool clean-local mostlyclean-am
|
||||
|
||||
distclean: distclean-am
|
||||
-rm -f Makefile
|
||||
distclean-am: clean-am distclean-generic
|
||||
|
||||
dvi: dvi-am
|
||||
|
||||
dvi-am:
|
||||
|
||||
html: html-am
|
||||
|
||||
html-am:
|
||||
|
||||
info: info-am
|
||||
|
||||
info-am:
|
||||
|
||||
install-data-am:
|
||||
|
||||
install-dvi: install-dvi-am
|
||||
|
||||
install-dvi-am:
|
||||
|
||||
install-exec-am: install-exec-local
|
||||
|
||||
install-html: install-html-am
|
||||
|
||||
install-html-am:
|
||||
|
||||
install-info: install-info-am
|
||||
|
||||
install-info-am:
|
||||
|
||||
install-man:
|
||||
|
||||
install-pdf: install-pdf-am
|
||||
|
||||
install-pdf-am:
|
||||
|
||||
install-ps: install-ps-am
|
||||
|
||||
install-ps-am:
|
||||
|
||||
installcheck-am:
|
||||
|
||||
maintainer-clean: maintainer-clean-am
|
||||
-rm -f Makefile
|
||||
maintainer-clean-am: distclean-am maintainer-clean-generic
|
||||
|
||||
mostlyclean: mostlyclean-am
|
||||
|
||||
mostlyclean-am: mostlyclean-generic mostlyclean-libtool
|
||||
|
||||
pdf: pdf-am
|
||||
|
||||
pdf-am:
|
||||
|
||||
ps: ps-am
|
||||
|
||||
ps-am:
|
||||
|
||||
uninstall-am: uninstall-local
|
||||
|
||||
.MAKE: install-am install-strip
|
||||
|
||||
.PHONY: all all-am all-local check check-am clean clean-generic \
|
||||
clean-libtool clean-local cscopelist-am ctags-am dist-hook \
|
||||
distclean distclean-generic distclean-libtool distdir dvi \
|
||||
dvi-am html html-am info info-am install install-am \
|
||||
install-data install-data-am install-dvi install-dvi-am \
|
||||
install-exec install-exec-am install-exec-local install-html \
|
||||
install-html-am install-info install-info-am install-man \
|
||||
install-pdf install-pdf-am install-ps install-ps-am \
|
||||
install-strip installcheck installcheck-am installdirs \
|
||||
maintainer-clean maintainer-clean-generic mostlyclean \
|
||||
mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
|
||||
tags-am uninstall uninstall-am uninstall-local
|
||||
|
||||
.PRECIOUS: Makefile
|
||||
|
||||
|
||||
# hack to handle back-in-the-hierarchy depedency on ipasetup.py
|
||||
.PHONY: $(top_builddir)/ipasetup.py
|
||||
$(top_builddir)/ipasetup.py:
|
||||
(cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) ipasetup.py)
|
||||
|
||||
all-local: $(top_builddir)/ipasetup.py
|
||||
cd $(srcdir); $(PYTHON) setup.py \
|
||||
$(VERBOSITY) \
|
||||
build \
|
||||
--build-base "$(abs_builddir)/build"
|
||||
|
||||
install-exec-local: $(top_builddir)/ipasetup.py
|
||||
if [ "x$(pkginstall)" != "xfalse" ]; then \
|
||||
$(PYTHON) $(srcdir)/setup.py \
|
||||
$(VERBOSITY) \
|
||||
build \
|
||||
--build-base "$(abs_builddir)/build" \
|
||||
install \
|
||||
--prefix "$(DESTDIR)$(prefix)" \
|
||||
--single-version-externally-managed \
|
||||
--record "$(DESTDIR)$(pkgpythondir)/install_files.txt" \
|
||||
--optimize 1 \
|
||||
$(PYTHON_INSTALL_EXTRA_OPTIONS); \
|
||||
fi
|
||||
|
||||
uninstall-local:
|
||||
if [ -f "$(DESTDIR)$(pkgpythondir)/install_files.txt" ]; then \
|
||||
cat "$(DESTDIR)$(pkgpythondir)/install_files.txt" | xargs rm -rf ; \
|
||||
fi
|
||||
rm -rf "$(DESTDIR)$(pkgpythondir)"
|
||||
|
||||
clean-local: $(top_builddir)/ipasetup.py
|
||||
$(PYTHON) "$(srcdir)/setup.py" \
|
||||
clean \
|
||||
--all
|
||||
--build-base "$(abs_builddir)/build"
|
||||
rm -rf "$(srcdir)/build" "$(srcdir)/dist" "$(srcdir)/MANIFEST"
|
||||
find "$(srcdir)" \
|
||||
-name "*.py[co]" -delete -o \
|
||||
-name "__pycache__" -delete -o \
|
||||
-name "*.egg-info" -exec rm -rf {} +
|
||||
|
||||
# take list of all Python source files and copy them into distdir
|
||||
# SOURCES.txt does not contain directories so we need to create those
|
||||
dist-hook: $(top_builddir)/ipasetup.py
|
||||
$(PYTHON) "$(srcdir)/setup.py" egg_info
|
||||
PYTHON_SOURCES=$$(cat "$(srcdir)/$(pkgname).egg-info/SOURCES.txt") || exit $$?; \
|
||||
for FILEN in $${PYTHON_SOURCES}; \
|
||||
do \
|
||||
if test -x "$(srcdir)/$${FILEN}"; then MODE=755; else MODE=644; fi; \
|
||||
$(INSTALL) -D -m $${MODE} "$(srcdir)/$${FILEN}" "$(distdir)/$${FILEN}" || exit $$?; \
|
||||
done
|
||||
.PHONY: bdist_wheel
|
||||
bdist_wheel: $(top_builddir)/ipasetup.py
|
||||
rm -rf $(WHEELDISTDIR)/$(pkgname)-*.whl
|
||||
$(PYTHON) "$(srcdir)/setup.py" \
|
||||
build \
|
||||
--build-base "$(abs_builddir)/build" \
|
||||
bdist_wheel \
|
||||
--dist-dir=$(WHEELDISTDIR)
|
||||
|
||||
# Tell versions [3.59,3.63) of GNU make to not export all variables.
|
||||
# Otherwise a system limit (for SysV at least) may be exceeded.
|
||||
.NOEXPORT:
|
||||
983
ipalib/__init__.py
Normal file
983
ipalib/__init__.py
Normal file
@@ -0,0 +1,983 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
'''
|
||||
Package containing the core library.
|
||||
|
||||
=============================
|
||||
Tutorial for Plugin Authors
|
||||
=============================
|
||||
|
||||
This tutorial will introduce you to writing plugins for freeIPA v2. It does
|
||||
not cover every detail, but it provides enough to get you started and is
|
||||
heavily cross-referenced with further documentation that (hopefully) fills
|
||||
in the missing details.
|
||||
|
||||
In addition to this tutorial, the many built-in plugins in `ipalib.plugins`
|
||||
and `ipaserver.plugins` provide real-life examples of how to write good
|
||||
plugins.
|
||||
|
||||
|
||||
----------------------------
|
||||
How this tutorial is written
|
||||
----------------------------
|
||||
|
||||
The code examples in this tutorial are presented as if entered into a Python
|
||||
interactive interpreter session. As such, when you create a real plugin in
|
||||
a source file, a few details will be different (in addition to the fact that
|
||||
you will never include the ``>>>`` nor ``...`` that the interpreter places at
|
||||
the beginning of each line of code).
|
||||
|
||||
The tutorial examples all have this pattern:
|
||||
|
||||
::
|
||||
|
||||
>>> from ipalib import Command, create_api
|
||||
>>> api = create_api()
|
||||
>>> class my_command(Command):
|
||||
... pass
|
||||
...
|
||||
>>> api.add_plugin(my_command)
|
||||
>>> api.finalize()
|
||||
|
||||
In the tutorial we call `create_api()` to create an *example* instance
|
||||
of `plugable.API` to work with. But a real plugin will simply use
|
||||
``ipalib.api``, the standard run-time instance of `plugable.API`.
|
||||
|
||||
A real plugin will have this pattern:
|
||||
|
||||
::
|
||||
|
||||
from ipalib import Command, Registry, api
|
||||
|
||||
register = Registry()
|
||||
|
||||
@register()
|
||||
class my_command(Command):
|
||||
pass
|
||||
|
||||
As seen above, also note that in a real plugin you will *not* call
|
||||
`plugable.API.finalize()`. When in doubt, look at some of the built-in
|
||||
plugins for guidance, like those in `ipalib.plugins`.
|
||||
|
||||
If you don't know what the Python *interactive interpreter* is, or are
|
||||
confused about what this *Python* is in the first place, then you probably
|
||||
should start with the Python tutorial:
|
||||
|
||||
http://docs.python.org/tutorial/index.html
|
||||
|
||||
|
||||
------------------------------------
|
||||
First steps: A simple command plugin
|
||||
------------------------------------
|
||||
|
||||
Our first example will create the most basic command plugin possible. This
|
||||
command will be seen in the list of command plugins, but it wont be capable
|
||||
of actually doing anything yet.
|
||||
|
||||
A command plugin simultaneously adds a new command that can be called through
|
||||
the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are
|
||||
one in the same, simply invoked in different ways.
|
||||
|
||||
A freeIPA plugin is a Python class, and when you create a plugin, you register
|
||||
this class itself (instead of an instance of the class). To be a command
|
||||
plugin, your plugin must subclass from `frontend.Command` (or from a subclass
|
||||
thereof). Here is our first example:
|
||||
|
||||
>>> from ipalib import Command, create_api
|
||||
>>> api = create_api()
|
||||
>>> class my_command(Command): # Step 1, define class
|
||||
... """My example plugin."""
|
||||
...
|
||||
>>> api.add_plugin(my_command) # Step 2, register class
|
||||
|
||||
Notice that we are registering the ``my_command`` class itself, not an
|
||||
instance of ``my_command``.
|
||||
|
||||
Until `plugable.API.finalize()` is called, your plugin class has not been
|
||||
instantiated nor does the ``Command`` namespace yet exist. For example:
|
||||
|
||||
>>> hasattr(api, 'Command')
|
||||
False
|
||||
>>> api.finalize() # plugable.API.finalize()
|
||||
>>> hasattr(api.Command, 'my_command')
|
||||
True
|
||||
>>> api.Command.my_command.doc
|
||||
Gettext('My example plugin.', domain='ipa', localedir=None)
|
||||
|
||||
Notice that your plugin instance is accessed through an attribute named
|
||||
``my_command``, the same name as your plugin class name.
|
||||
|
||||
|
||||
------------------------------
|
||||
Make your command do something
|
||||
------------------------------
|
||||
|
||||
This simplest way to make your example command plugin do something is to
|
||||
implement a ``run()`` method, like this:
|
||||
|
||||
>>> class my_command(Command):
|
||||
... """My example plugin with run()."""
|
||||
...
|
||||
... def run(self, **options):
|
||||
... return dict(result='My run() method was called!')
|
||||
...
|
||||
>>> api = create_api()
|
||||
>>> api.add_plugin(my_command)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.my_command(version=u'2.47') # Call your command
|
||||
{'result': 'My run() method was called!'}
|
||||
|
||||
When `frontend.Command.__call__()` is called, it first validates any arguments
|
||||
and options your command plugin takes (if any) and then calls its ``run()``
|
||||
method.
|
||||
|
||||
|
||||
------------------------
|
||||
Forwarding vs. execution
|
||||
------------------------
|
||||
|
||||
However, unlike the example above, a typical command plugin will implement an
|
||||
``execute()`` method instead of a ``run()`` method. Your command plugin can
|
||||
be loaded in two distinct contexts:
|
||||
|
||||
1. In a *client* context - Your command plugin is only used to validate
|
||||
any arguments and options it takes, and then ``self.forward()`` is
|
||||
called, which forwards the call over XML-RPC to an IPA server where
|
||||
the actual work is done.
|
||||
|
||||
2. In a *server* context - Your same command plugin validates any
|
||||
arguments and options it takes, and then ``self.execute()`` is called,
|
||||
which you should implement to perform whatever work your plugin does.
|
||||
|
||||
The base `frontend.Command.run()` method simply dispatches the call to
|
||||
``self.execute()`` if ``self.env.in_server`` is True, or otherwise
|
||||
dispatches the call to ``self.forward()``.
|
||||
|
||||
For example, say you have a command plugin like this:
|
||||
|
||||
>>> class my_command(Command):
|
||||
... """Forwarding vs. execution."""
|
||||
...
|
||||
... def forward(self, **options):
|
||||
... return dict(
|
||||
... result='forward(): in_server=%r' % self.env.in_server
|
||||
... )
|
||||
...
|
||||
... def execute(self, **options):
|
||||
... return dict(
|
||||
... result='execute(): in_server=%r' % self.env.in_server
|
||||
... )
|
||||
...
|
||||
|
||||
The ``options`` will contain a dict of command options. One option is added
|
||||
automatically: ``version``. It contains the API version of the client.
|
||||
In order to maintain forward compatibility, you should always specify the
|
||||
API version current at the time you're writing your client.
|
||||
|
||||
If ``my_command`` is loaded in a *client* context, ``forward()`` will be
|
||||
called:
|
||||
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = False # run() will dispatch to forward()
|
||||
>>> api.add_plugin(my_command)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.my_command(version=u'2.47') # Call your command plugin
|
||||
{'result': 'forward(): in_server=False'}
|
||||
|
||||
On the other hand, if ``my_command`` is loaded in a *server* context,
|
||||
``execute()`` will be called:
|
||||
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = True # run() will dispatch to execute()
|
||||
>>> api.add_plugin(my_command)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.my_command(version=u'2.47') # Call your command plugin
|
||||
{'result': 'execute(): in_server=True'}
|
||||
|
||||
Normally there should be no reason to override `frontend.Command.forward()`,
|
||||
but, as above, it can be done for demonstration purposes. In contrast, there
|
||||
*is* a reason you might want to override `frontend.Command.run()`: if it only
|
||||
makes sense to execute your command locally, if it should never be forwarded
|
||||
to the server. In this case, you should implement your *do-stuff* in the
|
||||
``run()`` method instead of in the ``execute()`` method.
|
||||
|
||||
For example, the ``ipa`` command line script has a ``help`` command
|
||||
(`ipalib.cli.help`) that is specific to the command-line-interface and should
|
||||
never be forwarded to the server.
|
||||
|
||||
|
||||
---------------
|
||||
Backend plugins
|
||||
---------------
|
||||
|
||||
There are two types of plugins:
|
||||
|
||||
1. *Frontend plugins* - These are loaded in both the *client* and *server*
|
||||
contexts. These need to be installed with any application built atop
|
||||
the `ipalib` library. The built-in frontend plugins can be found in
|
||||
`ipalib.plugins`. The ``my_command`` example above is a frontend
|
||||
plugin.
|
||||
|
||||
2. *Backend plugins* - These are only loaded in a *server* context and
|
||||
only need to be installed on the IPA server. The built-in backend
|
||||
plugins can be found in `ipaserver.plugins`.
|
||||
|
||||
Backend plugins should provide a set of methods that standardize how IPA
|
||||
interacts with some external system or library. For example, all interaction
|
||||
with LDAP is done through the ``ldap`` backend plugin defined in
|
||||
`ipaserver.plugins.b_ldap`. As a good rule of thumb, anytime you need to
|
||||
import some package that is not part of the Python standard library, you
|
||||
should probably interact with that package via a corresponding backend
|
||||
plugin you implement.
|
||||
|
||||
Backend plugins are much more free-form than command plugins. Aside from a
|
||||
few reserved attribute names, you can define arbitrary public methods on your
|
||||
backend plugin.
|
||||
|
||||
Here is a simple example:
|
||||
|
||||
>>> from ipalib import Backend
|
||||
>>> class my_backend(Backend):
|
||||
... """My example backend plugin."""
|
||||
...
|
||||
... def do_stuff(self):
|
||||
... """Part of your API."""
|
||||
... return 'Stuff got done.'
|
||||
...
|
||||
>>> api = create_api()
|
||||
>>> api.add_plugin(my_backend)
|
||||
>>> api.finalize()
|
||||
>>> api.Backend.my_backend.do_stuff()
|
||||
'Stuff got done.'
|
||||
|
||||
|
||||
-------------------------------
|
||||
How your command should do work
|
||||
-------------------------------
|
||||
|
||||
We now return to our ``my_command`` plugin example.
|
||||
|
||||
Plugins are separated into frontend and backend plugins so that there are not
|
||||
unnecessary dependencies required by an application that only uses `ipalib` and
|
||||
its built-in frontend plugins (and then forwards over XML-RPC for execution).
|
||||
|
||||
But how do we avoid introducing additional dependencies? For example, the
|
||||
``user_add`` command needs to talk to LDAP to add the user, yet we want to
|
||||
somehow load the ``user_add`` plugin on client machines without requiring the
|
||||
``python-ldap`` package (Python bindings to openldap) to be installed. To
|
||||
answer that, we consult our golden rule:
|
||||
|
||||
**The golden rule:** A command plugin should implement its ``execute()``
|
||||
method strictly via calls to methods on one or more backend plugins.
|
||||
|
||||
So the module containing the ``user_add`` command does not itself import the
|
||||
Python LDAP bindings, only the module containing the ``ldap`` backend plugin
|
||||
does that, and the backend plugins are only installed on the server. The
|
||||
``user_add.execute()`` method, which is only called when in a server context,
|
||||
is implemented as a series of calls to methods on the ``ldap`` backend plugin.
|
||||
|
||||
When `plugable.Plugin.__init__()` is called, each plugin stores a reference to
|
||||
the `plugable.API` instance it has been loaded into. So your plugin can
|
||||
access the ``my_backend`` plugin as ``self.api.Backend.my_backend``.
|
||||
|
||||
Additionally, convenience attributes are set for each namespace, so your
|
||||
plugin can also access the ``my_backend`` plugin as simply
|
||||
``self.Backend.my_backend``.
|
||||
|
||||
This next example will tie everything together. First we create our backend
|
||||
plugin:
|
||||
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = True # We want to execute, not forward
|
||||
>>> class my_backend(Backend):
|
||||
... """My example backend plugin."""
|
||||
...
|
||||
... def do_stuff(self):
|
||||
... """my_command.execute() calls this."""
|
||||
... return 'my_backend.do_stuff() indeed did do stuff!'
|
||||
...
|
||||
>>> api.add_plugin(my_backend)
|
||||
|
||||
Second, we have our frontend plugin, the command:
|
||||
|
||||
>>> class my_command(Command):
|
||||
... """My example command plugin."""
|
||||
...
|
||||
... def execute(self, **options):
|
||||
... """Implemented against Backend.my_backend"""
|
||||
... return dict(result=self.Backend.my_backend.do_stuff())
|
||||
...
|
||||
>>> api.add_plugin(my_command)
|
||||
|
||||
Lastly, we call ``api.finalize()`` and see what happens when we call
|
||||
``my_command()``:
|
||||
|
||||
>>> api.finalize()
|
||||
>>> api.Command.my_command(version=u'2.47')
|
||||
{'result': 'my_backend.do_stuff() indeed did do stuff!'}
|
||||
|
||||
When not in a server context, ``my_command.execute()`` never gets called, so
|
||||
it never tries to access the non-existent backend plugin at
|
||||
``self.Backend.my_backend.`` To emphasize this point, here is one last
|
||||
example:
|
||||
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = False # We want to forward, not execute
|
||||
>>> class my_command(Command):
|
||||
... """My example command plugin."""
|
||||
...
|
||||
... def execute(self, **options):
|
||||
... """Same as above."""
|
||||
... return dict(result=self.Backend.my_backend.do_stuff())
|
||||
...
|
||||
... def forward(self, **options):
|
||||
... return dict(result='Just my_command.forward() getting called here.')
|
||||
...
|
||||
>>> api.add_plugin(my_command)
|
||||
>>> api.finalize()
|
||||
|
||||
Notice that the ``my_backend`` plugin has certainly not be registered:
|
||||
|
||||
>>> hasattr(api.Backend, 'my_backend')
|
||||
False
|
||||
|
||||
And yet we can call ``my_command()``:
|
||||
|
||||
>>> api.Command.my_command(version=u'2.47')
|
||||
{'result': 'Just my_command.forward() getting called here.'}
|
||||
|
||||
|
||||
----------------------------------------
|
||||
Calling other commands from your command
|
||||
----------------------------------------
|
||||
|
||||
It can be useful to have your ``execute()`` method call other command plugins.
|
||||
Among other things, this allows for meta-commands that conveniently call
|
||||
several other commands in a single operation. For example:
|
||||
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = True # We want to execute, not forward
|
||||
>>> class meta_command(Command):
|
||||
... """My meta-command plugin."""
|
||||
...
|
||||
... def execute(self, **options):
|
||||
... """Calls command_1(), command_2()"""
|
||||
... msg = '%s; %s.' % (
|
||||
... self.Command.command_1()['result'],
|
||||
... self.Command.command_2()['result'],
|
||||
... )
|
||||
... return dict(result=msg)
|
||||
>>> class command_1(Command):
|
||||
... def execute(self, **options):
|
||||
... return dict(result='command_1.execute() called')
|
||||
...
|
||||
>>> class command_2(Command):
|
||||
... def execute(self, **options):
|
||||
... return dict(result='command_2.execute() called')
|
||||
...
|
||||
>>> api.add_plugin(meta_command)
|
||||
>>> api.add_plugin(command_1)
|
||||
>>> api.add_plugin(command_2)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.meta_command(version=u'2.47')
|
||||
{'result': 'command_1.execute() called; command_2.execute() called.'}
|
||||
|
||||
Because this is quite useful, we are going to revise our golden rule somewhat:
|
||||
|
||||
**The revised golden rule:** A command plugin should implement its
|
||||
``execute()`` method strictly via what it can access through ``self.api``,
|
||||
most likely via the backend plugins in ``self.api.Backend`` (which can also
|
||||
be conveniently accessed as ``self.Backend``).
|
||||
|
||||
|
||||
-----------------------------------------------
|
||||
Defining arguments and options for your command
|
||||
-----------------------------------------------
|
||||
|
||||
You can define a command that will accept specific arguments and options.
|
||||
For example:
|
||||
|
||||
>>> from ipalib import Str
|
||||
>>> class nudge(Command):
|
||||
... """Takes one argument, one option"""
|
||||
...
|
||||
... takes_args = ('programmer',)
|
||||
...
|
||||
... takes_options = (Str('stuff', default=u'documentation'))
|
||||
...
|
||||
... def execute(self, programmer, **kw):
|
||||
... return dict(
|
||||
... result='%s, go write more %s!' % (programmer, kw['stuff'])
|
||||
... )
|
||||
...
|
||||
>>> api = create_api()
|
||||
>>> api.env.in_server = True
|
||||
>>> api.add_plugin(nudge)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.nudge(u'Jason', version=u'2.47')
|
||||
{'result': u'Jason, go write more documentation!'}
|
||||
>>> api.Command.nudge(u'Jason', stuff=u'unit tests', version=u'2.47')
|
||||
{'result': u'Jason, go write more unit tests!'}
|
||||
|
||||
The ``args`` and ``options`` attributes are `plugable.NameSpace` instances
|
||||
containing a command's arguments and options, respectively, as you can see:
|
||||
|
||||
>>> list(api.Command.nudge.args) # Iterates through argument names
|
||||
['programmer']
|
||||
>>> api.Command.nudge.args.programmer
|
||||
Str('programmer')
|
||||
>>> list(api.Command.nudge.options) # Iterates through option names
|
||||
['stuff', 'version']
|
||||
>>> api.Command.nudge.options.stuff
|
||||
Str('stuff', default=u'documentation')
|
||||
>>> api.Command.nudge.options.stuff.default
|
||||
u'documentation'
|
||||
|
||||
The 'version' option is added to commands automatically.
|
||||
|
||||
The arguments and options must not contain colliding names. They are both
|
||||
merged together into the ``params`` attribute, another `plugable.NameSpace`
|
||||
instance, as you can see:
|
||||
|
||||
>>> api.Command.nudge.params
|
||||
NameSpace(<3 members>, sort=False)
|
||||
>>> list(api.Command.nudge.params) # Iterates through the param names
|
||||
['programmer', 'stuff', 'version']
|
||||
|
||||
When calling a command, its positional arguments can also be provided as
|
||||
keyword arguments, and in any order. For example:
|
||||
|
||||
>>> api.Command.nudge(stuff=u'lines of code', programmer=u'Jason', version=u'2.47')
|
||||
{'result': u'Jason, go write more lines of code!'}
|
||||
|
||||
When a command plugin is called, the values supplied for its parameters are
|
||||
put through a sophisticated processing pipeline that includes steps for
|
||||
normalization, type conversion, validation, and dynamically constructing
|
||||
the defaults for missing values. The details wont be covered here; however,
|
||||
here is a quick teaser:
|
||||
|
||||
>>> from ipalib import Int
|
||||
>>> class create_player(Command):
|
||||
... takes_options = (
|
||||
... 'first',
|
||||
... 'last',
|
||||
... Str('nick',
|
||||
... normalizer=lambda value: value.lower(),
|
||||
... default_from=lambda first, last: first[0] + last,
|
||||
... ),
|
||||
... Int('points', default=0),
|
||||
... )
|
||||
...
|
||||
>>> cp = create_player()
|
||||
>>> cp.finalize()
|
||||
>>> cp.convert(points=u' 1000 ')
|
||||
{'points': 1000}
|
||||
>>> cp.normalize(nick=u'NickName')
|
||||
{'nick': u'nickname'}
|
||||
>>> cp.get_default(first=u'Jason', last=u'DeRose')
|
||||
{'nick': u'jderose', 'points': 0}
|
||||
|
||||
For the full details on the parameter system, see the
|
||||
`frontend.parse_param_spec()` function, and the `frontend.Param` and
|
||||
`frontend.Command` classes.
|
||||
|
||||
|
||||
---------------------------------------
|
||||
Allowed return values from your command
|
||||
---------------------------------------
|
||||
|
||||
The return values from your command can be rendered by different user
|
||||
interfaces (CLI, web-UI); furthermore, a call to your command can be
|
||||
transparently forwarded over the network (XML-RPC, JSON). As such, the return
|
||||
values from your command must be usable by the least common denominator.
|
||||
|
||||
Your command should return only simple data types and simple data structures,
|
||||
the kinds that can be represented in an XML-RPC request or in the JSON format.
|
||||
The return values from your command's ``execute()`` method can include only
|
||||
the following:
|
||||
|
||||
Simple scalar values:
|
||||
These can be ``str``, ``unicode``, ``int``, and ``float`` instances,
|
||||
plus the ``True``, ``False``, and ``None`` constants.
|
||||
|
||||
Simple compound values:
|
||||
These can be ``dict``, ``list``, and ``tuple`` instances. These
|
||||
compound values must contain only the simple scalar values above or
|
||||
other simple compound values. These compound values can also be empty.
|
||||
For our purposes here, the ``list`` and ``tuple`` types are equivalent
|
||||
and can be used interchangeably.
|
||||
|
||||
Also note that your ``execute()`` method should not contain any ``print``
|
||||
statements or otherwise cause any output on ``sys.stdout``. Your command can
|
||||
(and should) produce log messages by using a module-level logger (see below).
|
||||
|
||||
To learn more about XML-RPC (XML Remote Procedure Call), see:
|
||||
|
||||
http://docs.python.org/library/xmlrpclib.html
|
||||
|
||||
http://en.wikipedia.org/wiki/XML-RPC
|
||||
|
||||
To learn more about JSON (Java Script Object Notation), see:
|
||||
|
||||
http://docs.python.org/library/json.html
|
||||
|
||||
http://www.json.org/
|
||||
|
||||
|
||||
---------------------------------------
|
||||
How your command should print to stdout
|
||||
---------------------------------------
|
||||
|
||||
As noted above, your command should not print anything while in its
|
||||
``execute()`` method. So how does your command format its output when
|
||||
called from the ``ipa`` script?
|
||||
|
||||
After the `cli.CLI.run_cmd()` method calls your command, it will call your
|
||||
command's ``output_for_cli()`` method (if you have implemented one).
|
||||
|
||||
If you implement an ``output_for_cli()`` method, it must have the following
|
||||
signature:
|
||||
|
||||
::
|
||||
|
||||
output_for_cli(textui, result, *args, **options)
|
||||
|
||||
textui
|
||||
An object implementing methods for outputting to the console.
|
||||
Currently the `ipalib.cli.textui` plugin is passed, which your method
|
||||
can also access as ``self.Backend.textui``. However, in case this
|
||||
changes in the future, your method should use the instance passed to
|
||||
it in this first argument.
|
||||
|
||||
result
|
||||
This is the return value from calling your command plugin. Depending
|
||||
upon how your command is implemented, this is probably the return
|
||||
value from your ``execute()`` method.
|
||||
|
||||
args
|
||||
The arguments your command was called with. If your command takes no
|
||||
arguments, you can omit this. You can also explicitly list your
|
||||
arguments rather than using the generic ``*args`` form.
|
||||
|
||||
options
|
||||
The options your command was called with. If your command takes no
|
||||
options, you can omit this. If your command takes any options, you
|
||||
must use the ``**options`` form as they will be provided strictly as
|
||||
keyword arguments.
|
||||
|
||||
For example, say we setup a command like this:
|
||||
|
||||
>>> class show_items(Command):
|
||||
...
|
||||
... takes_args = ('key?',)
|
||||
...
|
||||
... takes_options = (Flag('reverse'),)
|
||||
...
|
||||
... def execute(self, key, **options):
|
||||
... items = dict(
|
||||
... fruit=u'apple',
|
||||
... pet=u'dog',
|
||||
... city=u'Berlin',
|
||||
... )
|
||||
... if key in items:
|
||||
... return dict(result=items[key])
|
||||
... items = [
|
||||
... (k, items[k]) for k in sorted(items, reverse=options['reverse'])
|
||||
... ]
|
||||
... return dict(result=items)
|
||||
...
|
||||
... def output_for_cli(self, textui, result, key, **options):
|
||||
... result = result['result']
|
||||
... if key is not None:
|
||||
... textui.print_plain('%s = %r' % (key, result))
|
||||
... else:
|
||||
... textui.print_name(self.name)
|
||||
... textui.print_keyval(result)
|
||||
... format = '%d items'
|
||||
... if options['reverse']:
|
||||
... format += ' (in reverse order)'
|
||||
... textui.print_count(result, format)
|
||||
...
|
||||
>>> api = create_api()
|
||||
>>> api.bootstrap(in_server=True) # We want to execute, not forward
|
||||
>>> api.add_plugin(show_items)
|
||||
>>> api.finalize()
|
||||
|
||||
Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will
|
||||
register the `cli.textui` backend plugin, but for the sake of our example,
|
||||
we will just create an instance here:
|
||||
|
||||
>>> from ipalib import cli
|
||||
>>> textui = cli.textui() # We'll pass this to output_for_cli()
|
||||
|
||||
Now for what we are concerned with in this example, calling your command
|
||||
through the ``ipa`` script basically will do the following:
|
||||
|
||||
>>> result = api.Command.show_items()
|
||||
>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=False)
|
||||
-----------
|
||||
show-items:
|
||||
-----------
|
||||
city = u'Berlin'
|
||||
fruit = u'apple'
|
||||
pet = u'dog'
|
||||
-------
|
||||
3 items
|
||||
-------
|
||||
|
||||
Similarly, calling it with ``reverse=True`` would result in the following:
|
||||
|
||||
>>> result = api.Command.show_items(reverse=True)
|
||||
>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=True)
|
||||
-----------
|
||||
show-items:
|
||||
-----------
|
||||
pet = u'dog'
|
||||
fruit = u'apple'
|
||||
city = u'Berlin'
|
||||
--------------------------
|
||||
3 items (in reverse order)
|
||||
--------------------------
|
||||
|
||||
Lastly, providing a ``key`` would result in the following:
|
||||
|
||||
>>> result = api.Command.show_items(u'city')
|
||||
>>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False)
|
||||
city = u'Berlin'
|
||||
|
||||
See the `ipalib.cli.textui` plugin for a description of its methods.
|
||||
|
||||
|
||||
------------------------
|
||||
Logging from your plugin
|
||||
------------------------
|
||||
|
||||
Plugins should log through a module-level logger.
|
||||
For example:
|
||||
|
||||
>>> import logging
|
||||
>>> logger = logging.getLogger(__name__)
|
||||
>>> class paint_house(Command):
|
||||
...
|
||||
... takes_args = 'color'
|
||||
...
|
||||
... def execute(self, color, **options):
|
||||
... """Uses logger.error()"""
|
||||
... if color not in ('red', 'blue', 'green'):
|
||||
... logger.error("I don't have %s paint!", color) # Log error
|
||||
... return
|
||||
... return 'I painted the house %s.' % color
|
||||
...
|
||||
|
||||
Some basic knowledge of the Python ``logging`` module might be helpful. See:
|
||||
|
||||
http://docs.python.org/library/logging.html
|
||||
|
||||
The important thing to remember is that your plugin should not configure
|
||||
logging itself, but should instead simply use the module-level logger.
|
||||
|
||||
Also see the `plugable.API.bootstrap()` method for details on how the logging
|
||||
is configured.
|
||||
|
||||
|
||||
---------------------
|
||||
Environment variables
|
||||
---------------------
|
||||
|
||||
Plugins access configuration variables and run-time information through
|
||||
``self.api.env`` (or for convenience, ``self.env`` is equivalent). This
|
||||
attribute is a refences to the `ipalib.config.Env` instance created in
|
||||
`plugable.API.__init__()`.
|
||||
|
||||
After `API.bootstrap()` has been called, the `Env` instance will be populated
|
||||
with all the environment information used by the built-in plugins.
|
||||
This will be called before any plugins are registered, so plugin authors can
|
||||
assume these variables will all exist by the time the module containing their
|
||||
plugin (or plugins) is imported.
|
||||
|
||||
`Env._bootstrap()`, which is called by `API.bootstrap()`, will create several
|
||||
run-time variables that cannot be overridden in configuration files or through
|
||||
command-line options. Here is an overview of this run-time information:
|
||||
|
||||
============= ============================= =======================
|
||||
Key Example value Description
|
||||
============= ============================= =======================
|
||||
bin '/usr/bin' Dir. containing script
|
||||
dot_ipa '/home/jderose/.ipa' User config directory
|
||||
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
|
||||
site_packages '.../python2.5/site-packages' Dir. containing ipalib/
|
||||
============= ============================= =======================
|
||||
|
||||
If your plugin requires new environment variables *and* will be included in
|
||||
the freeIPA built-in plugins, you should add the defaults for your variables
|
||||
in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your
|
||||
new environment variables should have any auto-magic logic to determine their
|
||||
values if they haven't already been set by the time `config.Env._bootstrap()`,
|
||||
`config.Env._finalize_core()`, or `config.Env._finalize()` is called.
|
||||
|
||||
On the other hand, if your plugin requires new environment variables and will
|
||||
be installed in a 3rd-party package, your plugin should set these variables
|
||||
in the module it is defined in.
|
||||
|
||||
`config.Env` values work on a first-one-wins basis... after a value has been
|
||||
set, it can not be overridden with a new value. As any variables can be set
|
||||
using the command-line ``-e`` global option or set in a configuration file,
|
||||
your module must check whether a variable has already been set before
|
||||
setting its default value. For example:
|
||||
|
||||
>>> if 'message_of_the_day' not in api.env:
|
||||
... api.env.message_of_the_day = 'Hello, world!'
|
||||
...
|
||||
|
||||
Your plugin can access any environment variables via ``self.env``.
|
||||
For example:
|
||||
|
||||
>>> class motd(Command):
|
||||
... """Print message of the day."""
|
||||
...
|
||||
... def execute(self, **options):
|
||||
... return dict(result=self.env.message)
|
||||
...
|
||||
>>> api = create_api()
|
||||
>>> api.bootstrap(in_server=True, message='Hello, world!')
|
||||
>>> api.add_plugin(motd)
|
||||
>>> api.finalize()
|
||||
>>> api.Command.motd(version=u'2.47')
|
||||
{'result': u'Hello, world!'}
|
||||
|
||||
Also see the `plugable.API.bootstrap_with_global_options()` method.
|
||||
|
||||
|
||||
---------------------------------------------
|
||||
Indispensable ipa script commands and options
|
||||
---------------------------------------------
|
||||
|
||||
The ``console`` command will launch a custom interactive Python interpreter
|
||||
session. The global environment will have an ``api`` variable, which is the
|
||||
standard `plugable.API` instance found at ``ipalib.api``. All plugins will
|
||||
have been loaded (well, except the backend plugins if ``in_server`` is False)
|
||||
and ``api`` will be fully initialized. To launch the console from within the
|
||||
top-level directory in the source tree, just run ``ipa console`` from a
|
||||
terminal, like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa console
|
||||
|
||||
By default, ``in_server`` is False. If you want to start the console in a
|
||||
server context (so that all the backend plugins are loaded), you can use the
|
||||
``-e`` option to set the ``in_server`` environment variable, like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa -e in_server=True console
|
||||
|
||||
You can specify multiple environment variables by including the ``-e`` option
|
||||
multiple times, like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa -e in_server=True -e mode=dummy console
|
||||
|
||||
The space after the ``-e`` is optional. This is equivalent to the above command:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa -ein_server=True -emode=dummy console
|
||||
|
||||
The ``env`` command will print out the full environment in key=value pairs,
|
||||
like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa env
|
||||
|
||||
If you use the ``--server`` option, it will forward the call to the server
|
||||
over XML-RPC and print out what the environment is on the server, like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa env --server
|
||||
|
||||
The ``plugins`` command will show details of all the plugin that are loaded,
|
||||
like this:
|
||||
|
||||
::
|
||||
|
||||
$ ./ipa plugins
|
||||
|
||||
|
||||
-----------------------------------
|
||||
Learning more about freeIPA plugins
|
||||
-----------------------------------
|
||||
|
||||
To learn more about writing freeIPA plugins, you should:
|
||||
|
||||
1. Look at some of the built-in plugins, like the frontend plugins in
|
||||
`ipalib.plugins.f_user` and the backend plugins in
|
||||
`ipaserver.plugins.b_ldap`.
|
||||
|
||||
2. Learn about the base classes for frontend plugins in `ipalib.frontend`.
|
||||
|
||||
3. Learn about the core plugin framework in `ipalib.plugable`.
|
||||
|
||||
Furthermore, the freeIPA plugin architecture was inspired by the Bazaar plugin
|
||||
architecture. Although the two are different enough that learning how to
|
||||
write plugins for Bazaar will not particularly help you write plugins for
|
||||
freeIPA, some might be interested in the documentation on writing plugins for
|
||||
Bazaar, available here:
|
||||
|
||||
http://bazaar-vcs.org/WritingPlugins
|
||||
|
||||
If nothing else, we just want to give credit where credit is deserved!
|
||||
However, freeIPA does not use any *code* from Bazaar... it merely borrows a
|
||||
little inspiration.
|
||||
|
||||
|
||||
--------------------------
|
||||
A note on docstring markup
|
||||
--------------------------
|
||||
|
||||
Lastly, a quick note on markup: All the Python docstrings in freeIPA v2
|
||||
(including this tutorial) use the *reStructuredText* markup language. For
|
||||
information on reStructuredText, see:
|
||||
|
||||
http://docutils.sourceforge.net/rst.html
|
||||
|
||||
For information on using reStructuredText markup with epydoc, see:
|
||||
|
||||
http://epydoc.sourceforge.net/manual-othermarkup.html
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
Next steps: get involved with freeIPA development!
|
||||
--------------------------------------------------
|
||||
|
||||
The freeIPA team is always interested in feedback and contribution from the
|
||||
community. To get involved with freeIPA, see the *Contribute* page on
|
||||
freeIPA.org:
|
||||
|
||||
http://freeipa.org/page/Contribute
|
||||
|
||||
'''
|
||||
from ipapython.version import VERSION as __version__
|
||||
|
||||
def _enable_warnings(error=False):
|
||||
"""Enable additional warnings during development
|
||||
"""
|
||||
import ctypes
|
||||
import warnings
|
||||
|
||||
# get reference to Py_BytesWarningFlag from Python CAPI
|
||||
byteswarnings = ctypes.c_int.in_dll( # pylint: disable=no-member
|
||||
ctypes.pythonapi, 'Py_BytesWarningFlag')
|
||||
|
||||
if byteswarnings.value >= 2:
|
||||
# bytes warnings flag already set to error
|
||||
return
|
||||
|
||||
# default warning mode for all modules: warn once per location
|
||||
warnings.simplefilter('default', BytesWarning)
|
||||
if error:
|
||||
byteswarnings.value = 2
|
||||
action = 'error'
|
||||
else:
|
||||
byteswarnings.value = 1
|
||||
action = 'default'
|
||||
|
||||
module = '(ipa.*|__main__)'
|
||||
warnings.filterwarnings(action, category=BytesWarning, module=module)
|
||||
warnings.filterwarnings(action, category=DeprecationWarning,
|
||||
module=module)
|
||||
|
||||
# call this as early as possible
|
||||
if 'git' in __version__:
|
||||
_enable_warnings(False)
|
||||
|
||||
# noqa: E402
|
||||
from ipalib import plugable
|
||||
from ipalib.backend import Backend
|
||||
from ipalib.frontend import Command, LocalOrRemote, Updater
|
||||
from ipalib.frontend import Object, Method
|
||||
from ipalib.crud import Create, Retrieve, Update, Delete, Search
|
||||
from ipalib.parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam
|
||||
from ipalib.parameters import (BytesEnum, StrEnum, IntEnum, AccessTime, File,
|
||||
DateTime, DNSNameParam)
|
||||
from ipalib.errors import SkipPluginModule
|
||||
from ipalib.text import _, ngettext, GettextFactory, NGettextFactory
|
||||
|
||||
Registry = plugable.Registry
|
||||
|
||||
|
||||
class API(plugable.API):
|
||||
bases = (Command, Object, Method, Backend, Updater)
|
||||
|
||||
@property
|
||||
def packages(self):
|
||||
if self.env.in_server:
|
||||
# pylint: disable=import-error,ipa-forbidden-import
|
||||
import ipaserver.plugins
|
||||
# pylint: enable=import-error,ipa-forbidden-import
|
||||
result = (
|
||||
ipaserver.plugins,
|
||||
)
|
||||
else:
|
||||
import ipaclient.remote_plugins
|
||||
import ipaclient.plugins
|
||||
result = (
|
||||
ipaclient.remote_plugins.get_package(self),
|
||||
ipaclient.plugins,
|
||||
)
|
||||
|
||||
if self.env.context in ('installer', 'updates'):
|
||||
# pylint: disable=import-error,ipa-forbidden-import
|
||||
import ipaserver.install.plugins
|
||||
# pylint: enable=import-error,ipa-forbidden-import
|
||||
result += (ipaserver.install.plugins,)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_api(mode='dummy'):
|
||||
"""
|
||||
Return standard `plugable.API` instance.
|
||||
|
||||
This standard instance allows plugins that subclass from the following
|
||||
base classes:
|
||||
|
||||
- `frontend.Command`
|
||||
|
||||
- `frontend.Object`
|
||||
|
||||
- `frontend.Method`
|
||||
|
||||
- `backend.Backend`
|
||||
"""
|
||||
api = API()
|
||||
if mode is not None:
|
||||
api.env.mode = mode
|
||||
assert mode != 'production'
|
||||
return api
|
||||
|
||||
api = create_api(mode=None)
|
||||
268
ipalib/aci.py
Executable file
268
ipalib/aci.py
Executable file
@@ -0,0 +1,268 @@
|
||||
# Authors:
|
||||
# Rob Crittenden <rcritten@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import shlex
|
||||
import re
|
||||
|
||||
import six
|
||||
|
||||
# The Python re module doesn't do nested parenthesis
|
||||
|
||||
# Break the ACI into 3 pieces: target, name, permissions/bind_rules
|
||||
ACIPat = re.compile(r'\(version\s+3.0\s*;\s*ac[li]\s+\"([^\"]*)\"\s*;\s*([^;]*);\s*\)', re.UNICODE)
|
||||
|
||||
# Break the permissions/bind_rules out
|
||||
PermPat = re.compile(r'(\w+)\s*\(([^()]*)\)\s*(.*)', re.UNICODE)
|
||||
|
||||
# Break the bind rule out
|
||||
BindPat = re.compile(r'\(?([a-zA-Z0-9;\.]+)\s*(\!?=)\s*\"(.*)\"\)?',
|
||||
re.UNICODE)
|
||||
|
||||
ACTIONS = ["allow", "deny"]
|
||||
|
||||
PERMISSIONS = ["read", "write", "add", "delete", "search", "compare",
|
||||
"selfwrite", "proxy", "all"]
|
||||
|
||||
|
||||
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
|
||||
ACI String.
|
||||
"""
|
||||
__hash__ = None
|
||||
|
||||
def __init__(self,acistr=None):
|
||||
self.name = None
|
||||
self.source_group = None
|
||||
self.dest_group = None
|
||||
self.orig_acistr = acistr
|
||||
self.target = {}
|
||||
self.action = "allow"
|
||||
self.permissions = ["write"]
|
||||
self.bindrule = {}
|
||||
if acistr is not None:
|
||||
self._parse_acistr(acistr)
|
||||
|
||||
def __getitem__(self,key):
|
||||
"""Fake getting attributes by key for sorting"""
|
||||
if key == 0:
|
||||
return self.name
|
||||
if key == 1:
|
||||
return self.source_group
|
||||
if key == 2:
|
||||
return self.dest_group
|
||||
raise TypeError("Unknown key value %s" % key)
|
||||
|
||||
def __repr__(self):
|
||||
"""An alias for export_to_string()"""
|
||||
return self.export_to_string()
|
||||
|
||||
def export_to_string(self):
|
||||
"""Output a Directory Server-compatible ACI string"""
|
||||
self.validate()
|
||||
aci = ""
|
||||
for t, v in sorted(self.target.items()):
|
||||
op = v['operator']
|
||||
if type(v['expression']) in (tuple, list):
|
||||
target = ""
|
||||
for l in v['expression']:
|
||||
target = target + l + " || "
|
||||
target = target[:-4]
|
||||
aci = aci + "(%s %s \"%s\")" % (t, op, target)
|
||||
else:
|
||||
aci = aci + "(%s %s \"%s\")" % (t, op, v['expression'])
|
||||
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 _remove_quotes(self, s):
|
||||
# Remove leading and trailing quotes
|
||||
if s.startswith('"'):
|
||||
s = s[1:]
|
||||
if s.endswith('"'):
|
||||
s = s[:-1]
|
||||
return s
|
||||
|
||||
def _parse_target(self, aci):
|
||||
if six.PY2:
|
||||
aci = aci.encode('utf-8')
|
||||
lexer = shlex.shlex(aci)
|
||||
lexer.wordchars = lexer.wordchars + "."
|
||||
|
||||
var = False
|
||||
op = "="
|
||||
for token in lexer:
|
||||
# We should have the form (a = b)(a = b)...
|
||||
if token == "(":
|
||||
var = next(lexer).strip()
|
||||
operator = next(lexer)
|
||||
if operator != "=" and operator != "!=":
|
||||
# Peek at the next char before giving up
|
||||
operator = operator + next(lexer)
|
||||
if operator != "=" and operator != "!=":
|
||||
raise SyntaxError("No operator in target, got '%s'" % operator)
|
||||
op = operator
|
||||
val = next(lexer).strip()
|
||||
val = self._remove_quotes(val)
|
||||
end = next(lexer)
|
||||
if end != ")":
|
||||
raise SyntaxError('No end parenthesis in target, got %s' % end)
|
||||
|
||||
if var == 'targetattr':
|
||||
# Make a string of the form attr || attr || ... into a list
|
||||
t = re.split('[^a-zA-Z0-9;\*]+', val)
|
||||
self.target[var] = {}
|
||||
self.target[var]['operator'] = op
|
||||
self.target[var]['expression'] = t
|
||||
else:
|
||||
self.target[var] = {}
|
||||
self.target[var]['operator'] = op
|
||||
self.target[var]['expression'] = val
|
||||
|
||||
def _parse_acistr(self, acistr):
|
||||
vstart = acistr.find('version 3.0')
|
||||
if vstart < 0:
|
||||
raise SyntaxError("malformed ACI, unable to find version %s" % acistr)
|
||||
acimatch = ACIPat.match(acistr[vstart-1:])
|
||||
if not acimatch or len(acimatch.groups()) < 2:
|
||||
raise SyntaxError("malformed ACI, match for version and bind rule failed %s" % acistr)
|
||||
self._parse_target(acistr[:vstart-1])
|
||||
self.name = acimatch.group(1)
|
||||
bindperms = PermPat.match(acimatch.group(2))
|
||||
if not bindperms or len(bindperms.groups()) < 3:
|
||||
raise SyntaxError("malformed ACI, permissions match failed %s" % acistr)
|
||||
self.action = bindperms.group(1)
|
||||
self.permissions = bindperms.group(2).replace(' ','').split(',')
|
||||
self.set_bindrule(bindperms.group(3))
|
||||
|
||||
def validate(self):
|
||||
"""Do some basic verification that this will produce a
|
||||
valid LDAP ACI.
|
||||
|
||||
returns True if valid
|
||||
"""
|
||||
if type(self.permissions) not in (tuple, list):
|
||||
raise SyntaxError("permissions must be a list")
|
||||
for p in self.permissions:
|
||||
if p.lower() not in PERMISSIONS:
|
||||
raise SyntaxError("invalid permission: '%s'" % p)
|
||||
if not self.name:
|
||||
raise SyntaxError("name must be set")
|
||||
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")
|
||||
if not isinstance(self.bindrule, dict):
|
||||
raise SyntaxError("bindrule must be a dictionary")
|
||||
if not self.bindrule.get('operator') or not self.bindrule.get('keyword') or not self.bindrule.get('expression'):
|
||||
raise SyntaxError("bindrule is missing a component")
|
||||
return True
|
||||
|
||||
def set_target_filter(self, filter, operator="="):
|
||||
self.target['targetfilter'] = {}
|
||||
if not filter.startswith("("):
|
||||
filter = "(" + filter + ")"
|
||||
self.target['targetfilter']['expression'] = filter
|
||||
self.target['targetfilter']['operator'] = operator
|
||||
|
||||
def set_target_attr(self, attr, operator="="):
|
||||
if not attr:
|
||||
if 'targetattr' in self.target:
|
||||
del self.target['targetattr']
|
||||
return
|
||||
if type(attr) not in (tuple, list):
|
||||
attr = [attr]
|
||||
self.target['targetattr'] = {}
|
||||
self.target['targetattr']['expression'] = attr
|
||||
self.target['targetattr']['operator'] = operator
|
||||
|
||||
def set_target(self, target, operator="="):
|
||||
assert target.startswith("ldap:///")
|
||||
self.target['target'] = {}
|
||||
self.target['target']['expression'] = target
|
||||
self.target['target']['operator'] = operator
|
||||
|
||||
def set_bindrule(self, bindrule):
|
||||
if bindrule.startswith('(') != bindrule.endswith(')'):
|
||||
raise SyntaxError("non-matching parentheses in bindrule")
|
||||
|
||||
match = BindPat.match(bindrule)
|
||||
if not match or len(match.groups()) < 3:
|
||||
raise SyntaxError("malformed bind rule")
|
||||
self.set_bindrule_keyword(match.group(1))
|
||||
self.set_bindrule_operator(match.group(2))
|
||||
self.set_bindrule_expression(match.group(3).replace('"',''))
|
||||
|
||||
def set_bindrule_keyword(self, keyword):
|
||||
self.bindrule['keyword'] = keyword
|
||||
|
||||
def set_bindrule_operator(self, operator):
|
||||
self.bindrule['operator'] = operator
|
||||
|
||||
def set_bindrule_expression(self, expression):
|
||||
self.bindrule['expression'] = expression
|
||||
|
||||
def isequal(self, b):
|
||||
"""
|
||||
Compare the current ACI to another one to see if they are
|
||||
the same.
|
||||
|
||||
returns True if equal, False if not.
|
||||
"""
|
||||
assert isinstance(b, ACI)
|
||||
try:
|
||||
if self.name.lower() != b.name.lower():
|
||||
return False
|
||||
|
||||
if set(self.permissions) != set(b.permissions):
|
||||
return False
|
||||
|
||||
if self.bindrule.get('keyword') != b.bindrule.get('keyword'):
|
||||
return False
|
||||
if self.bindrule.get('operator') != b.bindrule.get('operator'):
|
||||
return False
|
||||
if self.bindrule.get('expression') != b.bindrule.get('expression'):
|
||||
return False
|
||||
|
||||
if self.target.get('targetfilter',{}).get('expression') != b.target.get('targetfilter',{}).get('expression'):
|
||||
return False
|
||||
if self.target.get('targetfilter',{}).get('operator') != b.target.get('targetfilter',{}).get('operator'):
|
||||
return False
|
||||
|
||||
if set(self.target.get('targetattr', {}).get('expression', ())) != set(b.target.get('targetattr',{}).get('expression', ())):
|
||||
return False
|
||||
if self.target.get('targetattr',{}).get('operator') != b.target.get('targetattr',{}).get('operator'):
|
||||
return False
|
||||
|
||||
if self.target.get('target',{}).get('expression') != b.target.get('target',{}).get('expression'):
|
||||
return False
|
||||
if self.target.get('target',{}).get('operator') != b.target.get('target',{}).get('operator'):
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
# If anything throws up then they are not equal
|
||||
return False
|
||||
|
||||
# We got this far so lets declare them the same
|
||||
return True
|
||||
|
||||
__eq__ = isequal
|
||||
|
||||
def __ne__(self, b):
|
||||
return not self == b
|
||||
154
ipalib/backend.py
Normal file
154
ipalib/backend.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base classes for all backed-end plugins.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
|
||||
from ipalib import plugable
|
||||
from ipalib.errors import PublicError, InternalError, CommandError
|
||||
from ipalib.request import context, Connection, destroy_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Backend(plugable.Plugin):
|
||||
"""
|
||||
Base class for all backend plugins.
|
||||
"""
|
||||
|
||||
|
||||
class Connectible(Backend):
|
||||
"""
|
||||
Base class for backend plugins that create connections.
|
||||
|
||||
In addition to the nicety of providing a standard connection API, all
|
||||
backend plugins that create connections should use this base class so that
|
||||
`request.destroy_context()` can properly close all open connections.
|
||||
"""
|
||||
|
||||
def __init__(self, api, shared_instance=False):
|
||||
Backend.__init__(self, api)
|
||||
if shared_instance:
|
||||
self.id = self.name
|
||||
else:
|
||||
self.id = '%s_%s' % (self.name, str(id(self)))
|
||||
|
||||
def connect(self, *args, **kw):
|
||||
"""
|
||||
Create thread-local connection.
|
||||
"""
|
||||
if hasattr(context, self.id):
|
||||
raise Exception(
|
||||
"{0} is already connected ({1} in {2})".format(
|
||||
self.name,
|
||||
self.id,
|
||||
threading.currentThread().getName()
|
||||
)
|
||||
)
|
||||
conn = self.create_connection(*args, **kw)
|
||||
setattr(context, self.id, Connection(conn, self.disconnect))
|
||||
assert self.conn is conn
|
||||
logger.debug('Created connection context.%s', self.id)
|
||||
|
||||
def create_connection(self, *args, **kw):
|
||||
raise NotImplementedError('%s.create_connection()' % self.id)
|
||||
|
||||
def disconnect(self):
|
||||
if not hasattr(context, self.id):
|
||||
raise Exception(
|
||||
"{0} is not connected ({1} in {2})".format(
|
||||
self.name,
|
||||
self.id,
|
||||
threading.currentThread().getName()
|
||||
)
|
||||
)
|
||||
self.destroy_connection()
|
||||
delattr(context, self.id)
|
||||
logger.debug('Destroyed connection context.%s', self.id)
|
||||
|
||||
def destroy_connection(self):
|
||||
raise NotImplementedError('%s.destroy_connection()' % self.id)
|
||||
|
||||
def isconnected(self):
|
||||
"""
|
||||
Return ``True`` if thread-local connection on `request.context` exists.
|
||||
"""
|
||||
return hasattr(context, self.id)
|
||||
|
||||
def __get_conn(self):
|
||||
"""
|
||||
Return thread-local connection.
|
||||
"""
|
||||
if not hasattr(context, self.id):
|
||||
raise AttributeError(
|
||||
"{0} is not connected ({1} in {2})".format(
|
||||
self.name,
|
||||
self.id,
|
||||
threading.currentThread().getName()
|
||||
)
|
||||
)
|
||||
return getattr(context, self.id).conn
|
||||
conn = property(__get_conn)
|
||||
|
||||
|
||||
class Executioner(Backend):
|
||||
|
||||
def create_context(self, ccache=None, client_ip=None):
|
||||
"""
|
||||
client_ip: The IP address of the remote client.
|
||||
"""
|
||||
|
||||
if ccache is not None:
|
||||
os.environ["KRB5CCNAME"] = ccache
|
||||
|
||||
if self.env.in_server:
|
||||
self.Backend.ldap2.connect(ccache=ccache,
|
||||
size_limit=None,
|
||||
time_limit=None)
|
||||
else:
|
||||
self.Backend.rpcclient.connect()
|
||||
if client_ip is not None:
|
||||
setattr(context, "client_ip", client_ip)
|
||||
|
||||
def destroy_context(self):
|
||||
destroy_context()
|
||||
|
||||
def execute(self, _name, *args, **options):
|
||||
error = None
|
||||
try:
|
||||
if _name not in self.Command:
|
||||
raise CommandError(name=_name)
|
||||
result = self.Command[_name](*args, **options)
|
||||
except PublicError as e:
|
||||
error = e
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'non-public: %s: %s', e.__class__.__name__, str(e)
|
||||
)
|
||||
error = InternalError()
|
||||
destroy_context()
|
||||
if error is None:
|
||||
return result
|
||||
assert isinstance(error, PublicError)
|
||||
raise error #pylint: disable=E0702
|
||||
500
ipalib/base.py
Normal file
500
ipalib/base.py
Normal file
@@ -0,0 +1,500 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
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(object):
|
||||
"""
|
||||
Base class for classes that can be locked into a read-only state.
|
||||
|
||||
Be forewarned that Python does not offer true read-only attributes for
|
||||
user-defined classes. Do *not* rely upon the read-only-ness of this
|
||||
class for security purposes!
|
||||
|
||||
The point of this class is not to make it impossible to set or to delete
|
||||
attributes after an instance is locked, but to make it impossible to do so
|
||||
*accidentally*. Rather than constantly reminding our programmers of things
|
||||
like, for example, "Don't set any attributes on this ``FooBar`` instance
|
||||
because doing so wont be thread-safe", this class offers a real way to
|
||||
enforce read-only attribute usage.
|
||||
|
||||
For example, before a `ReadOnly` instance is locked, you can set and delete
|
||||
its attributes as normal:
|
||||
|
||||
>>> class Person(ReadOnly):
|
||||
... pass
|
||||
...
|
||||
>>> p = Person()
|
||||
>>> p.name = 'John Doe'
|
||||
>>> p.phone = '123-456-7890'
|
||||
>>> del p.phone
|
||||
|
||||
But after an instance is locked, you cannot set its attributes:
|
||||
|
||||
>>> p.__islocked__() # Is this instance locked?
|
||||
False
|
||||
>>> p.__lock__() # This will lock the instance
|
||||
>>> p.__islocked__()
|
||||
True
|
||||
>>> p.department = 'Engineering'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot set Person.department to 'Engineering'
|
||||
|
||||
Nor can you deleted its attributes:
|
||||
|
||||
>>> del p.name
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot delete Person.name
|
||||
|
||||
However, as noted at the start, there are still obscure ways in which
|
||||
attributes can be set or deleted on a locked `ReadOnly` instance. For
|
||||
example:
|
||||
|
||||
>>> object.__setattr__(p, 'department', 'Engineering')
|
||||
>>> p.department
|
||||
'Engineering'
|
||||
>>> object.__delattr__(p, 'name')
|
||||
>>> hasattr(p, 'name')
|
||||
False
|
||||
|
||||
But again, the point is that a programmer would never employ the above
|
||||
techniques *accidentally*.
|
||||
|
||||
Lastly, this example aside, you should use the `lock()` function rather
|
||||
than the `ReadOnly.__lock__()` method. And likewise, you should
|
||||
use the `islocked()` function rather than the `ReadOnly.__islocked__()`
|
||||
method. For example:
|
||||
|
||||
>>> readonly = ReadOnly()
|
||||
>>> islocked(readonly)
|
||||
False
|
||||
>>> lock(readonly) is readonly # lock() returns the instance
|
||||
True
|
||||
>>> islocked(readonly)
|
||||
True
|
||||
"""
|
||||
|
||||
__locked = False
|
||||
|
||||
def __lock__(self):
|
||||
"""
|
||||
Put this instance into a read-only state.
|
||||
|
||||
After the instance has been locked, attempting to set or delete an
|
||||
attribute will raise an AttributeError.
|
||||
"""
|
||||
assert self.__locked is False, '__lock__() can only be called once'
|
||||
self.__locked = True
|
||||
|
||||
def __islocked__(self):
|
||||
"""
|
||||
Return True if instance is locked, otherwise False.
|
||||
"""
|
||||
return self.__locked
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
If unlocked, set attribute named ``name`` to ``value``.
|
||||
|
||||
If this instance is locked, an AttributeError will be raised.
|
||||
|
||||
:param name: Name of attribute to set.
|
||||
:param value: Value to assign to attribute.
|
||||
"""
|
||||
if self.__locked:
|
||||
raise AttributeError(
|
||||
SET_ERROR % (self.__class__.__name__, name, value)
|
||||
)
|
||||
return object.__setattr__(self, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
"""
|
||||
If unlocked, delete attribute named ``name``.
|
||||
|
||||
If this instance is locked, an AttributeError will be raised.
|
||||
|
||||
:param name: Name of attribute to delete.
|
||||
"""
|
||||
if self.__locked:
|
||||
raise AttributeError(
|
||||
DEL_ERROR % (self.__class__.__name__, name)
|
||||
)
|
||||
return object.__delattr__(self, name)
|
||||
|
||||
|
||||
def lock(instance):
|
||||
"""
|
||||
Lock an instance of the `ReadOnly` class or similar.
|
||||
|
||||
This function can be used to lock instances of any class that implements
|
||||
the same locking API as the `ReadOnly` class. For example, this function
|
||||
can lock instances of the `config.Env` class.
|
||||
|
||||
So that this function can be easily used within an assignment, ``instance``
|
||||
is returned after it is locked. For example:
|
||||
|
||||
>>> readonly = ReadOnly()
|
||||
>>> readonly is lock(readonly)
|
||||
True
|
||||
>>> readonly.attr = 'This wont work'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot set ReadOnly.attr to 'This wont work'
|
||||
|
||||
Also see the `islocked()` function.
|
||||
|
||||
:param instance: The instance of `ReadOnly` (or similar) to lock.
|
||||
"""
|
||||
assert instance.__islocked__() is False, 'already locked: %r' % instance
|
||||
instance.__lock__()
|
||||
assert instance.__islocked__() is True, 'failed to lock: %r' % instance
|
||||
return instance
|
||||
|
||||
|
||||
def islocked(instance):
|
||||
"""
|
||||
Return ``True`` if ``instance`` is locked.
|
||||
|
||||
This function can be used on an instance of the `ReadOnly` class or an
|
||||
instance of any other class implemented the same locking API.
|
||||
|
||||
For example:
|
||||
|
||||
>>> readonly = ReadOnly()
|
||||
>>> islocked(readonly)
|
||||
False
|
||||
>>> readonly.__lock__()
|
||||
>>> islocked(readonly)
|
||||
True
|
||||
|
||||
Also see the `lock()` function.
|
||||
|
||||
:param instance: The instance of `ReadOnly` (or similar) to interrogate.
|
||||
"""
|
||||
assert (
|
||||
hasattr(instance, '__lock__') and callable(instance.__lock__)
|
||||
), 'no __lock__() method: %r' % instance
|
||||
return instance.__islocked__()
|
||||
|
||||
|
||||
def check_name(name):
|
||||
"""
|
||||
Verify that ``name`` is suitable for a `NameSpace` member name.
|
||||
|
||||
In short, ``name`` must be a valid lower-case Python identifier that
|
||||
neither starts nor ends with an underscore. Otherwise an exception is
|
||||
raised.
|
||||
|
||||
This function will raise a ``ValueError`` if ``name`` does not match the
|
||||
`constants.NAME_REGEX` regular expression. For example:
|
||||
|
||||
>>> check_name('MyName')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'; got 'MyName'
|
||||
|
||||
Also, this function will raise a ``TypeError`` if ``name`` is not an
|
||||
``str`` instance. For example:
|
||||
|
||||
>>> check_name(u'my_name')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: name: need a <type 'str'>; got u'my_name' (a <type 'unicode'>)
|
||||
|
||||
So that `check_name()` can be easily used within an assignment, ``name``
|
||||
is returned unchanged if it passes the check. For example:
|
||||
|
||||
>>> n = check_name('my_name')
|
||||
>>> n
|
||||
'my_name'
|
||||
|
||||
:param name: Identifier to test.
|
||||
"""
|
||||
if type(name) is not str:
|
||||
raise TypeError(
|
||||
TYPE_ERROR % ('name', str, name, type(name))
|
||||
)
|
||||
if re.match(NAME_REGEX, name) is None:
|
||||
raise ValueError(
|
||||
NAME_ERROR % (NAME_REGEX, name)
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
class NameSpace(ReadOnly):
|
||||
"""
|
||||
A read-only name-space with handy container behaviours.
|
||||
|
||||
A `NameSpace` instance is an ordered, immutable mapping object whose values
|
||||
can also be accessed as attributes. A `NameSpace` instance is constructed
|
||||
from an iterable providing its *members*, which are simply arbitrary objects
|
||||
with a ``name`` attribute whose value:
|
||||
|
||||
1. Is unique among the members
|
||||
|
||||
2. Passes the `check_name()` function
|
||||
|
||||
Beyond that, no restrictions are placed on the members: they can be
|
||||
classes or instances, and of any type.
|
||||
|
||||
The members can be accessed as attributes on the `NameSpace` instance or
|
||||
through a dictionary interface. For example, say we create a `NameSpace`
|
||||
instance from a list containing a single member, like this:
|
||||
|
||||
>>> class my_member(object):
|
||||
... name = 'my_name'
|
||||
...
|
||||
>>> namespace = NameSpace([my_member])
|
||||
>>> namespace
|
||||
NameSpace(<1 member>, sort=True)
|
||||
|
||||
We can then access ``my_member`` both as an attribute and as a dictionary
|
||||
item:
|
||||
|
||||
>>> my_member is namespace.my_name # As an attribute
|
||||
True
|
||||
>>> my_member is namespace['my_name'] # As dictionary item
|
||||
True
|
||||
|
||||
For a more detailed example, say we create a `NameSpace` instance from a
|
||||
generator like this:
|
||||
|
||||
>>> class Member(object):
|
||||
... def __init__(self, i):
|
||||
... self.i = i
|
||||
... self.name = self.__name__ = 'member%d' % i
|
||||
... def __repr__(self):
|
||||
... return 'Member(%d)' % self.i
|
||||
...
|
||||
>>> ns = NameSpace(Member(i) for i in range(3))
|
||||
>>> ns
|
||||
NameSpace(<3 members>, sort=True)
|
||||
|
||||
As above, the members can be accessed as attributes and as dictionary items:
|
||||
|
||||
>>> ns.member0 is ns['member0']
|
||||
True
|
||||
>>> ns.member1 is ns['member1']
|
||||
True
|
||||
>>> ns.member2 is ns['member2']
|
||||
True
|
||||
|
||||
Members can also be accessed by index and by slice. For example:
|
||||
|
||||
>>> ns[0]
|
||||
Member(0)
|
||||
>>> ns[-1]
|
||||
Member(2)
|
||||
>>> ns[1:]
|
||||
(Member(1), Member(2))
|
||||
|
||||
(Note that slicing a `NameSpace` returns a ``tuple``.)
|
||||
|
||||
`NameSpace` instances provide standard container emulation for membership
|
||||
testing, counting, and iteration. For example:
|
||||
|
||||
>>> 'member3' in ns # Is there a member named 'member3'?
|
||||
False
|
||||
>>> 'member2' in ns # But there is a member named 'member2'
|
||||
True
|
||||
>>> len(ns) # The number of members
|
||||
3
|
||||
>>> list(ns) # Iterate through the member names
|
||||
['member0', 'member1', 'member2']
|
||||
|
||||
Although not a standard container feature, the `NameSpace.__call__()` method
|
||||
provides a convenient (and efficient) way to iterate through the *members*
|
||||
(as opposed to the member names). Think of it like an ordered version of
|
||||
the ``dict.itervalues()`` method. For example:
|
||||
|
||||
>>> list(ns[name] for name in ns) # One way to do it
|
||||
[Member(0), Member(1), Member(2)]
|
||||
>>> list(ns()) # A more efficient, simpler way to do it
|
||||
[Member(0), Member(1), Member(2)]
|
||||
|
||||
Another convenience method is `NameSpace.__todict__()`, which will return
|
||||
a copy of the ``dict`` mapping the member names to the members.
|
||||
For example:
|
||||
|
||||
>>> ns.__todict__()
|
||||
{'member1': Member(1), 'member0': Member(0), 'member2': Member(2)}
|
||||
|
||||
As `NameSpace.__init__()` locks the instance, `NameSpace` instances are
|
||||
read-only from the get-go. An ``AttributeError`` is raised if you try to
|
||||
set *any* attribute on a `NameSpace` instance. For example:
|
||||
|
||||
>>> ns.member3 = Member(3) # Lets add that missing 'member3'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot set NameSpace.member3 to Member(3)
|
||||
|
||||
(For information on the locking protocol, see the `ReadOnly` class, of which
|
||||
`NameSpace` is a subclass.)
|
||||
|
||||
By default the members will be sorted alphabetically by the member name.
|
||||
For example:
|
||||
|
||||
>>> sorted_ns = NameSpace([Member(7), Member(3), Member(5)])
|
||||
>>> sorted_ns
|
||||
NameSpace(<3 members>, sort=True)
|
||||
>>> list(sorted_ns)
|
||||
['member3', 'member5', 'member7']
|
||||
>>> sorted_ns[0]
|
||||
Member(3)
|
||||
|
||||
But if the instance is created with the ``sort=False`` keyword argument, the
|
||||
original order of the members is preserved. For example:
|
||||
|
||||
>>> unsorted_ns = NameSpace([Member(7), Member(3), Member(5)], sort=False)
|
||||
>>> unsorted_ns
|
||||
NameSpace(<3 members>, sort=False)
|
||||
>>> list(unsorted_ns)
|
||||
['member7', 'member3', 'member5']
|
||||
>>> unsorted_ns[0]
|
||||
Member(7)
|
||||
|
||||
As a special extension, NameSpace objects can be indexed by objects that
|
||||
have a "__name__" attribute (e.g. classes). These lookups are converted
|
||||
to lookups on the name:
|
||||
|
||||
>>> class_ns = NameSpace([Member(7), Member(3), Member(5)], sort=False)
|
||||
>>> unsorted_ns[Member(3)]
|
||||
Member(3)
|
||||
|
||||
The `NameSpace` class is used in many places throughout freeIPA. For a few
|
||||
examples, see the `plugable.API` and the `frontend.Command` classes.
|
||||
"""
|
||||
|
||||
def __init__(self, members, sort=True, name_attr='name'):
|
||||
"""
|
||||
:param members: An iterable providing the members.
|
||||
:param sort: Whether to sort the members by member name.
|
||||
"""
|
||||
if type(sort) is not bool:
|
||||
raise TypeError(
|
||||
TYPE_ERROR % ('sort', bool, sort, type(sort))
|
||||
)
|
||||
self.__sort = sort
|
||||
if sort:
|
||||
self.__members = tuple(
|
||||
sorted(members, key=lambda m: getattr(m, name_attr))
|
||||
)
|
||||
else:
|
||||
self.__members = tuple(members)
|
||||
self.__names = tuple(getattr(m, name_attr) for m in self.__members)
|
||||
self.__map = dict()
|
||||
for member in self.__members:
|
||||
name = check_name(getattr(member, name_attr))
|
||||
if name in self.__map:
|
||||
raise AttributeError(OVERRIDE_ERROR %
|
||||
(self.__class__.__name__, name, self.__map[name], member)
|
||||
)
|
||||
assert not hasattr(self, name), 'Ouch! Has attribute %r' % name
|
||||
self.__map[name] = member
|
||||
setattr(self, name, member)
|
||||
lock(self)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the number of members.
|
||||
"""
|
||||
return len(self.__members)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate through the member names.
|
||||
|
||||
If this instance was created with ``sort=False``, the names will be in
|
||||
the same order as the members were passed to the constructor; otherwise
|
||||
the names will be in alphabetical order (which is the default).
|
||||
|
||||
This method is like an ordered version of ``dict.iterkeys()``.
|
||||
"""
|
||||
for name in self.__names:
|
||||
yield name
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
Iterate through the members.
|
||||
|
||||
If this instance was created with ``sort=False``, the members will be
|
||||
in the same order as they were passed to the constructor; otherwise the
|
||||
members will be in alphabetical order by name (which is the default).
|
||||
|
||||
This method is like an ordered version of ``dict.itervalues()``.
|
||||
"""
|
||||
for member in self.__members:
|
||||
yield member
|
||||
|
||||
def __contains__(self, name):
|
||||
"""
|
||||
Return ``True`` if namespace has a member named ``name``.
|
||||
"""
|
||||
name = getattr(name, '__name__', name)
|
||||
return name in self.__map
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return a member by name or index, or return a slice of members.
|
||||
|
||||
:param key: The name or index of a member, or a slice object.
|
||||
"""
|
||||
key = getattr(key, '__name__', key)
|
||||
if isinstance(key, six.string_types):
|
||||
return self.__map[key]
|
||||
if type(key) in (int, slice):
|
||||
return self.__members[key]
|
||||
raise TypeError(
|
||||
TYPE_ERROR % ('key', (str, int, slice, 'object with __name__'),
|
||||
key, type(key))
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return a pseudo-valid expression that could create this instance.
|
||||
"""
|
||||
cnt = len(self)
|
||||
if cnt == 1:
|
||||
m = 'member'
|
||||
else:
|
||||
m = 'members'
|
||||
return '%s(<%d %s>, sort=%r)' % (
|
||||
self.__class__.__name__,
|
||||
cnt,
|
||||
m,
|
||||
self.__sort,
|
||||
)
|
||||
|
||||
def __todict__(self):
|
||||
"""
|
||||
Return a copy of the private dict mapping member name to member.
|
||||
"""
|
||||
return dict(self.__map)
|
||||
69
ipalib/capabilities.py
Normal file
69
ipalib/capabilities.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Authors:
|
||||
# Petr Viktorin <pviktori@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2012 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/>.
|
||||
|
||||
"""List of, and utilities for working with, client capabilities by API version
|
||||
|
||||
The API version is given in ipapython.version.API_VERSION.
|
||||
|
||||
This module defines a dict, ``capabilities``, that maps feature names to API
|
||||
versions they were introduced in.
|
||||
"""
|
||||
|
||||
from ipapython.ipautil import APIVersion
|
||||
|
||||
VERSION_WITHOUT_CAPABILITIES = u'2.51'
|
||||
|
||||
capabilities = dict(
|
||||
# messages: Server output may include an extra key, "messages", that
|
||||
# contains a list of warnings and other messages.
|
||||
# http://freeipa.org/page/V3/Messages
|
||||
messages=u'2.52',
|
||||
|
||||
# optional_uid_params: Before this version, UID & GID parameter defaults
|
||||
# were 999, which meant "assign dynamically", so was not possible to get
|
||||
# a user with UID=999. With the capability, these parameters are optional
|
||||
# and 999 really means 999.
|
||||
# https://fedorahosted.org/freeipa/ticket/2886
|
||||
optional_uid_params=u'2.54',
|
||||
|
||||
# permissions2: Reworked permission system
|
||||
# http://www.freeipa.org/page/V3/Permissions_V2
|
||||
permissions2=u'2.69',
|
||||
|
||||
# primary_key_types: Non-unicode primary keys in command output
|
||||
primary_key_types=u'2.83',
|
||||
|
||||
# support for datetime values on the client
|
||||
datetime_values=u'2.84',
|
||||
|
||||
# dns_name_values: dnsnames as objects
|
||||
dns_name_values=u'2.88',
|
||||
)
|
||||
|
||||
|
||||
def client_has_capability(client_version, capability):
|
||||
"""Determine whether the client has the given capability
|
||||
|
||||
:param capability: Name of the capability to test
|
||||
:param client_version: The API version string reported by the client
|
||||
"""
|
||||
|
||||
version = APIVersion(client_version)
|
||||
|
||||
return version >= APIVersion(capabilities[capability])
|
||||
1415
ipalib/cli.py
Normal file
1415
ipalib/cli.py
Normal file
File diff suppressed because it is too large
Load Diff
660
ipalib/config.py
Normal file
660
ipalib/config.py
Normal file
@@ -0,0 +1,660 @@
|
||||
# Authors:
|
||||
# Martin Nagy <mnagy@redhat.com>
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Process-wide static configuration and environment.
|
||||
|
||||
The standard run-time instance of the `Env` class is initialized early in the
|
||||
`ipalib` process and is then locked into a read-only state, after which no
|
||||
further changes can be made to the environment throughout the remaining life
|
||||
of the process.
|
||||
|
||||
For the per-request thread-local information, see `ipalib.request`.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
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
|
||||
from ipalib.base import check_name
|
||||
from ipalib.constants import (
|
||||
CONFIG_SECTION,
|
||||
OVERRIDE_ERROR, SET_ERROR, DEL_ERROR,
|
||||
TLS_VERSIONS
|
||||
)
|
||||
from ipalib import errors
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
class Env(object):
|
||||
"""
|
||||
Store and retrieve environment variables.
|
||||
|
||||
First an foremost, the `Env` class provides a handy container for
|
||||
environment variables. These variables can be both set *and* retrieved
|
||||
either as attributes *or* as dictionary items.
|
||||
|
||||
For example, you can set a variable as an attribute:
|
||||
|
||||
>>> env = Env()
|
||||
>>> env.attr = 'I was set as an attribute.'
|
||||
>>> env.attr
|
||||
u'I was set as an attribute.'
|
||||
>>> env['attr'] # Also retrieve as a dictionary item
|
||||
u'I was set as an attribute.'
|
||||
|
||||
Or you can set a variable as a dictionary item:
|
||||
|
||||
>>> env['item'] = 'I was set as a dictionary item.'
|
||||
>>> env['item']
|
||||
u'I was set as a dictionary item.'
|
||||
>>> env.item # Also retrieve as an attribute
|
||||
u'I was set as a dictionary item.'
|
||||
|
||||
The variable names must be valid lower-case Python identifiers that neither
|
||||
start nor end with an underscore. If your variable name doesn't meet these
|
||||
criteria, a ``ValueError`` will be raised when you try to set the variable
|
||||
(compliments of the `base.check_name()` function). For example:
|
||||
|
||||
>>> env.BadName = 'Wont work as an attribute'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'; got 'BadName'
|
||||
>>> env['BadName'] = 'Also wont work as a dictionary item'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'; got 'BadName'
|
||||
|
||||
The variable values can be ``str``, ``int``, or ``float`` instances, or the
|
||||
``True``, ``False``, or ``None`` constants. When the value provided is an
|
||||
``str`` instance, some limited automatic type conversion is performed, which
|
||||
allows values of specific types to be set easily from configuration files or
|
||||
command-line options.
|
||||
|
||||
So in addition to their actual values, the ``True``, ``False``, and ``None``
|
||||
constants can be specified with an ``str`` equal to what ``repr()`` would
|
||||
return. For example:
|
||||
|
||||
>>> env.true = True
|
||||
>>> env.also_true = 'True' # Equal to repr(True)
|
||||
>>> env.true
|
||||
True
|
||||
>>> env.also_true
|
||||
True
|
||||
|
||||
Note that the automatic type conversion is case sensitive. For example:
|
||||
|
||||
>>> env.not_false = 'false' # Not equal to repr(False)!
|
||||
>>> env.not_false
|
||||
u'false'
|
||||
|
||||
If an ``str`` value looks like an integer, it's automatically converted to
|
||||
the ``int`` type.
|
||||
|
||||
>>> env.lucky = '7'
|
||||
>>> env.lucky
|
||||
7
|
||||
|
||||
Leading and trailing white-space is automatically stripped from ``str``
|
||||
values. For example:
|
||||
|
||||
>>> env.message = ' Hello! ' # Surrounded by double spaces
|
||||
>>> env.message
|
||||
u'Hello!'
|
||||
>>> env.number = ' 42 ' # Still converted to an int
|
||||
>>> env.number
|
||||
42
|
||||
>>> env.false = ' False ' # Still equal to repr(False)
|
||||
>>> env.false
|
||||
False
|
||||
|
||||
Also, empty ``str`` instances are converted to ``None``. For example:
|
||||
|
||||
>>> env.empty = ''
|
||||
>>> env.empty is None
|
||||
True
|
||||
|
||||
`Env` variables are all set-once (first-one-wins). Once a variable has been
|
||||
set, trying to override it will raise an ``AttributeError``. For example:
|
||||
|
||||
>>> env.date = 'First'
|
||||
>>> env.date = 'Second'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: cannot override Env.date value u'First' with 'Second'
|
||||
|
||||
An `Env` instance can be *locked*, after which no further variables can be
|
||||
set. Trying to set variables on a locked `Env` instance will also raise
|
||||
an ``AttributeError``. For example:
|
||||
|
||||
>>> env = Env()
|
||||
>>> env.okay = 'This will work.'
|
||||
>>> env.__lock__()
|
||||
>>> env.nope = 'This wont work!'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot set Env.nope to 'This wont work!'
|
||||
|
||||
`Env` instances also provide standard container emulation for membership
|
||||
testing, counting, and iteration. For example:
|
||||
|
||||
>>> env = Env()
|
||||
>>> 'key1' in env # Has key1 been set?
|
||||
False
|
||||
>>> env.key1 = 'value 1'
|
||||
>>> 'key1' in env
|
||||
True
|
||||
>>> env.key2 = 'value 2'
|
||||
>>> len(env) # How many variables have been set?
|
||||
2
|
||||
>>> list(env) # What variables have been set?
|
||||
['key1', 'key2']
|
||||
|
||||
Lastly, in addition to all the handy container functionality, the `Env`
|
||||
class provides high-level methods for bootstraping a fresh `Env` instance
|
||||
into one containing all the run-time and configuration information needed
|
||||
by the built-in freeIPA plugins.
|
||||
|
||||
These are the `Env` bootstraping methods, in the order they must be called:
|
||||
|
||||
1. `Env._bootstrap()` - initialize the run-time variables and then
|
||||
merge-in variables specified on the command-line.
|
||||
|
||||
2. `Env._finalize_core()` - merge-in variables from the configuration
|
||||
files and then merge-in variables from the internal defaults, after
|
||||
which at least all the standard variables will be set. After this
|
||||
method is called, the plugins will be loaded, during which
|
||||
third-party plugins can merge-in defaults for additional variables
|
||||
they use (likely using the `Env._merge()` method).
|
||||
|
||||
3. `Env._finalize()` - one last chance to merge-in variables and then
|
||||
the instance is locked. After this method is called, no more
|
||||
environment variables can be set during the remaining life of the
|
||||
process.
|
||||
|
||||
However, normally none of these three bootstraping methods are called
|
||||
directly and instead only `plugable.API.bootstrap()` is called, which itself
|
||||
takes care of correctly calling the `Env` bootstrapping methods.
|
||||
"""
|
||||
|
||||
__locked = False
|
||||
|
||||
def __init__(self, **initialize):
|
||||
object.__setattr__(self, '_Env__d', {})
|
||||
object.__setattr__(self, '_Env__done', set())
|
||||
if initialize:
|
||||
self._merge(**initialize)
|
||||
|
||||
def __lock__(self):
|
||||
"""
|
||||
Prevent further changes to environment.
|
||||
"""
|
||||
if self.__locked is True:
|
||||
raise Exception(
|
||||
'%s.__lock__() already called' % self.__class__.__name__
|
||||
)
|
||||
object.__setattr__(self, '_Env__locked', True)
|
||||
|
||||
def __islocked__(self):
|
||||
"""
|
||||
Return ``True`` if locked.
|
||||
"""
|
||||
return self.__locked
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
Set the attribute named ``name`` to ``value``.
|
||||
|
||||
This just calls `Env.__setitem__()`.
|
||||
"""
|
||||
self[name] = value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
Set ``key`` to ``value``.
|
||||
"""
|
||||
if self.__locked:
|
||||
raise AttributeError(
|
||||
SET_ERROR % (self.__class__.__name__, key, value)
|
||||
)
|
||||
check_name(key)
|
||||
if key in self.__d:
|
||||
raise AttributeError(OVERRIDE_ERROR %
|
||||
(self.__class__.__name__, key, self.__d[key], value)
|
||||
)
|
||||
assert not hasattr(self, key)
|
||||
if isinstance(value, six.string_types):
|
||||
value = value.strip()
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
m = {
|
||||
'True': True,
|
||||
'False': False,
|
||||
'None': None,
|
||||
'': None,
|
||||
}
|
||||
if value in m:
|
||||
value = m[value]
|
||||
elif value.isdigit():
|
||||
value = int(value)
|
||||
elif key == 'basedn':
|
||||
value = DN(value)
|
||||
if type(value) not in (unicode, int, float, bool, type(None), DN):
|
||||
raise TypeError(key, value)
|
||||
object.__setattr__(self, key, value)
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
self.__d[key] = value
|
||||
# pylint: enable=unsupported-assignment-operation
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return the value corresponding to ``key``.
|
||||
"""
|
||||
return self.__d[key]
|
||||
|
||||
def __delattr__(self, name):
|
||||
"""
|
||||
Raise an ``AttributeError`` (deletion is never allowed).
|
||||
|
||||
For example:
|
||||
|
||||
>>> env = Env()
|
||||
>>> env.name = 'A value'
|
||||
>>> del env.name
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: locked: cannot delete Env.name
|
||||
"""
|
||||
raise AttributeError(
|
||||
DEL_ERROR % (self.__class__.__name__, name)
|
||||
)
|
||||
|
||||
def __contains__(self, key):
|
||||
"""
|
||||
Return True if instance contains ``key``; otherwise return False.
|
||||
"""
|
||||
return key in self.__d
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return number of variables currently set.
|
||||
"""
|
||||
return len(self.__d)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate through keys in ascending order.
|
||||
"""
|
||||
for key in sorted(self.__d):
|
||||
yield key
|
||||
|
||||
def _merge(self, **kw):
|
||||
"""
|
||||
Merge variables from ``kw`` into the environment.
|
||||
|
||||
Any variables in ``kw`` that have already been set will be ignored
|
||||
(meaning this method will *not* try to override them, which would raise
|
||||
an exception).
|
||||
|
||||
This method returns a ``(num_set, num_total)`` tuple containing first
|
||||
the number of variables that were actually set, and second the total
|
||||
number of variables that were provided.
|
||||
|
||||
For example:
|
||||
|
||||
>>> env = Env()
|
||||
>>> env._merge(one=1, two=2)
|
||||
(2, 2)
|
||||
>>> env._merge(one=1, three=3)
|
||||
(1, 2)
|
||||
>>> env._merge(one=1, two=2, three=3)
|
||||
(0, 3)
|
||||
|
||||
Also see `Env._merge_from_file()`.
|
||||
|
||||
:param kw: Variables provides as keyword arguments.
|
||||
"""
|
||||
i = 0
|
||||
for (key, value) in kw.items():
|
||||
if key not in self:
|
||||
self[key] = value
|
||||
i += 1
|
||||
return (i, len(kw))
|
||||
|
||||
def _merge_from_file(self, config_file):
|
||||
"""
|
||||
Merge variables from ``config_file`` into the environment.
|
||||
|
||||
Any variables in ``config_file`` that have already been set will be
|
||||
ignored (meaning this method will *not* try to override them, which
|
||||
would raise an exception).
|
||||
|
||||
If ``config_file`` does not exist or is not a regular file, or if there
|
||||
is an error parsing ``config_file``, ``None`` is returned.
|
||||
|
||||
Otherwise this method returns a ``(num_set, num_total)`` tuple
|
||||
containing first the number of variables that were actually set, and
|
||||
second the total number of variables found in ``config_file``.
|
||||
|
||||
Also see `Env._merge()`.
|
||||
|
||||
:param config_file: Path of the configuration file to load.
|
||||
"""
|
||||
if not path.isfile(config_file):
|
||||
return
|
||||
parser = RawConfigParser()
|
||||
try:
|
||||
parser.read(config_file)
|
||||
except ParsingError:
|
||||
return
|
||||
if not parser.has_section(CONFIG_SECTION):
|
||||
parser.add_section(CONFIG_SECTION)
|
||||
items = parser.items(CONFIG_SECTION)
|
||||
if len(items) == 0:
|
||||
return (0, 0)
|
||||
i = 0
|
||||
for (key, value) in items:
|
||||
if key not in self:
|
||||
self[key] = value
|
||||
i += 1
|
||||
if 'config_loaded' not in self: # we loaded at least 1 file
|
||||
self['config_loaded'] = True
|
||||
return (i, len(items))
|
||||
|
||||
def _join(self, key, *parts):
|
||||
"""
|
||||
Append path components in ``parts`` to base path ``self[key]``.
|
||||
|
||||
For example:
|
||||
|
||||
>>> env = Env()
|
||||
>>> env.home = '/people/joe'
|
||||
>>> env._join('home', 'Music', 'favourites')
|
||||
u'/people/joe/Music/favourites'
|
||||
"""
|
||||
if key in self and self[key] is not None:
|
||||
return path.join(self[key], *parts)
|
||||
|
||||
def __doing(self, name):
|
||||
if name in self.__done:
|
||||
raise Exception(
|
||||
'%s.%s() already called' % (self.__class__.__name__, name)
|
||||
)
|
||||
self.__done.add(name)
|
||||
|
||||
def __do_if_not_done(self, name):
|
||||
if name not in self.__done:
|
||||
getattr(self, name)()
|
||||
|
||||
def _isdone(self, name):
|
||||
return name in self.__done
|
||||
|
||||
def _bootstrap(self, **overrides):
|
||||
"""
|
||||
Initialize basic environment.
|
||||
|
||||
This method will perform the following steps:
|
||||
|
||||
1. Initialize certain run-time variables. These run-time variables
|
||||
are strictly determined by the external environment the process
|
||||
is running in; they cannot be specified on the command-line nor
|
||||
in the configuration files.
|
||||
|
||||
2. Merge-in the variables in ``overrides`` by calling
|
||||
`Env._merge()`. The intended use of ``overrides`` is to merge-in
|
||||
variables specified on the command-line.
|
||||
|
||||
3. Intelligently fill-in the *in_tree*, *context*, *conf*, and
|
||||
*conf_default* variables if they haven't been set already.
|
||||
|
||||
Also see `Env._finalize_core()`, the next method in the bootstrap
|
||||
sequence.
|
||||
|
||||
:param overrides: Variables specified via command-line options.
|
||||
"""
|
||||
self.__doing('_bootstrap')
|
||||
|
||||
# Set run-time variables (cannot be overridden):
|
||||
self.ipalib = path.dirname(path.abspath(__file__))
|
||||
self.site_packages = path.dirname(self.ipalib)
|
||||
self.script = path.abspath(sys.argv[0])
|
||||
self.bin = path.dirname(self.script)
|
||||
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:
|
||||
if 'in_tree' not in self:
|
||||
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'
|
||||
|
||||
# Set dot_ipa:
|
||||
if 'dot_ipa' not in self:
|
||||
self.dot_ipa = self._join('home', '.ipa')
|
||||
|
||||
# Set context
|
||||
if 'context' not in self:
|
||||
self.context = 'default'
|
||||
|
||||
# Set confdir:
|
||||
self.env_confdir = os.environ.get('IPA_CONFDIR')
|
||||
|
||||
if 'confdir' in self and self.env_confdir is not None:
|
||||
raise errors.EnvironmentError(
|
||||
"IPA_CONFDIR env cannot be set because explicit confdir "
|
||||
"is used")
|
||||
|
||||
if 'confdir' not in self:
|
||||
if self.env_confdir is not None:
|
||||
if (not path.isabs(self.env_confdir)
|
||||
or not path.isdir(self.env_confdir)):
|
||||
raise errors.EnvironmentError(
|
||||
"IPA_CONFDIR env var must be an absolute path to an "
|
||||
"existing directory, got '{}'.".format(
|
||||
self.env_confdir))
|
||||
self.confdir = self.env_confdir
|
||||
elif self.in_tree:
|
||||
self.confdir = self.dot_ipa
|
||||
else:
|
||||
self.confdir = path.join('/', 'etc', 'ipa')
|
||||
|
||||
# Set conf (config file for this context):
|
||||
if 'conf' not in self:
|
||||
self.conf = self._join('confdir', '%s.conf' % self.context)
|
||||
|
||||
# Set conf_default (default base config used in all contexts):
|
||||
if 'conf_default' not in self:
|
||||
self.conf_default = self._join('confdir', 'default.conf')
|
||||
|
||||
if 'nss_dir' not in self:
|
||||
self.nss_dir = self._join('confdir', 'nssdb')
|
||||
|
||||
if 'tls_ca_cert' not in self:
|
||||
self.tls_ca_cert = self._join('confdir', 'ca.crt')
|
||||
|
||||
# having tls_ca_cert an absolute path could help us extending this
|
||||
# in the future for different certificate providers simply by adding
|
||||
# a prefix to the path
|
||||
if not path.isabs(self.tls_ca_cert):
|
||||
raise errors.EnvironmentError(
|
||||
"tls_ca_cert has to be an absolute path to a CA certificate, "
|
||||
"got '{}'".format(self.tls_ca_cert))
|
||||
|
||||
# Set plugins_on_demand:
|
||||
if 'plugins_on_demand' not in self:
|
||||
self.plugins_on_demand = (self.context == 'cli')
|
||||
|
||||
def _finalize_core(self, **defaults):
|
||||
"""
|
||||
Complete initialization of standard IPA environment.
|
||||
|
||||
This method will perform the following steps:
|
||||
|
||||
1. Call `Env._bootstrap()` if it hasn't already been called.
|
||||
|
||||
2. Merge-in variables from the configuration file ``self.conf``
|
||||
(if it exists) by calling `Env._merge_from_file()`.
|
||||
|
||||
3. Merge-in variables from the defaults configuration file
|
||||
``self.conf_default`` (if it exists) by calling
|
||||
`Env._merge_from_file()`.
|
||||
|
||||
4. Intelligently fill-in the *in_server* , *logdir*, *log*, and
|
||||
*jsonrpc_uri* variables if they haven't already been set.
|
||||
|
||||
5. Merge-in the variables in ``defaults`` by calling `Env._merge()`.
|
||||
In normal circumstances ``defaults`` will simply be those
|
||||
specified in `constants.DEFAULT_CONFIG`.
|
||||
|
||||
After this method is called, all the environment variables used by all
|
||||
the built-in plugins will be available. As such, this method should be
|
||||
called *before* any plugins are loaded.
|
||||
|
||||
After this method has finished, the `Env` instance is still writable
|
||||
so that 3rd-party plugins can set variables they may require as the
|
||||
plugins are registered.
|
||||
|
||||
Also see `Env._finalize()`, the final method in the bootstrap sequence.
|
||||
|
||||
:param defaults: Internal defaults for all built-in variables.
|
||||
"""
|
||||
self.__doing('_finalize_core')
|
||||
self.__do_if_not_done('_bootstrap')
|
||||
|
||||
# Merge in context config file and then default config file:
|
||||
if self.__d.get('mode', None) != 'dummy':
|
||||
self._merge_from_file(self.conf)
|
||||
self._merge_from_file(self.conf_default)
|
||||
|
||||
# Determine if in_server:
|
||||
if 'in_server' not in self:
|
||||
self.in_server = (self.context == 'server')
|
||||
|
||||
# Set logdir:
|
||||
if 'logdir' not in self:
|
||||
if self.in_tree or not self.in_server:
|
||||
self.logdir = self._join('dot_ipa', 'log')
|
||||
else:
|
||||
self.logdir = path.join('/', 'var', 'log', 'ipa')
|
||||
|
||||
# Set log file:
|
||||
if 'log' not in self:
|
||||
self.log = self._join('logdir', '%s.log' % self.context)
|
||||
|
||||
if 'basedn' not in self and 'domain' in self:
|
||||
self.basedn = DN(*(('dc', dc) for dc in self.domain.split('.')))
|
||||
|
||||
# Derive xmlrpc_uri from server
|
||||
# (Note that this is done before deriving jsonrpc_uri from xmlrpc_uri
|
||||
# and server from jsonrpc_uri so that when only server or xmlrpc_uri
|
||||
# is specified, all 3 keys have a value.)
|
||||
if 'xmlrpc_uri' not in self and 'server' in self:
|
||||
self.xmlrpc_uri = 'https://{}/ipa/xml'.format(self.server)
|
||||
|
||||
# Derive ldap_uri from server
|
||||
if 'ldap_uri' not in self and 'server' in self:
|
||||
self.ldap_uri = 'ldap://{}'.format(self.server)
|
||||
|
||||
# Derive jsonrpc_uri from xmlrpc_uri
|
||||
if 'jsonrpc_uri' not in self:
|
||||
if 'xmlrpc_uri' in self:
|
||||
xmlrpc_uri = self.xmlrpc_uri
|
||||
else:
|
||||
xmlrpc_uri = defaults.get('xmlrpc_uri')
|
||||
if xmlrpc_uri:
|
||||
(scheme, netloc, uripath, params, query, fragment
|
||||
) = urlparse(xmlrpc_uri)
|
||||
uripath = uripath.replace('/xml', '/json', 1)
|
||||
self.jsonrpc_uri = urlunparse((
|
||||
scheme, netloc, uripath, params, query, fragment))
|
||||
|
||||
if 'server' not in self:
|
||||
if 'jsonrpc_uri' in self:
|
||||
jsonrpc_uri = self.jsonrpc_uri
|
||||
else:
|
||||
jsonrpc_uri = defaults.get('jsonrpc_uri')
|
||||
if jsonrpc_uri:
|
||||
parsed = urlparse(jsonrpc_uri)
|
||||
self.server = parsed.netloc
|
||||
|
||||
self._merge(**defaults)
|
||||
|
||||
# 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_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_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_max < self.tls_version_min:
|
||||
raise errors.EnvironmentError(
|
||||
"tls_version_min is set to a higher TLS version than "
|
||||
"tls_version_max.")
|
||||
|
||||
def _finalize(self, **lastchance):
|
||||
"""
|
||||
Finalize and lock environment.
|
||||
|
||||
This method will perform the following steps:
|
||||
|
||||
1. Call `Env._finalize_core()` if it hasn't already been called.
|
||||
|
||||
2. Merge-in the variables in ``lastchance`` by calling
|
||||
`Env._merge()`.
|
||||
|
||||
3. Lock this `Env` instance, after which no more environment
|
||||
variables can be set on this instance. Aside from unit-tests
|
||||
and example code, normally only one `Env` instance is created,
|
||||
which means that after this step, no more variables can be set
|
||||
during the remaining life of the process.
|
||||
|
||||
This method should be called after all plugins have been loaded and
|
||||
after `plugable.API.finalize()` has been called.
|
||||
|
||||
:param lastchance: Any final variables to merge-in before locking.
|
||||
"""
|
||||
self.__doing('_finalize')
|
||||
self.__do_if_not_done('_finalize_core')
|
||||
self._merge(**lastchance)
|
||||
self.__lock__()
|
||||
328
ipalib/constants.py
Normal file
328
ipalib/constants.py
Normal file
@@ -0,0 +1,328 @@
|
||||
# Authors:
|
||||
# Martin Nagy <mnagy@redhat.com>
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
All constants centralised in one file.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
from ipapython.dn import DN
|
||||
from ipapython.version import VERSION, API_VERSION
|
||||
|
||||
try:
|
||||
FQDN = socket.getfqdn()
|
||||
except Exception:
|
||||
try:
|
||||
FQDN = socket.gethostname()
|
||||
except Exception:
|
||||
FQDN = None
|
||||
|
||||
# regular expression NameSpace member names must match:
|
||||
NAME_REGEX = r'^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'
|
||||
|
||||
# Format for ValueError raised when name does not match above regex:
|
||||
NAME_ERROR = "name must match '%s'; got '%s'"
|
||||
|
||||
# Standard format for TypeError message:
|
||||
TYPE_ERROR = '%s: need a %r; got %r (a %r)'
|
||||
|
||||
# Stardard format for TypeError message when a callable is expected:
|
||||
CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)'
|
||||
|
||||
# Standard format for Exception message when overriding an attribute:
|
||||
OVERRIDE_ERROR = 'cannot override %s.%s value %r with %r'
|
||||
|
||||
# Standard format for AttributeError message when a read-only attribute is
|
||||
# already locked:
|
||||
SET_ERROR = 'locked: cannot set %s.%s to %r'
|
||||
DEL_ERROR = 'locked: cannot delete %s.%s'
|
||||
|
||||
# Used for a tab (or indentation level) when formatting for CLI:
|
||||
CLI_TAB = ' ' # Two spaces
|
||||
|
||||
# The section to read in the config files, i.e. [global]
|
||||
CONFIG_SECTION = 'global'
|
||||
|
||||
# The default configuration for api.env
|
||||
# This is a tuple instead of a dict so that it is immutable.
|
||||
# To create a dict with this config, just "d = dict(DEFAULT_CONFIG)".
|
||||
DEFAULT_CONFIG = (
|
||||
('api_version', API_VERSION),
|
||||
('version', VERSION),
|
||||
|
||||
# Domain, realm, basedn:
|
||||
# Following values do not have any reasonable default.
|
||||
# Do not initialize them so the code which depends on them blows up early
|
||||
# and does not do crazy stuff with default values instead of real ones.
|
||||
# ('domain', 'example.com'),
|
||||
# ('realm', 'EXAMPLE.COM'),
|
||||
# ('basedn', DN(('dc', 'example'), ('dc', 'com'))),
|
||||
|
||||
# LDAP containers:
|
||||
('container_accounts', DN(('cn', 'accounts'))),
|
||||
('container_user', DN(('cn', 'users'), ('cn', 'accounts'))),
|
||||
('container_deleteuser', DN(('cn', 'deleted users'), ('cn', 'accounts'), ('cn', 'provisioning'))),
|
||||
('container_stageuser', DN(('cn', 'staged users'), ('cn', 'accounts'), ('cn', 'provisioning'))),
|
||||
('container_group', DN(('cn', 'groups'), ('cn', 'accounts'))),
|
||||
('container_service', DN(('cn', 'services'), ('cn', 'accounts'))),
|
||||
('container_host', DN(('cn', 'computers'), ('cn', 'accounts'))),
|
||||
('container_hostgroup', DN(('cn', 'hostgroups'), ('cn', 'accounts'))),
|
||||
('container_rolegroup', DN(('cn', 'roles'), ('cn', 'accounts'))),
|
||||
('container_permission', DN(('cn', 'permissions'), ('cn', 'pbac'))),
|
||||
('container_privilege', DN(('cn', 'privileges'), ('cn', 'pbac'))),
|
||||
('container_automount', DN(('cn', 'automount'))),
|
||||
('container_policies', DN(('cn', 'policies'))),
|
||||
('container_configs', DN(('cn', 'configs'), ('cn', 'policies'))),
|
||||
('container_roles', DN(('cn', 'roles'), ('cn', 'policies'))),
|
||||
('container_applications', DN(('cn', 'applications'), ('cn', 'configs'), ('cn', 'policies'))),
|
||||
('container_policygroups', DN(('cn', 'policygroups'), ('cn', 'configs'), ('cn', 'policies'))),
|
||||
('container_policylinks', DN(('cn', 'policylinks'), ('cn', 'configs'), ('cn', 'policies'))),
|
||||
('container_netgroup', DN(('cn', 'ng'), ('cn', 'alt'))),
|
||||
('container_hbac', DN(('cn', 'hbac'))),
|
||||
('container_hbacservice', DN(('cn', 'hbacservices'), ('cn', 'hbac'))),
|
||||
('container_hbacservicegroup', DN(('cn', 'hbacservicegroups'), ('cn', 'hbac'))),
|
||||
('container_dns', DN(('cn', 'dns'))),
|
||||
('container_vault', DN(('cn', 'vaults'), ('cn', 'kra'))),
|
||||
('container_virtual', DN(('cn', 'virtual operations'), ('cn', 'etc'))),
|
||||
('container_sudorule', DN(('cn', 'sudorules'), ('cn', 'sudo'))),
|
||||
('container_sudocmd', DN(('cn', 'sudocmds'), ('cn', 'sudo'))),
|
||||
('container_sudocmdgroup', DN(('cn', 'sudocmdgroups'), ('cn', 'sudo'))),
|
||||
('container_automember', DN(('cn', 'automember'), ('cn', 'etc'))),
|
||||
('container_selinux', DN(('cn', 'usermap'), ('cn', 'selinux'))),
|
||||
('container_s4u2proxy', DN(('cn', 's4u2proxy'), ('cn', 'etc'))),
|
||||
('container_cifsdomains', DN(('cn', 'ad'), ('cn', 'etc'))),
|
||||
('container_trusts', DN(('cn', 'trusts'))),
|
||||
('container_adtrusts', DN(('cn', 'ad'), ('cn', 'trusts'))),
|
||||
('container_ranges', DN(('cn', 'ranges'), ('cn', 'etc'))),
|
||||
('container_dna', DN(('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_dna_posix_ids', DN(('cn', 'posix-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_realm_domains', DN(('cn', 'Realm Domains'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_otp', DN(('cn', 'otp'))),
|
||||
('container_radiusproxy', DN(('cn', 'radiusproxy'))),
|
||||
('container_views', DN(('cn', 'views'), ('cn', 'accounts'))),
|
||||
('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
|
||||
('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
|
||||
('container_locations', DN(('cn', 'locations'), ('cn', 'etc'))),
|
||||
('container_ca', DN(('cn', 'cas'), ('cn', 'ca'))),
|
||||
('container_dnsservers', DN(('cn', 'servers'), ('cn', 'dns'))),
|
||||
('container_custodia', DN(('cn', 'custodia'), ('cn', 'ipa'), ('cn', 'etc'))),
|
||||
('container_sysaccounts', DN(('cn', 'sysaccounts'), ('cn', 'etc'))),
|
||||
('container_certmap', DN(('cn', 'certmap'))),
|
||||
('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'))),
|
||||
|
||||
# Ports, hosts, and URIs:
|
||||
# Following values do not have any reasonable default.
|
||||
# Do not initialize them so the code which depends on them blows up early
|
||||
# and does not do crazy stuff with default values instead of real ones.
|
||||
# ('server', 'localhost'),
|
||||
# ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
|
||||
# ('jsonrpc_uri', 'http://localhost:8888/ipa/json'),
|
||||
# ('ldap_uri', 'ldap://localhost:389'),
|
||||
|
||||
('rpc_protocol', 'jsonrpc'),
|
||||
|
||||
# Define an inclusive range of SSL/TLS version support
|
||||
('tls_version_min', 'tls1.0'),
|
||||
('tls_version_max', 'tls1.2'),
|
||||
|
||||
# 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),
|
||||
|
||||
# Web Application mount points
|
||||
('mount_ipa', '/ipa/'),
|
||||
|
||||
# WebUI stuff:
|
||||
('webui_prod', True),
|
||||
|
||||
# Session stuff:
|
||||
|
||||
# Maximum time before a session expires forcing credentials to be reacquired.
|
||||
('session_auth_duration', '20 minutes'),
|
||||
# How a session expiration is computed, see SessionManager.set_session_expiration_time()
|
||||
('session_duration_type', 'inactivity_timeout'),
|
||||
('kinit_lifetime', None),
|
||||
|
||||
# Debugging:
|
||||
('verbose', 0),
|
||||
('debug', False),
|
||||
('startup_traceback', False),
|
||||
('mode', 'production'),
|
||||
('wait_for_dns', 0),
|
||||
|
||||
# CA plugin:
|
||||
('ca_host', FQDN), # Set in Env._finalize_core()
|
||||
('ca_port', 80),
|
||||
('ca_agent_port', 443),
|
||||
('ca_ee_port', 443),
|
||||
# For the following ports, None means a default specific to the installed
|
||||
# Dogtag version.
|
||||
('ca_install_port', None),
|
||||
('ca_agent_install_port', None),
|
||||
('ca_ee_install_port', None),
|
||||
|
||||
# Topology plugin
|
||||
('recommended_max_agmts', 4), # Recommended maximum number of replication
|
||||
# agreements
|
||||
|
||||
# Special CLI:
|
||||
('prompt_all', False),
|
||||
('interactive', True),
|
||||
('fallback', True),
|
||||
('delegate', False),
|
||||
|
||||
# Enable certain optional plugins:
|
||||
('enable_ra', False),
|
||||
('ra_plugin', 'selfsign'),
|
||||
('dogtag_version', 9),
|
||||
|
||||
# Used when verifying that the API hasn't changed. Not for production.
|
||||
('validate_api', False),
|
||||
|
||||
# Skip client vs. server API version checking. Can lead to errors/strange
|
||||
# behavior when newer clients talk to older servers. Use with caution.
|
||||
('skip_version_check', False),
|
||||
|
||||
# Ignore TTL. Perform schema call and download schema if not in cache.
|
||||
('force_schema_check', False),
|
||||
|
||||
# ********************************************************
|
||||
# The remaining keys are never set from the values here!
|
||||
# ********************************************************
|
||||
#
|
||||
# Env._bootstrap() or Env._finalize_core() will have filled in all the keys
|
||||
# below by the time DEFAULT_CONFIG is merged in, so the values below are
|
||||
# never actually used. They are listed both to provide a big picture and
|
||||
# also so DEFAULT_CONFIG contains at least all the keys that should be
|
||||
# present after Env._finalize_core() is called.
|
||||
#
|
||||
# Each environment variable below is sent to ``object``, which just happens
|
||||
# to be an invalid value for an environment variable, so if for some reason
|
||||
# any of these keys were set from the values here, an exception will be
|
||||
# raised.
|
||||
|
||||
# Non-overridable vars set in Env._bootstrap():
|
||||
('host', FQDN),
|
||||
('ipalib', object), # The directory containing ipalib/__init__.py
|
||||
('site_packages', object), # The directory contaning ipalib
|
||||
('script', object), # sys.argv[0]
|
||||
('bin', object), # The directory containing the script
|
||||
('home', object), # $HOME
|
||||
|
||||
# Vars set in Env._bootstrap():
|
||||
('in_tree', object), # Whether or not running in-tree (bool)
|
||||
('dot_ipa', object), # ~/.ipa directory
|
||||
('context', object), # Name of context, default is 'default'
|
||||
('confdir', object), # Directory containing config files
|
||||
('env_confdir', None), # conf dir specified by IPA_CONFDIR env variable
|
||||
('conf', object), # File containing context specific config
|
||||
('conf_default', object), # File containing context independent config
|
||||
('plugins_on_demand', object), # Whether to finalize plugins on-demand (bool)
|
||||
('nss_dir', object), # Path to nssdb, default {confdir}/nssdb
|
||||
('tls_ca_cert', object), # Path to CA cert file
|
||||
|
||||
# Set in Env._finalize_core():
|
||||
('in_server', object), # Whether or not running in-server (bool)
|
||||
('logdir', object), # Directory containing log files
|
||||
('log', object), # Path to context specific log file
|
||||
|
||||
)
|
||||
|
||||
LDAP_GENERALIZED_TIME_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
IPA_ANCHOR_PREFIX = ':IPA:'
|
||||
SID_ANCHOR_PREFIX = ':SID:'
|
||||
|
||||
# domains levels
|
||||
DOMAIN_LEVEL_0 = 0 # compat
|
||||
DOMAIN_LEVEL_1 = 1 # replica promotion, topology plugin
|
||||
|
||||
MIN_DOMAIN_LEVEL = DOMAIN_LEVEL_0
|
||||
MAX_DOMAIN_LEVEL = DOMAIN_LEVEL_1
|
||||
|
||||
# Constants used in generation of replication agreements and as topology
|
||||
# defaults
|
||||
|
||||
# List of attributes that need to be excluded from replication initialization.
|
||||
REPL_AGMT_TOTAL_EXCLUDES = ('entryusn',
|
||||
'krblastsuccessfulauth',
|
||||
'krblastfailedauth',
|
||||
'krbloginfailedcount')
|
||||
|
||||
# List of attributes that need to be excluded from normal replication.
|
||||
REPL_AGMT_EXCLUDES = ('memberof', 'idnssoaserial') + REPL_AGMT_TOTAL_EXCLUDES
|
||||
|
||||
# List of attributes that are not updated on empty replication
|
||||
REPL_AGMT_STRIP_ATTRS = ('modifiersName',
|
||||
'modifyTimestamp',
|
||||
'internalModifiersName',
|
||||
'internalModifyTimestamp')
|
||||
|
||||
DOMAIN_SUFFIX_NAME = 'domain'
|
||||
CA_SUFFIX_NAME = 'ca'
|
||||
PKI_GSSAPI_SERVICE_NAME = 'dogtag'
|
||||
IPA_CA_CN = u'ipa'
|
||||
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'
|
||||
# How long dbus clients should wait for CA certificate RPCs [seconds]
|
||||
CA_DBUS_TIMEOUT = 120
|
||||
|
||||
# regexp definitions
|
||||
PATTERN_GROUPUSER_NAME = '^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$'
|
||||
|
||||
# Kerberos Anonymous principal name
|
||||
ANON_USER = 'WELLKNOWN/ANONYMOUS'
|
||||
|
||||
# IPA API Framework user
|
||||
IPAAPI_USER = 'ipaapi'
|
||||
IPAAPI_GROUP = 'ipaapi'
|
||||
|
||||
# TLS related constants
|
||||
TLS_VERSIONS = [
|
||||
"ssl2",
|
||||
"ssl3",
|
||||
"tls1.0",
|
||||
"tls1.1",
|
||||
"tls1.2"
|
||||
]
|
||||
TLS_VERSION_MINIMAL = "tls1.0"
|
||||
# high ciphers without RC4, MD5, TripleDES, pre-shared key
|
||||
# and secure remote password
|
||||
TLS_HIGH_CIPHERS = "HIGH:!aNULL:!eNULL:!MD5:!RC4:!3DES:!PSK:!SRP"
|
||||
|
||||
# Use cache path
|
||||
USER_CACHE_PATH = (
|
||||
os.environ.get('XDG_CACHE_HOME') or
|
||||
os.path.join(
|
||||
os.environ.get(
|
||||
'HOME',
|
||||
os.path.expanduser('~')
|
||||
),
|
||||
'.cache'
|
||||
)
|
||||
)
|
||||
|
||||
SOFTHSM_DNSSEC_TOKEN_LABEL = u'ipaDNSSEC'
|
||||
351
ipalib/crud.py
Normal file
351
ipalib/crud.py
Normal file
@@ -0,0 +1,351 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base classes for standard CRUD operations.
|
||||
|
||||
These base classes are for `Method` plugins that provide standard
|
||||
Create, Retrieve, Updated, and Delete operations (CRUD) for their corresponding
|
||||
`Object` plugin. In particuar, these base classes provide logic to
|
||||
automatically create the plugin args and options by inspecting the params on
|
||||
their corresponding `Object` plugin. This provides a single point of definition
|
||||
for LDAP attributes and enforces a simple, consistent API for CRUD operations.
|
||||
|
||||
For example, say we want CRUD operations on a hypothetical "user" entry. First
|
||||
we need an `Object` plugin:
|
||||
|
||||
>>> from ipalib import Object, Str
|
||||
>>> class user(Object):
|
||||
... takes_params = (
|
||||
... Str('login', primary_key=True),
|
||||
... Str('first'),
|
||||
... Str('last'),
|
||||
... Str('ipauniqueid', flags=['no_create', 'no_update']),
|
||||
... )
|
||||
...
|
||||
|
||||
Next we need `Create`, `Retrieve`, `Updated`, and `Delete` plugins, and
|
||||
optionally a `Search` plugin. For brevity, we'll just define `Create` and
|
||||
`Retrieve` plugins:
|
||||
|
||||
>>> from ipalib import crud
|
||||
>>> class user_add(crud.Create):
|
||||
... pass
|
||||
...
|
||||
>>> class user_show(crud.Retrieve):
|
||||
... pass
|
||||
...
|
||||
|
||||
Now we'll register the plugins and finalize the `plugable.API` instance:
|
||||
|
||||
>>> from ipalib import create_api
|
||||
>>> api = create_api()
|
||||
>>> api.add_plugin(user)
|
||||
>>> api.add_plugin(user_add)
|
||||
>>> api.add_plugin(user_show)
|
||||
>>> api.finalize()
|
||||
|
||||
First, notice that our ``user`` `Object` has the params we defined with the
|
||||
``takes_params`` tuple:
|
||||
|
||||
>>> list(api.Object.user.params)
|
||||
['login', 'first', 'last', 'ipauniqueid']
|
||||
>>> api.Object.user.params.login
|
||||
Str('login', primary_key=True)
|
||||
|
||||
Although we defined neither ``takes_args`` nor ``takes_options`` for our
|
||||
``user_add`` plugin, the `Create` base class automatically generated them for
|
||||
us:
|
||||
|
||||
>>> list(api.Command.user_add.args)
|
||||
['login']
|
||||
>>> list(api.Command.user_add.options)
|
||||
['first', 'last', 'all', 'raw', 'version']
|
||||
|
||||
Notice that ``'ipauniqueid'`` isn't included in the options for our ``user_add``
|
||||
plugin. This is because of the ``'no_create'`` flag we used when defining the
|
||||
``ipauniqueid`` param. Often times there are LDAP attributes that are
|
||||
automatically created by the server and therefor should not be supplied as an
|
||||
option to the `Create` plugin. Often these same attributes shouldn't be
|
||||
update-able either, in which case you can also supply the ``'no_update'`` flag,
|
||||
as we did with our ``ipauniqueid`` param. Lastly, you can also use the ``'no_search'`` flag for attributes that shouldn't be search-able (because, for
|
||||
example, the attribute isn't indexed).
|
||||
|
||||
As with our ``user_add` plugin, we defined neither ``takes_args`` nor
|
||||
``takes_options`` for our ``user_show`` plugin; instead the `Retrieve` base
|
||||
class created them for us:
|
||||
|
||||
>>> list(api.Command.user_show.args)
|
||||
['login']
|
||||
>>> list(api.Command.user_show.options)
|
||||
['all', 'raw', 'version']
|
||||
|
||||
As you can see, `Retrieve` plugins take a single argument (the primary key) and
|
||||
no options. If needed, you can still specify options for your `Retrieve` plugin
|
||||
with a ``takes_options`` tuple.
|
||||
|
||||
Flags like ``'no_create'`` remove LDAP attributes from those that can be
|
||||
supplied as *input* to a `Method`, but they don't effect the attributes that can
|
||||
be returned as *output*. Regardless of what flags have been used, the output
|
||||
entry (or list of entries) can contain all the attributes defined on the
|
||||
`Object` plugin (in our case, the above ``user.params``).
|
||||
|
||||
For example, compare ``user.params`` with ``user_add.output_params`` and
|
||||
``user_show.output_params``:
|
||||
|
||||
>>> list(api.Object.user.params)
|
||||
['login', 'first', 'last', 'ipauniqueid']
|
||||
>>> list(api.Command.user_add.output_params)
|
||||
['login', 'first', 'last', 'ipauniqueid']
|
||||
>>> list(api.Command.user_show.output_params)
|
||||
['login', 'first', 'last', 'ipauniqueid']
|
||||
|
||||
Note that the above are all equal.
|
||||
"""
|
||||
|
||||
from ipalib.frontend import Method
|
||||
from ipalib import backend
|
||||
from ipalib import parameters
|
||||
from ipalib import output
|
||||
from ipalib.text import _
|
||||
|
||||
|
||||
class Create(Method):
|
||||
"""
|
||||
Create a new entry.
|
||||
"""
|
||||
|
||||
has_output = output.standard_entry
|
||||
|
||||
def __clone(self, param, **kw):
|
||||
if 'optional_create' in param.flags:
|
||||
kw['required'] = False
|
||||
return param.clone(**kw) if kw else param
|
||||
|
||||
def get_args(self):
|
||||
if self.obj.primary_key:
|
||||
yield self.__clone(self.obj.primary_key, attribute=True)
|
||||
for arg in super(Create, self).get_args():
|
||||
yield self.__clone(arg)
|
||||
|
||||
def get_options(self):
|
||||
if self.extra_options_first:
|
||||
for option in super(Create, self).get_options():
|
||||
yield self.__clone(option)
|
||||
for option in self.obj.params_minus(self.args):
|
||||
attribute = 'virtual_attribute' not in option.flags
|
||||
if 'no_create' in option.flags:
|
||||
continue
|
||||
if 'ask_create' in option.flags:
|
||||
yield option.clone(
|
||||
attribute=attribute, query=False, required=False,
|
||||
autofill=False, alwaysask=True
|
||||
)
|
||||
else:
|
||||
yield self.__clone(option, attribute=attribute)
|
||||
if not self.extra_options_first:
|
||||
for option in super(Create, self).get_options():
|
||||
yield self.__clone(option)
|
||||
|
||||
|
||||
class PKQuery(Method):
|
||||
"""
|
||||
Base class for `Retrieve`, `Update`, and `Delete`.
|
||||
"""
|
||||
|
||||
def get_args(self):
|
||||
if self.obj.primary_key:
|
||||
# Don't enforce rules on the primary key so we can reference
|
||||
# any stored entry, legal or not
|
||||
yield self.obj.primary_key.clone(attribute=True, query=True)
|
||||
for arg in super(PKQuery, self).get_args():
|
||||
yield arg
|
||||
|
||||
|
||||
class Retrieve(PKQuery):
|
||||
"""
|
||||
Retrieve an entry by its primary key.
|
||||
"""
|
||||
|
||||
has_output = output.standard_entry
|
||||
|
||||
|
||||
class Update(PKQuery):
|
||||
"""
|
||||
Update one or more attributes on an entry.
|
||||
"""
|
||||
|
||||
has_output = output.standard_entry
|
||||
|
||||
def get_options(self):
|
||||
if self.extra_options_first:
|
||||
for option in super(Update, self).get_options():
|
||||
yield option
|
||||
for option in self.obj.params_minus_pk():
|
||||
new_flags = option.flags
|
||||
attribute = 'virtual_attribute' not in option.flags
|
||||
if option.required:
|
||||
# Required options turn into non-required, since not specifying
|
||||
# them means that they are not changed.
|
||||
# However, they cannot be empty (i.e. explicitly set to None).
|
||||
new_flags = new_flags.union(['nonempty'])
|
||||
if 'no_update' in option.flags:
|
||||
continue
|
||||
if 'ask_update' in option.flags:
|
||||
yield option.clone(
|
||||
attribute=attribute, query=False, required=False,
|
||||
autofill=False, alwaysask=True, flags=new_flags,
|
||||
)
|
||||
elif 'req_update' in option.flags:
|
||||
yield option.clone(
|
||||
attribute=attribute, required=True, alwaysask=False,
|
||||
flags=new_flags,
|
||||
)
|
||||
else:
|
||||
yield option.clone(attribute=attribute, required=False,
|
||||
autofill=False, flags=new_flags,
|
||||
)
|
||||
if not self.extra_options_first:
|
||||
for option in super(Update, self).get_options():
|
||||
yield option
|
||||
|
||||
|
||||
class Delete(PKQuery):
|
||||
"""
|
||||
Delete one or more entries.
|
||||
"""
|
||||
|
||||
has_output = output.standard_delete
|
||||
|
||||
|
||||
class Search(Method):
|
||||
"""
|
||||
Retrieve all entries that match a given search criteria.
|
||||
"""
|
||||
|
||||
has_output = output.standard_list_of_entries
|
||||
|
||||
def get_args(self):
|
||||
yield parameters.Str(
|
||||
'criteria?', noextrawhitespace=False,
|
||||
doc=_('A string searched in all relevant object attributes'))
|
||||
for arg in super(Search, self).get_args():
|
||||
yield arg
|
||||
|
||||
def get_options(self):
|
||||
if self.extra_options_first:
|
||||
for option in super(Search, self).get_options():
|
||||
yield option
|
||||
for option in self.obj.params_minus(self.args):
|
||||
attribute = 'virtual_attribute' not in option.flags
|
||||
if 'no_search' in option.flags:
|
||||
continue
|
||||
if 'ask_search' in option.flags:
|
||||
yield option.clone(
|
||||
attribute=attribute, query=True, required=False,
|
||||
autofill=False, alwaysask=True
|
||||
)
|
||||
elif isinstance(option, parameters.Flag):
|
||||
yield option.clone_retype(
|
||||
option.name, parameters.Bool,
|
||||
attribute=attribute, query=True, required=False, autofill=False
|
||||
)
|
||||
else:
|
||||
yield option.clone(
|
||||
attribute=attribute, query=True, required=False, autofill=False
|
||||
)
|
||||
if not self.extra_options_first:
|
||||
for option in super(Search, self).get_options():
|
||||
yield option
|
||||
|
||||
|
||||
class CrudBackend(backend.Connectible):
|
||||
"""
|
||||
Base class defining generic CRUD backend API.
|
||||
"""
|
||||
|
||||
def create(self, **kw):
|
||||
"""
|
||||
Create a new entry.
|
||||
|
||||
This method should take key word arguments representing the
|
||||
attributes the created entry will have.
|
||||
|
||||
If this methods constructs the primary_key internally, it should raise
|
||||
an exception if the primary_key was passed. Likewise, if this method
|
||||
requires the primary_key to be passed in from the caller, it should
|
||||
raise an exception if the primary key was *not* passed.
|
||||
|
||||
This method should return a dict of the exact entry as it was created
|
||||
in the backing store, including any automatically created attributes.
|
||||
"""
|
||||
raise NotImplementedError('%s.create()' % self.name)
|
||||
|
||||
def retrieve(self, primary_key, attributes):
|
||||
"""
|
||||
Retrieve an existing entry.
|
||||
|
||||
This method should take a two arguments: the primary_key of the
|
||||
entry in question and a list of the attributes to be retrieved.
|
||||
If the list of attributes is None then all non-operational
|
||||
attributes will be returned.
|
||||
|
||||
If such an entry exists, this method should return a dict
|
||||
representing that entry. If no such entry exists, this method
|
||||
should return None.
|
||||
"""
|
||||
raise NotImplementedError('%s.retrieve()' % self.name)
|
||||
|
||||
def update(self, primary_key, **kw):
|
||||
"""
|
||||
Update an existing entry.
|
||||
|
||||
This method should take one required argument, the primary_key of the
|
||||
entry to modify, plus optional keyword arguments for each of the
|
||||
attributes being updated.
|
||||
|
||||
This method should return a dict representing the entry as it now
|
||||
exists in the backing store. If no such entry exists, this method
|
||||
should return None.
|
||||
"""
|
||||
raise NotImplementedError('%s.update()' % self.name)
|
||||
|
||||
def delete(self, primary_key):
|
||||
"""
|
||||
Delete an existing entry.
|
||||
|
||||
This method should take one required argument, the primary_key of the
|
||||
entry to delete.
|
||||
"""
|
||||
raise NotImplementedError('%s.delete()' % self.name)
|
||||
|
||||
def search(self, **kw):
|
||||
"""
|
||||
Return entries matching specific criteria.
|
||||
|
||||
This method should take keyword arguments representing the search
|
||||
criteria. If a key is the name of an entry attribute, the value
|
||||
should be treated as a filter on that attribute. The meaning of
|
||||
keys outside this namespace is left to the implementation.
|
||||
|
||||
This method should return and iterable containing the matched
|
||||
entries, where each entry is a dict. If no entries are matched,
|
||||
this method should return an empty iterable.
|
||||
"""
|
||||
raise NotImplementedError('%s.search()' % self.name)
|
||||
120
ipalib/dns.py
Normal file
120
ipalib/dns.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Authors:
|
||||
# Martin Kosek <mkosek@redhat.com>
|
||||
# Pavel Zuna <pzuna@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2010 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from ipalib import errors
|
||||
|
||||
# dnsrecord param name formats
|
||||
record_name_format = '%srecord'
|
||||
part_name_format = "%s_part_%s"
|
||||
extra_name_format = "%s_extra_%s"
|
||||
|
||||
|
||||
def get_record_rrtype(name):
|
||||
match = re.match('([^_]+)record$', name)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return match.group(1).upper()
|
||||
|
||||
|
||||
def get_part_rrtype(name):
|
||||
match = re.match('([^_]+)_part_.*$', name)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return match.group(1).upper()
|
||||
|
||||
|
||||
def get_extra_rrtype(name):
|
||||
match = re.match('([^_]+)_extra_.*$', name)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
return match.group(1).upper()
|
||||
|
||||
|
||||
def has_cli_options(cmd, options, no_option_msg, allow_empty_attrs=False):
|
||||
sufficient = ('setattr', 'addattr', 'delattr', 'rename')
|
||||
if any(k in options for k in sufficient):
|
||||
return
|
||||
|
||||
has_options = False
|
||||
for attr in options.keys():
|
||||
obj_params = [n for n in cmd.params
|
||||
if get_record_rrtype(n) or get_part_rrtype(n)]
|
||||
if attr in obj_params:
|
||||
if options[attr] or allow_empty_attrs:
|
||||
has_options = True
|
||||
break
|
||||
|
||||
if not has_options:
|
||||
raise errors.OptionError(no_option_msg)
|
||||
|
||||
|
||||
def get_rrparam_from_part(cmd, part_name):
|
||||
"""
|
||||
Get an instance of DNSRecord parameter that has part_name as its part.
|
||||
If such parameter is not found, None is returned
|
||||
|
||||
:param part_name Part parameter name
|
||||
"""
|
||||
try:
|
||||
param = cmd.params[part_name]
|
||||
|
||||
rrtype = (get_part_rrtype(param.name) or
|
||||
get_extra_rrtype(param.name))
|
||||
if not rrtype:
|
||||
return None
|
||||
|
||||
# All DNS record part or extra parameters contain a name of its
|
||||
# parent RR parameter in its hint attribute
|
||||
rrparam = cmd.params[record_name_format % rrtype.lower()]
|
||||
except (KeyError, AttributeError):
|
||||
return None
|
||||
|
||||
return rrparam
|
||||
|
||||
|
||||
def iterate_rrparams_by_parts(cmd, kw, skip_extra=False):
|
||||
"""
|
||||
Iterates through all DNSRecord instances that has at least one of its
|
||||
parts or extra options in given dictionary. It returns the DNSRecord
|
||||
instance only for the first occurence of part/extra option.
|
||||
|
||||
:param kw Dictionary with DNS record parts or extra options
|
||||
:param skip_extra Skip DNS record extra options, yield only DNS records
|
||||
with a real record part
|
||||
"""
|
||||
processed = []
|
||||
for opt in kw:
|
||||
rrparam = get_rrparam_from_part(cmd, opt)
|
||||
if rrparam is None:
|
||||
continue
|
||||
|
||||
if skip_extra and get_extra_rrtype(opt):
|
||||
continue
|
||||
|
||||
if rrparam.name not in processed:
|
||||
processed.append(rrparam.name)
|
||||
yield rrparam
|
||||
2011
ipalib/errors.py
Normal file
2011
ipalib/errors.py
Normal file
File diff suppressed because it is too large
Load Diff
1473
ipalib/frontend.py
Normal file
1473
ipalib/frontend.py
Normal file
File diff suppressed because it is too large
Load Diff
3
ipalib/install/__init__.py
Normal file
3
ipalib/install/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
680
ipalib/install/certmonger.py
Normal file
680
ipalib/install/certmonger.py
Normal file
@@ -0,0 +1,680 @@
|
||||
# Authors: Rob Crittenden <rcritten@redhat.com>
|
||||
# David Kupka <dkupka@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2010 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/>.
|
||||
#
|
||||
|
||||
# Some certmonger functions, mostly around updating the request file.
|
||||
# This is used so we can add tracking to the Apache and 389-ds
|
||||
# server certificates created during the IPA server installation.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import dbus
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
from ipalib import api
|
||||
from ipalib.constants import CA_DBUS_TIMEOUT
|
||||
from ipapython.dn import DN
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform import services
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DBUS_CM_PATH = '/org/fedorahosted/certmonger'
|
||||
DBUS_CM_IF = 'org.fedorahosted.certmonger'
|
||||
DBUS_CM_NAME = 'org.fedorahosted.certmonger'
|
||||
DBUS_CM_REQUEST_IF = 'org.fedorahosted.certmonger.request'
|
||||
DBUS_CM_CA_IF = 'org.fedorahosted.certmonger.ca'
|
||||
DBUS_PROPERTY_IF = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
|
||||
class _cm_dbus_object(object):
|
||||
"""
|
||||
Auxiliary class for convenient DBus object handling.
|
||||
"""
|
||||
def __init__(self, bus, parent, object_path, object_dbus_interface,
|
||||
parent_dbus_interface=None, property_interface=False):
|
||||
"""
|
||||
bus - DBus bus object, result of dbus.SystemBus() or dbus.SessionBus()
|
||||
Object is accesible over this DBus bus instance.
|
||||
object_path - path to requested object on DBus bus
|
||||
object_dbus_interface
|
||||
parent_dbus_interface
|
||||
property_interface - create DBus property interface? True or False
|
||||
"""
|
||||
if bus is None or object_path is None or object_dbus_interface is None:
|
||||
raise RuntimeError(
|
||||
"bus, object_path and dbus_interface must not be None.")
|
||||
if parent_dbus_interface is None:
|
||||
parent_dbus_interface = object_dbus_interface
|
||||
self.bus = bus
|
||||
self.parent = parent
|
||||
self.path = object_path
|
||||
self.obj_dbus_if = object_dbus_interface
|
||||
self.parent_dbus_if = parent_dbus_interface
|
||||
self.obj = bus.get_object(parent_dbus_interface, object_path)
|
||||
self.obj_if = dbus.Interface(self.obj, object_dbus_interface)
|
||||
if property_interface:
|
||||
self.prop_if = dbus.Interface(self.obj, DBUS_PROPERTY_IF)
|
||||
|
||||
|
||||
class _certmonger(_cm_dbus_object):
|
||||
"""
|
||||
Create a connection to certmonger.
|
||||
By default use SystemBus. When not available use private connection
|
||||
over Unix socket.
|
||||
This solution is really ugly and should be removed as soon as DBus
|
||||
SystemBus is available at system install time.
|
||||
"""
|
||||
timeout = 300
|
||||
|
||||
def _start_private_conn(self):
|
||||
sock_filename = os.path.join(tempfile.mkdtemp(), 'certmonger')
|
||||
self._proc = subprocess.Popen([paths.CERTMONGER, '-n', '-L', '-P',
|
||||
sock_filename])
|
||||
for _t in range(0, self.timeout, 5):
|
||||
if os.path.exists(sock_filename):
|
||||
return "unix:path=%s" % sock_filename
|
||||
time.sleep(5)
|
||||
self._stop_private_conn()
|
||||
raise RuntimeError("Failed to start certmonger: Timed out")
|
||||
|
||||
def _stop_private_conn(self):
|
||||
if self._proc:
|
||||
retcode = self._proc.poll()
|
||||
if retcode is not None:
|
||||
return
|
||||
self._proc.terminate()
|
||||
for _t in range(0, self.timeout, 5):
|
||||
retcode = self._proc.poll()
|
||||
if retcode is not None:
|
||||
return
|
||||
time.sleep(5)
|
||||
logger.error("Failed to stop certmonger.")
|
||||
|
||||
def __del__(self):
|
||||
self._stop_private_conn()
|
||||
|
||||
def __init__(self):
|
||||
self._proc = None
|
||||
self._bus = None
|
||||
try:
|
||||
self._bus = dbus.SystemBus()
|
||||
except dbus.DBusException as e:
|
||||
err_name = e.get_dbus_name()
|
||||
if err_name not in ['org.freedesktop.DBus.Error.NoServer',
|
||||
'org.freedesktop.DBus.Error.FileNotFound']:
|
||||
logger.error("Failed to connect to certmonger over "
|
||||
"SystemBus: %s", e)
|
||||
raise
|
||||
try:
|
||||
self._private_sock = self._start_private_conn()
|
||||
self._bus = dbus.connection.Connection(self._private_sock)
|
||||
except dbus.DBusException as e:
|
||||
logger.error("Failed to connect to certmonger over "
|
||||
"private socket: %s", e)
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self._bus.get_name_owner(DBUS_CM_NAME)
|
||||
except dbus.DBusException:
|
||||
try:
|
||||
services.knownservices.certmonger.start()
|
||||
except Exception as e:
|
||||
logger.error("Failed to start certmonger: %s", e)
|
||||
raise
|
||||
|
||||
for _t in range(0, self.timeout, 5):
|
||||
try:
|
||||
self._bus.get_name_owner(DBUS_CM_NAME)
|
||||
break
|
||||
except dbus.DBusException:
|
||||
pass
|
||||
time.sleep(5)
|
||||
raise RuntimeError('Failed to start certmonger')
|
||||
|
||||
super(_certmonger, self).__init__(self._bus, None, DBUS_CM_PATH,
|
||||
DBUS_CM_IF)
|
||||
|
||||
|
||||
def _get_requests(criteria=dict()):
|
||||
"""
|
||||
Get all requests that matches the provided criteria.
|
||||
"""
|
||||
if not isinstance(criteria, dict):
|
||||
raise TypeError('"criteria" must be dict.')
|
||||
|
||||
cm = _certmonger()
|
||||
requests = []
|
||||
requests_paths = []
|
||||
if 'nickname' in criteria:
|
||||
request_path = cm.obj_if.find_request_by_nickname(criteria['nickname'])
|
||||
if request_path:
|
||||
requests_paths = [request_path]
|
||||
else:
|
||||
requests_paths = cm.obj_if.get_requests()
|
||||
|
||||
for request_path in requests_paths:
|
||||
request = _cm_dbus_object(cm.bus, cm, request_path, DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
for criterion in criteria:
|
||||
if criterion == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
ca = _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
value = ca.obj_if.get_nickname()
|
||||
else:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if value != criteria[criterion]:
|
||||
break
|
||||
else:
|
||||
requests.append(request)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _get_request(criteria):
|
||||
"""
|
||||
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:
|
||||
return None
|
||||
elif len(requests) == 1:
|
||||
return requests[0]
|
||||
else:
|
||||
raise RuntimeError("Criteria expected to be met by 1 request, got %s."
|
||||
% len(requests))
|
||||
|
||||
|
||||
def get_request_value(request_id, directive):
|
||||
"""
|
||||
Get property of request.
|
||||
"""
|
||||
try:
|
||||
request = _get_request(dict(nickname=request_id))
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
if directive == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
ca = _cm_dbus_object(request.bus, request, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
return ca.obj_if.get_nickname()
|
||||
else:
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, directive)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_requests_for_dir(dir):
|
||||
"""
|
||||
Return a list containing the request ids for a given NSS database
|
||||
directory.
|
||||
"""
|
||||
reqid = []
|
||||
criteria = {'cert-storage': 'NSSDB', 'key-storage': 'NSSDB',
|
||||
'cert-database': dir, 'key-database': dir, }
|
||||
requests = _get_requests(criteria)
|
||||
for request in requests:
|
||||
reqid.append(request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname'))
|
||||
|
||||
return reqid
|
||||
|
||||
|
||||
def add_request_value(request_id, directive, value):
|
||||
"""
|
||||
Add a new directive to a certmonger request file.
|
||||
"""
|
||||
try:
|
||||
request = _get_request({'nickname': request_id})
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
request.obj_if.modify({directive: value})
|
||||
|
||||
|
||||
def add_principal(request_id, principal):
|
||||
"""
|
||||
In order for a certmonger request to be renewable it needs a principal.
|
||||
|
||||
When an existing certificate is added via start-tracking it won't have
|
||||
a principal.
|
||||
"""
|
||||
add_request_value(request_id, 'template-principal', [principal])
|
||||
|
||||
|
||||
def add_subject(request_id, subject):
|
||||
"""
|
||||
In order for a certmonger request to be renwable it needs the subject
|
||||
set in the request file.
|
||||
|
||||
When an existing certificate is added via start-tracking it won't have
|
||||
a subject_template set.
|
||||
"""
|
||||
add_request_value(request_id, 'template-subject', subject)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Execute certmonger to request a server certificate.
|
||||
|
||||
The method also waits for the certificate to be available.
|
||||
"""
|
||||
reqId = request_cert(certpath, subject, principal, nickname,
|
||||
passwd_fname, dns, ca, profile,
|
||||
pre_command, post_command, storage, perms)
|
||||
state = wait_for_request(reqId, api.env.startup_timeout)
|
||||
ca_error = get_request_value(reqId, 'ca-error')
|
||||
if state != 'MONITORING' or ca_error:
|
||||
raise RuntimeError("Certificate issuance failed ({})".format(state))
|
||||
return reqId
|
||||
|
||||
|
||||
def request_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):
|
||||
"""
|
||||
Execute certmonger to request a server certificate.
|
||||
|
||||
``dns``
|
||||
A sequence of DNS names to appear in SAN request extension.
|
||||
``perms``
|
||||
A tuple of (cert, key) permissions in e.g., (0644,0660)
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
certfile, keyfile = certpath
|
||||
# This is a workaround for certmonger having different Subject
|
||||
# representation with NSS and OpenSSL
|
||||
# https://pagure.io/certmonger/issue/62
|
||||
subject = str(DN(*reversed(DN(subject))))
|
||||
else:
|
||||
certfile = certpath
|
||||
keyfile = certpath
|
||||
|
||||
cm = _certmonger()
|
||||
ca_path = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if not ca_path:
|
||||
raise RuntimeError('{} CA not found'.format(ca))
|
||||
request_parameters = dict(KEY_STORAGE=storage, CERT_STORAGE=storage,
|
||||
CERT_LOCATION=certfile, KEY_LOCATION=keyfile,
|
||||
SUBJECT=subject, CA=ca_path)
|
||||
if nickname:
|
||||
request_parameters["CERT_NICKNAME"] = nickname
|
||||
request_parameters["KEY_NICKNAME"] = nickname
|
||||
if principal:
|
||||
request_parameters['PRINCIPAL'] = [principal]
|
||||
if dns is not None and len(dns) > 0:
|
||||
request_parameters['DNS'] = dns
|
||||
if passwd_fname:
|
||||
request_parameters['KEY_PIN_FILE'] = passwd_fname
|
||||
if profile:
|
||||
request_parameters['ca-profile'] = profile
|
||||
|
||||
certmonger_cmd_template = paths.CERTMONGER_COMMAND_TEMPLATE
|
||||
if pre_command:
|
||||
if not os.path.isabs(pre_command):
|
||||
pre_command = certmonger_cmd_template % (pre_command)
|
||||
request_parameters['cert-presave-command'] = pre_command
|
||||
if post_command:
|
||||
if not os.path.isabs(post_command):
|
||||
post_command = certmonger_cmd_template % (post_command)
|
||||
request_parameters['cert-postsave-command'] = post_command
|
||||
|
||||
if perms:
|
||||
request_parameters['cert-perms'] = perms[0]
|
||||
request_parameters['key-perms'] = perms[1]
|
||||
|
||||
result = cm.obj_if.add_request(request_parameters)
|
||||
try:
|
||||
if result[0]:
|
||||
request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
else:
|
||||
raise RuntimeError('add_request() returned False')
|
||||
except Exception as e:
|
||||
logger.error('Failed to create a new request: %s', e)
|
||||
raise
|
||||
return request.obj_if.get_nickname()
|
||||
|
||||
|
||||
def start_tracking(
|
||||
certpath, ca='IPA', nickname=None, pin=None, pinfile=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.
|
||||
|
||||
This uses the generic certmonger command getcert so we can specify
|
||||
a different helper.
|
||||
|
||||
:param certpath:
|
||||
The path to an NSS database or a tuple (PEM certificate, private key).
|
||||
:param ca:
|
||||
Nickanme of the CA for which the given certificate should be tracked.
|
||||
:param nickname:
|
||||
Nickname of the NSS certificate in ``certpath`` to be tracked.
|
||||
:param pin:
|
||||
The passphrase for either NSS database containing ``nickname`` or
|
||||
for the encrypted key in the ``certpath`` tuple.
|
||||
:param pinfile:
|
||||
Similar to ``pin`` parameter except this is a path to a file containing
|
||||
the required passphrase.
|
||||
:param pre_command:
|
||||
Specifies a command for certmonger to run before it renews a
|
||||
certificate. This command must reside in /usr/lib/ipa/certmonger
|
||||
to work with SELinux.
|
||||
:param post_command:
|
||||
Specifies a command for certmonger to run after it has renewed a
|
||||
certificate. This command must reside in /usr/lib/ipa/certmonger
|
||||
to work with SELinux.
|
||||
:param storage:
|
||||
One of "NSSDB" or "FILE", describes whether certmonger should use
|
||||
NSS or OpenSSL backend to track the certificate in ``certpath``
|
||||
:param profile:
|
||||
Which certificate profile should be used.
|
||||
:returns: certificate tracking nickname.
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
certfile, keyfile = certpath
|
||||
else:
|
||||
certfile = certpath
|
||||
keyfile = certpath
|
||||
|
||||
cm = _certmonger()
|
||||
certmonger_cmd_template = paths.CERTMONGER_COMMAND_TEMPLATE
|
||||
|
||||
ca_path = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if not ca_path:
|
||||
raise RuntimeError('{} CA not found'.format(ca))
|
||||
|
||||
params = {
|
||||
'TRACK': True,
|
||||
'CERT_STORAGE': storage,
|
||||
'KEY_STORAGE': storage,
|
||||
'CERT_LOCATION': certfile,
|
||||
'KEY_LOCATION': keyfile,
|
||||
'CA': ca_path
|
||||
}
|
||||
if nickname:
|
||||
params['CERT_NICKNAME'] = nickname
|
||||
params['KEY_NICKNAME'] = nickname
|
||||
if pin:
|
||||
params['KEY_PIN'] = pin
|
||||
if pinfile:
|
||||
params['KEY_PIN_FILE'] = os.path.abspath(pinfile)
|
||||
if pre_command:
|
||||
if not os.path.isabs(pre_command):
|
||||
pre_command = certmonger_cmd_template % (pre_command)
|
||||
params['cert-presave-command'] = pre_command
|
||||
if post_command:
|
||||
if not os.path.isabs(post_command):
|
||||
post_command = certmonger_cmd_template % (post_command)
|
||||
params['cert-postsave-command'] = post_command
|
||||
if profile:
|
||||
params['ca-profile'] = profile
|
||||
|
||||
result = cm.obj_if.add_request(params)
|
||||
try:
|
||||
if result[0]:
|
||||
request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
else:
|
||||
raise RuntimeError('add_request() returned False')
|
||||
except Exception as e:
|
||||
logger.error('Failed to add new request: %s', e)
|
||||
raise
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname')
|
||||
|
||||
|
||||
def stop_tracking(secdir=None, request_id=None, nickname=None, certfile=None):
|
||||
"""
|
||||
Stop tracking the current request using either the request_id or nickname.
|
||||
|
||||
Returns True or False
|
||||
"""
|
||||
if request_id is None and nickname is None and certfile is None:
|
||||
raise RuntimeError('One of request_id, nickname and certfile is'
|
||||
' required.')
|
||||
if secdir is not None and certfile is not None:
|
||||
raise RuntimeError("Can't specify both secdir and certfile.")
|
||||
|
||||
criteria = dict()
|
||||
if secdir:
|
||||
criteria['cert-database'] = secdir
|
||||
if request_id:
|
||||
criteria['nickname'] = request_id
|
||||
if nickname:
|
||||
criteria['cert-nickname'] = nickname
|
||||
if certfile:
|
||||
criteria['cert-file'] = certfile
|
||||
try:
|
||||
request = _get_request(criteria)
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
request.parent.obj_if.remove_request(request.path)
|
||||
|
||||
|
||||
def modify(request_id, ca=None, profile=None, template_v2=None):
|
||||
update = {}
|
||||
if ca is not None:
|
||||
cm = _certmonger()
|
||||
update['CA'] = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if profile is not None:
|
||||
update['template-profile'] = profile
|
||||
if template_v2 is not None:
|
||||
update['template-ms-certificate-template'] = template_v2
|
||||
|
||||
if len(update) > 0:
|
||||
request = _get_request({'nickname': request_id})
|
||||
request.obj_if.modify(update)
|
||||
|
||||
|
||||
def resubmit_request(
|
||||
request_id,
|
||||
ca=None,
|
||||
profile=None,
|
||||
template_v2=None,
|
||||
is_ca=False):
|
||||
"""
|
||||
:param request_id: the certmonger numeric request ID
|
||||
:param ca: the nickname for the certmonger CA, e.g. IPA or SelfSign
|
||||
:param profile: the profile to use, e.g. SubCA. For requests using the
|
||||
Dogtag CA, this is the profile to use. This also causes
|
||||
the Microsoft certificate tempalte name extension to the
|
||||
CSR (for telling AD CS what template to use).
|
||||
:param template_v2: Microsoft V2 template specifier extension value.
|
||||
Format: <oid>:<major-version>[:<minor-version>]
|
||||
:param is_ca: boolean that if True adds the CA basic constraint
|
||||
"""
|
||||
request = _get_request({'nickname': request_id})
|
||||
if request:
|
||||
update = {}
|
||||
if ca is not None:
|
||||
cm = _certmonger()
|
||||
update['CA'] = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if profile is not None:
|
||||
update['template-profile'] = profile
|
||||
if template_v2 is not None:
|
||||
update['template-ms-certificate-template'] = template_v2
|
||||
if is_ca:
|
||||
update['template-is-ca'] = True
|
||||
update['template-ca-path-length'] = -1 # no path length
|
||||
|
||||
if len(update) > 0:
|
||||
request.obj_if.modify(update)
|
||||
request.obj_if.resubmit()
|
||||
|
||||
|
||||
def _find_IPA_ca():
|
||||
"""
|
||||
Look through all the certmonger CA files to find the one that
|
||||
has id=IPA
|
||||
|
||||
We can use find_request_value because the ca files have the
|
||||
same file format.
|
||||
"""
|
||||
cm = _certmonger()
|
||||
ca_path = cm.obj_if.find_ca_by_nickname('IPA')
|
||||
return _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF, DBUS_CM_IF, True)
|
||||
|
||||
|
||||
def add_principal_to_cas(principal):
|
||||
"""
|
||||
If the hostname we were passed to use in ipa-client-install doesn't
|
||||
match the value of gethostname() then we need to append
|
||||
-k host/HOSTNAME@REALM to the ca helper defined for
|
||||
/usr/libexec/certmonger/ipa-submit.
|
||||
|
||||
We also need to restore this on uninstall.
|
||||
"""
|
||||
ca = _find_IPA_ca()
|
||||
if ca:
|
||||
ext_helper = ca.prop_if.Get(DBUS_CM_CA_IF, 'external-helper')
|
||||
if ext_helper and '-k' not in shlex.split(ext_helper):
|
||||
ext_helper = '%s -k %s' % (ext_helper.strip(), principal)
|
||||
ca.prop_if.Set(DBUS_CM_CA_IF, 'external-helper', ext_helper)
|
||||
|
||||
|
||||
def remove_principal_from_cas():
|
||||
"""
|
||||
Remove any -k principal options from the ipa_submit helper.
|
||||
"""
|
||||
ca = _find_IPA_ca()
|
||||
if ca:
|
||||
ext_helper = ca.prop_if.Get(DBUS_CM_CA_IF, 'external-helper')
|
||||
if ext_helper and '-k' in shlex.split(ext_helper):
|
||||
ext_helper = shlex.split(ext_helper)[0]
|
||||
ca.prop_if.Set(DBUS_CM_CA_IF, 'external-helper', ext_helper)
|
||||
|
||||
|
||||
def modify_ca_helper(ca_name, helper):
|
||||
"""
|
||||
Modify certmonger CA helper.
|
||||
|
||||
Applies the new helper and return the previous configuration.
|
||||
"""
|
||||
bus = dbus.SystemBus()
|
||||
obj = bus.get_object('org.fedorahosted.certmonger',
|
||||
'/org/fedorahosted/certmonger')
|
||||
iface = dbus.Interface(obj, 'org.fedorahosted.certmonger')
|
||||
path = iface.find_ca_by_nickname(ca_name)
|
||||
if not path:
|
||||
raise RuntimeError("{} is not configured".format(ca_name))
|
||||
else:
|
||||
ca_obj = bus.get_object('org.fedorahosted.certmonger', path)
|
||||
ca_iface = dbus.Interface(ca_obj,
|
||||
'org.freedesktop.DBus.Properties')
|
||||
old_helper = ca_iface.Get('org.fedorahosted.certmonger.ca',
|
||||
'external-helper')
|
||||
ca_iface.Set('org.fedorahosted.certmonger.ca',
|
||||
'external-helper', helper,
|
||||
# Give dogtag extra time to generate cert
|
||||
timeout=CA_DBUS_TIMEOUT)
|
||||
return old_helper
|
||||
|
||||
|
||||
def get_pin(token):
|
||||
"""
|
||||
Dogtag stores its NSS pin in a file formatted as token:PIN.
|
||||
|
||||
The caller is expected to handle any exceptions raised.
|
||||
"""
|
||||
with open(paths.PKI_TOMCAT_PASSWORD_CONF, 'r') as f:
|
||||
for line in f:
|
||||
(tok, pin) = line.split('=', 1)
|
||||
if token == tok:
|
||||
return pin.strip()
|
||||
return None
|
||||
|
||||
|
||||
def check_state(dirs):
|
||||
"""
|
||||
Given a set of directories and nicknames verify that we are no longer
|
||||
tracking certificates.
|
||||
|
||||
dirs is a list of directories to test for. We will return a tuple
|
||||
of nicknames for any tracked certificates found.
|
||||
|
||||
This can only check for NSS-based certificates.
|
||||
"""
|
||||
reqids = []
|
||||
for dir in dirs:
|
||||
reqids.extend(get_requests_for_dir(dir))
|
||||
|
||||
return reqids
|
||||
|
||||
|
||||
def wait_for_request(request_id, timeout=120):
|
||||
for _i in range(0, timeout, 5):
|
||||
state = get_request_value(request_id, 'status')
|
||||
logger.debug("certmonger request is in state %r", state)
|
||||
if state in ('CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
|
||||
'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'):
|
||||
break
|
||||
time.sleep(5)
|
||||
else:
|
||||
raise RuntimeError("request timed out")
|
||||
|
||||
return state
|
||||
|
||||
if __name__ == '__main__':
|
||||
request_id = request_cert(paths.HTTPD_ALIAS_DIR,
|
||||
"cn=tiger.example.com,O=IPA",
|
||||
"HTTP/tiger.example.com@EXAMPLE.COM", "Test")
|
||||
csr = get_request_value(request_id, 'csr')
|
||||
print(csr)
|
||||
stop_tracking(request_id)
|
||||
406
ipalib/install/certstore.py
Normal file
406
ipalib/install/certstore.py
Normal file
@@ -0,0 +1,406 @@
|
||||
# Authors:
|
||||
# Jan Cholasta <jcholast@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2014 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
LDAP shared certificate store.
|
||||
"""
|
||||
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
from ipapython.dn import DN
|
||||
from ipapython.certdb import get_ca_nickname, TrustFlags
|
||||
from ipalib import errors, x509
|
||||
from ipalib.constants import IPA_CA_CN
|
||||
|
||||
|
||||
def _parse_cert(cert):
|
||||
try:
|
||||
subject = DN(cert.subject)
|
||||
issuer = DN(cert.issuer)
|
||||
serial_number = cert.serial_number
|
||||
public_key_info = cert.public_key_info_bytes
|
||||
except (ValueError, PyAsn1Error) as e:
|
||||
raise ValueError("failed to decode certificate: %s" % e)
|
||||
|
||||
subject = str(subject).replace('\\;', '\\3b')
|
||||
issuer = str(issuer).replace('\\;', '\\3b')
|
||||
issuer_serial = '%s;%s' % (issuer, serial_number)
|
||||
|
||||
return subject, issuer_serial, public_key_info
|
||||
|
||||
|
||||
def init_ca_entry(entry, cert, nickname, trusted, ext_key_usage):
|
||||
"""
|
||||
Initialize certificate store entry for a CA certificate.
|
||||
"""
|
||||
subject, issuer_serial, public_key = _parse_cert(cert)
|
||||
|
||||
if ext_key_usage is not None:
|
||||
try:
|
||||
cert_eku = cert.extended_key_usage
|
||||
except ValueError as e:
|
||||
raise ValueError("failed to decode certificate: %s" % e)
|
||||
if cert_eku is not None:
|
||||
cert_eku -= {x509.EKU_SERVER_AUTH, x509.EKU_CLIENT_AUTH,
|
||||
x509.EKU_EMAIL_PROTECTION, x509.EKU_CODE_SIGNING,
|
||||
x509.EKU_ANY, x509.EKU_PLACEHOLDER}
|
||||
ext_key_usage = ext_key_usage | cert_eku
|
||||
|
||||
entry['objectClass'] = ['ipaCertificate', 'pkiCA', 'ipaKeyPolicy']
|
||||
entry['cn'] = [nickname]
|
||||
|
||||
entry['ipaCertSubject'] = [subject]
|
||||
entry['ipaCertIssuerSerial'] = [issuer_serial]
|
||||
entry['ipaPublicKey'] = [public_key]
|
||||
entry['cACertificate;binary'] = [cert]
|
||||
|
||||
if trusted is not None:
|
||||
entry['ipaKeyTrust'] = ['trusted' if trusted else 'distrusted']
|
||||
if ext_key_usage is not None:
|
||||
ext_key_usage = list(ext_key_usage)
|
||||
if not ext_key_usage:
|
||||
ext_key_usage.append(x509.EKU_PLACEHOLDER)
|
||||
entry['ipaKeyExtUsage'] = ext_key_usage
|
||||
|
||||
|
||||
def update_compat_ca(ldap, base_dn, cert):
|
||||
"""
|
||||
Update the CA certificate in cn=CAcert,cn=ipa,cn=etc,SUFFIX.
|
||||
"""
|
||||
dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'), base_dn)
|
||||
try:
|
||||
entry = ldap.get_entry(dn, attrs_list=['cACertificate;binary'])
|
||||
entry.single_value['cACertificate;binary'] = cert
|
||||
ldap.update_entry(entry)
|
||||
except errors.NotFound:
|
||||
entry = ldap.make_entry(dn)
|
||||
entry['objectClass'] = ['nsContainer', 'pkiCA']
|
||||
entry.single_value['cn'] = 'CAcert'
|
||||
entry.single_value['cACertificate;binary'] = cert
|
||||
ldap.add_entry(entry)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def clean_old_config(ldap, base_dn, dn, config_ipa, config_compat):
|
||||
"""
|
||||
Remove ipaCA and compatCA flags from their previous carriers.
|
||||
"""
|
||||
if not config_ipa and not config_compat:
|
||||
return
|
||||
|
||||
try:
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn),
|
||||
filter='(|(ipaConfigString=ipaCA)(ipaConfigString=compatCA))',
|
||||
attrs_list=['ipaConfigString'])
|
||||
except errors.NotFound:
|
||||
return
|
||||
|
||||
for entry in result:
|
||||
if entry.dn == dn:
|
||||
continue
|
||||
for config in list(entry['ipaConfigString']):
|
||||
if config.lower() == 'ipaca' and config_ipa:
|
||||
entry['ipaConfigString'].remove(config)
|
||||
elif config.lower() == 'compatca' and config_compat:
|
||||
entry['ipaConfigString'].remove(config)
|
||||
try:
|
||||
ldap.update_entry(entry)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def add_ca_cert(ldap, base_dn, cert, nickname, trusted=None,
|
||||
ext_key_usage=None, config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add new entry for a CA certificate to the certificate store.
|
||||
"""
|
||||
container_dn = DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn)
|
||||
dn = DN(('cn', nickname), container_dn)
|
||||
entry = ldap.make_entry(dn)
|
||||
|
||||
init_ca_entry(entry, cert, nickname, trusted, ext_key_usage)
|
||||
|
||||
if config_ipa:
|
||||
entry.setdefault('ipaConfigString', []).append('ipaCA')
|
||||
if config_compat:
|
||||
entry.setdefault('ipaConfigString', []).append('compatCA')
|
||||
|
||||
if config_compat:
|
||||
update_compat_ca(ldap, base_dn, cert)
|
||||
|
||||
ldap.add_entry(entry)
|
||||
clean_old_config(ldap, base_dn, dn, config_ipa, config_compat)
|
||||
|
||||
|
||||
def update_ca_cert(ldap, base_dn, cert, trusted=None, ext_key_usage=None,
|
||||
config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Update existing entry for a CA certificate in the certificate store.
|
||||
"""
|
||||
subject, issuer_serial, public_key = _parse_cert(cert)
|
||||
|
||||
filter = ldap.make_filter({'ipaCertSubject': subject})
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn),
|
||||
filter=filter,
|
||||
attrs_list=['cn', 'ipaCertSubject', 'ipaCertIssuerSerial',
|
||||
'ipaPublicKey', 'ipaKeyTrust', 'ipaKeyExtUsage',
|
||||
'ipaConfigString', 'cACertificate;binary'])
|
||||
entry = result[0]
|
||||
dn = entry.dn
|
||||
|
||||
for old_cert in entry['cACertificate;binary']:
|
||||
# Check if we are adding a new cert
|
||||
if old_cert == cert:
|
||||
break
|
||||
else:
|
||||
# We are adding a new cert, validate it
|
||||
if entry.single_value['ipaCertSubject'].lower() != subject.lower():
|
||||
raise ValueError("subject name mismatch")
|
||||
if entry.single_value['ipaPublicKey'] != public_key:
|
||||
raise ValueError("subject public key info mismatch")
|
||||
entry['ipaCertIssuerSerial'].append(issuer_serial)
|
||||
entry['cACertificate;binary'].append(cert)
|
||||
|
||||
# Update key trust
|
||||
if trusted is not None:
|
||||
old_trust = entry.single_value.get('ipaKeyTrust')
|
||||
new_trust = 'trusted' if trusted else 'distrusted'
|
||||
if old_trust is not None and old_trust.lower() != new_trust:
|
||||
raise ValueError("inconsistent trust")
|
||||
entry.single_value['ipaKeyTrust'] = new_trust
|
||||
|
||||
# Update extended key usage
|
||||
if trusted is not False:
|
||||
if ext_key_usage is not None:
|
||||
old_eku = set(entry.get('ipaKeyExtUsage', []))
|
||||
old_eku.discard(x509.EKU_PLACEHOLDER)
|
||||
new_eku = old_eku | ext_key_usage
|
||||
if not new_eku:
|
||||
new_eku.add(x509.EKU_PLACEHOLDER)
|
||||
entry['ipaKeyExtUsage'] = list(new_eku)
|
||||
else:
|
||||
entry.pop('ipaKeyExtUsage', None)
|
||||
|
||||
# Update configuration flags
|
||||
is_ipa = False
|
||||
is_compat = False
|
||||
for config in entry.get('ipaConfigString', []):
|
||||
if config.lower() == 'ipaca':
|
||||
is_ipa = True
|
||||
elif config.lower() == 'compatca':
|
||||
is_compat = True
|
||||
if config_ipa and not is_ipa:
|
||||
entry.setdefault('ipaConfigString', []).append('ipaCA')
|
||||
if config_compat and not is_compat:
|
||||
entry.setdefault('ipaConfigString', []).append('compatCA')
|
||||
|
||||
if is_compat or config_compat:
|
||||
update_compat_ca(ldap, base_dn, cert)
|
||||
|
||||
ldap.update_entry(entry)
|
||||
clean_old_config(ldap, base_dn, dn, config_ipa, config_compat)
|
||||
|
||||
|
||||
def put_ca_cert(ldap, base_dn, cert, nickname, trusted=None,
|
||||
ext_key_usage=None, config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add or update entry for a CA certificate in the certificate store.
|
||||
|
||||
:param cert: IPACertificate
|
||||
"""
|
||||
try:
|
||||
update_ca_cert(ldap, base_dn, cert, trusted, ext_key_usage,
|
||||
config_ipa=config_ipa, config_compat=config_compat)
|
||||
except errors.NotFound:
|
||||
add_ca_cert(ldap, base_dn, cert, nickname, trusted, ext_key_usage,
|
||||
config_ipa=config_ipa, config_compat=config_compat)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def make_compat_ca_certs(certs, realm, ipa_ca_subject):
|
||||
"""
|
||||
Make CA certificates and associated key policy from DER certificates.
|
||||
"""
|
||||
result = []
|
||||
|
||||
for cert in certs:
|
||||
subject, _issuer_serial, _public_key_info = _parse_cert(cert)
|
||||
subject = DN(subject)
|
||||
|
||||
if ipa_ca_subject is not None and subject == DN(ipa_ca_subject):
|
||||
nickname = get_ca_nickname(realm)
|
||||
ext_key_usage = {x509.EKU_SERVER_AUTH,
|
||||
x509.EKU_CLIENT_AUTH,
|
||||
x509.EKU_EMAIL_PROTECTION,
|
||||
x509.EKU_CODE_SIGNING}
|
||||
else:
|
||||
nickname = str(subject)
|
||||
ext_key_usage = {x509.EKU_SERVER_AUTH}
|
||||
|
||||
result.append((cert, nickname, True, ext_key_usage))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_ca_certs(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=None):
|
||||
"""
|
||||
Get CA certificates and associated key policy from the certificate store.
|
||||
"""
|
||||
if filter_subject is not None:
|
||||
if not isinstance(filter_subject, list):
|
||||
filter_subject = [filter_subject]
|
||||
filter_subject = [str(subj).replace('\\;', '\\3b')
|
||||
for subj in filter_subject]
|
||||
|
||||
certs = []
|
||||
config_dn = DN(('cn', 'ipa'), ('cn', 'etc'), base_dn)
|
||||
container_dn = DN(('cn', 'certificates'), config_dn)
|
||||
try:
|
||||
# Search the certificate store for CA certificate entries
|
||||
filters = ['(objectClass=ipaCertificate)', '(objectClass=pkiCA)']
|
||||
if filter_subject:
|
||||
filter = ldap.make_filter({'ipaCertSubject': filter_subject})
|
||||
filters.append(filter)
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=container_dn,
|
||||
filter=ldap.combine_filters(filters, ldap.MATCH_ALL),
|
||||
attrs_list=['cn', 'ipaCertSubject', 'ipaCertIssuerSerial',
|
||||
'ipaPublicKey', 'ipaKeyTrust', 'ipaKeyExtUsage',
|
||||
'cACertificate;binary'])
|
||||
|
||||
for entry in result:
|
||||
nickname = entry.single_value['cn']
|
||||
trusted = entry.single_value.get('ipaKeyTrust', 'unknown').lower()
|
||||
if trusted == 'trusted':
|
||||
trusted = True
|
||||
elif trusted == 'distrusted':
|
||||
trusted = False
|
||||
else:
|
||||
trusted = None
|
||||
ext_key_usage = entry.get('ipaKeyExtUsage')
|
||||
if ext_key_usage is not None:
|
||||
ext_key_usage = set(str(p) for p in ext_key_usage)
|
||||
ext_key_usage.discard(x509.EKU_PLACEHOLDER)
|
||||
|
||||
for cert in entry.get('cACertificate;binary', []):
|
||||
try:
|
||||
_parse_cert(cert)
|
||||
except ValueError:
|
||||
certs = []
|
||||
break
|
||||
certs.append((cert, nickname, trusted, ext_key_usage))
|
||||
except errors.NotFound:
|
||||
try:
|
||||
ldap.get_entry(container_dn, [''])
|
||||
except errors.NotFound:
|
||||
# Fallback to cn=CAcert,cn=ipa,cn=etc,SUFFIX
|
||||
dn = DN(('cn', 'CAcert'), config_dn)
|
||||
entry = ldap.get_entry(dn, ['cACertificate;binary'])
|
||||
|
||||
cert = entry.single_value['cACertificate;binary']
|
||||
try:
|
||||
subject, _issuer_serial, _public_key_info = _parse_cert(cert)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if filter_subject is not None and subject not in filter_subject:
|
||||
raise errors.NotFound(reason="no matching entry found")
|
||||
|
||||
if compat_ipa_ca:
|
||||
ca_subject = subject
|
||||
else:
|
||||
ca_subject = None
|
||||
certs = make_compat_ca_certs([cert], compat_realm, ca_subject)
|
||||
|
||||
if certs:
|
||||
return certs
|
||||
else:
|
||||
raise errors.NotFound(reason="no such entry")
|
||||
|
||||
|
||||
def trust_flags_to_key_policy(trust_flags):
|
||||
"""
|
||||
Convert certutil trust flags to certificate store key policy.
|
||||
"""
|
||||
return trust_flags[1:]
|
||||
|
||||
|
||||
def key_policy_to_trust_flags(trusted, ca, ext_key_usage):
|
||||
"""
|
||||
Convert certificate store key policy to certutil trust flags.
|
||||
"""
|
||||
return TrustFlags(False, trusted, ca, ext_key_usage)
|
||||
|
||||
|
||||
def put_ca_cert_nss(ldap, base_dn, cert, nickname, trust_flags,
|
||||
config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add or update entry for a CA certificate in the certificate store.
|
||||
|
||||
:param cert: IPACertificate
|
||||
"""
|
||||
trusted, ca, ext_key_usage = trust_flags_to_key_policy(trust_flags)
|
||||
if ca is False:
|
||||
raise ValueError("must be CA certificate")
|
||||
|
||||
put_ca_cert(ldap, base_dn, cert, nickname, trusted, ext_key_usage,
|
||||
config_ipa, config_compat)
|
||||
|
||||
|
||||
def get_ca_certs_nss(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=None):
|
||||
"""
|
||||
Get CA certificates and associated trust flags from the certificate store.
|
||||
"""
|
||||
nss_certs = []
|
||||
|
||||
certs = get_ca_certs(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=filter_subject)
|
||||
for cert, nickname, trusted, ext_key_usage in certs:
|
||||
trust_flags = key_policy_to_trust_flags(trusted, True, ext_key_usage)
|
||||
nss_certs.append((cert, nickname, trust_flags))
|
||||
|
||||
return nss_certs
|
||||
|
||||
|
||||
def get_ca_subject(ldap, container_ca, base_dn):
|
||||
"""
|
||||
Look for the IPA CA certificate subject.
|
||||
"""
|
||||
dn = DN(('cn', IPA_CA_CN), container_ca, base_dn)
|
||||
try:
|
||||
cacert_subject = ldap.get_entry(dn)['ipacasubjectdn'][0]
|
||||
except errors.NotFound:
|
||||
# if the entry doesn't exist, we are dealing with a pre-v4.4
|
||||
# installation, where the default CA subject was always based
|
||||
# on the subject_base.
|
||||
attrs = ldap.get_ipa_config()
|
||||
subject_base = attrs.get('ipacertificatesubjectbase')[0]
|
||||
cacert_subject = DN(('CN', 'Certificate Authority'), subject_base)
|
||||
|
||||
return cacert_subject
|
||||
59
ipalib/install/hostname.py
Normal file
59
ipalib/install/hostname.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Host name installer module
|
||||
"""
|
||||
|
||||
from ipapython.install import typing
|
||||
from ipapython.install.core import knob
|
||||
from ipapython.ipautil import CheckedIPAddress
|
||||
|
||||
from . import service
|
||||
from .service import prepare_only
|
||||
|
||||
|
||||
class HostNameInstallInterface(service.ServiceInstallInterface):
|
||||
"""
|
||||
Interface common to all service installers which create DNS address
|
||||
records for `host_name`
|
||||
"""
|
||||
|
||||
ip_addresses = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[CheckedIPAddress], None,
|
||||
description="Specify IP address that should be added to DNS. This "
|
||||
"option can be used multiple times",
|
||||
cli_names='--ip-address',
|
||||
cli_metavar='IP_ADDRESS',
|
||||
)
|
||||
ip_addresses = prepare_only(ip_addresses)
|
||||
|
||||
@ip_addresses.validator
|
||||
def ip_addresses(self, values):
|
||||
for value in values:
|
||||
try:
|
||||
CheckedIPAddress(value)
|
||||
except Exception as e:
|
||||
raise ValueError("invalid IP address {0}: {1}".format(
|
||||
value, e))
|
||||
|
||||
all_ip_addresses = knob(
|
||||
None,
|
||||
description="All routable IP addresses configured on any interface "
|
||||
"will be added to DNS",
|
||||
)
|
||||
all_ip_addresses = prepare_only(all_ip_addresses)
|
||||
|
||||
no_host_dns = knob(
|
||||
None,
|
||||
description="Do not use DNS for hostname lookup during installation",
|
||||
)
|
||||
no_host_dns = prepare_only(no_host_dns)
|
||||
|
||||
no_wait_for_dns = knob(
|
||||
None,
|
||||
description="do not wait until the host is resolvable in DNS",
|
||||
)
|
||||
no_wait_for_dns = prepare_only(no_wait_for_dns)
|
||||
125
ipalib/install/kinit.py
Normal file
125
ipalib/install/kinit.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import gssapi
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.ipautil import run
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cannot contact any KDC for requested realm
|
||||
KRB5_KDC_UNREACH = 2529639068
|
||||
|
||||
# A service is not available that s required to process the request
|
||||
KRB5KDC_ERR_SVC_UNAVAILABLE = 2529638941
|
||||
|
||||
|
||||
def kinit_keytab(principal, keytab, ccache_name, config=None, attempts=1):
|
||||
"""
|
||||
Given a ccache_path, keytab file and a principal kinit as that user.
|
||||
|
||||
The optional parameter 'attempts' specifies how many times the credential
|
||||
initialization should be attempted in case of non-responsive KDC.
|
||||
"""
|
||||
errors_to_retry = {KRB5KDC_ERR_SVC_UNAVAILABLE,
|
||||
KRB5_KDC_UNREACH}
|
||||
logger.debug("Initializing principal %s using keytab %s",
|
||||
principal, keytab)
|
||||
logger.debug("using ccache %s", ccache_name)
|
||||
for attempt in range(1, attempts + 1):
|
||||
old_config = os.environ.get('KRB5_CONFIG')
|
||||
if config is not None:
|
||||
os.environ['KRB5_CONFIG'] = config
|
||||
else:
|
||||
os.environ.pop('KRB5_CONFIG', None)
|
||||
try:
|
||||
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
|
||||
store = {'ccache': ccache_name,
|
||||
'client_keytab': keytab}
|
||||
cred = gssapi.Credentials(name=name, store=store, usage='initiate')
|
||||
logger.debug("Attempt %d/%d: success", attempt, attempts)
|
||||
return cred
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
if e.min_code not in errors_to_retry: # pylint: disable=no-member
|
||||
raise
|
||||
logger.debug("Attempt %d/%d: failed: %s", attempt, attempts, e)
|
||||
if attempt == attempts:
|
||||
logger.debug("Maximum number of attempts (%d) reached",
|
||||
attempts)
|
||||
raise
|
||||
logger.debug("Waiting 5 seconds before next retry")
|
||||
time.sleep(5)
|
||||
finally:
|
||||
if old_config is not None:
|
||||
os.environ['KRB5_CONFIG'] = old_config
|
||||
else:
|
||||
os.environ.pop('KRB5_CONFIG', None)
|
||||
|
||||
def kinit_password(principal, password, ccache_name, config=None,
|
||||
armor_ccache_name=None, canonicalize=False,
|
||||
enterprise=False, lifetime=None):
|
||||
"""
|
||||
perform interactive kinit as principal using password. If using FAST for
|
||||
web-based authentication, use armor_ccache_path to specify http service
|
||||
ccache.
|
||||
"""
|
||||
logger.debug("Initializing principal %s using password", principal)
|
||||
args = [paths.KINIT, principal, '-c', ccache_name]
|
||||
if armor_ccache_name is not None:
|
||||
logger.debug("Using armor ccache %s for FAST webauth",
|
||||
armor_ccache_name)
|
||||
args.extend(['-T', armor_ccache_name])
|
||||
|
||||
if lifetime:
|
||||
args.extend(['-l', lifetime])
|
||||
|
||||
if canonicalize:
|
||||
logger.debug("Requesting principal canonicalization")
|
||||
args.append('-C')
|
||||
|
||||
if enterprise:
|
||||
logger.debug("Using enterprise principal")
|
||||
args.append('-E')
|
||||
|
||||
env = {'LC_ALL': 'C'}
|
||||
if config is not None:
|
||||
env['KRB5_CONFIG'] = config
|
||||
|
||||
# this workaround enables us to capture stderr and put it
|
||||
# into the raised exception in case of unsuccessful authentication
|
||||
result = run(args, stdin=password, env=env, raiseonerr=False,
|
||||
capture_error=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.error_output)
|
||||
|
||||
|
||||
def kinit_armor(ccache_name, pkinit_anchors=None):
|
||||
"""
|
||||
perform anonymous pkinit to obtain anonymous ticket to be used as armor
|
||||
for FAST.
|
||||
|
||||
:param ccache_name: location of the armor ccache
|
||||
:param pkinit_anchor: if not None, the location of PKINIT anchor file to
|
||||
use. Otherwise the value from Kerberos client library configuration is
|
||||
used
|
||||
|
||||
:raises: CalledProcessError if the anonymous PKINIT fails
|
||||
"""
|
||||
logger.debug("Initializing anonymous ccache")
|
||||
|
||||
env = {'LC_ALL': 'C'}
|
||||
args = [paths.KINIT, '-n', '-c', ccache_name]
|
||||
|
||||
if pkinit_anchors is not None:
|
||||
for pkinit_anchor in pkinit_anchors:
|
||||
args.extend(['-X', 'X509_anchors=FILE:{}'.format(pkinit_anchor)])
|
||||
|
||||
# this workaround enables us to capture stderr and put it
|
||||
# into the raised exception in case of unsuccessful authentication
|
||||
run(args, env=env, raiseonerr=True, capture_error=True)
|
||||
178
ipalib/install/service.py
Normal file
178
ipalib/install/service.py
Normal file
@@ -0,0 +1,178 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Base service installer module
|
||||
"""
|
||||
|
||||
from ipalib.util import validate_domain_name
|
||||
from ipapython.install import common, core, typing
|
||||
from ipapython.install.core import group, knob
|
||||
|
||||
|
||||
def prepare_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in the prepare
|
||||
phase of the install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'enroll'}
|
||||
return obj
|
||||
|
||||
|
||||
def enroll_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in the enroll
|
||||
phase of the install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'prepare'}
|
||||
return obj
|
||||
|
||||
|
||||
def master_install_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in master install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'replica_install'}
|
||||
return obj
|
||||
|
||||
|
||||
def replica_install_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in replica install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'master_install'}
|
||||
return obj
|
||||
|
||||
|
||||
def _does(cls, arg):
|
||||
def remove(name):
|
||||
def removed(self):
|
||||
raise AttributeError(name)
|
||||
|
||||
return property(removed)
|
||||
|
||||
return type(
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
n: remove(n) for n in dir(cls)
|
||||
if arg in getattr(getattr(cls, n), '__exclude__', set())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def prepares(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to the prepare
|
||||
phase of the install
|
||||
"""
|
||||
return _does(cls, 'prepare')
|
||||
|
||||
|
||||
def enrolls(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to the enroll
|
||||
phase of the install
|
||||
"""
|
||||
return _does(cls, 'enroll')
|
||||
|
||||
|
||||
def installs_master(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to master
|
||||
install
|
||||
"""
|
||||
return _does(cls, 'master_install')
|
||||
|
||||
|
||||
def installs_replica(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to replica
|
||||
install
|
||||
"""
|
||||
return _does(cls, 'replica_install')
|
||||
|
||||
|
||||
@group
|
||||
class ServiceInstallInterface(common.Installable,
|
||||
common.Interactive,
|
||||
core.Composite):
|
||||
"""
|
||||
Interface common to all service installers
|
||||
"""
|
||||
description = "Basic"
|
||||
|
||||
domain_name = knob(
|
||||
str, None,
|
||||
description="primary DNS domain of the IPA deployment "
|
||||
"(not necessarily related to the current hostname)",
|
||||
cli_names='--domain',
|
||||
)
|
||||
|
||||
@domain_name.validator
|
||||
def domain_name(self, value):
|
||||
validate_domain_name(value)
|
||||
|
||||
servers = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[str], None,
|
||||
description="FQDN of IPA server",
|
||||
cli_names='--server',
|
||||
cli_metavar='SERVER',
|
||||
)
|
||||
|
||||
realm_name = knob(
|
||||
str, None,
|
||||
description="Kerberos realm name of the IPA deployment (typically "
|
||||
"an upper-cased name of the primary DNS domain)",
|
||||
cli_names='--realm',
|
||||
)
|
||||
|
||||
host_name = knob(
|
||||
str, None,
|
||||
description="The hostname of this machine (FQDN). If specified, the "
|
||||
"hostname will be set and the system configuration will "
|
||||
"be updated to persist over reboot. By default the result "
|
||||
"of getfqdn() call from Python's socket module is used.",
|
||||
cli_names='--hostname',
|
||||
)
|
||||
|
||||
ca_cert_files = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[str], None,
|
||||
description="load the CA certificate from this file",
|
||||
cli_names='--ca-cert-file',
|
||||
cli_metavar='FILE',
|
||||
)
|
||||
|
||||
replica_file = knob(
|
||||
str, None,
|
||||
description="a file generated by ipa-replica-prepare",
|
||||
)
|
||||
replica_file = replica_install_only(replica_file)
|
||||
|
||||
dm_password = knob(
|
||||
str, None,
|
||||
sensitive=True,
|
||||
description="Directory Manager password (for the existing master)",
|
||||
)
|
||||
|
||||
|
||||
class ServiceAdminInstallInterface(ServiceInstallInterface):
|
||||
"""
|
||||
Interface common to all service installers which require admin user
|
||||
authentication
|
||||
"""
|
||||
|
||||
principal = knob(
|
||||
str, None,
|
||||
)
|
||||
principal = enroll_only(principal)
|
||||
principal = replica_install_only(principal)
|
||||
|
||||
admin_password = knob(
|
||||
str, None,
|
||||
sensitive=True,
|
||||
)
|
||||
admin_password = enroll_only(admin_password)
|
||||
453
ipalib/install/sysrestore.py
Normal file
453
ipalib/install/sysrestore.py
Normal file
@@ -0,0 +1,453 @@
|
||||
# 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
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
|
||||
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(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
|
||||
|
||||
|
||||
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 - so long as a 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)
|
||||
|
||||
filename = ""
|
||||
for _i in range(8):
|
||||
h = "%02x" % self.random.randint(0,255)
|
||||
filename += h
|
||||
filename += "-"+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
|
||||
31
ipalib/ipalib.egg-info/PKG-INFO
Normal file
31
ipalib/ipalib.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,31 @@
|
||||
Metadata-Version: 1.2
|
||||
Name: ipalib
|
||||
Version: 4.6.2
|
||||
Summary: FreeIPA common python library
|
||||
Home-page: http://www.freeipa.org/
|
||||
Author: FreeIPA Developers
|
||||
Author-email: freeipa-devel@redhat.com
|
||||
License: GPLv3
|
||||
Download-URL: http://www.freeipa.org/page/Downloads
|
||||
Description: FreeIPA common python library
|
||||
|
||||
Platform: Linux
|
||||
Platform: Solaris
|
||||
Platform: Unix
|
||||
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.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: Unix
|
||||
Classifier: Topic :: Internet :: Name Service (DNS)
|
||||
Classifier: Topic :: Security
|
||||
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
||||
Requires-Python: >=2.7.5,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
|
||||
38
ipalib/ipalib.egg-info/SOURCES.txt
Normal file
38
ipalib/ipalib.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
__init__.py
|
||||
aci.py
|
||||
backend.py
|
||||
base.py
|
||||
capabilities.py
|
||||
cli.py
|
||||
config.py
|
||||
constants.py
|
||||
crud.py
|
||||
dns.py
|
||||
errors.py
|
||||
frontend.py
|
||||
krb_utils.py
|
||||
messages.py
|
||||
misc.py
|
||||
output.py
|
||||
parameters.py
|
||||
pkcs10.py
|
||||
plugable.py
|
||||
request.py
|
||||
rpc.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
text.py
|
||||
util.py
|
||||
x509.py
|
||||
install/__init__.py
|
||||
install/certmonger.py
|
||||
install/certstore.py
|
||||
install/hostname.py
|
||||
install/kinit.py
|
||||
install/service.py
|
||||
install/sysrestore.py
|
||||
ipalib.egg-info/PKG-INFO
|
||||
ipalib.egg-info/SOURCES.txt
|
||||
ipalib.egg-info/dependency_links.txt
|
||||
ipalib.egg-info/requires.txt
|
||||
ipalib.egg-info/top_level.txt
|
||||
1
ipalib/ipalib.egg-info/dependency_links.txt
Normal file
1
ipalib/ipalib.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
9
ipalib/ipalib.egg-info/requires.txt
Normal file
9
ipalib/ipalib.egg-info/requires.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
ipaplatform==4.6.2
|
||||
ipapython==4.6.2
|
||||
netaddr
|
||||
pyasn1
|
||||
pyasn1-modules
|
||||
six
|
||||
|
||||
[install]
|
||||
ipaplatform
|
||||
1
ipalib/ipalib.egg-info/top_level.txt
Normal file
1
ipalib/ipalib.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
||||
ipalib
|
||||
198
ipalib/krb_utils.py
Normal file
198
ipalib/krb_utils.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# Authors: John Dennis <jdennis@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2012 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import re
|
||||
|
||||
import six
|
||||
import gssapi
|
||||
|
||||
from ipalib import errors
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Kerberos error codes
|
||||
KRB5_CC_NOTFOUND = 2529639053 # Matching credential not found
|
||||
KRB5_FCC_NOFILE = 2529639107 # No credentials cache found
|
||||
KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN = 2529638918 # client not found in Kerberos db
|
||||
KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN = 2529638919 # Server not found in Kerberos database
|
||||
KRB5KRB_AP_ERR_TKT_EXPIRED = 2529638944 # Ticket expired
|
||||
KRB5_FCC_PERM = 2529639106 # Credentials cache permissions incorrect
|
||||
KRB5_CC_FORMAT = 2529639111 # Bad format in credentials cache
|
||||
KRB5_REALM_CANT_RESOLVE = 2529639132 # Cannot resolve network address for KDC in requested realm
|
||||
|
||||
krb_ticket_expiration_threshold = 60*5 # number of seconds to accmodate clock skew
|
||||
krb5_time_fmt = '%m/%d/%y %H:%M:%S'
|
||||
ccache_name_re = re.compile(r'^((\w+):)?(.+)')
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
def krb5_parse_ccache(ccache_name):
|
||||
'''
|
||||
Given a Kerberos ccache name parse it into it's scheme and
|
||||
location components. Currently valid values for the scheme
|
||||
are:
|
||||
|
||||
* FILE
|
||||
* MEMORY
|
||||
|
||||
The scheme is always returned as upper case. If the scheme
|
||||
does not exist it defaults to FILE.
|
||||
|
||||
:parameters:
|
||||
ccache_name
|
||||
The name of the Kerberos ccache.
|
||||
:returns:
|
||||
A two-tuple of (scheme, ccache)
|
||||
'''
|
||||
match = ccache_name_re.search(ccache_name)
|
||||
if match:
|
||||
scheme = match.group(2)
|
||||
location = match.group(3)
|
||||
if scheme is None:
|
||||
scheme = 'FILE'
|
||||
else:
|
||||
scheme = scheme.upper()
|
||||
|
||||
return scheme, location
|
||||
else:
|
||||
raise ValueError('Invalid ccache name = "%s"' % ccache_name)
|
||||
|
||||
def krb5_unparse_ccache(scheme, name):
|
||||
return '%s:%s' % (scheme.upper(), name)
|
||||
|
||||
|
||||
def krb5_format_service_principal_name(service, host, realm):
|
||||
'''
|
||||
|
||||
Given a Kerberos service principal name, the host where the
|
||||
service is running and a Kerberos realm return the Kerberos V5
|
||||
service principal name.
|
||||
|
||||
:parameters:
|
||||
service
|
||||
Service principal name.
|
||||
host
|
||||
The DNS name of the host where the service is located.
|
||||
realm
|
||||
The Kerberos realm the service exists in.
|
||||
:returns:
|
||||
Kerberos V5 service principal name.
|
||||
'''
|
||||
return '%s/%s@%s' % (service, host, realm)
|
||||
|
||||
def krb5_format_tgt_principal_name(realm):
|
||||
'''
|
||||
Given a Kerberos realm return the Kerberos V5 TGT name.
|
||||
|
||||
:parameters:
|
||||
realm
|
||||
The Kerberos realm the TGT exists in.
|
||||
:returns:
|
||||
Kerberos V5 TGT name.
|
||||
'''
|
||||
return krb5_format_service_principal_name('krbtgt', realm, realm)
|
||||
|
||||
def krb5_format_time(timestamp):
|
||||
'''
|
||||
Given a UNIX timestamp format it into a string in the same
|
||||
manner the MIT Kerberos library does. Kerberos timestamps are
|
||||
always in local time.
|
||||
|
||||
:parameters:
|
||||
timestamp
|
||||
Unix timestamp
|
||||
:returns:
|
||||
formated string
|
||||
'''
|
||||
return time.strftime(krb5_time_fmt, time.localtime(timestamp))
|
||||
|
||||
def get_credentials(name=None, ccache_name=None):
|
||||
'''
|
||||
Obtains GSSAPI credentials with given principal name from ccache. When no
|
||||
principal name specified, it retrieves the default one for given
|
||||
credentials cache.
|
||||
|
||||
:parameters:
|
||||
name
|
||||
gssapi.Name object specifying principal or None for the default
|
||||
ccache_name
|
||||
string specifying Kerberos credentials cache name or None for the
|
||||
default
|
||||
:returns:
|
||||
gssapi.Credentials object
|
||||
'''
|
||||
store = None
|
||||
if ccache_name:
|
||||
store = {'ccache': ccache_name}
|
||||
try:
|
||||
return gssapi.Credentials(usage='initiate', name=name, store=store)
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
if e.min_code == KRB5_FCC_NOFILE: # pylint: disable=no-member
|
||||
raise ValueError('"%s", ccache="%s"' % (e, ccache_name))
|
||||
raise
|
||||
|
||||
def get_principal(ccache_name=None):
|
||||
'''
|
||||
Gets default principal name from given credentials cache.
|
||||
|
||||
:parameters:
|
||||
ccache_name
|
||||
string specifying Kerberos credentials cache name or None for the
|
||||
default
|
||||
:returns:
|
||||
Default principal name as string
|
||||
:raises:
|
||||
errors.CCacheError if the principal cannot be retrieved from given
|
||||
ccache
|
||||
'''
|
||||
try:
|
||||
creds = get_credentials(ccache_name=ccache_name)
|
||||
return unicode(creds.name)
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
raise errors.CCacheError(message=unicode(e))
|
||||
|
||||
def get_credentials_if_valid(name=None, ccache_name=None):
|
||||
'''
|
||||
Obtains GSSAPI credentials with principal name from ccache. When no
|
||||
principal name specified, it retrieves the default one for given
|
||||
credentials cache. When the credentials cannot be retrieved or aren't valid
|
||||
it returns None.
|
||||
|
||||
:parameters:
|
||||
name
|
||||
gssapi.Name object specifying principal or None for the default
|
||||
ccache_name
|
||||
string specifying Kerberos credentials cache name or None for the
|
||||
default
|
||||
:returns:
|
||||
gssapi.Credentials object or None if valid credentials weren't found
|
||||
'''
|
||||
|
||||
try:
|
||||
creds = get_credentials(name=name, ccache_name=ccache_name)
|
||||
if creds.lifetime > 0:
|
||||
return creds
|
||||
return None
|
||||
except gssapi.exceptions.ExpiredCredentialsError:
|
||||
return None
|
||||
except gssapi.exceptions.GSSError:
|
||||
return None
|
||||
498
ipalib/messages.py
Normal file
498
ipalib/messages.py
Normal file
@@ -0,0 +1,498 @@
|
||||
# Authors:
|
||||
# Petr Viktorin <pviktori@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2012 Red Hat
|
||||
# see file 'COPYING' for use and warranty inmsgion
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Custom message (debug, info, wraning) classes passed through RPC.
|
||||
|
||||
These are added to the "messages" entry in a RPC response, and printed to the
|
||||
user as log messages.
|
||||
|
||||
Each message class has a unique numeric "errno" attribute from the 10000-10999
|
||||
range, so that it does not clash with PublicError numbers.
|
||||
|
||||
Messages also have the 'type' argument, set to one of 'debug', 'info',
|
||||
'warning', 'error'. This determines the severity of themessage.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
from inspect import isclass
|
||||
|
||||
import six
|
||||
|
||||
from ipalib.constants import TYPE_ERROR
|
||||
from ipalib.text import _ as ugettext
|
||||
from ipalib.text import Gettext, NGettext
|
||||
from ipalib.capabilities import client_has_capability
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
def add_message(version, result, message):
|
||||
if client_has_capability(version, 'messages'):
|
||||
result.setdefault('messages', []).append(message.to_dict())
|
||||
|
||||
|
||||
def process_message_arguments(obj, format=None, message=None, **kw):
|
||||
for key, value in kw.items():
|
||||
if not isinstance(value, six.integer_types):
|
||||
try:
|
||||
kw[key] = unicode(value)
|
||||
except UnicodeError:
|
||||
pass
|
||||
obj.kw = kw
|
||||
name = obj.__class__.__name__
|
||||
if obj.format is not None and format is not None:
|
||||
raise ValueError(
|
||||
'non-generic %r needs format=None; got format=%r' % (
|
||||
name, format)
|
||||
)
|
||||
if message is None:
|
||||
if obj.format is None:
|
||||
if format is None:
|
||||
raise ValueError(
|
||||
'%s.format is None yet format=None, message=None' % name
|
||||
)
|
||||
obj.format = format
|
||||
obj.forwarded = False
|
||||
obj.msg = obj.format % kw
|
||||
if isinstance(obj.format, six.string_types):
|
||||
obj.strerror = ugettext(obj.format) % kw
|
||||
else:
|
||||
obj.strerror = obj.format % kw
|
||||
if 'instructions' in kw:
|
||||
def convert_instructions(value):
|
||||
if isinstance(value, list):
|
||||
result = u'\n'.join(unicode(line) for line in value)
|
||||
return result
|
||||
return value
|
||||
instructions = u'\n'.join((unicode(_('Additional instructions:')),
|
||||
convert_instructions(kw['instructions'])))
|
||||
obj.strerror = u'\n'.join((obj.strerror, instructions))
|
||||
else:
|
||||
if isinstance(message, (Gettext, NGettext)):
|
||||
message = unicode(message)
|
||||
elif type(message) is not unicode:
|
||||
raise TypeError(
|
||||
TYPE_ERROR % ('message', unicode, message, type(message))
|
||||
)
|
||||
obj.forwarded = True
|
||||
obj.msg = message
|
||||
obj.strerror = message
|
||||
for (key, value) in kw.items():
|
||||
assert not hasattr(obj, key), 'conflicting kwarg %s.%s = %r' % (
|
||||
name, key, value,
|
||||
)
|
||||
setattr(obj, key, value)
|
||||
|
||||
|
||||
_texts = []
|
||||
|
||||
def _(message):
|
||||
_texts.append(message)
|
||||
return message
|
||||
|
||||
|
||||
class PublicMessage(UserWarning):
|
||||
"""
|
||||
**10000** Base class for messages that can be forwarded in an RPC response.
|
||||
"""
|
||||
def __init__(self, format=None, message=None, **kw):
|
||||
process_message_arguments(self, format, message, **kw)
|
||||
super(PublicMessage, self).__init__(self.msg)
|
||||
|
||||
errno = 10000
|
||||
format = None
|
||||
|
||||
def to_dict(self):
|
||||
"""Export this message to a dict that can be sent through RPC"""
|
||||
return dict(
|
||||
type=unicode(self.type),
|
||||
name=unicode(type(self).__name__),
|
||||
message=self.strerror,
|
||||
code=self.errno,
|
||||
data=self.kw,
|
||||
)
|
||||
|
||||
|
||||
class VersionMissing(PublicMessage):
|
||||
"""
|
||||
**13001** Used when client did not send the API version.
|
||||
|
||||
For example:
|
||||
|
||||
>>> VersionMissing(server_version='2.123').strerror
|
||||
u"API Version number was not sent, forward compatibility not guaranteed. Assuming server's API version, 2.123"
|
||||
|
||||
"""
|
||||
|
||||
errno = 13001
|
||||
type = 'warning'
|
||||
format = _("API Version number was not sent, forward compatibility not "
|
||||
"guaranteed. Assuming server's API version, %(server_version)s")
|
||||
|
||||
|
||||
class ForwardersWarning(PublicMessage):
|
||||
"""
|
||||
**13002** Used when (master) zone contains forwarders
|
||||
"""
|
||||
|
||||
errno = 13002
|
||||
type = 'warning'
|
||||
format = _(
|
||||
u"DNS forwarder semantics changed since IPA 4.0.\n"
|
||||
u"You may want to use forward zones (dnsforwardzone-*) instead.\n"
|
||||
u"For more details read the docs.")
|
||||
|
||||
|
||||
class DNSSECWarning(PublicMessage):
|
||||
"""
|
||||
**13003** Used when user change DNSSEC settings
|
||||
"""
|
||||
|
||||
errno = 13003
|
||||
type = "warning"
|
||||
format = _("DNSSEC support is experimental.\n%(additional_info)s")
|
||||
|
||||
|
||||
class OptionDeprecatedWarning(PublicMessage):
|
||||
"""
|
||||
**13004** Used when user uses a deprecated option
|
||||
"""
|
||||
|
||||
errno = 13004
|
||||
type = "warning"
|
||||
format = _(u"'%(option)s' option is deprecated. %(additional_info)s")
|
||||
|
||||
|
||||
class OptionSemanticChangedWarning(PublicMessage):
|
||||
"""
|
||||
**13005** Used when option which recently changes its semantic is used
|
||||
"""
|
||||
|
||||
errno = 13005
|
||||
type = "warning"
|
||||
format = _(u"Semantic of %(label)s was changed. %(current_behavior)s\n"
|
||||
u"%(hint)s")
|
||||
|
||||
|
||||
class DNSServerValidationWarning(PublicMessage):
|
||||
"""
|
||||
**13006** Used when a DNS server is not to able to resolve query
|
||||
"""
|
||||
|
||||
errno = 13006
|
||||
type = "warning"
|
||||
format = _(u"DNS server %(server)s: %(error)s.")
|
||||
|
||||
|
||||
class DNSServerDoesNotSupportDNSSECWarning(PublicMessage):
|
||||
"""
|
||||
**13007** Used when a DNS server does not support DNSSEC validation
|
||||
"""
|
||||
|
||||
errno = 13007
|
||||
type = "warning"
|
||||
format = _(u"DNS server %(server)s does not support DNSSEC: %(error)s.\n"
|
||||
u"If DNSSEC validation is enabled on IPA server(s), "
|
||||
u"please disable it.")
|
||||
|
||||
|
||||
class ForwardzoneIsNotEffectiveWarning(PublicMessage):
|
||||
"""
|
||||
**13008** Forwardzone is not effective, forwarding will not work because
|
||||
there is authoritative parent zone, without proper NS delegation
|
||||
"""
|
||||
|
||||
errno = 13008
|
||||
type = "warning"
|
||||
format = _(u"forward zone \"%(fwzone)s\" is not effective because of "
|
||||
u"missing proper NS delegation in authoritative zone "
|
||||
u"\"%(authzone)s\". Please add NS record "
|
||||
u"\"%(ns_rec)s\" to parent zone \"%(authzone)s\".")
|
||||
|
||||
|
||||
class DNSServerDoesNotSupportEDNS0Warning(PublicMessage):
|
||||
"""
|
||||
**13009** Used when a DNS server does not support EDNS0, required for
|
||||
DNSSEC support
|
||||
"""
|
||||
|
||||
errno = 13009
|
||||
type = "warning"
|
||||
format = _(u"DNS server %(server)s does not support EDNS0 (RFC 6891): "
|
||||
u"%(error)s.\n"
|
||||
u"If DNSSEC validation is enabled on IPA server(s), "
|
||||
u"please disable it.")
|
||||
|
||||
|
||||
class DNSSECValidationFailingWarning(PublicMessage):
|
||||
"""
|
||||
**13010** Used when a DNSSEC validation failed on IPA DNS server
|
||||
"""
|
||||
|
||||
errno = 13010
|
||||
type = "warning"
|
||||
format = _(u"DNSSEC validation failed: %(error)s.\n"
|
||||
u"Please verify your DNSSEC configuration or disable DNSSEC "
|
||||
u"validation on all IPA servers.")
|
||||
|
||||
|
||||
class KerberosTXTRecordCreationFailure(PublicMessage):
|
||||
"""
|
||||
**13011** Used when a _kerberos TXT record could not be added to
|
||||
a DNS zone.
|
||||
"""
|
||||
|
||||
errno = 13011
|
||||
type = "warning"
|
||||
format = _(
|
||||
"The _kerberos TXT record from domain %(domain)s could not be created "
|
||||
"(%(error)s).\nThis can happen if the zone is not managed by IPA. "
|
||||
"Please create the record manually, containing the following "
|
||||
"value: '%(realm)s'"
|
||||
)
|
||||
|
||||
|
||||
class KerberosTXTRecordDeletionFailure(PublicMessage):
|
||||
"""
|
||||
**13012** Used when a _kerberos TXT record could not be removed from
|
||||
a DNS zone.
|
||||
"""
|
||||
|
||||
errno = 13012
|
||||
type = "warning"
|
||||
format = _(
|
||||
"The _kerberos TXT record from domain %(domain)s could not be removed "
|
||||
"(%(error)s).\nThis can happen if the zone is not managed by IPA. "
|
||||
"Please remove the record manually."
|
||||
)
|
||||
|
||||
class DNSSECMasterNotInstalled(PublicMessage):
|
||||
"""
|
||||
**13013** Used when a DNSSEC is not installed on system (no DNSSEC
|
||||
master server is installed).
|
||||
"""
|
||||
|
||||
errno = 13013
|
||||
type = "warning"
|
||||
format = _(
|
||||
"No DNSSEC key master is installed. DNSSEC zone signing will not work "
|
||||
"until the DNSSEC key master is installed."
|
||||
)
|
||||
|
||||
|
||||
class DNSSuspiciousRelativeName(PublicMessage):
|
||||
"""
|
||||
**13014** Relative name "record.zone" is being added into zone "zone.",
|
||||
which is probably a mistake. User probably wanted to either specify
|
||||
relative name "record" or use FQDN "record.zone.".
|
||||
"""
|
||||
|
||||
errno = 13014
|
||||
type = "warning"
|
||||
format = _(
|
||||
"Relative record name '%(record)s' contains the zone name '%(zone)s' "
|
||||
"as a suffix, which results in FQDN '%(fqdn)s'. This is usually a "
|
||||
"mistake caused by a missing dot at the end of the name specification."
|
||||
)
|
||||
|
||||
|
||||
class CommandDeprecatedWarning(PublicMessage):
|
||||
"""
|
||||
**13015** Used when user uses a deprecated option
|
||||
"""
|
||||
|
||||
errno = 13015
|
||||
type = "warning"
|
||||
format = _(u"'%(command)s' is deprecated. %(additional_info)s")
|
||||
|
||||
|
||||
class ExternalCommandOutput(PublicMessage):
|
||||
"""
|
||||
**13016** Line of output from an external command.
|
||||
"""
|
||||
|
||||
errno = 13016
|
||||
type = "info"
|
||||
format = _("%(line)s")
|
||||
|
||||
|
||||
class SearchResultTruncated(PublicMessage):
|
||||
"""
|
||||
**13017** Results of LDAP search has been truncated
|
||||
"""
|
||||
|
||||
errno = 13017
|
||||
type = "warning"
|
||||
format = _("Search result has been truncated: %(reason)s")
|
||||
|
||||
|
||||
class BrokenTrust(PublicMessage):
|
||||
"""
|
||||
**13018** Trust for a specified domain is broken
|
||||
"""
|
||||
|
||||
errno = 13018
|
||||
type = "warning"
|
||||
format = _("Your trust to %(domain)s is broken. Please re-create it by "
|
||||
"running 'ipa trust-add' again.")
|
||||
|
||||
|
||||
class ResultFormattingError(PublicMessage):
|
||||
"""
|
||||
**13019** Unable to correctly format some part of the result
|
||||
"""
|
||||
type = "warning"
|
||||
errno = 13019
|
||||
|
||||
|
||||
class FailedToRemoveHostDNSRecords(PublicMessage):
|
||||
"""
|
||||
**13020** Failed to remove host DNS records
|
||||
"""
|
||||
|
||||
errno = 13020
|
||||
type = "warning"
|
||||
format = _("DNS record(s) of host %(host)s could not be removed. "
|
||||
"(%(reason)s)")
|
||||
|
||||
|
||||
class DNSForwardPolicyConflictWithEmptyZone(PublicMessage):
|
||||
"""
|
||||
**13021** Forward zone 1.10.in-addr.arpa with policy "first"
|
||||
will not forward anything because BIND automatically prefers
|
||||
empty zone "10.in-addr.arpa.".
|
||||
"""
|
||||
|
||||
errno = 13021
|
||||
type = "warning"
|
||||
format = _(
|
||||
"Forwarding policy conflicts with some automatic empty zones. "
|
||||
"Queries for zones specified by RFC 6303 will ignore "
|
||||
"forwarding and recursion and always result in NXDOMAIN answers. "
|
||||
"To override this behavior use forward policy 'only'."
|
||||
)
|
||||
|
||||
|
||||
class DNSUpdateOfSystemRecordFailed(PublicMessage):
|
||||
"""
|
||||
**13022** Update of a DNS system record failed
|
||||
"""
|
||||
errno = 13022
|
||||
type = "warning"
|
||||
format = _(
|
||||
"Update of system record '%(record)s' failed with error: %(error)s"
|
||||
)
|
||||
|
||||
|
||||
class DNSUpdateNotIPAManagedZone(PublicMessage):
|
||||
"""
|
||||
**13023** Zone for system records is not managed by IPA
|
||||
"""
|
||||
errno = 13023
|
||||
type = "warning"
|
||||
format = _(
|
||||
"IPA does not manage the zone %(zone)s, please add records "
|
||||
"to your DNS server manually"
|
||||
)
|
||||
|
||||
|
||||
class AutomaticDNSRecordsUpdateFailed(PublicMessage):
|
||||
"""
|
||||
**13024** Automatic update of DNS records failed
|
||||
"""
|
||||
errno = 13024
|
||||
type = "warning"
|
||||
format = _(
|
||||
"Automatic update of DNS system records failed. "
|
||||
"Please re-run update of system records manually to get list of "
|
||||
"missing records."
|
||||
)
|
||||
|
||||
|
||||
class ServiceRestartRequired(PublicMessage):
|
||||
"""
|
||||
**13025** Service restart is required
|
||||
"""
|
||||
errno = 13025
|
||||
type = "warning"
|
||||
format = _(
|
||||
"Service %(service)s requires restart on IPA server %(server)s to "
|
||||
"apply configuration changes."
|
||||
)
|
||||
|
||||
|
||||
class LocationWithoutDNSServer(PublicMessage):
|
||||
"""
|
||||
**13026** Location without DNS server
|
||||
"""
|
||||
errno = 13026
|
||||
type = "warning"
|
||||
format = _(
|
||||
"No DNS servers in IPA location %(location)s. Without DNS servers "
|
||||
"location is not working as expected."
|
||||
)
|
||||
|
||||
|
||||
class ServerRemovalInfo(PublicMessage):
|
||||
"""
|
||||
**13027** Informative message printed during removal of IPA server
|
||||
"""
|
||||
errno = 13027
|
||||
type = "info"
|
||||
|
||||
|
||||
class ServerRemovalWarning(PublicMessage):
|
||||
"""
|
||||
**13028** Warning raised during removal of IPA server
|
||||
"""
|
||||
errno = 13028
|
||||
type = "warning"
|
||||
|
||||
|
||||
class CertificateInvalid(PublicMessage):
|
||||
"""
|
||||
**13029** Failed to parse a certificate
|
||||
"""
|
||||
errno = 13029
|
||||
type = "error"
|
||||
format = _("%(subject)s: Invalid certificate. "
|
||||
"%(reason)s")
|
||||
|
||||
|
||||
def iter_messages(variables, base):
|
||||
"""Return a tuple with all subclasses
|
||||
"""
|
||||
for (key, value) in variables.items():
|
||||
if key.startswith('_') or not isclass(value):
|
||||
continue
|
||||
if issubclass(value, base):
|
||||
yield value
|
||||
|
||||
|
||||
public_messages = tuple(sorted(
|
||||
iter_messages(globals(), PublicMessage), key=lambda E: E.errno))
|
||||
|
||||
def print_report(label, classes):
|
||||
for cls in classes:
|
||||
print('%d\t%s' % (cls.errno, cls.__name__))
|
||||
print('(%d %s)' % (len(classes), label))
|
||||
|
||||
if __name__ == '__main__':
|
||||
print_report('public messages', public_messages)
|
||||
132
ipalib/misc.py
Normal file
132
ipalib/misc.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import re
|
||||
from ipalib import LocalOrRemote, _, ngettext
|
||||
from ipalib.output import Output, summary
|
||||
from ipalib import Flag
|
||||
from ipalib.plugable import Registry
|
||||
|
||||
register = Registry()
|
||||
|
||||
# FIXME: We should not let env return anything in_server
|
||||
# when mode == 'production'. This would allow an attacker to see the
|
||||
# configuration of the server, potentially revealing compromising
|
||||
# information. However, it's damn handy for testing/debugging.
|
||||
|
||||
|
||||
class env(LocalOrRemote):
|
||||
__doc__ = _('Show environment variables.')
|
||||
|
||||
msg_summary = _('%(count)d variables')
|
||||
|
||||
takes_args = (
|
||||
'variables*',
|
||||
)
|
||||
|
||||
takes_options = LocalOrRemote.takes_options + (
|
||||
Flag(
|
||||
'all',
|
||||
cli_name='all',
|
||||
doc=_('retrieve and print all attributes from the server. '
|
||||
'Affects command output.'),
|
||||
exclude='webui',
|
||||
flags=['no_option', 'no_output'],
|
||||
default=True,
|
||||
),
|
||||
)
|
||||
|
||||
has_output = (
|
||||
Output(
|
||||
'result',
|
||||
type=dict,
|
||||
doc=_('Dictionary mapping variable name to value'),
|
||||
),
|
||||
Output(
|
||||
'total',
|
||||
type=int,
|
||||
doc=_('Total number of variables env (>= count)'),
|
||||
flags=['no_display'],
|
||||
),
|
||||
Output(
|
||||
'count',
|
||||
type=int,
|
||||
doc=_('Number of variables returned (<= total)'),
|
||||
flags=['no_display'],
|
||||
),
|
||||
summary,
|
||||
)
|
||||
|
||||
def __find_keys(self, variables):
|
||||
keys = set()
|
||||
for query in variables:
|
||||
if '*' in query:
|
||||
pat = re.compile(query.replace('*', '.*') + '$')
|
||||
for key in self.env:
|
||||
if pat.match(key):
|
||||
keys.add(key)
|
||||
elif query in self.env:
|
||||
keys.add(query)
|
||||
return keys
|
||||
|
||||
def execute(self, variables=None, **options):
|
||||
if variables is None:
|
||||
keys = self.env
|
||||
else:
|
||||
keys = self.__find_keys(variables)
|
||||
ret = dict(
|
||||
result=dict(
|
||||
(key, self.env[key]) for key in keys
|
||||
),
|
||||
count=len(keys),
|
||||
total=len(self.env),
|
||||
)
|
||||
if len(keys) > 1:
|
||||
ret['summary'] = self.msg_summary % ret
|
||||
else:
|
||||
ret['summary'] = None
|
||||
return ret
|
||||
|
||||
|
||||
class plugins(LocalOrRemote):
|
||||
__doc__ = _('Show all loaded plugins.')
|
||||
|
||||
msg_summary = ngettext(
|
||||
'%(count)d plugin loaded', '%(count)d plugins loaded', 0
|
||||
)
|
||||
|
||||
takes_options = LocalOrRemote.takes_options + (
|
||||
Flag(
|
||||
'all',
|
||||
cli_name='all',
|
||||
doc=_('retrieve and print all attributes from the server. '
|
||||
'Affects command output.'),
|
||||
exclude='webui',
|
||||
flags=['no_option', 'no_output'],
|
||||
default=True,
|
||||
),
|
||||
)
|
||||
|
||||
has_output = (
|
||||
Output('result', dict, 'Dictionary mapping plugin names to bases'),
|
||||
Output(
|
||||
'count',
|
||||
type=int,
|
||||
doc=_('Number of plugins loaded'),
|
||||
),
|
||||
summary,
|
||||
)
|
||||
|
||||
def execute(self, **options):
|
||||
result = {}
|
||||
for namespace in self.api:
|
||||
for plugin in self.api[namespace]():
|
||||
cls = type(plugin)
|
||||
key = '{}.{}'.format(cls.__module__, cls.__name__)
|
||||
result.setdefault(key, []).append(namespace.decode('utf-8'))
|
||||
|
||||
return dict(
|
||||
result=result,
|
||||
count=len(result),
|
||||
)
|
||||
229
ipalib/output.py
Normal file
229
ipalib/output.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2009 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/>.
|
||||
|
||||
"""
|
||||
Simple description of return values.
|
||||
"""
|
||||
import six
|
||||
|
||||
from ipalib.plugable import ReadOnly, lock
|
||||
from ipalib.capabilities import client_has_capability
|
||||
from ipalib.text import _
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
class Output(ReadOnly):
|
||||
"""
|
||||
Simple description of a member in the return value ``dict``.
|
||||
|
||||
This class controls both the type of object being returned by
|
||||
a command as well as how the output will be displayed.
|
||||
|
||||
For example, this class defines two return results: an entry
|
||||
and a value.
|
||||
|
||||
>>> from ipalib import crud, output
|
||||
>>> class user(crud.Update):
|
||||
...
|
||||
... has_output = (
|
||||
... output.Entry('result'),
|
||||
... output.value,
|
||||
... )
|
||||
|
||||
The order of the values in has_output controls the order of output.
|
||||
If you have values that you don't want to be printed then add
|
||||
``'no_display'`` to flags.
|
||||
|
||||
The difference between ``'no_display'`` and ``'no_output'`` is
|
||||
that ``'no_output'`` will prevent a Param value from being returned
|
||||
at all. ``'no_display'`` will cause the API to return a value, it
|
||||
simply won't be displayed to the user. This is so some things may
|
||||
be returned that while not interesting to us, but may be to others.
|
||||
|
||||
>>> from ipalib import crud, output
|
||||
>>> myvalue = output.Output('myvalue', unicode,
|
||||
... 'Do not print this value', flags=['no_display'],
|
||||
... )
|
||||
>>> class user(crud.Update):
|
||||
...
|
||||
... has_output = (
|
||||
... output.Entry('result'),
|
||||
... myvalue,
|
||||
... )
|
||||
"""
|
||||
|
||||
type = None
|
||||
validate = None
|
||||
doc = None
|
||||
flags = []
|
||||
|
||||
def __init__(self, name, type=None, doc=None, flags=[]):
|
||||
self.name = name
|
||||
if type is not None:
|
||||
if not isinstance(type, tuple):
|
||||
type = (type,)
|
||||
self.type = type
|
||||
if doc is not None:
|
||||
self.doc = doc
|
||||
self.flags = flags
|
||||
lock(self)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (
|
||||
self.__class__.__name__,
|
||||
', '.join(self.__repr_iter())
|
||||
)
|
||||
|
||||
def __repr_iter(self):
|
||||
yield repr(self.name)
|
||||
for key in ('type', 'doc', 'flags'):
|
||||
value = self.__dict__.get(key)
|
||||
if not value:
|
||||
continue
|
||||
if isinstance(value, tuple):
|
||||
value = repr(list(value))
|
||||
else:
|
||||
value = repr(value)
|
||||
yield '%s=%s' % (key, value)
|
||||
|
||||
|
||||
class Entry(Output):
|
||||
type = dict
|
||||
doc = _('A dictionary representing an LDAP entry')
|
||||
|
||||
|
||||
emsg = """%s.validate_output() => %s.validate():
|
||||
output[%r][%d]: need a %r; got a %r: %r"""
|
||||
|
||||
class ListOfEntries(Output):
|
||||
type = (list, tuple)
|
||||
doc = _('A list of LDAP entries')
|
||||
|
||||
def validate(self, cmd, entries, version):
|
||||
assert isinstance(entries, self.type)
|
||||
for (i, entry) in enumerate(entries):
|
||||
if not isinstance(entry, dict):
|
||||
raise TypeError(emsg % (cmd.name, self.__class__.__name__,
|
||||
self.name, i, dict, type(entry), entry)
|
||||
)
|
||||
|
||||
class PrimaryKey(Output):
|
||||
def validate(self, cmd, value, version):
|
||||
if client_has_capability(version, 'primary_key_types'):
|
||||
if hasattr(cmd, 'obj') and cmd.obj and cmd.obj.primary_key:
|
||||
types = cmd.obj.primary_key.allowed_types
|
||||
else:
|
||||
types = (unicode,)
|
||||
types = types + (type(None),)
|
||||
else:
|
||||
types = (unicode,)
|
||||
if not isinstance(value, types):
|
||||
raise TypeError(
|
||||
"%s.validate_output() => %s.validate():\n"
|
||||
" output[%r]: need %r; got %r: %r" % (
|
||||
cmd.name, self.__class__.__name__, self.name,
|
||||
types[0], type(value), value))
|
||||
|
||||
class ListOfPrimaryKeys(Output):
|
||||
def validate(self, cmd, values, version):
|
||||
if client_has_capability(version, 'primary_key_types'):
|
||||
types = (tuple, list)
|
||||
else:
|
||||
types = (unicode,)
|
||||
if not isinstance(values, types):
|
||||
raise TypeError(
|
||||
"%s.validate_output() => %s.validate():\n"
|
||||
" output[%r]: need %r; got %r: %r" % (
|
||||
cmd.name, self.__class__.__name__, self.name,
|
||||
types[0], type(values), values))
|
||||
|
||||
if client_has_capability(version, 'primary_key_types'):
|
||||
if hasattr(cmd, 'obj') and cmd.obj and cmd.obj.primary_key:
|
||||
types = cmd.obj.primary_key.allowed_types
|
||||
else:
|
||||
types = (unicode,)
|
||||
for (i, value) in enumerate(values):
|
||||
if not isinstance(value, types):
|
||||
raise TypeError(emsg % (
|
||||
cmd.name, self.__class__.__name__, i, self.name,
|
||||
types[0], type(value), value))
|
||||
|
||||
|
||||
result = Output('result', doc=_('All commands should at least have a result'))
|
||||
|
||||
summary = Output('summary', (unicode, type(None)),
|
||||
_('User-friendly description of action performed')
|
||||
)
|
||||
|
||||
value = PrimaryKey('value', None,
|
||||
_("The primary_key value of the entry, e.g. 'jdoe' for a user"),
|
||||
flags=['no_display'],
|
||||
)
|
||||
|
||||
standard = (summary, result)
|
||||
|
||||
standard_entry = (
|
||||
summary,
|
||||
Entry('result'),
|
||||
value,
|
||||
)
|
||||
|
||||
standard_list_of_entries = (
|
||||
summary,
|
||||
ListOfEntries('result'),
|
||||
Output('count', int, _('Number of entries returned')),
|
||||
Output('truncated', bool, _('True if not all results were returned')),
|
||||
)
|
||||
|
||||
standard_delete = (
|
||||
summary,
|
||||
Output('result', dict, _('List of deletions that failed')),
|
||||
value,
|
||||
)
|
||||
|
||||
standard_multi_delete = (
|
||||
summary,
|
||||
Output('result', dict, _('List of deletions that failed')),
|
||||
ListOfPrimaryKeys('value', flags=['no_display']),
|
||||
)
|
||||
|
||||
standard_boolean = (
|
||||
summary,
|
||||
Output('result', bool, _('True means the operation was successful')),
|
||||
value,
|
||||
)
|
||||
|
||||
standard_value = standard_boolean
|
||||
|
||||
simple_value = (
|
||||
summary,
|
||||
Output('result', bool, _('True means the operation was successful')),
|
||||
Output('value', unicode, flags=['no_display']),
|
||||
)
|
||||
|
||||
# custom shim for commands like `trustconfig-show`,
|
||||
# `automember-default-group-*` which put stuff into output['value'] despite not
|
||||
# having primary key themselves. Designing commands like this is not a very
|
||||
# good practice, so please do not use this for new code.
|
||||
simple_entry = (
|
||||
summary,
|
||||
Entry('result'),
|
||||
Output('value', unicode, flags=['no_display']),
|
||||
)
|
||||
2114
ipalib/parameters.py
Normal file
2114
ipalib/parameters.py
Normal file
File diff suppressed because it is too large
Load Diff
8
ipalib/pkcs10.py
Normal file
8
ipalib/pkcs10.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
|
||||
print(
|
||||
"ipalib.pkcs10 module is deprecated and will be removed in FreeIPA 4.6. "
|
||||
"To load CSRs, please, use python-cryptography instead.",
|
||||
file=sys.stderr
|
||||
)
|
||||
820
ipalib/plugable.py
Normal file
820
ipalib/plugable.py
Normal file
@@ -0,0 +1,820 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Plugin framework.
|
||||
|
||||
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
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
from os import path
|
||||
import optparse # pylint: disable=deprecated-module
|
||||
import textwrap
|
||||
import collections
|
||||
import importlib
|
||||
|
||||
import six
|
||||
|
||||
from ipalib import errors
|
||||
from ipalib.config import Env
|
||||
from ipalib.text import _
|
||||
from ipalib.util import classproperty
|
||||
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
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FIXME: Updated constants.TYPE_ERROR to use this clearer format from wehjit:
|
||||
TYPE_ERROR = '%s: need a %r; got a %r: %r'
|
||||
|
||||
|
||||
# FIXME: This function has no unit test
|
||||
def find_modules_in_dir(src_dir):
|
||||
"""
|
||||
Iterate through module names found in ``src_dir``.
|
||||
"""
|
||||
if not (os.path.abspath(src_dir) == src_dir and os.path.isdir(src_dir)):
|
||||
return
|
||||
if os.path.islink(src_dir):
|
||||
return
|
||||
suffix = '.py'
|
||||
for name in sorted(os.listdir(src_dir)):
|
||||
if not name.endswith(suffix):
|
||||
continue
|
||||
pyfile = os.path.join(src_dir, name)
|
||||
if not os.path.isfile(pyfile):
|
||||
continue
|
||||
module = name[:-len(suffix)]
|
||||
if module == '__init__':
|
||||
continue
|
||||
yield module
|
||||
|
||||
|
||||
class Registry(object):
|
||||
"""A decorator that makes plugins available to the API
|
||||
|
||||
Usage::
|
||||
|
||||
register = Registry()
|
||||
|
||||
@register()
|
||||
class obj_mod(...):
|
||||
...
|
||||
|
||||
For forward compatibility, make sure that the module-level instance of
|
||||
this object is named "register".
|
||||
"""
|
||||
def __init__(self):
|
||||
self.__registry = collections.OrderedDict()
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
def register(plugin):
|
||||
"""
|
||||
Register the plugin ``plugin``.
|
||||
|
||||
:param plugin: A subclass of `Plugin` to attempt to register.
|
||||
"""
|
||||
if not callable(plugin):
|
||||
raise TypeError('plugin must be callable; got %r' % plugin)
|
||||
|
||||
# Raise DuplicateError if this exact class was already registered:
|
||||
if plugin in self.__registry:
|
||||
raise errors.PluginDuplicateError(plugin=plugin)
|
||||
|
||||
# The plugin is okay, add to __registry:
|
||||
self.__registry[plugin] = dict(kwargs, plugin=plugin)
|
||||
|
||||
return plugin
|
||||
|
||||
return register
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__registry.values())
|
||||
|
||||
|
||||
class Plugin(ReadOnly):
|
||||
"""
|
||||
Base class for all plugins.
|
||||
"""
|
||||
|
||||
version = '1'
|
||||
|
||||
def __init__(self, api):
|
||||
assert api is not None
|
||||
self.__api = api
|
||||
self.__finalize_called = False
|
||||
self.__finalized = False
|
||||
self.__finalize_lock = threading.RLock()
|
||||
log_mgr.get_logger(self, True)
|
||||
|
||||
@classmethod
|
||||
def __name_getter(cls):
|
||||
return cls.__name__
|
||||
|
||||
# you know nothing, pylint
|
||||
name = classproperty(__name_getter)
|
||||
|
||||
@classmethod
|
||||
def __full_name_getter(cls):
|
||||
return '{}/{}'.format(cls.name, cls.version)
|
||||
|
||||
full_name = classproperty(__full_name_getter)
|
||||
|
||||
@classmethod
|
||||
def __bases_getter(cls):
|
||||
return cls.__bases__
|
||||
|
||||
bases = classproperty(__bases_getter)
|
||||
|
||||
@classmethod
|
||||
def __doc_getter(cls):
|
||||
return cls.__doc__
|
||||
|
||||
doc = classproperty(__doc_getter)
|
||||
|
||||
@classmethod
|
||||
def __summary_getter(cls):
|
||||
doc = cls.doc
|
||||
if not _(doc).msg:
|
||||
return u'<%s.%s>' % (cls.__module__, cls.__name__)
|
||||
else:
|
||||
return unicode(doc).split('\n\n', 1)[0].strip()
|
||||
|
||||
summary = classproperty(__summary_getter)
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
"""
|
||||
Return `API` instance passed to `__init__()`.
|
||||
"""
|
||||
return self.__api
|
||||
|
||||
# FIXME: for backward compatibility only
|
||||
@property
|
||||
def env(self):
|
||||
return self.__api.env
|
||||
|
||||
# FIXME: for backward compatibility only
|
||||
@property
|
||||
def Backend(self):
|
||||
return self.__api.Backend
|
||||
|
||||
# FIXME: for backward compatibility only
|
||||
@property
|
||||
def Command(self):
|
||||
return self.__api.Command
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
Finalize plugin initialization.
|
||||
|
||||
This method calls `_on_finalize()` and locks the plugin object.
|
||||
|
||||
Subclasses should not override this method. Custom finalization is done
|
||||
in `_on_finalize()`.
|
||||
"""
|
||||
with self.__finalize_lock:
|
||||
assert self.__finalized is False
|
||||
if self.__finalize_called:
|
||||
# No recursive calls!
|
||||
return
|
||||
self.__finalize_called = True
|
||||
self._on_finalize()
|
||||
self.__finalized = True
|
||||
if not self.__api.is_production_mode():
|
||||
lock(self)
|
||||
|
||||
def _on_finalize(self):
|
||||
"""
|
||||
Do custom finalization.
|
||||
|
||||
This method is called from `finalize()`. Subclasses can override this
|
||||
method in order to add custom finalization.
|
||||
"""
|
||||
pass
|
||||
|
||||
def ensure_finalized(self):
|
||||
"""
|
||||
Finalize plugin initialization if it has not yet been finalized.
|
||||
"""
|
||||
with self.__finalize_lock:
|
||||
if not self.__finalized:
|
||||
self.finalize()
|
||||
|
||||
class finalize_attr(object):
|
||||
"""
|
||||
Create a stub object for plugin attribute that isn't set until the
|
||||
finalization of the plugin initialization.
|
||||
|
||||
When the stub object is accessed, it calls `ensure_finalized()` to make
|
||||
sure the plugin initialization is finalized. The stub object is expected
|
||||
to be replaced with the actual attribute value during the finalization
|
||||
(preferably in `_on_finalize()`), otherwise an `AttributeError` is
|
||||
raised.
|
||||
|
||||
This is used to implement on-demand finalization of plugin
|
||||
initialization.
|
||||
"""
|
||||
__slots__ = ('name', 'value')
|
||||
|
||||
def __init__(self, name, value=None):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
if obj is None or obj.api is None:
|
||||
return self.value
|
||||
obj.ensure_finalized()
|
||||
try:
|
||||
return getattr(obj, self.name)
|
||||
except RuntimeError:
|
||||
# If the actual attribute value is not set in _on_finalize(),
|
||||
# getattr() calls __get__() again, which leads to infinite
|
||||
# recursion. This can happen only if the plugin is written
|
||||
# badly, so advise the developer about that instead of giving
|
||||
# them a generic "maximum recursion depth exceeded" error.
|
||||
raise AttributeError(
|
||||
"attribute '%s' of plugin '%s' was not set in finalize()" % (self.name, obj.name)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return 'module_name.class_name()' representation.
|
||||
|
||||
This representation could be used to instantiate this Plugin
|
||||
instance given the appropriate environment.
|
||||
"""
|
||||
return '%s.%s()' % (
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
|
||||
class APINameSpace(collections.Mapping):
|
||||
def __init__(self, api, base):
|
||||
self.__api = api
|
||||
self.__base = base
|
||||
self.__plugins = None
|
||||
self.__plugins_by_key = None
|
||||
|
||||
def __enumerate(self):
|
||||
if self.__plugins is not None and self.__plugins_by_key is not None:
|
||||
return
|
||||
|
||||
default_map = self.__api._API__default_map
|
||||
plugins = set()
|
||||
key_dict = self.__plugins_by_key = {}
|
||||
|
||||
for plugin in self.__api._API__plugins:
|
||||
if not any(issubclass(b, self.__base) for b in plugin.bases):
|
||||
continue
|
||||
plugins.add(plugin)
|
||||
key_dict[plugin] = plugin
|
||||
key_dict[plugin.name, plugin.version] = plugin
|
||||
key_dict[plugin.full_name] = plugin
|
||||
if plugin.version == default_map.get(plugin.name, '1'):
|
||||
key_dict[plugin.name] = plugin
|
||||
|
||||
self.__plugins = sorted(plugins, key=operator.attrgetter('full_name'))
|
||||
|
||||
def __len__(self):
|
||||
self.__enumerate()
|
||||
return len(self.__plugins)
|
||||
|
||||
def __contains__(self, key):
|
||||
self.__enumerate()
|
||||
return key in self.__plugins_by_key
|
||||
|
||||
def __iter__(self):
|
||||
self.__enumerate()
|
||||
return iter(self.__plugins)
|
||||
|
||||
def get_plugin(self, key):
|
||||
self.__enumerate()
|
||||
return self.__plugins_by_key[key]
|
||||
|
||||
def __getitem__(self, key):
|
||||
plugin = self.get_plugin(key)
|
||||
return self.__api._get(plugin)
|
||||
|
||||
def __call__(self):
|
||||
return six.itervalues(self)
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
raise AttributeError(key)
|
||||
|
||||
|
||||
class API(ReadOnly):
|
||||
"""
|
||||
Dynamic API object through which `Plugin` instances are accessed.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(API, self).__init__()
|
||||
self.__plugins = set()
|
||||
self.__plugins_by_key = {}
|
||||
self.__default_map = {}
|
||||
self.__instances = {}
|
||||
self.__next = {}
|
||||
self.__done = set()
|
||||
self.env = Env()
|
||||
|
||||
@property
|
||||
def bases(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def packages(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the number of plugin namespaces in this API object.
|
||||
"""
|
||||
return len(self.bases)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate (in ascending order) through plugin namespace names.
|
||||
"""
|
||||
return (base.__name__ for base in self.bases)
|
||||
|
||||
def __contains__(self, name):
|
||||
"""
|
||||
Return True if this API object contains plugin namespace ``name``.
|
||||
|
||||
:param name: The plugin namespace name to test for membership.
|
||||
"""
|
||||
return name in set(self)
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""
|
||||
Return the plugin namespace corresponding to ``name``.
|
||||
|
||||
:param name: The name of the plugin namespace you wish to retrieve.
|
||||
"""
|
||||
if name in self:
|
||||
try:
|
||||
return getattr(self, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise KeyError(name)
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
Iterate (in ascending order by name) through plugin namespaces.
|
||||
"""
|
||||
for name in self:
|
||||
try:
|
||||
yield getattr(self, name)
|
||||
except AttributeError:
|
||||
raise KeyError(name)
|
||||
|
||||
def is_production_mode(self):
|
||||
"""
|
||||
If the object has self.env.mode defined and that mode is
|
||||
production return True, otherwise return False.
|
||||
"""
|
||||
return getattr(self.env, 'mode', None) == 'production'
|
||||
|
||||
def __doing(self, name):
|
||||
if name in self.__done:
|
||||
raise Exception(
|
||||
'%s.%s() already called' % (self.__class__.__name__, name)
|
||||
)
|
||||
self.__done.add(name)
|
||||
|
||||
def __do_if_not_done(self, name):
|
||||
if name not in self.__done:
|
||||
getattr(self, name)()
|
||||
|
||||
def isdone(self, name):
|
||||
return name in self.__done
|
||||
|
||||
def bootstrap(self, parser=None, **overrides):
|
||||
"""
|
||||
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))
|
||||
|
||||
# Add the argument parser
|
||||
if not parser:
|
||||
parser = self.build_global_parser()
|
||||
self.parser = parser
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
# If logging has already been configured somewhere else (like in the
|
||||
# installer), don't add handlers or change levels:
|
||||
if root_logger.handlers or self.env.validate_api:
|
||||
return
|
||||
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
root_logger.setLevel(level)
|
||||
|
||||
for attr in self.env:
|
||||
match = re.match(r'^log_logger_level_'
|
||||
r'(debug|info|warn|warning|error|critical|\d+)$',
|
||||
attr)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
level = ipa_log_manager.convert_log_level(match.group(1))
|
||||
|
||||
value = getattr(self.env, attr)
|
||||
regexps = re.split('\s*,\s*', value)
|
||||
|
||||
# Add the regexp, it maps to the configured level
|
||||
for regexp in regexps:
|
||||
root_logger.addFilter(ipa_log_manager.Filter(regexp, level))
|
||||
|
||||
# Add stderr handler:
|
||||
level = logging.INFO
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
if self.env.context == 'cli':
|
||||
if self.env.verbose > 0:
|
||||
level = logging.INFO
|
||||
else:
|
||||
level = logging.WARNING
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(ipa_log_manager.Formatter(LOGGING_FORMAT_STDERR))
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# check after logging is set up but before we create files.
|
||||
fse = sys.getfilesystemencoding()
|
||||
if fse.lower() not in {'utf-8', 'utf8'}:
|
||||
raise errors.SystemEncodingError(encoding=fse)
|
||||
|
||||
# Add file handler:
|
||||
if self.env.mode in ('dummy', 'unit_test'):
|
||||
return # But not if in unit-test mode
|
||||
if self.env.log is None:
|
||||
return
|
||||
log_dir = path.dirname(self.env.log)
|
||||
if not path.isdir(log_dir):
|
||||
try:
|
||||
os.makedirs(log_dir)
|
||||
except OSError:
|
||||
logger.error('Could not create log_dir %r', log_dir)
|
||||
return
|
||||
|
||||
level = logging.INFO
|
||||
if self.env.debug:
|
||||
level = logging.DEBUG
|
||||
try:
|
||||
handler = logging.FileHandler(self.env.log)
|
||||
except IOError as e:
|
||||
logger.error('Cannot open log file %r: %s', self.env.log, e)
|
||||
return
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(ipa_log_manager.Formatter(LOGGING_FORMAT_FILE))
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
def build_global_parser(self, parser=None, context=None):
|
||||
"""
|
||||
Add global options to an optparse.OptionParser instance.
|
||||
"""
|
||||
def config_file_callback(option, opt, value, parser):
|
||||
if not os.path.isfile(value):
|
||||
parser.error(
|
||||
_("%(filename)s: file not found") % dict(filename=value))
|
||||
|
||||
parser.values.conf = value
|
||||
|
||||
if parser is None:
|
||||
parser = optparse.OptionParser(
|
||||
add_help_option=False,
|
||||
formatter=IPAHelpFormatter(),
|
||||
usage='%prog [global-options] COMMAND [command-options]',
|
||||
description='Manage an IPA domain',
|
||||
version=('VERSION: %s, API_VERSION: %s'
|
||||
% (VERSION, API_VERSION)),
|
||||
epilog='\n'.join([
|
||||
'See "ipa help topics" for available help topics.',
|
||||
'See "ipa help <TOPIC>" for more information on a '
|
||||
'specific topic.',
|
||||
'See "ipa help commands" for the full list of commands.',
|
||||
'See "ipa <COMMAND> --help" for more information on a '
|
||||
'specific command.',
|
||||
]))
|
||||
parser.disable_interspersed_args()
|
||||
parser.add_option("-h", "--help", action="help",
|
||||
help='Show this help message and exit')
|
||||
|
||||
parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append',
|
||||
help='Set environment variable KEY to VAL',
|
||||
)
|
||||
parser.add_option('-c', dest='conf', metavar='FILE', action='callback',
|
||||
callback=config_file_callback, type='string',
|
||||
help='Load configuration from FILE.',
|
||||
)
|
||||
parser.add_option('-d', '--debug', action='store_true',
|
||||
help='Produce full debuging output',
|
||||
)
|
||||
parser.add_option('--delegate', action='store_true',
|
||||
help='Delegate the TGT to the IPA server',
|
||||
)
|
||||
parser.add_option('-v', '--verbose', action='count',
|
||||
help='Produce more verbose output. A second -v displays the XML-RPC request',
|
||||
)
|
||||
if context == 'cli':
|
||||
parser.add_option('-a', '--prompt-all', action='store_true',
|
||||
help='Prompt for ALL values (even if optional)'
|
||||
)
|
||||
parser.add_option('-n', '--no-prompt', action='store_false',
|
||||
dest='interactive',
|
||||
help='Prompt for NO values (even if required)'
|
||||
)
|
||||
parser.add_option('-f', '--no-fallback', action='store_false',
|
||||
dest='fallback',
|
||||
help='Only use the server configured in /etc/ipa/default.conf'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def bootstrap_with_global_options(self, parser=None, context=None):
|
||||
parser = self.build_global_parser(parser, context)
|
||||
(options, args) = parser.parse_args()
|
||||
overrides = {}
|
||||
if options.env is not None:
|
||||
assert type(options.env) is list
|
||||
for item in options.env:
|
||||
try:
|
||||
(key, value) = item.split('=', 1)
|
||||
except ValueError:
|
||||
# FIXME: this should raise an IPA exception with an
|
||||
# error code.
|
||||
# --Jason, 2008-10-31
|
||||
pass
|
||||
overrides[str(key.strip())] = value.strip()
|
||||
for key in ('conf', 'debug', 'verbose', 'prompt_all', 'interactive',
|
||||
'fallback', 'delegate'):
|
||||
value = getattr(options, key, None)
|
||||
if value is not None:
|
||||
overrides[key] = value
|
||||
if hasattr(options, 'prod'):
|
||||
overrides['webui_prod'] = options.prod
|
||||
if context is not None:
|
||||
overrides['context'] = context
|
||||
self.bootstrap(parser, **overrides)
|
||||
return (options, args)
|
||||
|
||||
def load_plugins(self):
|
||||
"""
|
||||
Load plugins from all standard locations.
|
||||
|
||||
`API.bootstrap` will automatically be called if it hasn't been
|
||||
already.
|
||||
"""
|
||||
self.__doing('load_plugins')
|
||||
self.__do_if_not_done('bootstrap')
|
||||
if self.env.mode in ('dummy', 'unit_test'):
|
||||
return
|
||||
for package in self.packages:
|
||||
self.add_package(package)
|
||||
|
||||
# FIXME: This method has no unit test
|
||||
def add_package(self, package):
|
||||
"""
|
||||
Add plugin modules from the ``package``.
|
||||
|
||||
:param package: A package from which to add modules.
|
||||
"""
|
||||
package_name = package.__name__
|
||||
package_file = package.__file__
|
||||
package_dir = path.dirname(path.abspath(package_file))
|
||||
|
||||
parent = sys.modules[package_name.rpartition('.')[0]]
|
||||
parent_dir = path.dirname(path.abspath(parent.__file__))
|
||||
if parent_dir == package_dir:
|
||||
raise errors.PluginsPackageError(
|
||||
name=package_name, file=package_file
|
||||
)
|
||||
|
||||
logger.debug("importing all plugin modules in %s...", package_name)
|
||||
modules = getattr(package, 'modules', find_modules_in_dir(package_dir))
|
||||
modules = ['.'.join((package_name, name)) for name in modules]
|
||||
|
||||
for name in modules:
|
||||
logger.debug("importing plugin module %s", name)
|
||||
try:
|
||||
module = importlib.import_module(name)
|
||||
except errors.SkipPluginModule as e:
|
||||
logger.debug("skipping plugin module %s: %s", name, e.reason)
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.env.startup_traceback:
|
||||
logger.exception("could not load plugin module %s", name)
|
||||
raise
|
||||
|
||||
try:
|
||||
self.add_module(module)
|
||||
except errors.PluginModuleError as e:
|
||||
logger.debug("%s", e)
|
||||
|
||||
def add_module(self, module):
|
||||
"""
|
||||
Add plugins from the ``module``.
|
||||
|
||||
:param module: A module from which to add plugins.
|
||||
"""
|
||||
try:
|
||||
register = module.register
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(register, Registry):
|
||||
for kwargs in register:
|
||||
self.add_plugin(**kwargs)
|
||||
return
|
||||
|
||||
raise errors.PluginModuleError(name=module.__name__)
|
||||
|
||||
def add_plugin(self, plugin, override=False, no_fail=False):
|
||||
"""
|
||||
Add the plugin ``plugin``.
|
||||
|
||||
:param plugin: A subclass of `Plugin` to attempt to add.
|
||||
:param override: If true, override an already added plugin.
|
||||
"""
|
||||
if not callable(plugin):
|
||||
raise TypeError('plugin must be callable; got %r' % plugin)
|
||||
|
||||
# Find the base class or raise SubclassError:
|
||||
for base in plugin.bases:
|
||||
if issubclass(base, self.bases):
|
||||
break
|
||||
else:
|
||||
raise errors.PluginSubclassError(
|
||||
plugin=plugin,
|
||||
bases=self.bases,
|
||||
)
|
||||
|
||||
# Check override:
|
||||
prev = self.__plugins_by_key.get(plugin.full_name)
|
||||
if prev:
|
||||
if not override:
|
||||
if no_fail:
|
||||
return
|
||||
else:
|
||||
# Must use override=True to override:
|
||||
raise errors.PluginOverrideError(
|
||||
base=base.__name__,
|
||||
name=plugin.name,
|
||||
plugin=plugin,
|
||||
)
|
||||
|
||||
self.__plugins.remove(prev)
|
||||
self.__next[plugin] = prev
|
||||
else:
|
||||
if override:
|
||||
if no_fail:
|
||||
return
|
||||
else:
|
||||
# There was nothing already registered to override:
|
||||
raise errors.PluginMissingOverrideError(
|
||||
base=base.__name__,
|
||||
name=plugin.name,
|
||||
plugin=plugin,
|
||||
)
|
||||
|
||||
# The plugin is okay, add to sub_d:
|
||||
self.__plugins.add(plugin)
|
||||
self.__plugins_by_key[plugin.full_name] = plugin
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
Finalize the registration, instantiate the plugins.
|
||||
|
||||
`API.bootstrap` will automatically be called if it hasn't been
|
||||
already.
|
||||
"""
|
||||
self.__doing('finalize')
|
||||
self.__do_if_not_done('load_plugins')
|
||||
|
||||
if self.env.env_confdir is not None:
|
||||
if self.env.env_confdir == self.env.confdir:
|
||||
logger.info(
|
||||
"IPA_CONFDIR env sets confdir to '%s'.", self.env.confdir)
|
||||
|
||||
for plugin in self.__plugins:
|
||||
if not self.env.validate_api:
|
||||
if plugin.full_name not in DEFAULT_PLUGINS:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
default_version = self.__default_map[plugin.name]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# Technicall plugin.version is not an API version. The
|
||||
# APIVersion class can handle plugin versions. It's more
|
||||
# lean than pkg_resource.parse_version().
|
||||
version = ipautil.APIVersion(plugin.version)
|
||||
default_version = ipautil.APIVersion(default_version)
|
||||
if version < default_version:
|
||||
continue
|
||||
self.__default_map[plugin.name] = plugin.version
|
||||
|
||||
production_mode = self.is_production_mode()
|
||||
|
||||
for base in self.bases:
|
||||
for plugin in self.__plugins:
|
||||
if not any(issubclass(b, base) for b in plugin.bases):
|
||||
continue
|
||||
if not self.env.plugins_on_demand:
|
||||
self._get(plugin)
|
||||
|
||||
name = base.__name__
|
||||
if not production_mode:
|
||||
assert not hasattr(self, name)
|
||||
setattr(self, name, APINameSpace(self, base))
|
||||
|
||||
for instance in six.itervalues(self.__instances):
|
||||
if not production_mode:
|
||||
assert instance.api is self
|
||||
if not self.env.plugins_on_demand:
|
||||
instance.ensure_finalized()
|
||||
if not production_mode:
|
||||
assert islocked(instance)
|
||||
|
||||
self.__finalized = True
|
||||
|
||||
if not production_mode:
|
||||
lock(self)
|
||||
|
||||
def _get(self, plugin):
|
||||
if not callable(plugin):
|
||||
raise TypeError('plugin must be callable; got %r' % plugin)
|
||||
if plugin not in self.__plugins:
|
||||
raise KeyError(plugin)
|
||||
|
||||
try:
|
||||
instance = self.__instances[plugin]
|
||||
except KeyError:
|
||||
instance = self.__instances[plugin] = plugin(self)
|
||||
|
||||
return instance
|
||||
|
||||
def get_plugin_next(self, plugin):
|
||||
if not callable(plugin):
|
||||
raise TypeError('plugin must be callable; got %r' % plugin)
|
||||
|
||||
return self.__next[plugin]
|
||||
|
||||
|
||||
class IPAHelpFormatter(optparse.IndentedHelpFormatter):
|
||||
def format_epilog(self, text):
|
||||
text_width = self.width - self.current_indent
|
||||
indent = " " * self.current_indent
|
||||
lines = text.splitlines()
|
||||
result = '\n'.join(
|
||||
textwrap.fill(line, text_width, initial_indent=indent,
|
||||
subsequent_indent=indent)
|
||||
for line in lines)
|
||||
return '\n%s\n' % result
|
||||
79
ipalib/request.py
Normal file
79
ipalib/request.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Authors:
|
||||
# Rob Crittenden <rcritten@redhat.com>
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 Red Hat
|
||||
# see file 'COPYING' for use and warranty contextrmation
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Per-request thread-local data.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import threading
|
||||
|
||||
from ipalib.base import ReadOnly, lock
|
||||
from ipalib.constants import CALLABLE_ERROR
|
||||
|
||||
|
||||
# Thread-local storage of most per-request information
|
||||
context = threading.local()
|
||||
|
||||
|
||||
class _FrameContext(object):
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def context_frame():
|
||||
try:
|
||||
frame_back = context.current_frame
|
||||
except AttributeError:
|
||||
pass
|
||||
context.current_frame = _FrameContext()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
try:
|
||||
context.current_frame = frame_back
|
||||
except UnboundLocalError:
|
||||
del context.current_frame
|
||||
|
||||
|
||||
class Connection(ReadOnly):
|
||||
"""
|
||||
Base class for connection objects stored on `request.context`.
|
||||
"""
|
||||
|
||||
def __init__(self, conn, disconnect):
|
||||
self.conn = conn
|
||||
if not callable(disconnect):
|
||||
raise TypeError(
|
||||
CALLABLE_ERROR % ('disconnect', disconnect, type(disconnect))
|
||||
)
|
||||
self.disconnect = disconnect
|
||||
lock(self)
|
||||
|
||||
|
||||
def destroy_context():
|
||||
"""
|
||||
Delete all attributes on thread-local `request.context`.
|
||||
"""
|
||||
# need to use a list of values, 'cos value.disconnect modifies the dict
|
||||
for value in list(context.__dict__.values()):
|
||||
if isinstance(value, Connection):
|
||||
value.disconnect()
|
||||
context.__dict__.clear()
|
||||
1279
ipalib/rpc.py
Normal file
1279
ipalib/rpc.py
Normal file
File diff suppressed because it is too large
Load Diff
5
ipalib/setup.cfg
Normal file
5
ipalib/setup.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
license_file = ../COPYING
|
||||
50
ipalib/setup.py
Normal file
50
ipalib/setup.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/python2
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
"""FreeIPA common python library
|
||||
|
||||
FreeIPA is a server for identity, policy, and audit.
|
||||
"""
|
||||
from os.path import abspath, dirname
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
# include ../ for ipasetup.py
|
||||
sys.path.append(dirname(dirname(abspath(__file__))))
|
||||
from ipasetup import ipasetup # noqa: E402
|
||||
|
||||
ipasetup(
|
||||
name="ipalib",
|
||||
doc=__doc__,
|
||||
package_dir={'ipalib': ''},
|
||||
packages=[
|
||||
"ipalib",
|
||||
"ipalib.install",
|
||||
],
|
||||
install_requires=[
|
||||
"ipaplatform",
|
||||
"ipapython",
|
||||
"netaddr",
|
||||
"pyasn1",
|
||||
"pyasn1-modules",
|
||||
"six",
|
||||
],
|
||||
extras_require={
|
||||
"install": ["ipaplatform"],
|
||||
},
|
||||
)
|
||||
564
ipalib/text.py
Normal file
564
ipalib/text.py
Normal file
@@ -0,0 +1,564 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2009 Red Hat
|
||||
# see file 'COPYING' for use and warranty contextrmation
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Defers gettext translation till request time.
|
||||
|
||||
IPA presents some tricky gettext challenges. On the one hand, most translatable
|
||||
message are defined as class attributes on the plugins, which means these get
|
||||
evaluated at module-load time. But on the other hand, each request to the
|
||||
server can be in a different locale, so the actual translation must not occur
|
||||
till request time.
|
||||
|
||||
The `text` module provides a mechanism for for deferred gettext translation. It
|
||||
was designed to:
|
||||
|
||||
1. Allow translatable strings to be marked with the usual ``_()`` and
|
||||
``ngettext()`` functions so that standard tools like xgettext can still
|
||||
be used
|
||||
|
||||
2. Allow programmers to mark strings in a natural way without burdening them
|
||||
with details of the deferred translation mechanism
|
||||
|
||||
A typical plugin will use the deferred translation like this:
|
||||
|
||||
>>> from ipalib import Command, _, ngettext
|
||||
>>> class my_plugin(Command):
|
||||
... my_string = _('Hello, %(name)s.')
|
||||
... my_plural = ngettext('%(count)d goose', '%(count)d geese', 0)
|
||||
...
|
||||
|
||||
With normal gettext usage, the *my_string* and *my_plural* message would be
|
||||
translated at module-load-time when your ``my_plugin`` class is defined. This
|
||||
would mean that all message are translated in the locale of the server rather
|
||||
than the locale of the request.
|
||||
|
||||
However, the ``_()`` function above is actually a `GettextFactory` instance,
|
||||
which when called returns a `Gettext` instance. A `Gettext` instance stores the
|
||||
message to be translated, and the gettext domain and localedir, but it doesn't
|
||||
perform the translation till `Gettext.__unicode__()` is called. For example:
|
||||
|
||||
>>> my_plugin.my_string
|
||||
Gettext('Hello, %(name)s.', domain='ipa', localedir=None)
|
||||
>>> unicode(my_plugin.my_string)
|
||||
u'Hello, %(name)s.'
|
||||
|
||||
Translation can also be performed via the `Gettext.__mod__()` convenience
|
||||
method. For example, these two are equivalent:
|
||||
|
||||
>>> my_plugin.my_string % dict(name='Joe')
|
||||
u'Hello, Joe.'
|
||||
>>> unicode(my_plugin.my_string) % dict(name='Joe') # Long form
|
||||
u'Hello, Joe.'
|
||||
|
||||
Similar to ``_()``, the ``ngettext()`` function above is actually an
|
||||
`NGettextFactory` instance, which when called returns an `NGettext` instance.
|
||||
An `NGettext` instance stores the singular and plural messages, and the gettext
|
||||
domain and localedir, but it doesn't perform the translation till
|
||||
`NGettext.__call__()` is called. For example:
|
||||
|
||||
>>> my_plugin.my_plural
|
||||
NGettext('%(count)d goose', '%(count)d geese', domain='ipa', localedir=None)
|
||||
>>> my_plugin.my_plural(1)
|
||||
u'%(count)d goose'
|
||||
>>> my_plugin.my_plural(2)
|
||||
u'%(count)d geese'
|
||||
|
||||
Translation can also be performed via the `NGettext.__mod__()` convenience
|
||||
method. For example, these two are equivalent:
|
||||
|
||||
>>> my_plugin.my_plural % dict(count=1)
|
||||
u'1 goose'
|
||||
>>> my_plugin.my_plural(1) % dict(count=1) # Long form
|
||||
u'1 goose'
|
||||
|
||||
Lastly, 3rd-party plugins can create factories bound to a different gettext
|
||||
domain. The default domain is ``'ipa'``, which is also the domain of the
|
||||
standard ``ipalib._()`` and ``ipalib.ngettext()`` factories. But 3rd-party
|
||||
plugins can create their own factories like this:
|
||||
|
||||
>>> from ipalib import GettextFactory, NGettextFactory
|
||||
>>> _ = GettextFactory(domain='ipa_foo')
|
||||
>>> ngettext = NGettextFactory(domain='ipa_foo')
|
||||
>>> class foo(Command):
|
||||
... msg1 = _('Foo!')
|
||||
... msg2 = ngettext('%(count)d bar', '%(count)d bars', 0)
|
||||
...
|
||||
|
||||
Notice that these messages are bound to the ``'ipa_foo'`` domain:
|
||||
|
||||
>>> foo.msg1
|
||||
Gettext('Foo!', domain='ipa_foo', localedir=None)
|
||||
>>> foo.msg2
|
||||
NGettext('%(count)d bar', '%(count)d bars', domain='ipa_foo', localedir=None)
|
||||
|
||||
For additional details, see `GettextFactory` and `Gettext`, and for plural
|
||||
forms, see `NGettextFactory` and `NGettext`.
|
||||
"""
|
||||
|
||||
import gettext
|
||||
|
||||
import six
|
||||
|
||||
from ipalib.request import context
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
|
||||
def create_translation(key):
|
||||
assert key not in context.__dict__
|
||||
(domain, localedir) = key
|
||||
translation = gettext.translation(domain,
|
||||
localedir=localedir,
|
||||
languages=getattr(context, 'languages', None),
|
||||
fallback=True,
|
||||
)
|
||||
context.__dict__[key] = translation
|
||||
return translation
|
||||
|
||||
|
||||
class LazyText(object):
|
||||
"""
|
||||
Base class for deferred translation.
|
||||
|
||||
This class is not used directly. See the `Gettext` and `NGettext`
|
||||
subclasses.
|
||||
|
||||
Concatenating LazyText objects with the + operator gives
|
||||
ConcatenatedLazyText objects.
|
||||
"""
|
||||
|
||||
__slots__ = ('domain', 'localedir', 'key', 'args')
|
||||
__hash__ = None
|
||||
|
||||
def __init__(self, domain=None, localedir=None):
|
||||
"""
|
||||
Initialize.
|
||||
|
||||
:param domain: The gettext domain in which this message will be
|
||||
translated, e.g. ``'ipa'`` or ``'ipa_3rd_party'``; default is
|
||||
``None``
|
||||
:param localedir: The directory containing the gettext translations,
|
||||
e.g. ``'/usr/share/locale/'``; default is ``None``, in which case
|
||||
gettext will use the default system locale directory.
|
||||
"""
|
||||
self.domain = domain
|
||||
self.localedir = localedir
|
||||
self.key = (domain, localedir)
|
||||
self.args = None
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Return ``True`` if this instances is equal to *other*.
|
||||
|
||||
Note that this method cannot be used on the `LazyText` base class itself
|
||||
as subclasses must define an *args* instance attribute.
|
||||
"""
|
||||
if type(other) is not self.__class__:
|
||||
return False
|
||||
return self.args == other.args
|
||||
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
Return ``True`` if this instances is not equal to *other*.
|
||||
|
||||
Note that this method cannot be used on the `LazyText` base class itself
|
||||
as subclasses must define an *args* instance attribute.
|
||||
"""
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __add__(self, other):
|
||||
return ConcatenatedLazyText(self) + other
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + ConcatenatedLazyText(self)
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class Gettext(LazyText):
|
||||
"""
|
||||
Deferred translation using ``gettext.ugettext()``.
|
||||
|
||||
Normally the `Gettext` class isn't used directly and instead is created via
|
||||
a `GettextFactory` instance. However, for illustration, we can create one
|
||||
like this:
|
||||
|
||||
>>> msg = Gettext('Hello, %(name)s.')
|
||||
|
||||
When you create a `Gettext` instance, the message is stored on the *msg*
|
||||
attribute:
|
||||
|
||||
>>> msg.msg
|
||||
'Hello, %(name)s.'
|
||||
|
||||
No translation is performed till `Gettext.__unicode__()` is called. This
|
||||
will translate *msg* using ``gettext.ugettext()``, which will return the
|
||||
translated string as a Python ``unicode`` instance. For example:
|
||||
|
||||
>>> unicode(msg)
|
||||
u'Hello, %(name)s.'
|
||||
|
||||
`Gettext.__unicode__()` should be called at request time, which in a
|
||||
nutshell means it should be called from within your plugin's
|
||||
``Command.execute()`` method. `Gettext.__unicode__()` will perform the
|
||||
translation based on the locale of the current request.
|
||||
|
||||
`Gettext.__mod__()` is a convenience method for Python "percent" string
|
||||
formatting. It will translate your message using `Gettext.__unicode__()`
|
||||
and then perform the string substitution on the translated message. For
|
||||
example, these two are equivalent:
|
||||
|
||||
>>> msg % dict(name='Joe')
|
||||
u'Hello, Joe.'
|
||||
>>> unicode(msg) % dict(name='Joe') # Long form
|
||||
u'Hello, Joe.'
|
||||
|
||||
See `GettextFactory` for additional details. If you need to pick between
|
||||
singular and plural form, use `NGettext` instances via the
|
||||
`NGettextFactory`.
|
||||
"""
|
||||
|
||||
__slots__ = ('msg')
|
||||
|
||||
def __init__(self, msg, domain=None, localedir=None):
|
||||
super(Gettext, self).__init__(domain, localedir)
|
||||
self.msg = msg
|
||||
self.args = (msg, domain, localedir)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, domain=%r, localedir=%r)' % (self.__class__.__name__,
|
||||
self.msg, self.domain, self.localedir)
|
||||
|
||||
def as_unicode(self):
|
||||
"""
|
||||
Translate this message and return as a ``unicode`` instance.
|
||||
"""
|
||||
if self.key in context.__dict__:
|
||||
t = context.__dict__[self.key]
|
||||
else:
|
||||
t = create_translation(self.key)
|
||||
if six.PY2:
|
||||
return t.ugettext(self.msg) # pylint: disable=no-member
|
||||
else:
|
||||
return t.gettext(self.msg)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self.as_unicode())
|
||||
|
||||
def __json__(self):
|
||||
return unicode(self) #pylint: disable=no-member
|
||||
|
||||
def __mod__(self, kw):
|
||||
return unicode(self) % kw #pylint: disable=no-member
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class FixMe(Gettext):
|
||||
"""
|
||||
Non-translated place-holder for UI labels.
|
||||
|
||||
`FixMe` is a subclass of `Gettext` and is used for automatically created
|
||||
place-holder labels. It generally behaves exactly like `Gettext` except no
|
||||
translation is ever performed.
|
||||
|
||||
`FixMe` allows programmers to get plugins working without first filling in
|
||||
all the labels that will ultimately be required, while at the same time it
|
||||
creates conspicuous looking UI labels that remind the programmer to
|
||||
"fix-me!". For example, the typical usage would be something like this:
|
||||
|
||||
>>> class Plugin(object):
|
||||
... label = None
|
||||
... def __init__(self):
|
||||
... self.name = self.__class__.__name__
|
||||
... if self.label is None:
|
||||
... self.label = FixMe(self.name + '.label')
|
||||
... assert isinstance(self.label, Gettext)
|
||||
...
|
||||
>>> class user(Plugin):
|
||||
... pass # Oops, we didn't set user.label yet
|
||||
...
|
||||
>>> u = user()
|
||||
>>> u.label
|
||||
FixMe('user.label')
|
||||
|
||||
Note that as `FixMe` is a subclass of `Gettext`, is passes the above type
|
||||
check using ``isinstance()``.
|
||||
|
||||
Calling `FixMe.__unicode__()` performs no translation, but instead returns
|
||||
said conspicuous looking label:
|
||||
|
||||
>>> unicode(u.label)
|
||||
u'<user.label>'
|
||||
|
||||
For more examples of how `FixMe` is used, see `ipalib.parameters`.
|
||||
"""
|
||||
|
||||
__slots__ = tuple()
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, self.msg)
|
||||
|
||||
def __str__(self):
|
||||
return u'<%s>' % self.msg
|
||||
|
||||
|
||||
class NGettext(LazyText):
|
||||
"""
|
||||
Deferred translation for plural forms using ``gettext.ungettext()``.
|
||||
|
||||
Normally the `NGettext` class isn't used directly and instead is created via
|
||||
a `NGettextFactory` instance. However, for illustration, we can create one
|
||||
like this:
|
||||
|
||||
>>> msg = NGettext('%(count)d goose', '%(count)d geese')
|
||||
|
||||
When you create an `NGettext` instance, the singular and plural forms of
|
||||
your message are stored on the *singular* and *plural* instance attributes:
|
||||
|
||||
>>> msg.singular
|
||||
'%(count)d goose'
|
||||
>>> msg.plural
|
||||
'%(count)d geese'
|
||||
|
||||
The translation and number selection isn't performed till
|
||||
`NGettext.__call__()` is called. This will translate and pick the correct
|
||||
number using ``gettext.ungettext()``. As a callable, an `NGettext` instance
|
||||
takes a single argument, an integer specifying the count. For example:
|
||||
|
||||
>>> msg(0)
|
||||
u'%(count)d geese'
|
||||
>>> msg(1)
|
||||
u'%(count)d goose'
|
||||
>>> msg(2)
|
||||
u'%(count)d geese'
|
||||
|
||||
`NGettext.__mod__()` is a convenience method for Python "percent" string
|
||||
formatting. It can only be used if your substitution ``dict`` contains the
|
||||
count in a ``'count'`` item. For example:
|
||||
|
||||
>>> msg % dict(count=0)
|
||||
u'0 geese'
|
||||
>>> msg % dict(count=1)
|
||||
u'1 goose'
|
||||
>>> msg % dict(count=2)
|
||||
u'2 geese'
|
||||
|
||||
Alternatively, these longer forms have the same effect as the three examples
|
||||
above:
|
||||
|
||||
>>> msg(0) % dict(count=0)
|
||||
u'0 geese'
|
||||
>>> msg(1) % dict(count=1)
|
||||
u'1 goose'
|
||||
>>> msg(2) % dict(count=2)
|
||||
u'2 geese'
|
||||
|
||||
A ``KeyError`` is raised if your substitution ``dict`` doesn't have a
|
||||
``'count'`` item. For example:
|
||||
|
||||
>>> msg2 = NGettext('%(num)d goose', '%(num)d geese')
|
||||
>>> msg2 % dict(num=0)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
KeyError: 'count'
|
||||
|
||||
However, in this case you can still use the longer, explicit form for string
|
||||
substitution:
|
||||
|
||||
>>> msg2(0) % dict(num=0)
|
||||
u'0 geese'
|
||||
|
||||
See `NGettextFactory` for additional details.
|
||||
"""
|
||||
|
||||
__slots__ = ('singular', 'plural')
|
||||
|
||||
def __init__(self, singular, plural, domain=None, localedir=None):
|
||||
super(NGettext, self).__init__(domain, localedir)
|
||||
self.singular = singular
|
||||
self.plural = plural
|
||||
self.args = (singular, plural, domain, localedir)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, %r, domain=%r, localedir=%r)' % (self.__class__.__name__,
|
||||
self.singular, self.plural, self.domain, self.localedir)
|
||||
|
||||
def __mod__(self, kw):
|
||||
count = kw['count']
|
||||
return self(count) % kw
|
||||
|
||||
def __call__(self, count):
|
||||
if self.key in context.__dict__:
|
||||
t = context.__dict__[self.key]
|
||||
else:
|
||||
t = create_translation(self.key)
|
||||
if six.PY2:
|
||||
# pylint: disable=no-member
|
||||
return t.ungettext(self.singular, self.plural, count)
|
||||
# pylint: enable=no-member
|
||||
else:
|
||||
return t.ngettext(self.singular, self.plural, count)
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class ConcatenatedLazyText(object):
|
||||
"""Concatenation of multiple strings, or any objects convertible to unicode
|
||||
|
||||
Used to concatenate several LazyTexts together.
|
||||
This allows large strings like help text to be split, so translators
|
||||
do not have to re-translate the whole text when only a small part changes.
|
||||
|
||||
Additional strings may be added to the end with the + or += operators.
|
||||
"""
|
||||
def __init__(self, *components):
|
||||
self.components = list(components)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, self.components)
|
||||
|
||||
def __str__(self):
|
||||
return u''.join(unicode(c) for c in self.components)
|
||||
|
||||
def __json__(self):
|
||||
return unicode(self)
|
||||
|
||||
def __mod__(self, kw):
|
||||
return unicode(self) % kw
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, ConcatenatedLazyText):
|
||||
return ConcatenatedLazyText(*self.components + other.components)
|
||||
else:
|
||||
return ConcatenatedLazyText(*self.components + [other])
|
||||
|
||||
def __radd__(self, other):
|
||||
if isinstance(other, ConcatenatedLazyText):
|
||||
return ConcatenatedLazyText(*other.components + self.components)
|
||||
else:
|
||||
return ConcatenatedLazyText(*[other] + self.components)
|
||||
|
||||
|
||||
class GettextFactory(object):
|
||||
"""
|
||||
Factory for creating ``_()`` functions.
|
||||
|
||||
A `GettextFactory` allows you to mark translatable messages that are
|
||||
evaluated at initialization time, but deferred their actual translation till
|
||||
request time.
|
||||
|
||||
When you create a `GettextFactory` you can provide a specific gettext
|
||||
*domain* and *localedir*. By default the *domain* will be ``'ipa'`` and
|
||||
the *localedir* will be ``None``. Both are available via instance
|
||||
attributes of the same name. For example:
|
||||
|
||||
>>> _ = GettextFactory()
|
||||
>>> _.domain
|
||||
'ipa'
|
||||
>>> _.localedir is None
|
||||
True
|
||||
|
||||
When the *localedir* is ``None``, gettext will use the default system
|
||||
localedir (typically ``'/usr/share/locale/'``). In general, you should
|
||||
**not** provide a *localedir*... it is intended only to support in-tree
|
||||
testing.
|
||||
|
||||
Third party plugins will most likely want to use a different gettext
|
||||
*domain*. For example:
|
||||
|
||||
>>> _ = GettextFactory(domain='ipa_3rd_party')
|
||||
>>> _.domain
|
||||
'ipa_3rd_party'
|
||||
|
||||
When you call your `GettextFactory` instance, it will return a `Gettext`
|
||||
instance associated with the same *domain* and *localedir*. For example:
|
||||
|
||||
>>> my_msg = _('Hello world')
|
||||
>>> my_msg.domain
|
||||
'ipa_3rd_party'
|
||||
>>> my_msg.localedir is None
|
||||
True
|
||||
|
||||
The message isn't translated till `Gettext.__unicode__()` is called, which
|
||||
should be done during each request. See the `Gettext` class for additional
|
||||
details.
|
||||
"""
|
||||
|
||||
def __init__(self, domain='ipa', localedir=None):
|
||||
"""
|
||||
Initialize.
|
||||
|
||||
:param domain: The gettext domain in which this message will be
|
||||
translated, e.g. ``'ipa'`` or ``'ipa_3rd_party'``; default is
|
||||
``'ipa'``
|
||||
:param localedir: The directory containing the gettext translations,
|
||||
e.g. ``'/usr/share/locale/'``; default is ``None``, in which case
|
||||
gettext will use the default system locale directory.
|
||||
"""
|
||||
self.domain = domain
|
||||
self.localedir = localedir
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(domain=%r, localedir=%r)' % (self.__class__.__name__,
|
||||
self.domain, self.localedir)
|
||||
|
||||
def __call__(self, msg):
|
||||
return Gettext(msg, self.domain, self.localedir)
|
||||
|
||||
|
||||
class NGettextFactory(GettextFactory):
|
||||
"""
|
||||
Factory for creating ``ngettext()`` functions.
|
||||
|
||||
`NGettextFactory` is similar to `GettextFactory`, except `NGettextFactory`
|
||||
is for plural forms.
|
||||
|
||||
So that standard tools like xgettext can find your plural forms, you should
|
||||
reference your `NGettextFactory` instance using a variable named
|
||||
*ngettext*. For example:
|
||||
|
||||
>>> ngettext = NGettextFactory()
|
||||
>>> ngettext
|
||||
NGettextFactory(domain='ipa', localedir=None)
|
||||
|
||||
When you call your `NGettextFactory` instance to create a deferred
|
||||
translation, you provide the *singular* message, the *plural* message, and
|
||||
a dummy *count*. An `NGettext` instance will be returned. For example:
|
||||
|
||||
>>> my_msg = ngettext('%(count)d goose', '%(count)d geese', 0)
|
||||
>>> my_msg
|
||||
NGettext('%(count)d goose', '%(count)d geese', domain='ipa', localedir=None)
|
||||
|
||||
The *count* is ignored (because the translation is deferred), but you should
|
||||
still provide it so parsing tools aren't confused. For consistency, it is
|
||||
recommended to always provide ``0`` for the *count*.
|
||||
|
||||
See `NGettext` for details on how the deferred translation is later
|
||||
performed. See `GettextFactory` for details on setting a different gettext
|
||||
*domain* (likely needed for 3rd-party plugins).
|
||||
"""
|
||||
|
||||
def __call__(self, singular, plural, count):
|
||||
return NGettext(singular, plural, self.domain, self.localedir)
|
||||
|
||||
|
||||
# Process wide factories:
|
||||
_ = GettextFactory()
|
||||
ngettext = NGettextFactory()
|
||||
ugettext = _
|
||||
1197
ipalib/util.py
Normal file
1197
ipalib/util.py
Normal file
File diff suppressed because it is too large
Load Diff
680
ipalib/x509.py
Normal file
680
ipalib/x509.py
Normal file
@@ -0,0 +1,680 @@
|
||||
# Authors:
|
||||
# Rob Crittenden <rcritten@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2010 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/>.
|
||||
|
||||
# Certificates should be stored internally DER-encoded. We can be passed
|
||||
# a certificate several ways: read if from LDAP, read it from a 3rd party
|
||||
# app (dogtag, candlepin, etc) or as user input.
|
||||
|
||||
# Conventions
|
||||
#
|
||||
# Where possible the following naming conventions are used:
|
||||
#
|
||||
# cert: the certificate is a PEM-encoded certificate
|
||||
# dercert: the certificate is DER-encoded
|
||||
# rawcert: the cert is in an unknown format
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import binascii
|
||||
import datetime
|
||||
import ipaddress
|
||||
import ssl
|
||||
import base64
|
||||
import re
|
||||
|
||||
from cryptography import x509 as crypto_x509
|
||||
from cryptography import utils as crypto_utils
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding, PublicFormat
|
||||
)
|
||||
from pyasn1.type import univ, char, namedtype, tag
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
# from pyasn1.codec.native import decoder, encoder
|
||||
from pyasn1_modules import rfc2315, rfc2459
|
||||
import six
|
||||
|
||||
from ipalib import errors
|
||||
from ipapython.dnsutil import DNSName
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
PEM = 0
|
||||
DER = 1
|
||||
|
||||
PEM_REGEX = re.compile(
|
||||
b'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
|
||||
re.DOTALL)
|
||||
|
||||
EKU_SERVER_AUTH = '1.3.6.1.5.5.7.3.1'
|
||||
EKU_CLIENT_AUTH = '1.3.6.1.5.5.7.3.2'
|
||||
EKU_CODE_SIGNING = '1.3.6.1.5.5.7.3.3'
|
||||
EKU_EMAIL_PROTECTION = '1.3.6.1.5.5.7.3.4'
|
||||
EKU_PKINIT_CLIENT_AUTH = '1.3.6.1.5.2.3.4'
|
||||
EKU_PKINIT_KDC = '1.3.6.1.5.2.3.5'
|
||||
EKU_ANY = '2.5.29.37.0'
|
||||
EKU_PLACEHOLDER = '1.3.6.1.4.1.3319.6.10.16'
|
||||
|
||||
SAN_UPN = '1.3.6.1.4.1.311.20.2.3'
|
||||
SAN_KRB5PRINCIPALNAME = '1.3.6.1.5.2.2'
|
||||
|
||||
|
||||
@crypto_utils.register_interface(crypto_x509.Certificate)
|
||||
class IPACertificate(object):
|
||||
"""
|
||||
A proxy class wrapping a python-cryptography certificate representation for
|
||||
FreeIPA purposes
|
||||
"""
|
||||
def __init__(self, cert, backend=None):
|
||||
"""
|
||||
:param cert: A python-cryptography Certificate object
|
||||
:param backend: A python-cryptography Backend object
|
||||
"""
|
||||
self._cert = cert
|
||||
self.backend = default_backend() if backend is None else backend()
|
||||
|
||||
# initialize the certificate fields
|
||||
# we have to do it this way so that some systems don't explode since
|
||||
# some field types encode-decoding is not strongly defined
|
||||
self._subject = self.__get_der_field('subject')
|
||||
self._issuer = self.__get_der_field('issuer')
|
||||
self._serial_number = self.__get_der_field('serialNumber')
|
||||
|
||||
def __getstate__(self):
|
||||
state = {
|
||||
'_cert': self.public_bytes(Encoding.DER),
|
||||
'_subject': self.subject_bytes,
|
||||
'_issuer': self.issuer_bytes,
|
||||
'_serial_number': self._serial_number,
|
||||
}
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._subject = state['_subject']
|
||||
self._issuer = state['_issuer']
|
||||
self._issuer = state['_serial_number']
|
||||
self._cert = crypto_x509.load_der_x509_certificate(
|
||||
state['_cert'], backend=default_backend())
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Checks equality.
|
||||
|
||||
:param other: either cryptography.Certificate or IPACertificate or
|
||||
bytes representing a DER-formatted certificate
|
||||
"""
|
||||
if (isinstance(other, (crypto_x509.Certificate, IPACertificate))):
|
||||
return (self.public_bytes(Encoding.DER) ==
|
||||
other.public_bytes(Encoding.DER))
|
||||
elif isinstance(other, bytes):
|
||||
return self.public_bytes(Encoding.DER) == other
|
||||
else:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
Checks not equal.
|
||||
"""
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
"""
|
||||
Computes a hash of the wrapped cryptography.Certificate.
|
||||
"""
|
||||
return hash(self._cert)
|
||||
|
||||
def __encode_extension(self, oid, critical, value):
|
||||
# TODO: have another proxy for crypto_x509.Extension which would
|
||||
# provide public_bytes on the top of what python-cryptography has
|
||||
ext = rfc2459.Extension()
|
||||
# TODO: this does not have to be so weird, pyasn1 now has codecs
|
||||
# which are capable of providing python-native types
|
||||
ext['extnID'] = univ.ObjectIdentifier(oid)
|
||||
ext['critical'] = univ.Boolean(critical)
|
||||
ext['extnValue'] = univ.Any(encoder.encode(univ.OctetString(value)))
|
||||
ext = encoder.encode(ext)
|
||||
return ext
|
||||
|
||||
def __get_pyasn1_field(self, field):
|
||||
"""
|
||||
:returns: a field of the certificate in pyasn1 representation
|
||||
"""
|
||||
cert_bytes = self.tbs_certificate_bytes
|
||||
cert = decoder.decode(cert_bytes, rfc2459.TBSCertificate())[0]
|
||||
field = cert[field]
|
||||
return field
|
||||
|
||||
def __get_der_field(self, field):
|
||||
"""
|
||||
:field: the name of the field of the certificate
|
||||
:returns: bytes representing the value of a certificate field
|
||||
"""
|
||||
return encoder.encode(self.__get_pyasn1_field(field))
|
||||
|
||||
def public_bytes(self, encoding):
|
||||
"""
|
||||
Serializes the certificate to PEM or DER format.
|
||||
"""
|
||||
return self._cert.public_bytes(encoding)
|
||||
|
||||
def is_self_signed(self):
|
||||
"""
|
||||
:returns: True if this certificate is self-signed, False otherwise
|
||||
"""
|
||||
return self._cert.issuer == self._cert.subject
|
||||
|
||||
def fingerprint(self, algorithm):
|
||||
"""
|
||||
Counts fingerprint of the wrapped cryptography.Certificate
|
||||
"""
|
||||
return self._cert.fingerprint(algorithm)
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
return self._cert.serial_number
|
||||
|
||||
@property
|
||||
def serial_number_bytes(self):
|
||||
return self._serial_number
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._cert.version
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self._cert.subject
|
||||
|
||||
@property
|
||||
def subject_bytes(self):
|
||||
return self._subject
|
||||
|
||||
@property
|
||||
def signature_hash_algorithm(self):
|
||||
"""
|
||||
Returns a HashAlgorithm corresponding to the type of the digest signed
|
||||
in the certificate.
|
||||
"""
|
||||
return self._cert.signature_hash_algorithm
|
||||
|
||||
@property
|
||||
def signature_algorithm_oid(self):
|
||||
"""
|
||||
Returns the ObjectIdentifier of the signature algorithm.
|
||||
"""
|
||||
return self._cert.signature_algorithm_oid
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
"""
|
||||
Returns the signature bytes.
|
||||
"""
|
||||
return self._cert.signature
|
||||
|
||||
@property
|
||||
def issuer(self):
|
||||
return self._cert.issuer
|
||||
|
||||
@property
|
||||
def issuer_bytes(self):
|
||||
return self._issuer
|
||||
|
||||
@property
|
||||
def not_valid_before(self):
|
||||
return self._cert.not_valid_before
|
||||
|
||||
@property
|
||||
def not_valid_after(self):
|
||||
return self._cert.not_valid_after
|
||||
|
||||
@property
|
||||
def tbs_certificate_bytes(self):
|
||||
return self._cert.tbs_certificate_bytes
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
# TODO: own Extension and Extensions classes proxying
|
||||
# python-cryptography
|
||||
return self._cert.extensions
|
||||
|
||||
def public_key(self):
|
||||
return self._cert.public_key()
|
||||
|
||||
@property
|
||||
def public_key_info_bytes(self):
|
||||
return self._cert.public_key().public_bytes(
|
||||
encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo)
|
||||
|
||||
@property
|
||||
def extended_key_usage(self):
|
||||
try:
|
||||
ext_key_usage = self._cert.extensions.get_extension_for_oid(
|
||||
crypto_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE).value
|
||||
except crypto_x509.ExtensionNotFound:
|
||||
return None
|
||||
|
||||
return set(oid.dotted_string for oid in ext_key_usage)
|
||||
|
||||
@property
|
||||
def extended_key_usage_bytes(self):
|
||||
eku = self.extended_key_usage
|
||||
if eku is None:
|
||||
return
|
||||
|
||||
ekurfc = rfc2459.ExtKeyUsageSyntax()
|
||||
for i, oid in enumerate(eku):
|
||||
ekurfc[i] = univ.ObjectIdentifier(oid)
|
||||
ekurfc = encoder.encode(ekurfc)
|
||||
return self.__encode_extension('2.5.29.37', EKU_ANY not in eku, ekurfc)
|
||||
|
||||
@property
|
||||
def san_general_names(self):
|
||||
"""
|
||||
Return SAN general names from a python-cryptography
|
||||
certificate object. If the SAN extension is not present,
|
||||
return an empty sequence.
|
||||
|
||||
Because python-cryptography does not yet provide a way to
|
||||
handle unrecognised critical extensions (which may occur),
|
||||
we must parse the certificate and extract the General Names.
|
||||
For uniformity with other code, we manually construct values
|
||||
of python-crytography GeneralName subtypes.
|
||||
|
||||
python-cryptography does not yet provide types for
|
||||
ediPartyName or x400Address, so we drop these name types.
|
||||
|
||||
otherNames are NOT instantiated to more specific types where
|
||||
the type is known. Use ``process_othernames`` to do that.
|
||||
|
||||
When python-cryptography can handle certs with unrecognised
|
||||
critical extensions and implements ediPartyName and
|
||||
x400Address, this function (and helpers) will be redundant
|
||||
and should go away.
|
||||
|
||||
"""
|
||||
gns = self.__pyasn1_get_san_general_names()
|
||||
|
||||
GENERAL_NAME_CONSTRUCTORS = {
|
||||
'rfc822Name': lambda x: crypto_x509.RFC822Name(unicode(x)),
|
||||
'dNSName': lambda x: crypto_x509.DNSName(unicode(x)),
|
||||
'directoryName': _pyasn1_to_cryptography_directoryname,
|
||||
'registeredID': _pyasn1_to_cryptography_registeredid,
|
||||
'iPAddress': _pyasn1_to_cryptography_ipaddress,
|
||||
'uniformResourceIdentifier':
|
||||
lambda x: crypto_x509.UniformResourceIdentifier(unicode(x)),
|
||||
'otherName': _pyasn1_to_cryptography_othername,
|
||||
}
|
||||
|
||||
result = []
|
||||
|
||||
for gn in gns:
|
||||
gn_type = gn.getName()
|
||||
if gn_type in GENERAL_NAME_CONSTRUCTORS:
|
||||
result.append(
|
||||
GENERAL_NAME_CONSTRUCTORS[gn_type](gn.getComponent()))
|
||||
|
||||
return result
|
||||
|
||||
def __pyasn1_get_san_general_names(self):
|
||||
# pyasn1 returns None when the key is not present in the certificate
|
||||
# but we need an iterable
|
||||
extensions = self.__get_pyasn1_field('extensions') or []
|
||||
OID_SAN = univ.ObjectIdentifier('2.5.29.17')
|
||||
gns = []
|
||||
for ext in extensions:
|
||||
if ext['extnID'] == OID_SAN:
|
||||
der = decoder.decode(
|
||||
ext['extnValue'], asn1Spec=univ.OctetString())[0]
|
||||
gns = decoder.decode(der, asn1Spec=rfc2459.SubjectAltName())[0]
|
||||
break
|
||||
return gns
|
||||
|
||||
@property
|
||||
def san_a_label_dns_names(self):
|
||||
gns = self.__pyasn1_get_san_general_names()
|
||||
result = []
|
||||
|
||||
for gn in gns:
|
||||
if gn.getName() == 'dNSName':
|
||||
result.append(unicode(gn.getComponent()))
|
||||
|
||||
return result
|
||||
|
||||
def match_hostname(self, hostname):
|
||||
match_cert = {}
|
||||
|
||||
match_cert['subject'] = match_subject = []
|
||||
for rdn in self._cert.subject.rdns:
|
||||
match_rdn = []
|
||||
for ava in rdn:
|
||||
if ava.oid == crypto_x509.oid.NameOID.COMMON_NAME:
|
||||
match_rdn.append(('commonName', ava.value))
|
||||
match_subject.append(match_rdn)
|
||||
|
||||
values = self.san_a_label_dns_names
|
||||
if values:
|
||||
match_cert['subjectAltName'] = match_san = []
|
||||
for value in values:
|
||||
match_san.append(('DNS', value))
|
||||
|
||||
ssl.match_hostname(match_cert, DNSName(hostname).ToASCII())
|
||||
|
||||
|
||||
def load_pem_x509_certificate(data):
|
||||
"""
|
||||
Load an X.509 certificate in PEM format.
|
||||
|
||||
:returns: a ``IPACertificate`` object.
|
||||
:raises: ``ValueError`` if unable to load the certificate.
|
||||
"""
|
||||
return IPACertificate(
|
||||
crypto_x509.load_pem_x509_certificate(data, backend=default_backend())
|
||||
)
|
||||
|
||||
|
||||
def load_der_x509_certificate(data):
|
||||
"""
|
||||
Load an X.509 certificate in DER format.
|
||||
|
||||
:returns: a ``IPACertificate`` object.
|
||||
:raises: ``ValueError`` if unable to load the certificate.
|
||||
"""
|
||||
return IPACertificate(
|
||||
crypto_x509.load_der_x509_certificate(data, backend=default_backend())
|
||||
)
|
||||
|
||||
|
||||
def load_unknown_x509_certificate(data):
|
||||
"""
|
||||
Only use this function when you can't be sure what kind of format does
|
||||
your certificate have, e.g. input certificate files in installers
|
||||
|
||||
:returns: a ``IPACertificate`` object.
|
||||
:raises: ``ValueError`` if unable to load the certificate.
|
||||
"""
|
||||
try:
|
||||
return load_pem_x509_certificate(data)
|
||||
except ValueError:
|
||||
return load_der_x509_certificate(data)
|
||||
|
||||
|
||||
def load_certificate_from_file(filename, dbdir=None):
|
||||
"""
|
||||
Load a certificate from a PEM file.
|
||||
|
||||
Returns a python-cryptography ``Certificate`` object.
|
||||
"""
|
||||
with open(filename, mode='rb') as f:
|
||||
return load_pem_x509_certificate(f.read())
|
||||
|
||||
|
||||
def load_certificate_list(data):
|
||||
"""
|
||||
Load a certificate list from a sequence of concatenated PEMs.
|
||||
|
||||
Return a list of python-cryptography ``Certificate`` objects.
|
||||
"""
|
||||
certs = PEM_REGEX.findall(data)
|
||||
return [load_pem_x509_certificate(cert) for cert in certs]
|
||||
|
||||
|
||||
def load_certificate_list_from_file(filename):
|
||||
"""
|
||||
Load a certificate list from a PEM file.
|
||||
|
||||
Return a list of python-cryptography ``Certificate`` objects.
|
||||
|
||||
"""
|
||||
with open(filename, 'rb') as f:
|
||||
return load_certificate_list(f.read())
|
||||
|
||||
|
||||
def pkcs7_to_certs(data, datatype=PEM):
|
||||
"""
|
||||
Extract certificates from a PKCS #7 object.
|
||||
|
||||
:returns: a ``list`` of ``IPACertificate`` objects.
|
||||
"""
|
||||
if datatype == PEM:
|
||||
match = re.match(
|
||||
br'-----BEGIN PKCS7-----(.*?)-----END PKCS7-----',
|
||||
data,
|
||||
re.DOTALL)
|
||||
if not match:
|
||||
raise ValueError("not a valid PKCS#7 PEM")
|
||||
|
||||
data = base64.b64decode(match.group(1))
|
||||
|
||||
content_info, tail = decoder.decode(data, rfc2315.ContentInfo())
|
||||
if tail:
|
||||
raise ValueError("not a valid PKCS#7 message")
|
||||
|
||||
if content_info['contentType'] != rfc2315.signedData:
|
||||
raise ValueError("not a PKCS#7 signed data message")
|
||||
|
||||
signed_data, tail = decoder.decode(bytes(content_info['content']),
|
||||
rfc2315.SignedData())
|
||||
if tail:
|
||||
raise ValueError("not a valid PKCS#7 signed data message")
|
||||
|
||||
result = []
|
||||
|
||||
for certificate in signed_data['certificates']:
|
||||
certificate = encoder.encode(certificate)
|
||||
certificate = load_der_x509_certificate(certificate)
|
||||
result.append(certificate)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_pem_x509_certificate(cert):
|
||||
"""
|
||||
Perform cert validation by trying to load it via python-cryptography.
|
||||
"""
|
||||
try:
|
||||
load_pem_x509_certificate(cert)
|
||||
except ValueError as e:
|
||||
raise errors.CertificateFormatError(error=str(e))
|
||||
|
||||
|
||||
def validate_der_x509_certificate(cert):
|
||||
"""
|
||||
Perform cert validation by trying to load it via python-cryptography.
|
||||
"""
|
||||
try:
|
||||
load_der_x509_certificate(cert)
|
||||
except ValueError as e:
|
||||
raise errors.CertificateFormatError(error=str(e))
|
||||
|
||||
|
||||
def write_certificate(cert, filename):
|
||||
"""
|
||||
Write the certificate to a file in PEM format.
|
||||
|
||||
The cert value can be either DER or PEM-encoded, it will be normalized
|
||||
to DER regardless, then back out to PEM.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(filename, 'wb') as fp:
|
||||
fp.write(cert.public_bytes(Encoding.PEM))
|
||||
except (IOError, OSError) as e:
|
||||
raise errors.FileError(reason=str(e))
|
||||
|
||||
|
||||
def write_certificate_list(certs, filename):
|
||||
"""
|
||||
Write a list of certificates to a file in PEM format.
|
||||
|
||||
:param certs: a list of IPACertificate objects to be written to a file
|
||||
:param filename: a path to the file the certificates should be written into
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(filename, 'wb') as f:
|
||||
for cert in certs:
|
||||
f.write(cert.public_bytes(Encoding.PEM))
|
||||
except (IOError, OSError) as e:
|
||||
raise errors.FileError(reason=str(e))
|
||||
|
||||
|
||||
class _PrincipalName(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('name-type', univ.Integer().subtype(
|
||||
explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
|
||||
),
|
||||
namedtype.NamedType('name-string', univ.SequenceOf(char.GeneralString()).subtype(
|
||||
explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class _KRB5PrincipalName(univ.Sequence):
|
||||
componentType = namedtype.NamedTypes(
|
||||
namedtype.NamedType('realm', char.GeneralString().subtype(
|
||||
explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
|
||||
),
|
||||
namedtype.NamedType('principalName', _PrincipalName().subtype(
|
||||
explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _decode_krb5principalname(data):
|
||||
principal = decoder.decode(data, asn1Spec=_KRB5PrincipalName())[0]
|
||||
realm = (unicode(principal['realm']).replace('\\', '\\\\')
|
||||
.replace('@', '\\@'))
|
||||
name = principal['principalName']['name-string']
|
||||
name = u'/'.join(unicode(n).replace('\\', '\\\\')
|
||||
.replace('/', '\\/')
|
||||
.replace('@', '\\@') for n in name)
|
||||
name = u'%s@%s' % (name, realm)
|
||||
return name
|
||||
|
||||
|
||||
class KRB5PrincipalName(crypto_x509.general_name.OtherName):
|
||||
def __init__(self, type_id, value):
|
||||
super(KRB5PrincipalName, self).__init__(type_id, value)
|
||||
self.name = _decode_krb5principalname(value)
|
||||
|
||||
|
||||
class UPN(crypto_x509.general_name.OtherName):
|
||||
def __init__(self, type_id, value):
|
||||
super(UPN, self).__init__(type_id, value)
|
||||
self.name = unicode(
|
||||
decoder.decode(value, asn1Spec=char.UTF8String())[0])
|
||||
|
||||
|
||||
OTHERNAME_CLASS_MAP = {
|
||||
SAN_KRB5PRINCIPALNAME: KRB5PrincipalName,
|
||||
SAN_UPN: UPN,
|
||||
}
|
||||
|
||||
|
||||
def process_othernames(gns):
|
||||
"""
|
||||
Process python-cryptography GeneralName values, yielding
|
||||
OtherName values of more specific type if type is known.
|
||||
|
||||
"""
|
||||
for gn in gns:
|
||||
if isinstance(gn, crypto_x509.general_name.OtherName):
|
||||
cls = OTHERNAME_CLASS_MAP.get(
|
||||
gn.type_id.dotted_string,
|
||||
crypto_x509.general_name.OtherName)
|
||||
yield cls(gn.type_id, gn.value)
|
||||
else:
|
||||
yield gn
|
||||
|
||||
|
||||
def _pyasn1_to_cryptography_directoryname(dn):
|
||||
attrs = []
|
||||
|
||||
# Name is CHOICE { RDNSequence } (only one possibility)
|
||||
for rdn in dn.getComponent():
|
||||
for ava in rdn:
|
||||
attr = crypto_x509.NameAttribute(
|
||||
_pyasn1_to_cryptography_oid(ava['type']),
|
||||
unicode(decoder.decode(ava['value'])[0])
|
||||
)
|
||||
attrs.append(attr)
|
||||
|
||||
return crypto_x509.DirectoryName(crypto_x509.Name(attrs))
|
||||
|
||||
|
||||
def _pyasn1_to_cryptography_registeredid(oid):
|
||||
return crypto_x509.RegisteredID(_pyasn1_to_cryptography_oid(oid))
|
||||
|
||||
|
||||
def _pyasn1_to_cryptography_ipaddress(octet_string):
|
||||
return crypto_x509.IPAddress(
|
||||
ipaddress.ip_address(bytes(octet_string)))
|
||||
|
||||
|
||||
def _pyasn1_to_cryptography_othername(on):
|
||||
return crypto_x509.OtherName(
|
||||
_pyasn1_to_cryptography_oid(on['type-id']),
|
||||
bytes(on['value'])
|
||||
)
|
||||
|
||||
|
||||
def _pyasn1_to_cryptography_oid(oid):
|
||||
return crypto_x509.ObjectIdentifier(str(oid))
|
||||
|
||||
|
||||
def chunk(size, s):
|
||||
"""Yield chunks of the specified size from the given string.
|
||||
|
||||
The input must be a multiple of the chunk size (otherwise
|
||||
trailing characters are dropped).
|
||||
|
||||
Works on character strings only.
|
||||
|
||||
"""
|
||||
return (u''.join(span) for span in six.moves.zip(*[iter(s)] * size))
|
||||
|
||||
|
||||
def add_colons(s):
|
||||
"""Add colons between each nibble pair in a hex string."""
|
||||
return u':'.join(chunk(2, s))
|
||||
|
||||
|
||||
def to_hex_with_colons(bs):
|
||||
"""Convert bytes to a hex string with colons."""
|
||||
return add_colons(binascii.hexlify(bs).decode('utf-8'))
|
||||
|
||||
|
||||
class UTC(datetime.tzinfo):
|
||||
ZERO = datetime.timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.ZERO
|
||||
|
||||
def dst(self, dt):
|
||||
return self.ZERO
|
||||
|
||||
|
||||
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"))
|
||||
Reference in New Issue
Block a user