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

440
doc/examples/examples.py Normal file
View File

@@ -0,0 +1,440 @@
# Authors:
# 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/>.
"""
Example plugins
"""
# Hey guys, so you're interested in writing plugins for IPA? Great!
# We compiled this small file with examples on how to extend IPA to suit
# your needs. We'll be going from very simple to pretty complex plugins
# hopefully covering most of what our framework has to offer.
# First, let's import some stuff.
# errors is a module containing all IPA specific exceptions.
from ipalib import errors
# Command is the base class for command plugin.
from ipalib import Command
# Str is a subclass of Param, it is used to define string parameters for
# command. We'll go through all other subclasses of Param supported by IPA
# later in this file
from ipalib import Str
# output is a module containing the most common output patterns.
# Command plugin do output validation based on these patterns.
# You can define your own as we're going to show you later.
from ipalib import output
# To make the example ready for Python 3, we alias "unicode" to strings.
import six
if six.PY3:
unicode = str
# We're going to create an example command plugin, that takes a name as its
# only argument. Commands in IPA support input validation by defining
# functions we're going to call 'validators'. This is an example of such
# function:
def validate_name(ugettext, name):
"""
Validate names for the exhelloworld command. Names starting with 'Y'
(picked at random) are considered invalid.
"""
if name.startswith('Y'):
raise errors.ValidationError(
name='name',
error='Names starting with \'Y\' are invalid!'
)
# If the validator doesn't return anything (i.e. it returns None),
# the parameter passes validation.
class exhelloworld(Command):
"""
Example command: Hello world!
"""
# takes_args is an attribute of Command. It's a tuple containing
# instances of Param (or its subclasses such as Str) that define
# what position arguments are accepted by the command.
takes_args = (
# The first argument of Param constructor is the name that will be
# used to identify this parameter. It can be followed by validator
# functions. The constructor can also take a bunch of keyword
# arguments. Here we use default, to set the parameters default value
# and autofill, that fills the default value if the parameter isn't
# present.
# Note the ? at the end of the parameter name. It makes the parameter
# optional.
Str('name?', validate_name,
default=u'anonymous coward',
autofill=True,
),
)
# has_output is an attribute of Command, it is a tuple containing
# output.Output instances that define its output pattern.
# Commands in IPA return dicts with keys corresponding to items
# in the has_output tuple.
has_output = (
# output.summary is one of the basic patterns.
# It's a string that should be filled with a user-friendly
# decription of the action performed by the command.
output.summary,
)
# Every command needs to override the execute method.
# This is where the command functionality should go.
# It is always executed on the server-side, so don't rely
# on client-side stuff in here!
def execute(self, name, **options):
return dict(summary='Hello world, %s!' % name)
# register the command, uncomment this line if you want to try it out
#api.register(exhelloworld)
# Anyway, that was a pretty bad example of a command or, to be more precise,
# a bad example of resource use. When a client executes a command locally, its
# name and parameters are transfered to the server over XML-RPC. The command
# execute method is then executed on the server and results are transfered
# back to the client. The command does nothing, but create a string - a task
# that could be easily done locally. This can be done by overriding the Command
# forward method. It has the same signature as execute and is normally
# responsible for transferring stuff to the server.
# Most commands will, however, need to perfom tasks on the server. I didn't
# want to start with forward and confuse the hell out of you. :)
# Okey, time to look at something a little more advance. A command that
# actually communicates with the LDAP backend.
# Let's import a new parameter type: Flag.
# Parameters of type Flag do not have values per say. They are either enabled
# or disabled (True or False), so there's no need to make then optional, ever.
from ipalib import Flag
class exshowuser(Command):
"""
Example command: retrieve an user entry from LDAP
"""
takes_args = (
Str('username'),
)
# takes_options is another attribute of Command. It works the same
# way as takes_args, but instead of positional arguments, it enables
# us to define what options the commmand takes.
# Note that an options can be both required and optional.
takes_options = (
Flag('all',
# the doc keyword argument is what you see when you go
# `ipa COMMAND --help` or `ipa help COMMAND`
doc='retrieve and print all attributes from the server. Affects command output.',
flags=['no_output'],
),
)
has_output = (
# Here, you can see a custom output pattern. The pattern constructor
# takes the output name (key in the dictionary returned by execute),
# the allowed type(s) (can be a tuple with several types), a
# simple description and a list of flags. Currently, only
# the 'no_display' flag is supported by the Command.output_for_cli
# method, but you can always use your own if you plan
# to override it - I'll show you how later.
output.Output('result', dict, 'user entry without DN'),
output.Output('dn', unicode, 'DN of the user entry', ['no_display']),
)
# Notice the ** argument notation for options. It is not required, but
# we strongly recommend you to use it. In some cases, special options
# are added automatically to commands and not listing them or using **
# may lead to exception flying around... and nobody likes exceptions
# flying around.
def execute(self, username, **options):
# OK, I said earlier that this command is going to communicate
# with the LDAP backend, You could always use python-ldap to do
# that, but there's also this nice class we have... it's called
# ldap2 and this is how you get a handle to it:
ldap = self.api.Backend.ldap2
# ldap2 enables you to do a lot of crazy stuff with LDAP and it's
# specially crafted to suit IPA plugin needs. I recommend you either
# look at ipaserver/plugins/ldap2 or checkout some of the generated
# HTML docs on www.freeipa.org as I won't be able to cover everything
# it offers in this file.
# We want to retrieve an user entry from LDAP. We need to know its
# DN first. There's a bunch of method in ldap2 to build DNs. For our
# purpose, this will do:
dn = ldap.make_dn_from_attr(
'uid', username, self.api.env.container_user
)
# Note that api.env contains a lot of useful constant. We recommend
# you to check them out and use them whenever possible.
# Let's check if the --all option is enabled. If it is, let's
# retrieve all of the entry attributes. If not, only retrieve some
# basic stuff like the username, first and last names.
if options.get('all', False):
attrs_list = ['*']
else:
attrs_list = ['uid', 'givenname', 'sn']
# Give us the entry, LDAP!
(dn, entry_attrs) = ldap.get_entry(dn, attrs_list)
return dict(result=entry_attrs, dn=dn)
# register the command, uncomment this line if you want to try it out
#api.register(exshowuser)
# Now let's a take a look on how you can modify the command output if you don't
# like the default.
class exshowuser2(exshowuser):
"""
Example command: exusershow with custom output
"""
# Just some values we're going to use for textui.print_entry
attr_order = ['uid', 'givenname', 'sn']
attr_labels = {
'uid': 'User login', 'givenname': 'First name', 'sn': 'Last name'
}
def output_for_cli(self, textui, output, *args, **options):
# Now we've done it! We have overridden the default output_for_cli.
# textui is a class that implements a lot of useful outputting methods,
# please use it when you can
# output contains the dict returned by execute
# args, options contain the command parameters
textui.print_dashed('User entry:')
textui.print_indented('DN: %s' % output['dn'])
textui.print_entry(output['result'], self.attr_order, self.attr_labels)
# register the command, uncomment this line if you want to try it out
#api.register(exshowuser2)
# Alright, so now you'll always want to define your own output_for_cli...
# No, you won't! Because the default output_for_cli isn't as stupid as it looks.
# It can take information from the command parameters and output patterns
# to produce nice output like all real IPA commands have.
class exshowuser3(exshowuser):
"""
Example command: exusershow that takes full advantage of the default output
"""
takes_args = (
# We're going to rename the username argument to uid to match
# the attribute name it represent. The cli_name kwarg is what
# users will see in the CLI and label is what the default
# output_for_cli is going to use when printing the attribute value.
Str('uid',
cli_name='username',
label='User login',
),
)
# has_output_params works the same way as takes_args and takes_options,
# but is only used to define output attributes. These won't show up
# as parameters for the command.
has_output_params = (
Str('givenname',
label='First name',
),
Str('sn',
label='Last name',
),
)
# standard_entry includes an entry 'result' (dict), a summary 'summary'
# and the entry primary key 'value'
# It also makes the command automatically add two special options:
# --all and --raw. Look at the description of nearly any real IPA command
# to see what they're about.
has_output = output.standard_entry
# Since --all and --raw are added automatically thanks to standard_entry,
# we need to clear takes_options from the base class otherwise we would
# get a parameter conflict.
takes_options = tuple()
def execute(self, *args, **options):
# Let's just call execute of the base class, extract it's output
# and fit it into the standard_entry output pattern.
output = super(exshowuser3, self).execute(*args, **options)
output['result']['dn'] = output['dn']
return dict(result=output['result'], value=args[0])
# register the command, uncomment this line if you want to try it out
#api.register(exshowuser3)
# Pretty cool, right? But you will probably want to implement a set of commands
# to manage a certain type of entries (like users in the above examples).
# To save you the massive PITA of parameter copy&paste, we introduced
# the Object and Method plugin classes. Let's see how they work.
from ipalib import Object, Method
# First, we're going to create an object that represent the user entry.
class exuser(Object):
"""
Example plugin: user object
"""
# takes_params is an attribute of Object. It is used to define output
# parameters for associated Methods. Methods can also use them to
# to generate their own parameters as you'll see in a while.
takes_params = (
Str('uid',
cli_name='username',
label='User login',
# The primary_key kwarg is used to, well, specify the object's
# primary key.
primary_key=True,
),
Str('givenname?',
cli_name='first',
label='First name',
),
Str('sn?',
cli_name='last',
label='Last name',
),
)
# register the object, uncomment this line if you want to try it out
#api.register(exuser)
# Next, we're going to create a set of methods to manage this type of object
# i.e. to manage user entries. We're only going to do "read" commands, because
# we don't want to damage your user entries - adding, deleting, modifying is a
# bit more complicated and will be covered later in this file.
# Methods are automatically associated with a parent Object based on class
# names. They can then access their parent Object using self.obj.
# Simply said, Methods are just Commands associated with an Object.
class exuser_show(Method):
has_output = output.standard_entry
# get_args is a method of Command used to generate positional arguments
# we're going to use it to extract parameters from the parent
# Object
def get_args(self):
# self.obj.primary_key contains a reference the parameter with
# primary_key kwarg set to True.
# Parameters can be cloned to create new instance with additional
# kwargs. Here we add the attribute kwargs, that tells the framework
# the parameters corresponds to an LDAP attribute. The query kwargs
# tells the framework to skip parameter validation (i.e. do NOT call
# validators).
yield self.obj.primary_key.clone(attribute=True, query=True)
def execute(self, *args, **options):
ldap = self.api.Backend.ldap2
dn = ldap.make_dn_from_attr(
'uid', args[0], self.api.env.container_user
)
if options.get('all', False):
attrs_list = ['*']
else:
attrs_list = [p.name for p in self.output_params()]
(dn, entry_attrs) = ldap.get_entry(dn, attrs_list)
entry_attrs['dn'] = dn
return dict(result=entry_attrs, value=args[0])
# register the command, uncomment this line if you want to try it out
#api.register(exuser_show)
class exuser_find(Method):
# standard_list_of_entries is an output pattern that
# define a dict with a list of entries, their count
# and a truncated flag. The truncated flag is used to mark
# truncated (incomplete) search results - for example due to
# timeouts.
has_output = output.standard_list_of_entries
# get_options is similar to get_args, but is used to generate
# options instead of positional arguments
def get_options(self):
for option in self.obj.params():
yield option.clone(
attribute=True, query=True, required=False
)
def execute(self, *args, **options):
ldap = self.api.Backend.ldap2
# args_options_2_entry is a helper method of Command used
# to create a dictionary from the command parameters that
# have the attribute kwargs set to True.
search_kw = self.args_options_2_entry(*args, **options)
# make_filter will create an LDAP filter from attribute values
# exact=False means the values are surrounded with * when constructing
# the filter and rules=ldap.MATCH_ALL means the filter is going
# to use the & operators. More complex filters can be constructed
# by joining simpler filters using ldap2.combine_filters.
attr_filter = ldap.make_filter(
search_kw, exact=False, rules=ldap.MATCH_ALL
)
if options.get('all', False):
attrs_list = ['*']
else:
attrs_list = [p.name for p in self.output_params()]
# perform the search
(entries, truncated) = ldap.find_entries(
attr_filter, attrs_list, self.api.env.container_user,
scope=ldap.SCOPE_ONELEVEL
)
# find_entries returns DNs and attributes separately, but the output
# patter expects them in one dict. We need to arrange that.
for e in entries:
e[1]['dn'] = e[0]
entries = [e for (_dn, e) in entries]
return dict(result=entries, count=len(entries), truncated=truncated)
# register the command, uncomment this line if you want to try it out
#api.register(exuser_find)
# As most commands associated with objects are used to manage entries in LDAP,
# we defined a basic set of base classes for your plugins implementing CRUD
# operations. This is maily to save you from defining your own has_output,
# get_args, get_options and to have a standardized way of doing things for the
# sake of consistency. We won't cover them here, because you probably won't
# need to use them. So why did we botter? Well, you're going to see in
# a while. If interested anyway, check them out in ipalib/crud.py.
# At this point, if you've already seen some of the real plugins, you might
# be going like "WTH is this !@#^&? The user_show plugin is only like 4 lines
# of code and does much more than the exshowuser crap. Well yes, that's because
# it is based on one of the awesome plugin base classes we created to save
# authors from doing all the dirty work. Let's take a look at them.
# COMING SOON: baseldap.py classes, extending existing plugins, etc.

54
doc/examples/python-api.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/python2
# 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/>.
#
from __future__ import print_function
from ipalib import api
def example():
# 1. Initialize ipalib
#
# Run ./python-api.py --help to see the global options. Some useful
# options:
#
# -v Produce more verbose output
# -d Produce full debugging output
# -e in_server=True Force running in server mode
# -e xmlrpc_uri=https://foo.com/ipa/xml # Connect to a specific server
api.bootstrap_with_global_options(context='example')
api.finalize()
# You will need to create a connection. If you're in_server, call
# Backend.ldap.connect(), otherwise Backend.rpcclient.connect().
if api.env.in_server:
api.Backend.ldap2.connect()
else:
api.Backend.rpcclient.connect()
# Now that you're connected, you can make calls to api.Command.whatever():
print('The admin user:')
print(api.Command.user_show(u'admin'))
if __name__ == '__main__':
example()

36
doc/guide/Makefile Normal file
View File

@@ -0,0 +1,36 @@
FILE=guide.org
XML=$(addsuffix .xml, $(basename $(FILE)))
PDF=$(addsuffix .pdf, $(basename $(FILE)))
TXT=$(addsuffix .txt, $(basename $(FILE)))
HTML=$(addsuffix .html, $(basename $(FILE)))
FO=$(addsuffix .fo, $(basename $(FILE)))
all: $(PDF) $(TXT) $(HTML)
@echo Finished: $? are created
plain: $(FILE)
@echo -n "Building HTML, Docbook, and plain text ..."
@emacs -batch -q --no-site-file -eval "(require 'org)" \
--visit $< -f org-export-as-html \
--visit $< -f org-export-as-docbook \
--visit $< -f org-export-as-ascii 2>/dev/null
@echo "done, see $(HTML), $(XML), $(TXT)"
$(TXT): plain
$(HTML): plain
$(XML): plain
$(FO): $(XML)
@xmlto --skip-validation fo $< 2>/dev/null
$(PDF): $(FO)
@echo -n "Building PDF ... "
@fop -fo $< -pdf $@ -l en -a 2>/dev/null
@echo "done, see $(PDF)"
.PHONY: clean
clean:
@rm -f *.html *.txt *.xml *.fo *.pdf *~

36
doc/guide/README Normal file
View File

@@ -0,0 +1,36 @@
Extending FreeIPA
-----------------
"Extending FreeIPA" is a developer guide to understand how FreeIPA core
framework is built and how to extend it.
The Guide is written using Emacs Org Mode, see http://orgmode.org/org.html
for extensive manual of supported markup features.
You don't need to use Emacs to edit it, the markup is a plain text.
Building the guide
------------------
There is Makefile which can be used to convert the Guide from
Emacs Org Mode format to different targets.
Prerequisites:
==============
On Fedora system following packages are required to generate The Guide:
docbook-style-xsl
fop
emacs
xmlto
HTML, Docbook, and plain text
---
As Org Mode is part of Emacs since version 22, building HTML, TXT, and
Docbook targets requires Emacs v22 and above (tested with v23.3 in Fedora).
PDF
---
Building PDF is done first generating Docbook source, converting it to FO format,
and then running 'fop' processor.

997
doc/guide/guide.org Normal file
View File

@@ -0,0 +1,997 @@
#+OPTIONS: ^:{}
#+EMAIL: abokovoy@redhat.com
#+AUTHOR: Alexander Bokovoy
#+STYLE: <style type="text/css">
#+STYLE: pre {
#+STYLE: border: 1pt solid #000000;
#+STYLE: background-color: #404040;
#+STYLE: color: white;
#+STYLE: }
#+STYLE: .src {width: 940px;}
#+STYLE: dt {width: 400px; margin 25px auto;}
#+STYLE: dd {width: 940px;}
#+STYLE: p {text-align:justify;}
#+STYLE: body {width: 960px;
#+STYLE: margin: 0 auto;
#+STYLE: }
#+STYLE: div#content {margin: 0 10px 0 10px;
#+STYLE: display: inline;
#+STYLE: float: left;
#+STYLE: width: 940px;
#+STYLE: overflow: hidden;}
#+STYLE: </style>
Extending FreeIPA
* Introduction
FreeIPA is an integrated security information management solution. There is a common
framework written in Python to command LDAP server provided by a 389-ds project, certificate
services of a Dogtag project, and a MIT Kerberos server, as well as configuring various other
services typically used to maintain integrity of an enterprise environment, like DNS and
time management (NTP). The framework is written in Python, runs at a server side, and
provides access via command line tools or web-based user interface.
As core parts of the framework are implemented as pluggable modules, it is possible to
extend FreeIPA on multiple levels. This document attempts to present general ideas and
ways to make use of most of extensibility points in FreeIPA.
For information management solutions extensibility could mean multiple things. Information
objects that are managed could be extended themselves or new objects could be added. New
operations on existing objects might become needed or certain aspects of an object should
be hidden in a specific environment. All these tasks may require quite different approaches
to implement.
Following chapters will cover high-level design of FreeIPA and dive into details of its core
framework. Knowledge of Python programming language basics is required. Understanding
LDAP concepts is desirable, though it is not required for simple
extensions as FreeIPA attempts to provide sufficient mapping of LDAP concepts onto less
complex structures and Python objects, lowering a barrier to fine tune FreeIPA for
the specific use cases.
* High level design
FreeIPA core is written in Python programming language. The data is stored in LDAP
database, and client-server paradigm is used for managing it. A FreeIPA server instance
runs its own LDAP database, provided by 389-ds project (formerly Fedora Directory
Server). A single instance of LDAP database corresponds to the single FreeIPA
domain. Access to all information stored in the database is provided via FreeIPA server
core which is run as a simple WSGI application which uses XML-RPC and JSON to exchange
requests with its own clients.
Multiple replicas of the FreeIPA instance can be created on different servers, they are
managed with the help of replication mechanisms of 389-ds directory server.
As LDAP database is used for data storage, LDAP's Access Control Model is used to provide
privilege separation and Kerberos tickets are used to pass-through assertion of
authenticity. As Kerberos server is using the same LDAP database instance, use of Kerberos
tickets allows to perform operations against the database on the server if a client is
capable to forward such tickets via communication channels selected for the operation.
When FreeIPA client connects to FreeIPA server, a Kerberos ticket is forwarded
to the server and operations against LDAP database are performed under identity
authenticated when the ticket was issued. As LDAP database also uses Kerberos to establish
identity of a client, Access Control Information attributes can be used to limit what
entries could be accessed and what operations could be performed.
The approach allows to delegate operations from a FreeIPA client to the FreeIPA server
and in general gives FreeIPA server ability to interact with any Kerberos-aware service on
behalf of the client. It also allows to keep FreeIPA client side implementation relatively
light-weight: all it needs to do is to be able to forward Kerberos ticket, process XML-RPC or
JSON, and present resulting responses to the user.
Besides run-time core, FreeIPA includes few configuration tools. These tools
are split between server and client. Server-side tools are used when an instance of
FreeIPA server is set up and configured, while client-side tools are used to configure client
systems. While the server tools are used to configure LDAP database, put proper schema
definitions in use, create Kerberos domain, Certificate Authority and configure all
corresponding services, client side is more limited to configure PAM/NSS modules to work
against FreeIPA server, and make sure that appropriate information about the client host
is recorded in FreeIPA databases.
* Core plug-in framework
FreeIPA core defines few fundamentals. These are managed objects, their properties, and
methods to apply actions to the objects. Methods, in turn, are commands that are
associated with a specific object. Additionally, there are commands that do not have
directly associated objects and may perform actions over few of those. Objects are stored
using data store represented by a back end, and one of most useful back ends is LDAP store
back end.
Altogether, set of =Object=, =Method=, =Command=, and =Backend= instances
represent application programming interface, API, of FreeIPA core framework.
In Python programming language object oriented support is implemented using a fairly
simple concept that allows to modify instances in place, extending or removing their
properties and methods. While this concept is highly useful, in security-oriented
frameworks ability to lock down and trace origins of changes is also important. FreeIPA core
attempts to implement locking down feature by artificially making instances of foundation
classes read-only after their initialization has happened. If an attempt to modify object
happens after it was locked down, an exception is thrown. There are many classes
following this pattern.
For example, =ipalib.frontend.Command= class is derived from =ipalib.frontend.HasParam= class
that derives from =ipalib.plugable.Plugin= class which, in turn, is derived from
=ipalib.base.ReadOnly= class.
As result, every command has typed parameters and can dynamically be added to the
framework. At the same time, one cannot modify the properties of the command accidentally
once it is instantiated. This protects from modifications and enforces true nature of the
commands: they cannot have state that is carried over across multiple calls to the same
command unless the state is changing globally the whole environment around.
Environment also holds information about the context of execution. The /context/ is
important part of the FreeIPA framework as it also defines which methods of
the command instance are called in order to perform action. /Context/ in itself is defined
by the /environment/ which gives means to catch and store certain information about execution.
As with commands themselves, once instantiated, environment cannot be changed.
By default, for primary FreeIPA use, there are three major contexts defined: server,
client, and installer/updates.
- /server context/ :: plugins are registered and communicate with clients via XML-RPC and JSON
listeners. They validate any arguments and options defined and then execute whatever
action they supposed to perform
- /client context/ :: plugins are used to validate any arguments and options they take and
then forward the request to the FreeIPA server.
- /installer context/, /updates context/ :: plugins specific to installation and update
are loaded and registered. This context can be used to extend possible operations
during set up of FreeIPA server.
A user may define any context they want. FreeIPA names server context as '~server~'. When
using the ~ipa~ command line tool the context is '~cli~'. Server installation tools, in
particular, '~ipa-ldap-updater~', use special '~updates~' context to load specialized
plugins useful during update of the installed FreeIPA server.
Because these utilities use the same framework they will do the same validation, set default
values, and perform other basic actions in all contexts. This can help to save a
round-trip when testing for invalid data. However, for client-server communication, the
server is always authoritative and can re-define what the client has sent.
** Name space
FreeIPA has one special type of read-only objects: =NameSpace=. =NameSpace= class gives an
ordered, immutable mapping object whose values can also be accessed as attributes. A
=NameSpace= instance is constructed from iterable providing its members, which are simply
arbitrary objects with =name= attribute. This attribute must conform to two following
rules:
- Its value must be unique among the members of the name space
- Its value must pass the =check_name()= function =ipalib.base= module.
=check_name()= function encodes a simple rule of a lower-case Python identifier that
neither starts nor ends with an underscore. Actual regular expression that codifies this
rule is =NAME_REGEX= within =ipalib.constants= module.
Once name space is created, it locks itself down and becomes read-only. It means that
while original objects accessed through the name space might change, the references to
them via name space will stay intact. They cannot be removed or changed to point to other
objects.
The name spaces are used widely in FreeIPA core framework. As mentioned earlier, API
includes set of objects, commands, and methods. Objects include properties that are
defined before lock-down. At object's lock-down parameters are placed into a name space
and that locks them down so that no parameter specification can change. Command's
parameters and options also locked down and cannot change once command instance is
instantiated.
** Parameters
=Param= class is used to define attributes, arguments, or options throughout FreeIPA core
framework. The =Param= base class is not used directly but rather sub-classed to define
properties like passwords or specific data types like =Str= or =Int=.
Instances of classes inherited from =Param= base class give uniform access to the
properties required to command line interface, Web UI, and internally to FreeIPA
code. Following properties are most important:
- /name/ :: name of the parameter used internally to address the parameter in Python
code. The /name/ could include special characters to designate a =Param= spec.
- /cli_name/ :: optional name of the parameter to use in command line
interface. FreeIPA's CLI sets a mechanism to automatically translate
from a command line option name to a parameter's /name/ if /cli_name/
is specified.
- /label/ :: A short phrase describing the parameter. It is used on the CLI when
interactively prompting for the values, and as a label for the form inputs
in the Web UI. The /label/ should start with an initial capital letter.
- /doc/ :: A long description of the parameter. It is used by the CLI when displaying the
help information for a command, and as an extra instruction for the form input
on the Web UI. By default the /doc/ is the same as the /label/ but can be
overridden when a =Param= instance is created. As with /label/, /doc/ should
start with an initial capital letter and additionally should not end with any
punctuation.
- /required/ :: If set to =True=, means this parameter is required to supply. All
parameters are required by default and that means that /required/
property should only be specified when parameter *is not required*.
- /multivalue/ :: if set to =True=, means this parameter can accept a Python's tuple of
values. By default all parameters are *single-valued*.
When parameter /name/ has any of ~?~, ~*~, or ~+~ characters, it is treated as parameter
spec and is used to specify whether parameter is required, and should it be
multivalued. Following syntax is used:
| Spec | Name | Required | Multivalue |
|--------+-------+----------+------------|
| 'var' | 'var' | True | False |
| 'var?' | 'var' | False | False |
| 'var*' | 'var' | False | True |
| 'var+' | 'var' | True | True |
Access to the value stored by the =Param= class is given through a callable interface:
#+BEGIN_SRC python
age = Int('age', label='Age', default=100)
print age(10)
#+END_SRC
Following parameter classes are defined and used throughout FreeIPA framework:
- /Bool/ :: boolean parameters that are stored in Python's ~bool~ type, therefore, they
return either ~True~ or ~False~ value. However, they accept ~1~, ~True~
(Python boolean), or Unicode strings '~1~', '~true~' and '~TRUE~' as truth value, and ~0~,
~False~ (Python boolean), or Unicode strings '~0~', '~false~', and '~FALSE~' as false.
- /Flag/ :: boolean parameters which always have default value. Property /default/ can be
used to set the value. Defaults to ~False~:
#+BEGIN_SRC python
verbose = Flag('verbose', default=True)
#+END_SRC
- /Int/ :: integer parameters that are stored in Python's int type. Two additional properties can be
specified when constructing =Int= parameter:
- /minvalue/ :: minimal value that this parameter accepts, defaults to =MININT=
- /maxvalue/ :: maximum value this parameter can accept, defaults to =MAXINT=
- /Decimal/ :: floating point parameters that are stored in Python's Decimal type. =Decimal= has
the same two additional properties as =Int=. Unlike =Int=, there are no
default values for the minimal and maximum boundaries.
- /Bytes/ :: a parameter to represent binary data.
- /Str/ :: parameter representing a Unicode text. Both /Bytes/ and /Str/ parameters accept
following additional properties:
- /minlength/ :: minimal length of the parameter
- /maxlength/ :: maximum length of the parameter
- /length/ :: length of the parameters
- /pattern/ :: regular expression applied to the parameter's value to check its
validness
- /pattern_errmsg/ :: an error message to show when regular expression check fails
- /IA5Str/ :: string parameter as defined by RFC 4517. It means all characters of the
string must be ASCII characters (7-bit).
- /Password/ :: parameter to store passwords in Python =unicode= type. /Password/ has one
additional property:
- /confirm/ :: boolean specifying whether password should be confirmed
when entered. The confirmation is enabled by default.
- /Enum/ :: parameter can have one of predefined values that are specified with /values/
property which is a Python's =tuple=.
For most common case of enumerable strings there are two parameters:
- /BytesEnum/ :: parameter value should be one of predefined =unicode= strings
- /StrEnum/ :: equivalent to /BytesEnum/. Originally /BytesEnum/ was stored in Python's
=str= class instances but to be aligned with Python 3.0 changes both
classes moved to store as =unicode=.
When more than one value should be accepted, there is /List/ parameter that allows to
provide list of strings separated by a separator, default to ','. Also, the /List/
parameter skips spaces before the next item in the list unless property /skipspace/ is set to False:
#+BEGIN_SRC python
names = List('names', separator=',', skipspace=True)
names_list = names(u'John Doe, John Lee, Brad Moe')
# names_list is (u'John Doe', u'John Lee', u'Brad Moe')
names = List('names', separator=',', skipspace=False)
names_list = names(u'John Doe, John Lee, Brad Moe')
# names_list is (u'John Doe', u' John Lee', u' Brad Moe')
#+END_SRC
** Objects
The data manipulated by FreeIPA is represented by an Object class instances. Instance of
an Object class is a collection of properties, accepted parameters, action methods, and a
reference to where this object's data is preserved. Each object also has a reference to a
property that represents a primary key for retrieving the object.
In addition to properties and parameters, Object class instances hold their labels to use
in user interfaces. In practice, there are few differences in how labels are presented
depending on whether it is command line interface or a Web UI, but they can be ignored at
this point.
To be useful, all Object sub-classes need to override =takes_param= property. This is
where most of flexibility of FreeIPA comes from.
*** takes_param attribute
Properties of every object derived from Object class can be specified manually but FreeIPA
gives a handy mechanism to perform descriptive specification. Each =Object= class has
=Object.takes_param= attribute which defines a specification of all parameters this object
type is accepting.
Next example shows how to create new object type. We create an aquarium tank by defining
its dimensions and specifying which fish is living there.
#+BEGIN_SRC python -n -r -l '(%s)'
from ipalib import api, Object
class tank(Object):
takes_params = (
StrEnum('species*', label=u'Species', doc=u'Fish species',
values=(u'Angelfish', u'Betta', u'Cichlid', u'Firemouth')),
Decimal('height', label=u'Height', doc=u'height in mm', default='400.0'),
Decimal('width', label=u'Width', doc=u'width in mm', default='400.0'),
Decimal('depth', label=u'Depth', doc=u'Depth in mm', default='300.0')
)
api.register(tank) (ref:register)
api.finalize() (ref:finalize)
print list(api.Object.tank.params)
# ['species', 'height', 'width', 'depth']
#+END_SRC
First we define new class, =tank=, that takes four parameters. On line [[(register)]] we register the class
in FreeIPA's API instance, api. This creates =tank= object in =api.Object= name
space. Many objects can be added into the API up until =api.finalize()= is called as we do
on line [[(finalize)]].
When =api.finalize()= is called, all name spaces are locked down and all registered Python
objects in those name spaces are also finalized which in turn locks their structure down
as well.
As result, once we have finalized our API instance, every registered Object can be
accessed through =api.Object.<name>=. Our aquarium tank object now has defined =params=
attribute which is a name space holding all =Param= instances. Thus we can introspect and
see which parameters this object has.
At this point we can't do anything reasonable with our aquarium tank yet because we
haven't defined methods to handle it. In addition, our object isn't very useful as it does
not know how to store the information about aquarium's dimensions and species living in
it.
*** Object methods
Methods perform actions on the associated objects. The association of methods and objects
is done through naming convention rather than using programming language features. FreeIPA
expects methods operating on an object =<name>= to be named =<name>_<action>=:
#+BEGIN_SRC python
class tank_create(Method):
def execute(self, **options):
# create new aquarium tank
api.register(tank_create)
class tank_populate(Method):
def execute(self, **options):
# populate the aquarium tank with fish
api.register(tank_populate)
#+END_SRC
As can be seen, each method is a separate Python class. This approach allows to maintain
complexity of methods isolated from each other and from the complexity of the objects and
their storage which is probably most important aspect due to LDAP complexity overall.
The linking between objects and their methods goes further. All parameters defined for an
object, may be used as arguments of the methods without explicit declaration. This means
=api.Method.tank_populate= will accept ~species~ argument.
*** Methods with storage back ends
In order to store the information, =Object= class instances require a back end. FreeIPA
defines several back ends but the ones that could store data are derived of
=ipalib.CrudBackend=. CRUD, or /Create/, /Retrieve/, /Update/, and /Delete/, are basic
operations that could be performed with corresponding objects. =ipalib.crud.CrudBackend=
is an abstract class, it only defines functions that should be overridden in classes that
actually implement the back end operations.
As back end is not used directly, FreeIPA defines methods that could use back end and
operate on object's defined by certain criteria. Each method is defined as a separate
Python class. As CRUD acronym suggests, there are four base operations:
=ipalib.crud.Create=, =ipalib.crud.Retrieve=, =ipalib.crud.Update=,
=ipalib.crud.Delete=. In addition, method =ipalib.crud.Search= allows to retrieve all
entries that match a given search criteria.
When objects are defined and the back end is known, methods can be used to manipulate
information stored by the back end. Most of useful operations combine some of CRUD base
operations to perform their tasks.
In order to support flexible way to extend methods, FreeIPA gives special treatment for
the LDAP back end. Methods using LDAP back end hide complexity of handling LDAP queries and
allow to register user-provided functions that are called before or after method. This
mechanism is defined by ipalib.plugins.baseldap.CallbackInterface and used by LDAP-aware
CRUD classes, =LDAPCreate=, =LDAPRetrieve=, =LDAPUpdate=, =LDAPDelete=, and an analogue to
=ipalib.crud.Search=, =LDAPSearch=. There are also classes that define methods to operate
on reverse relationships between objects in LDAP to allow addition or removal of
membership information both in forward and reverse directions: =LDAPAddMember=,
=LDAPModMember=, =LDAPRemoveMember=, =LDAPAddReverseMember=, =LDAPModReverseMember=, =LDAPRemoveReverseMember=.
Most of CRUD classes are based on a =LDAPQuery= class which generalizes concept of
querying a record addressed with a primary key and supports JSON marshalling of the
queried attributes and their values.
Base LDAP operation classes implement everything needed to create typical methods to
work with self-contained objects stored in LDAP.
*** LDAPObject class
A large class of objects is LDAPObject. LDAPObject instances represent entries stored in
FreeIPA LDAP database instance. They are referenced by their distinguished name, DN, and
able to represent complex relationships between entries in LDAP like direct and indirect
membership.
Any class derived from LDAPObject needs to re-define few properties so that base class can
properly function for the specific object that is defined by the class. Below are commonly
redefined properties:
- /container_dn/ :: DN of the container for this object entries in LDAP. This one
usually comes from the environment associated with the API and by default is populated
from the =DEFAULT_CONFIG= of =ipalibs.constants=. For example, all accounts are
stored under =cn=accounts=, with users are under =cn=users,cn=accounts= and groups
are under =cn=groups,cn=accounts=. In case of a new object added, it
is reasonable to select its container coordinated to default configuration.
- /object_class/ :: list of LDAP object classes associated with the object
- /search_attributes/ :: list of attributes that will be used for search
- /default_attributes/ :: list of attributes that are always returned by searches
- /uuid_attribute/ :: an attribute that defines uniqueness of the entry
- /attribute_members/ :: a dict defining relations between other objects and this
one. Key is the name of attribute and value is a list of objects this attribute may
refer to. For example, =host= object defines that =memberof= attribute of a
host may refer to a =hostgroup=, =netgroup=, =role=, =hbacrule=, or =sudorule=
object. In other words, it means that =host= could be a member of any of those
objects.
- /reverse_members/ :: a dict defining reverse relations between this object and other
objects. Key is the name of attribute and value is the name of an object that refers
to this object with the attribute. For example, =role= object defines that =member=
attribute of a =privilege= refers to a =role= object.
- /password_attributes/ :: list of pairs defining an attribute in LDAP and a property of
a Python dictionary representing the LDAP object attributes that will be set
accordingly if such attribute exists in the LDAP entry. As passwords have restricted
access, often one needs only to know that there is a password set on the entry to
perform additional processing.
- /relationships/ :: a dict defining existing relationship criteria associated with the
object. These are used in Web UI to allow filtering of objects by the criteria. The
value is defined as a tuple of an UI label and two prefixes: inclusive and exclusive
that are prepended to the attribute parameter when options are generated by the
framework. LDAPObject defines few default criteria: /member/, /memberof/,
/memberindirect/, /memberofindirect/, and objects can redefine or append more. Due
to regularity of the design of LDAP objects, default criteria already makes it
possible to apply searches almost uniformly: one can ask for membership of a user in
a group, as well as for a membership of a role in a privilege without explicitly
defining those relationships.
These properties define how translation would go from Python side to and from an LDAP
backend.
As an example, let's see how role is defined. This is fully functioning plugin that
provides operations on roles:
#+INCLUDE "role.py.txt" src python -n
* Extending existing object
As said earlier, until API instance is finalized, objects, methods, and commands can be
added, removed, or modified freely. This allows to extend existing objects. Before API is
finalized, we cannot address objects through the unified interface as =api.Object.foo=,
but for almost all cases an object named =foo= is defined in a plugin
=ipalib.plugins.foo=.
1. Add new parameter:
#+BEGIN_SRC python -n
from ipalib.plugins.user import user
from ipalib import Str, _
user.takes_params += (
Str('foo',
cli_name='foo',
label=_('Foo'),
),
)
#+END_SRC
2. Re-define User object label to use organisation-specific terminology in Web UI:
#+BEGIN_SRC python -n
from ipalib.plugins.user import user
from ipalib import text
_ = text.GettextFactory(domain='extend-ipa')
user.label = _('Staff')
user.label_singular = _('Engineer')
#+END_SRC
Note that we re-defined locally =_= method to use different ~GettextFactory~. As
GettextFactory is supporting a single translation domain, all new translation terms need
to be placed in a separate translation domain and referred accordingly. Python rules for
scoping will keep this symbol as ~<package>._~ and as nobody imports it explicitly, it
will not interfere with the framework's provided ~text._~.
3. Assume =/dev/null= as default shell for all new users:
#+BEGIN_SRC python -n -r
from ipalib.plugins.user import user_add
def override_default_shell_cb(self, ldap, dn.
entry_attrs, attrs_list,
*keys, **options):
if 'loginshell' in entry_attrs:
default_shell = [self.api.Object.user.params['loginshell'].default]
if entry_attrs['loginshell'] == default_shell:
entry_attrs['loginshell'] = [u'/dev/null']
user_add.register_pre_callback(override_default_shell_cb)
#+END_SRC
The last example exploits a powerful feature available for every method of LDAPObject:
registered callbacks.
* Extending existing method
For objects stored in LDAP database instance all methods support adding callbacks. A
/callback/ is a user-provided function that is called at certain point of execution of a
method.
There are four types of callbacks:
- /PRE callback/ :: called before executing the method's action. Allows to modify passed
arguments, do additional validation or data transformation and
specific access control beyond what is provided by the framework.
- /POST callback/ :: called after executing the method's action. Allows to analyze results
of the action and perform additional actions or modify output.
- /EXC callback/ :: called in case execution of the method's action caused an execution
error. These callbacks provide means to recover from an erroneous execution.
- /INTERACTIVE callback/ :: called at a client context to allow a command to decide if
additional parameters should be requested from an user. This mechanism especially
useful to simplify complex interaction when there are several levels of possible
scenarios depending on what was provided at a client side.
All callback types are available to any class derived from =CallbackInterface=
class. These include all LDAP-based CRUD methods.
Callback registration methods accept a reference to callable and optionally ordering
argument =first= (~False~ by default) to allow the callback be executed before previously
registered callbacks of this type.
=CallbackInterface= class provides following class methods:
- =register_pre_callback= :: registers /PRE/ callback
- =register_post_callback= :: registers /POST/ callback
- =register_exc_callback= :: registers /EXC/ callback for purpose of recovering from
execution errors
- =register_interactive_prompt_callback= :: registers callbacks called by the client
context.
Let's look again at the last example:
#+BEGIN_SRC python -n -r
from ipalib.plugins.user import user_add
def override_default_shell_cb(self, ldap, dn.
entry_attrs, attrs_list,
*keys, **options):
if 'loginshell' in entry_attrs:
default_shell = [self.api.Object.user.params['loginshell'].default]
if entry_attrs['loginshell'] == default_shell:
entry_attrs['loginshell'] = [u'/dev/null']
user_add.register_pre_callback(override_default_shell_cb)
#+END_SRC
This extension defines a pre-processing callback that accepts number of arguments:
- /ldap/ :: reference to the back end to store and retrieve the object's data
- /dn/ :: reference to the object data in LDAP
- /entry_attrs/ :: arguments and options of the command and their values as a
dictionary. All values in /entry_attrs/ will be used for communicating
with LDAP store, thus replacing values should be done with care. For
details please see Python LDAP module documentation
- /attrs_list/ :: list of all attributes we intend to fetch from the back end
- /keys/ :: arguments of the command
- /options/ :: all other unidentified parameters passed to the method
Arguments of a post-processing callback, /POST/, are slightly different. As action is
already performed and the attributes of the entry are fetched back from the back end,
there is no need to provide =attrs_list=:
#+BEGIN_SRC python -n -r
from ipalib.plugins.user import user_add
def verify_shell_cb(self, ldap, dn. entry_attrs,
*keys, **options):
if 'loginshell' in entry_attrs:
default_shell = [self.api.Object.user.params['loginshell'].default]
if entry_attrs['loginshell'] == default_shell:
# report that default shell is assigned
user_add.register_post_callback(verify_shell_cb)
#+END_SRC
Execution error callback, /EXC/, has following signature:
#+BEGIN_SRC python -n
def user_add_error_cb(self, args, options, exc,
call_func, *call_args, **call_kwargs):
return
#+END_SRC
where arguments have following meaning:
- /args/ :: arguments of the original method
- /options/ :: options of the original method
- /exc/ :: exception object thrown by a /call_func/
- /call_func/ :: function that was called by the method and caused the error of
execution. In case of LDAP-based methods this is often =ldap.add_entry()=
or =ldap.modify_entry()=, or a similar function
- /call_args/ :: first argument passed to the /call_func/
- /call_kwargs/ :: remaining arguments of /call_func/
Finally, interactive prompt callback receives /kw/ argument which is a dictionary of all
arguments of the command.
All callbacks are supplied with a reference to the method instance, ~self~, unless the
callback itself has an attribute called '~im_self~'. As can be seen in callback examples,
self reference recursively provides access to the whole FreeIPA API structure.
This approach gives complete control of existing FreeIPA methods without
deep dive into details of LDAP programming even if the framework allows such a deep dive.
* Web UI
FreeIPA framework has two major client applications: Web UI and command line-based client
tool, ~ipa~. Web UI communicates with a FreeIPA server running WSGI application that
accepts JSON-formatted requests and translates them to calls to FreeIPA plugins.
A following code in ~install/share/ui/wsgi.py~ defines FreeIPA web application:
#+INCLUDE "wsgi.py.txt" src python -n -r
At line [[(wsgi-app-bootstrap)]] we set up FreeIPA framework with server context. This means
plugins are loaded and initialized from following locations:
- ~ipalib/plugins/~ -- general FreeIPA plugins, available for all contexts
- ~ipaserver/plugins/~ -- server-specific plugins, available in '~server~' context
With =api.finalize()= call at line [[(wsgi-app-finalize)]] FreeIPA framework is locked down and all
components provided by plugins are registered at ~api~ name spaces: =api.Object=,
=api.Method=, =api.Command=, =api.Backend=.
At this point, ~api~ name spaces become usable and our WSGI entry point, defined on lines
[[(wsgi-app-start)]] to [[(wsgi-app-end)]] can access =api.Backend.session()= to generate
response for WSGI request.
Web UI itself is written in JavaScript and utilizes JQuery framework. It can be split into
three major parts:
- /communication/ :: tools defined in ~ipa.js~ to allow talking with FreeIPA server using
AJAX requests and JSON formatting
- /presentation/ :: tools in ~facet.js~, ~entity.js~, ~search.js~, ~widget.js~, ~add.js~,
and ~details.js~ to give basic building blocks of Web UI
- /objects/ :: actual implementation of Web UI for FreeIPA objects (user, group, host,
rule, and other available objects registered at =api.Object= by the server
side)
The code of these JavaScript files is loaded in ~index.html~ and kicked into work by
~webui.js~ where main navigation and document's ~onready~ event handler are defined. In
addition, ~index.html~ imports ~extension.js~ file where all extensions to Web UI can be
registered or referenced. As ~extension.js~ is loaded after all other Web UI JavaScript
files but before ~webui.js~, it can already use all tools of the Web UI.
The execution of Web UI starts with the call of =IPA.init()= function which does
following:
1. Set up AJAX asynchronous communication via POST method using JSON format.
2. Fetches meta-data about FreeIPA methods available on the server using JSON format and
makes them available as =IPA.methods=.
3. Fetches meta-data about FreeIPA objects available on the server using JSON format and
makes them available as =IPA.objects=.
4. Fetches translations of messages used in the Web UI and makes them available as
=IPA.messages=.
5. Fetches identity of the user running the Web UI, accessible as =IPA.whoami=.
6. Fetches FreeIPA environment specific for Web UI, accessible as =IPA.env=.
The communication with FreeIPA server is done using =IPA.command()= function. Commands
created with =IPA.command()= can later be executed with =execute()= method. This
separation of construction and actual execution allows to create multiple commands and
combine them together in a single request. Batch requests are created with
=IPA.batch_command()= function and command are added to them with =add_command()=
method. In addition, FreeIPA Web UI allows to run commands concurrently with
=IPA.concurrent_command()= function.
Web UI has following DOM structure:
|-----------------------+-----------------------------------+------------+-----------|
| | Container | | |
|-----------------------+-----------------------------------+------------+-----------|
| background | header | navigation | content |
| background-header | header-logo | | |
| background-navigation | header-network-activity-indicator | | |
| background-left | loggedinas | | |
| background-right | | | |
|-----------------------+-----------------------------------+------------+-----------|
~Container~ div is a top-level one, it includes background, header, navigation, content
divs. These divs and their parts can be manipulated from the JavaScript code to represent
the UI. However, FreeIPA gives an easier way to accomplish this.
** Facets
Facet is a smallest block of FreeIPA Web UI. When facet is defined, it has name, label,
link to an entity it is part of, and methods to create, show, load, and hide itself.
** Entities
Entity is addressable group of facets. FreeIPA Web UI provides a declarative way of
creating entities and defining their facets based on JavaScript's syntax. Following
example is a complete definition of a netgroup facet:
#+INCLUDE "netgroup.js" src js2-mode -n
This definition of a netgroup facet describes:
- /details facet/ :: a facet named '~identity~' and three fields, ~cn~, ~description~,
and ~nisdomainname~. In addition, ~description~ field is a text area widget. This
facet is used to display existing netgroup information.
- /association facets/ :: number of facets, linking this one with others. In case of a
netgroup, netgroups are linked to facet group ~member~ via different attributes. The
definition also adds standard association facets defined in ~entity.js~.
- /adder dialog/ :: a dialog to create a new netgroup. The dialog has two fields: ~cn~ and
~description~ where ~description~ is again a text area widget.
Similarly to FreeIPA core framework, created entity needs to be registered to the Web UI
via =IPA.register()= method.
In order to add new entity to the Web UI, one can use ~extension.js~. This file in
~/usr/share/ipa/html~ is empty and provided specifically for this purpose.
As an example, let's define an entity 'Tank' corresponding to our aquarium tank:
#+BEGIN_SRC js2-mode -n
IPA.tank = {};
IPA.tank.entity = function(spec) {
var that = IPA.entity(spec);
that.init = function(params) {
details_facet({
sections: [
{
name: 'identity',
fields: [
'species', 'height', 'width', 'depth'
]
}
]
}).
standard_association_facets().
adder_dialog({
fields: [
'species', 'height', 'width', 'depth'
]
});
};
};
IPA.register('tank', IPA.tank.entity);
#+END_SRC
* Command line tools
As an alternative to Web UI, FreeIPA server can be controlled via command-line interface
provided by the ~ipa~ utility. This utility is operating under '~client~' context and
looks even simpler than Web UI's ~wsgi.py~:
#+BEGIN_SRC python -n
import sys
from ipalib import api, cli
if __name__ == '__main__':
cli.run(api)
#+END_SRC
=cli.run()= is the central running point defined in ~ipalib/cli.py~:
#+BEGIN_SRC python -n
# <cli.py code> ....
cli_plugins = (
cli,
textui,
console,
help,
show_mappings,
)
def run(api):
error = None
try:
(_options, argv) = api.bootstrap_with_global_options(context='cli')
for klass in cli_plugins:
api.add_plugin(klass)
api.finalize()
if not 'config_loaded' in api.env and not 'help' in argv:
raise NotConfiguredError()
sys.exit(api.Backend.cli.run(argv))
except KeyboardInterrupt:
print('')
logger.info('operation aborted')
except PublicError as e:
error = e
except Exception as e:
logger.exception('%s: %s', e.__class__.__name__, str(e))
error = InternalError()
if error is not None:
assert isinstance(error, PublicError)
logger.error(error.strerror)
sys.exit(error.rval)
#+END_SRC
As with WSGI, =api= is bootstraped, though with a client context and using global options
from ~/etc/ipa/default.conf~, and command line arguments. In addition to common plugins
available in ~ipalib/plugins~, ~cli.py~ adds few command-line specific classes defined in
the module itself:
- ~cli~ :: a backend for executing from command line interface which does translation of
command line option names, basic verification of commands and fallback to show
help messages with ~help~ command, execution of the command, and translation of
the output to command-line friendly format if this is defined for the command.
- ~textui~ :: a backend to nicely format output to stdout which handles conversion from
binary to base64, prints text word-wrapped to the terminal width, formats
returned complex values so that they can be easily understood by a human
being.
#+BEGIN_EXAMPLE
>>> entry = {'name' : u'Test example', 'age' : u'100'}
>>> api.Backend.textui.print_entry(entry)
age: 100
name: Test example
#+END_EXAMPLE
- ~console~ :: starts interactive Python console with FreeIPA commands
- ~help~ :: generates help for every command and method of FreeIPA and structures it into
sections according to the registered FreeIPA objects.
#+BEGIN_EXAMPLE
>>> api.Command.help(u'user-show')
Purpose: Display information about a user.
Usage: ipa [global-options] user-show LOGIN [options]
Options:
-h, --help show this help message and exit
--rights Display the access rights of this entry (requires --all). See
ipa man page for details.
--all Retrieve and print all attributes from the server. Affects
command output.
--raw Print entries as stored on the server. Only affects output
format.
#+END_EXAMPLE
- ~show_mappings~ :: displays mappings between command's parameters and LDAP attributes:
#+BEGIN_EXAMPLE
>>> api.Command.show_mappings(command_name=u"role-find")
Parameter : LDAP attribute
========= : ==============
name : cn
desc : description
timelimit : timelimit?
sizelimit : sizelimit?
#+END_EXAMPLE
** Extending command line utility
Since ~ipa~ utility operates under client context, it loads all command plugins from
~ipalib/plugins~. A simple way to extend command line is to drop its plugin file into
~ipalib/plugins~ on the machine where ~ipa~ utility is executed. Next time ~ipa~ is
started, new plugin will be loaded together with all other plugins from ~ipalib/plugins~
and commands provided by it will be added to the =api=.
Let's add a command line plugin that allows to ping a server and measures round trip time:
#+BEGIN_SRC python -n
from ipalib import frontend
from ipalib import output
from ipalib import _, ngettext
from ipalib import api
import time
__doc__ = _("""
Local extensions to FreeIPA commands
""")
class timed_ping(frontend.Command):
__doc__ = _('Ping remote FreeIPA server and measure round-trip')
has_output = (
output.summary,
)
def run(self):
t1 = time.time()
result = self.api.Command.ping()
t2 = time.time()
summary = u"""Round-trip to the server is %f ms.
Server response is %s"""
return dict(summary=summary % ((t2-t1)*1000.0, result['summary']))
api.register(timed_ping)
#+END_SRC
When this plugin code is placed into ~ipalib/plugins/extend-cli.py~ (name of the plugin
file can be set arbitrarily), ~ipa timed-ping~ will produce following output:
#+BEGIN_EXAMPLE
$ ipa timed-ping
-----------------------------------------------------------------------------
Round-trip to the server is 286.306143 ms.
Server response is IPA server version 2.1.3GIT8a254ca. API version 2.13
-----------------------------------------------------------------------------
#+END_EXAMPLE
In this example we have created ~timed-ping~ command and overrode its =run()=
method. Effectively, this command will only work properly on the client. If the client is
also FreeIPA server (all FreeIPA servers are enrolled as FreeIPA clients), the same code
will also be loaded by the server context and will be accessible to the Web UI as well,
albeit its usefulness will be questionable as it will be measuring the round-trip to the
server from the server itself.
* File paths
Finally, it should be noted that depending on installed Python version and operating
system, paths where plugins are loaded from may differ. Usually Python extensions are
placed in ~site-packages~ Python sub-directory. In Fedora and RHEL distributions, this is
~/usr/lib/python<version>/site-packages~. Thus, full path to ~extend-cli.py~ would be
~/usr/lib/python<version>/site-packages/ipalib/plugins/extend-cli.py~.
On recent Fedora distribution, following paths are used:
|--------------------+---------------------------+------------------------------------------------------------|
| Plugins | Python module prefix | File path |
|--------------------+---------------------------+------------------------------------------------------------|
| common | ipalib/plugins | /usr/lib/python2.7/site-packages/ipalib/plugins |
| server | ipaserver/plugins | /usr/lib/python2.7/site-packages/ipaserver/plugins |
| installer, updates | ipaserver/install/plugins | /usr/lib/python2.7/site-packages/ipaserver/install/plugins |
|--------------------+---------------------------+------------------------------------------------------------|
Next table explains use of contexts in FreeIPA applications:
|---------+------------------+-------------------------+----------------------------------------|
| Context | Application | Plugins | Description |
|---------+------------------+-------------------------+----------------------------------------|
| server | wsgi.py | common, server | Main FreeIPA server, server context |
| cli | ipa | common | Command line interface, client context |
| updates | ipa-ldap-updater | common, server, updates | LDAP schema updater |
|---------+------------------+-------------------------+----------------------------------------|
* Platform portability
Originally FreeIPA was created utilizing packages available in Fedora and RHEL
distributions. During configuration stages multiple system services need to be stopped
and started again, scheduled to start after reboot and re-configured. In addition, when
operating system utilizing security measures to harden the server setup, appropriate
activities need to be done as well for preserving proper security contexts. As
configuration details, service names, security features and management tools differ
substantially between various GNU/Linux distributions and other operating systems, porting
FreeIPA project's code to other environment has proven to be problematic.
When Fedora project has decided to migrate to systemd for services management, FreeIPA
packages for Fedora needed to be updated as well, at the same time preserving support for
older SystemV initialization scheme used in older releases. This prompted to develop a
'platformization' support allowing to abstract services management between different
platforms.
FreeIPA 2.1.3 includes first cut of platformization work to support Fedora 16 distribution
based on systemd. At the same time, there is an effort to port FreeIPA client side code to
Ubuntu distributions.
Platform portability in FreeIPA means centralization of code to manage system-provided
services, authentication setup, and means to manage security context and host names. It is
going to be extended in future to cover other areas as well, both client- and server-side.
The code that implements platform-specific adaptation is placed under
~ipaplatform~. As of FreeIPA 4.4.2, there are two major "platforms" supported:
- /rhel/ :: Red Hat Enterprise Linux 7-based distributions utilizing Systemd
such as CentOS 7 and Scientific Linux 7.
- /fedora/ :: Fedora distribution version 23 above are supported by this platform
module. It is based on ~systemd~ system management tool and utilizes
common code in ~ipaplatform/base/services.py~. ~fedora~ contains
only differentiation required to cover Fedora 23-specific implementation
of systemd use, depending on changes to Dogtag, Tomcat6, and 389-ds
packages.
Each platform-specific adaptation should provide few basic building blocks:
*** AuthConfig class and tasks module
=ipaplatform.tasks= module implements system-independent interface to configure system
resources. In Red Hat systems some of these tasks are done with authconfig(8) utility.
=AuthConfig= class is nothing more than a tool to gather configuration options and execute
their processing. These options then converted by an actual implementation to series of a
system calls to appropriate utilities performing real configuration.
From FreeIPA code perspective, the system configuration should be done with
use of ~ipaplatform.tasks.tasks~:
#+BEGIN_SRC python -n
from ipaplatform.tasks import tasks
tasks.set_nisdomain('nisdomain.example')
#+END_SRC
The actual implementation can differ. ~redhat~ platform module builds up arguments to
authconfig(8) tool and on =execute()= method runs it with those arguments. Other systems
will need to have processing based on their respective tools.
*** PlatformService class
=PlatformService= class abstracts out an external process running on the system which is
possible to administer: start, stop, check its status, schedule for automatic startup,
etc.
Services are used thoroughly through FreeIPA server and client install tools. There are
several services that are used especially often and they are selected to be accessible via
Python properties of =ipaplatform.services.knownservices= instance.
To facilitate more expressive way of working with often used services, ipaplatform.services
module provides a shortcut to access them by name via
ipaplatform.services.knownservices.<service>. A typical code change looks like this:
#+BEGIN_EXAMPLE
import ipaplatform.services.knownservices
....
- service.restart("dirsrv")
- service.restart("krb5kdc")
- service.restart("httpd")
+ ipaplatform.services.knownservices.dirsrv.restart()
+ ipaplatform.services.knownservices.krb5kdc.restart()
+ ipaplatform.services.knownservices.httpd.restart()
#+END_EXAMPLE
Besides expression change this also makes more explicit to platform providers access to
what services they have to implement. Service names are defined in
ipaplatform.platform.base.wellknownservices and represent definitive names to access these
services from FreeIPA code. Of course, platform provider should remap those names to
platform-specific ones -- for ipaplatform.redhat provider mapping is identity.
Porting to a new platform may be hard as can be witnessed by this example:
https://www.redhat.com/archives/freeipa-devel/2011-September/msg00408.html
If there is doubt, always consult existing providers. ~redhat/services.py~ is canonical -- it
represents the code which was used throughout FreeIPA v2 development.
*** Enabling new platform provider
When support for new platform is implemented and appropriate provider is placed to
~ipaplatform/platform/~, it is time to enable its use by the FreeIPA. Since FreeIPA is
supposed to be rolled out uniformly on multiple clients and servers, best approach is to
build and distribute software packages using platform-provided package management tools.
With this in mind, platform code selection in FreeIPA is static and run at package
production time. In order to select proper platform provider, one needs to pass
~--with-ipaplatform~ argument to FreeIPA's configure process:
#+BEGIN_EXAMPLE
./configure --with-ipaplatform=fedora
#+END_EXAMPLE

62
doc/guide/netgroup.js Normal file
View File

@@ -0,0 +1,62 @@
IPA.netgroup = {};
IPA.netgroup.entity = function(spec) {
var that = IPA.entity(spec);
that.init = function(params) {
params.builder.search_facet({
columns: [
'cn',
'description'
]
}).
details_facet({
sections: [
{
name: 'identity',
fields: [
'cn',
{
factory: IPA.textarea_widget,
name: 'description'
},
'nisdomainname'
]
}
]
}).
association_facet({
name: 'memberhost_host',
facet_group: 'member'
}).
association_facet({
name: 'memberhost_hostgroup',
facet_group: 'member'
}).
association_facet({
name: 'memberuser_user',
facet_group: 'member'
}).
association_facet({
name: 'memberuser_group',
facet_group: 'member'
}).
association_facet({
name: 'memberof_netgroup',
associator: IPA.serial_associator
}).
standard_association_facets().
adder_dialog({
fields: [
'cn',
{
factory: IPA.textarea_widget,
name: 'description'
}
]
});
};
return that;
};
IPA.register('netgroup', IPA.netgroup.entity);

140
doc/guide/role.py.txt Normal file
View File

@@ -0,0 +1,140 @@
from ipalib.plugins.baseldap import *
from ipalib import api, Str, _, ngettext
from ipalib import Command
from ipalib.plugins import privilege
class role(LDAPObject):
"""
Role object.
"""
container_dn = api.env.container_rolegroup
object_name = _('role')
object_name_plural = _('roles')
object_class = ['groupofnames', 'nestedgroup']
default_attributes = ['cn', 'description', 'member', 'memberof',
'memberindirect', 'memberofindirect',
]
attribute_members = {
'member': ['user', 'group', 'host', 'hostgroup'],
'memberof': ['privilege'],
}
reverse_members = {
'member': ['privilege'],
}
rdnattr='cn'
label = _('Roles')
label_singular = _('Role')
takes_params = (
Str('cn',
cli_name='name',
label=_('Role name'),
primary_key=True,
),
Str('description',
cli_name='desc',
label=_('Description'),
doc=_('A description of this role-group'),
),
)
api.register(role)
class role_add(LDAPCreate):
__doc__ = _('Add a new role.')
msg_summary = _('Added role "%(value)s"')
api.register(role_add)
class role_del(LDAPDelete):
__doc__ = _('Delete a role.')
msg_summary = _('Deleted role "%(value)s"')
api.register(role_del)
class role_mod(LDAPUpdate):
__doc__ = _('Modify a role.')
msg_summary = _('Modified role "%(value)s"')
api.register(role_mod)
class role_find(LDAPSearch):
__doc__ = _('Search for roles.')
msg_summary = ngettext(
'%(count)d role matched', '%(count)d roles matched', 0
)
api.register(role_find)
class role_show(LDAPRetrieve):
__doc__ = _('Display information about a role.')
api.register(role_show)
class role_add_member(LDAPAddMember):
__doc__ = _('Add members to a role.')
api.register(role_add_member)
class role_remove_member(LDAPRemoveMember):
__doc__ = _('Remove members from a role.')
api.register(role_remove_member)
class role_add_privilege(LDAPAddReverseMember):
__doc__ = _('Add privileges to a role.')
show_command = 'role_show'
member_command = 'privilege_add_member'
reverse_attr = 'privilege'
member_attr = 'role'
has_output = (
output.Entry('result'),
output.Output('failed',
type=dict,
doc=_('Members that could not be added'),
),
output.Output('completed',
type=int,
doc=_('Number of privileges added'),
),
)
api.register(role_add_privilege)
class role_remove_privilege(LDAPRemoveReverseMember):
__doc__ = _('Remove privileges from a role.')
show_command = 'role_show'
member_command = 'privilege_remove_member'
reverse_attr = 'privilege'
member_attr = 'role'
has_output = (
output.Entry('result'),
output.Output('failed',
type=dict,
doc=_('Members that could not be added'),
),
output.Output('completed',
type=int,
doc=_('Number of privileges removed'),
),
)
api.register(role_remove_privilege)

23
doc/guide/wsgi.py.txt Normal file
View File

@@ -0,0 +1,23 @@
import logging
import os
from ipaplatform.paths import paths
from ipalib import api
logger = logging.getLogger(os.path.basename(__file__))
api.bootstrap(context='server', confdir=paths.ETC_IPA, log=None) (ref:wsgi-app-bootstrap)
try:
api.finalize() (ref:wsgi-app-finalize)
except Exception as e:
logger.error('Failed to start IPA: %s', e)
else:
logger.info('*** PROCESS START ***')
# This is the WSGI callable:
def application(environ, start_response): (ref:wsgi-app-start)
if not environ['wsgi.multithread']:
return api.Backend.session(environ, start_response)
else:
logger.error("IPA does not work with the threaded MPM, "
"use the pre-fork MPM") (ref:wsgi-app-end)