Plugin structure

Plugin is a zip archive comprising following files:

Warning

The size of compressed archive cannot exceed 10 MB. Uncompressed, total files’ size cannot exceed 100 MB.

manifest.json

The manifest declares plugin’s essential meta data and variables used by password modifier and verifier.

Parameter Description
name Unique name allowing to identify the plugin.
plugin_version

Plugin’s revision.

Note

We suggest using the MAJOR.MINOR.PATCH semantic versioning described at https://semver.org/.

type In case of both - password changer and verifier, this should be set to password_changer.
engine_version Fudo PAM provides plugins execution environment in a specific revision. Plugin requires declaration of the compatible engine version.
timeout Maximum script execution time (expressed in seconds). In case the modification/verification script does not finish successfully, the process responsible for its execution will be terminated and the password change/verification attempt will be considered unsuccessful.

The manifest also declares a list of variables used by the modifier and the verifier in the change and the verify sections respectively. The variables can either refer to existing data model objects or be defined manually. A variable is defined by the following structure:

Parameter Type Required Description
name string ok Variable name.
description string fail Variable description.
required boolean ok Specifies whether the variable is required or not.
object_type string fail Type of the object that the variable refers to.
object_property string fail Referenced object’s property that will be used to initiate variable’s value.
encrypt boolean ? Specifies whether the value should be encrypted or not. Required if object_type and object_property have not been defined.

Available objects and their properties

Object/property Description
server Server object defined in the local database.
name
Object’s name.
bind_ip
IP address used by Fudo PAM to communicate with the server.
ca_certificate
CA certificate.
port
Port number the target host uses to listen for connection requests.
protocol
Target host communication protocol: citrixsf, http, ica, modbus, mysql, oracle, rdp, ssh, system, tcp, tds, telnet, tn3270, tn5250, vnc.
secproto
Security protocol used by an RDP server: nla, tls, std.
ssl_to_server
1 if the server uses SSL/TLS, 0 if the server does not use SSL/TLS.
ssl_v2
1 if the SSL version 2.0 is allowed by the target host; 0 if the target host does not allow SSL 2.0 communication.
ssl_v3
1 if the SSL version 3.0 is allowed by the target host; 0 if the target host does not allow SSL 3.0 communication.
subnet
Dynamic server network subnet specifier, e.g. 192.168.0.0/24
   
server_address Server IP address. In case of dynamic servers, a single object can have many IP addresses assigned.
host
Server address.
certificate
Certificate for specific IP address.
public_key
Public SSH key for specific IP address.
Object/property Description
account Account object defined in the local database.
name
Object’s name.
description
Object’s description.
login
Privileged account login.
method
Authentication method - can be either password or ssh key
secret
Secret used in authentication process.

Example:

{
  "name": "Redmine",
  "plugin_version": "1.0.3",
  "type": "password changer",
  "engine_version": "1.0.0",
  "timeout": "300",
  "change":
  {
        "variables":
        [
          {
                "name": "transport_login",
                "description": "User name used to login to account.",
                "required": true,
                "object_type": "account",
                "object_property": "login"
          },
          {
                "name": "transport_secret",
                "description": "A secret to be used when logging in.",
                "required": true,
                "object_type": "account",
                "object_property": "secret"
          },
          {
                "name": "transport_host",
                "description": "Host name or IP address. IPv4 and IPv6 are both supported.",
                "required": true,
                "object_type": "server_address",
                "object_property": "host"
          },
          {
                "name": "account_login",
                "description": "User name for which to change password.",
                "required": true,
                "object_type": "account",
                "object_property": "login"
          }
        ]
  },
  "verify":
  {
        "variables":
        [
          {
                "name": "transport_login",
                "description": "User name used to login to account. This user's password will be verified.",
                "required": true,
                "object_type": "account",
                "object_property": "login"
          },
          {
                "name": "transport_secret",
                "description": "A secret that will be verified.",
                "required": true,
                "object_type": "account",
                "object_property": "secret"
          },
          {
                "name": "transport_host",
                "description": "Host name or IP address. IPv4 and IPv6 are both supported.",
                "required": true,
                "object_type": "server_address",
                "object_property": "host"
          }
        ]
  }
}

change script

Script used to execute the actual password changing code.

Example:

#!/bin/sh
CURR_DIR="$(realpath $(dirname "${0}"))"

echo "Script located in '${CURR_DIR}' directory."

export PYTHONPATH="${CURR_DIR}/site-packages"
python3 "${CURR_DIR}/redmine_changer.py" change

verify script

Script used to execute the actual password verifying code.

Example:

#!/bin/sh
CURR_DIR="$(realpath $(dirname "${0}"))"

echo "Script located in '${CURR_DIR}' directory."

export PYTHONPATH="${CURR_DIR}/site-packages"
python3 "${CURR_DIR}/redmine_changer.py" verify

Password changing code

Note

All variables declared in the manifest.json file are available through environment variables. Apart from those, there is a special account_new_secret variable available only in the password changing script. This value is initiated automatically by Fudo PAM.

Exemplary application:

import os

print('New secret: {}'.format(os.environ['account_new_secret']))

Example of Python code used to change passwords to Redmine using REST API:

import os
import sys

import requests


MODE_CHANGE = 1
MODE_VERIFY = 2


def eprint(*args, **kwargs):
        print(*args, file=sys.stderr, **kwargs)


class RedmineChangerError(Exception):
        pass


def redmine_get_user_id(server_uri, admin_login, admin_password, user_login):
        req = requests.get(
                server_uri + '/users.json',
                params={'name': user_login},
                auth=(admin_login, admin_password),
                verify=False,
        )
        if req.status_code != 200:
                raise RedmineChangerError(
                        'HTTP status code {} from {}.'.format(req.status_code, server_uri)
                )

        user_list = [x for x in req.json()['users'] if x['login'] == user_login]
        if len(user_list) > 1:
                raise RedmineChangerError(
                        'Ambigious answer from {}: Multiple users with "{}" login'.format(
                                server_uri, user_login
                        )
                )
        if len(user_list) < 1:
                raise RedmineChangerError(
                        'Response from {} doesn\'t contain user with login "{}"'.format(
                                server_uri, user_login
                        )
                )

        try:
                user_id = user_list[0]['id']
        except KeyError:
                raise RedmineChangerError(
                        'Response from {} doesn\'t contain "id".'.format(server_uri)
                )
        return user_id


def redmine_set_user_password(
        server_uri, admin_login, admin_password, user_id, user_password
):
        uri = '{}/users/{}.json'.format(server_uri, user_id)
        req = requests.put(
                uri,
                json={'user': {'password': user_password}},
                auth=(admin_login, admin_password),
                verify=False,
        )
        if req.status_code != 200:
                raise RedmineChangerError(
                        'HTTP status code {} from {}.'.format(req.status_code, server_uri)
                )


# https://redmine.hostonly.vm/users/current.json
def redmine_get_current_user_login(server_uri, admin_login, admin_password):
        req = requests.get(
                server_uri + '/users/current.json',
                auth=(admin_login, admin_password),
                verify=False,
        )
        if req.status_code != 200:
                raise RedmineChangerError(
                        'HTTP status code {} from {}.'.format(req.status_code, server_uri)
                )

        try:
                login = req.json()['user']['login']
        except KeyError:
                raise RedmineChangerError('Unable to get "user.login".')

        return login


def change(
        transport_login,
        transport_secret,
        transport_uri,
        account_login,
        account_new_secret,
):
        try:
                user_id = redmine_get_user_id(
                        transport_uri, transport_login, transport_secret, account_login
                )
        except RedmineChangerError as err:
                print('Error getting user id: {}'.format(err), file=sys.stderr)
                return 1

        print('User "{}" has id {}.'.format(account_login, user_id))

        try:
                redmine_set_user_password(
                        transport_uri,
                        transport_login,
                        transport_secret,
                        user_id,
                        account_new_secret,
                )
        except RedmineChangerError as err:
                print('Error setting user password: {}'.format(err), file=sys.stderr)
                return 1

        print('Successfully changed password for user "{}".'.format(account_login))
        return 0


def verify(transport_login, transport_secret, transport_uri):
        try:
                login = redmine_get_current_user_login(
                        transport_uri, transport_login, transport_secret
                )
        except RedmineChangerError as err:
                print(
                        'Error getting current user login: {}'.format(err), file=sys.stderr
                )
                return 1

        if login != transport_login:
                print(
                        'Server {} returned wrong login "{}" - expected "{}".'.format(
                                transport_uri, login, transport_login
                        ),
                        file=sys.stderr,
                )
                return 1

        print('Successfully logged in as "{}".'.format(transport_login))
        return 0


# TODO: There are some improvements that we can implement in future versions of
# plugin to test update procedure:
# - respect TLS: at the moment we assume TLS is on and connect using HTTPS,
# - verify server certificate,
# - optionally, get port of the server.
def main():
        if len(sys.argv) != 2:
                print('Provide "change" or "verify" as plugin mode', file=sys.stderr)
                sys.exit(1)

        if sys.argv[1] == 'change':
                mode = MODE_CHANGE
        elif sys.argv[1] == 'verify':
                mode = MODE_VERIFY
        else:
                print('Incorrect plugin mode: "{}".'.format(sys.argv[1]))
                sys.exit(1)

        transport_login = os.environ['transport_login']
        transport_secret = os.environ['transport_secret']
        transport_uri = 'https://' + os.environ['transport_host']
        if mode == MODE_CHANGE:
                account_login = os.environ['account_login']
                account_new_secret = os.environ['account_new_secret']

        result = 1
        if mode == MODE_CHANGE:
                result = change(
                        transport_login,
                        transport_secret,
                        transport_uri,
                        account_login,
                        account_new_secret,
                )
        else:
                result = verify(transport_login, transport_secret, transport_uri)

        sys.exit(result)


if __name__ == '__main__':
        main()

Note

Successfully executed code should exit with status 0. Any other value will be interpreted as a failure.

Related topics: