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 | Variable name. | |
description |
string | Variable description. | |
required |
boolean | Specifies whether the variable is required or not. | |
object_type |
string | Type of the object that the variable refers to. | |
object_property |
string | 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. |
|
Object’s name. |
|
IP address used by Fudo PAM to communicate with the server. |
|
CA certificate. |
|
Port number the target host uses to listen for connection requests. |
|
Target host communication protocol: citrixsf , http , ica , modbus , mysql , oracle , rdp , ssh , system , tcp , tds , telnet , tn3270 , tn5250 , vnc . |
|
Security protocol used by an RDP server: nla , tls , std . |
|
1 if the server uses SSL/TLS, 0 if the server does not use SSL/TLS. |
|
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. |
|
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. |
|
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. |
|
Server address. |
|
Certificate for specific IP address. |
|
Public SSH key for specific IP address. |
Object/property | Description |
---|---|
account |
Account object defined in the local database. |
|
Object’s name. |
|
Object’s description. |
|
Privileged account login. |
|
Authentication method - can be either password or ssh key |
|
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: