Imported Upstream version 4.6.2
This commit is contained in:
440
doc/examples/examples.py
Normal file
440
doc/examples/examples.py
Normal 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
54
doc/examples/python-api.py
Executable 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
36
doc/guide/Makefile
Normal 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
36
doc/guide/README
Normal 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
997
doc/guide/guide.org
Normal 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
62
doc/guide/netgroup.js
Normal 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
140
doc/guide/role.py.txt
Normal 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
23
doc/guide/wsgi.py.txt
Normal 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)
|
||||
Reference in New Issue
Block a user