6.2. Plugin development¶
Follow the steps in Getting started to set up the correct Python environment. There are administrative steps bellow to describe the plugin. After adding some code to the plugin, the directory can be packaged in a ZIP file and uploaded to SPS.
6.2.1. Administrative tasks¶
Update the following files with information correct for your plugin:
MANIFEST
: plugin name, descriptions, etc. For details consult the Creating custom Authentication and Authorization
plugins developer’s guide. To determine the api version, see the Getting started.
Pipfile
: describes what additional Python3 PIP packages to package in the eventual plugin and also what packages to
use at development time. Note that the Plugin SDK should not be listed in the [packages] section, as the Plugin SDK
is pre-installed on Safeguard for Privileges Sessions. There are other pre-installed python packages on the system,
consult the developer’s guide for more information.
6.2.2. Basic functionality¶
In the lib/plugin.py
file, there is a simple skeleton code. The methods
AAPlugin.do_authenticate
,
AAPlugin.do_authorize
and
AAPlugin.do_session_ended
are called by the
AAPlugin base class after common
functionality is executed and parameters of the plugin invocation are pre-processed. All information about the
connection and pre-processing will be presented as attributes on self
in these functions.
The expected return values are defined in the technical document Creating custom Authentication and Authorization
plugins. For simplicity one can use AAResponse
class
to create the return value.
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin, AAResponse
class Plugin(AAPlugin):
def do_authenticate(self):
# To get a configuration value:
config_value = self.plugin_configuration.get('section', 'option')
# To log something:
self.logger.debug("This is a debug message")
# To access connection information
session_id = self.connection.session_id
return AAResponse.accept('the reason to accept')
def do_authorize(self):
return AAResponse.accept('the reason to accept')
def do_session_ended(self):
pass
6.2.3. Pre-defined attributes on self¶
On top of attributes defined by PluginBase, such as
self.plugin_configuration
and
self.logger
the following attributes are available in all the above methods, except where otherwise noted:
self.connection
which is a read-only object to show
a record of the SPS connection that is being processed. For example to find out the protocol used in the connection,
write self.connection.protocol
. Note: only ‘session_id’ is available in
do_session_ended
method.
Also ‘target’ related values may not be available in
do_authenticate
method due to protocol/environmental reasons.
self.username
contains the effective (gateway) user
name for this connection.
self.mfa_identity
contains the identity to be
used when contacting the MFA service.
self.mfa_password
contains the password acquired
from the user to be used in multi factor authentication.
self.mfa_password
may contain empty string to
indicate that the user requires push notification.
Note: only available in do_authenticate
method.
6.2.4. Asking the end-user¶
If the plugin needs extra information such as reponse to a challenge question, then the
AAPlugin.do_authenticate
method should return a value created by
AAResponse.need_info
function.
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin, AAResponse
class Plugin(AAPlugin):
def do_authenticate(self):
if 'magic' not in self.connection.key_value_pairs:
return AAResponse.need_info("What's the magic word?", "magic")
if self.connection.key_value_pairs['magic'] == "please":
return AAResponse.accept()
else:
return AAResponse.deny()
6.2.5. Using cookies¶
It is possible to directly read and write self.cookie
dictionary. The contents will be retained between invocations of the plugin.
It is also possible to add a cookie value on the fly when returning with a verdict from
AAPlugin.do_authenticate
or AAPlugin.do_authorize
by using the
AAResponse.with_cookie
method:
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin, AAResponse
class Plugin(AAPlugin):
def do_authenticate(self):
return AAResponse.accept().with_cookie(dict(somekey='somevalue'))
A more sophisticated way is to define attributes with the
@cookie_property
decorator.
The same things apply to self.session_cookie
.
6.2.6. Setting gateway user and groups¶
AAPlugin shall set the gateway user and gateway groups automatically in case
self.username
is different from
self.connection
.gateway_user. In other words,
if AAPlugin detects that the authenticated user is different from what SPS thinks is the gateway user, then AAPlugin
overwrites the SPS gateway user.
In order to modify the gateway user and groups of the session from
AAPlugin.do_authenticate
, use the
AAResponse.with_gateway_user
method.
Using AAResponse.with_gateway_user
method overwrites the automatic gateway user setting.
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin, AAResponse
class Plugin(AAPlugin):
def do_authenticate(self):
return AAResponse.accept().with_gateway_user('some-user')
6.2.7. Setting additional meta data¶
In order to set the additional meta data which is stored beside the session, use
AAResponse.with_additional_metadata
method.
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin, AAResponse
class Plugin(AAPlugin):
def do_authenticate(self):
return AAResponse.accept().with_additional_metadata('my data')
6.2.8. Avoiding costly calculations¶
To avoid redoing costly calculations or communication with external systems, it is advisable to store results of
such calculations in attributes decorated with
@cookie_property
decorator. This might be necessary if
the plugin returns “need info” to SPS, thus starting a new instance and execution of the plugin when the reply arrives.
6.2.9. Adding to the constructor¶
To enhance the class constructor, one may overload the
__init__
function
and add new functionality. Do keep the original call to super().__init__
at the top.
Note that the configuration parameter is the raw string representation of the plugin configuration, which will be
turned into
self.plugin_configuration
by the
base class.
#!/usr/bin/env pluginwrapper3
from safeguard.sessions.plugin import AAPlugin
class Plugin(AAPlugin):
def __init__(self, configuration):
super().__init__(configuration)
# your setup code
6.2.10. Authentication cache¶
There is an authentication cache implemented, which allows bypassing the multi factor authentication if the client IP address and gateway username matches the previous login inside a certain time frame. This is inherently unsafe as the IP address can be spoofed and the attacker only needs to get the gateway password of the user.
[authentication_cache]
; hard_timeout=90
; soft_timeout=15
; reuse_limit=0
Here hard_timeout
is the maximum number of seconds that the cache is valid for. The soft_timeout
can be
set to force re-authentication if the user does not reuse the cache quickly enough. And finally reuse limit is the
number of times the cache can be reused. The default for reuse_limit
is 0, which means that the authentication
cache is turned off. In the example, if reuse limit is for example 10, and the user successfully authenticated with
multi factor authentication, then the next 10 authentication are bypassed in the next 90 seconds, provided that
there is no gap bigger than 15 seconds between them.
6.2.11. Altering the steps¶
The calls from SPS will be translated to discrete steps by
AAPlugin
. It is possible to alter the list of
steps. For example to add self.mystep
before do_authenticate()
, which is the last step:
class MyPlugin(AAPlugin):
def _authentication_steps(self):
steps = list(super()._authentication_steps())
steps.insert(len(steps)-1, self.mystep)
return steps
def mystep(self):
pass
For example to add a step called self.mystep
before self._transform_username
:
class MyPlugin(AAPlugin):
def _authentication_steps(self):
steps = list(super()._authentication_steps())
steps_names = [item.__name__ for item in steps]
index_of_transform_username = steps_names.index('_transform_username')
steps.insert(index_of_transform_username, self.mystep)
return steps
def mystep(self):
pass