Table Of Contents

Previous topic

6.1. Common configuration options

Next topic

6.3. Plugin response

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.

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. In most cases the Plugin SDK should not be listed in the [dev-packages] section either as it is installed manually as explained in Getting started.

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

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.cookie represents the cookie passed to and returned by the plugin.

self.session_cookie represents the session cookie passed to and returned by the plugin.

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 should simply copy paste the __init__ function and add new functionality. Do keep the original call to super().__init__ at the top.

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