Imported Upstream version 4.6.2

This commit is contained in:
Mario Fetka
2021-07-25 07:32:41 +02:00
commit 8ff3be4216
1788 changed files with 1900965 additions and 0 deletions

1
ipalib/Makefile.am Normal file
View File

@@ -0,0 +1 @@
include $(top_srcdir)/Makefile.python.am

618
ipalib/Makefile.in Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

660
ipalib/config.py Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1473
ipalib/frontend.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#

View 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
View 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

View 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
View 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
View 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)

View 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

View 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.*

View 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

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
ipaplatform==4.6.2
ipapython==4.6.2
netaddr
pyasn1
pyasn1-modules
six
[install]
ipaplatform

View File

@@ -0,0 +1 @@
ipalib

198
ipalib/krb_utils.py Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

8
ipalib/pkcs10.py Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

5
ipalib/setup.cfg Normal file
View File

@@ -0,0 +1,5 @@
[bdist_wheel]
universal = 1
[metadata]
license_file = ../COPYING

50
ipalib/setup.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

680
ipalib/x509.py Normal file
View 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"))