Struktura wtyczki

Wtyczka stanowi archiwum plików, w skład którego wchodzą:

Ostrzeżenie

Rozmiar skompresowanego archiwum nie może przekraczać 10 MB. Po rozpakowaniu, sumaryczny rozmiar plików wtyczki nie może przekraczać 100 MB.

manifest.json

Manifest deklaruje kluczowe meta dane wtyczki oraz zmienne wykorzystywane przez modyfikator oraz weryfikator haseł.

Parametr Opis
name Unikatowa nazwa umożliwiająca jednoznaczną identyfikację wtyczki.
plugin_version

Wersja wtyczki.

Informacja

Producent sugeruje zastosowanie wersjonowania semantycznego MAJOR.MINOR.PATCH opisanego na stronie https://semver.org/.

type Zarówno w przypadku modyfikatora jak i weryfikatora, typem powinien być password_changer.
engine_version Fudo PAM zapewnia środowisko wykonywania wtyczek w określonej wersji. Wtyczka wymaga zadeklarowania kompatybilnej wersji silnika.
timeout Maksymalny czas wykonania skryptu, wyrażony w sekundach. W przypadku gdy skrypt modyfikujący/weryfikujący hasło nie wykona się prawidłowo w wyznaczonym czasie, proces odpowiedzialny za jego wykonanie zostanie zakończony a próba zmiany/weryfikacji hasła zostanie uznana za nieudaną.

Manifest definiuje także listę zmiennych modyfikatora i weryfikatora haseł odpowiednio w sekcji change i verify. Zmienne mogą odwoływać się do obiektów modelu danych lub być definiowane ręcznie. Definicja zmiennej opisana jest następującą strukturą:

Parametr Typ Wymagany Opis
name string ok Nazwa zmiennej.
description string fail Opis zmiennej.
required boolean ok Specyfikacja obowiązku zdefiniowania zmiennej.
object_type string fail Typ obiektu, do której odwołuje się zmienna.
object_property string fail Parametr obiektu, którego wartość zostanie użyta do zainicjowania zmiennej.
encrypt boolean ? Specyfikator szyfrowania zmiennej w bazie danych. Wymagane, w przypadku gdy nie zostały określone object_type oraz object_property.

Dostępne obiekty i ich właściwości

Obiekt/parametr Opis
server Obiekt serwer zdefiniowany w lokalnej bazie danych.
name
Nazwa obiektu.
bind_ip
Adres IP Fudo PAM wykorzystany do komunikacji z serwerem.
ca_certificate
Certyfikat CA.
port
Numer portu, na którym nasłuchuje serwer docelowy.
protocol
Protokół komunikacji z serwerem docelowym: citrixsf, http, ica, modbus, mysql, oracle, rdp, ssh, system, tcp, tds, telnet, tn3270, tn5250, vnc.
secproto
Protokół bezpieczeństwa używany przez serwer RDP: nla, tls, std.
ssl_to_server
1 jeśli serwer korzysta z szyfrowania SSL/TLS, 0 jeśli SSL/TLS nie jest wykorzystywany.
ssl_v2
1 jeśli protokół SSL w wersji 2.0 może zostać użyty do komunikacji z serwerem docelowym, 0 jeśli komunikacja z serwerem poprzez protokół SSL 2.0 nie jest dopuszczalna.
ssl_v3
1 jeśli protokół SSL w wersji 3.0 może zostać użyty do komunikacji z serwerem docelowym, 0 jeśli komunikacja z serwerem poprzez protokół SSL 3.0 nie jest dopuszczalna.
subnet
Specyfikacja podsieci podawana w przypadku serwerów dynamicznych, np. 192.168.0.0/24
Obiekt/parametr Opis
server_address Adres IP serwera. W przypadku serwerów dynamicznych, pojedynczy obiekt może mieć przypisanych wiele adresów IP.
host
Adres serwera.
certificate
Certyfikat dla danego adresu IP serwera.
public_key
Klucz publiczny SSH dla danego adresu IP.
   
account Obiekt konto zdefiniowany w lokalnej bazie danych.
name
Nazwa obiektu.
description
Opis obiektu.
login
Login konta uprzywilejowanego na monitorowanym serwerze.
method
Metoda uwierzytelnienia, przyjmuje wartość hasła lub klucz SSH.
secret
Sekret używany w procesie uwierzytelnienia.

Przykład:

{
  "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"
          }
        ]
  }
}

Skrypt change

Skrypt inicjujący wykonanie właściwego kodu zmieniającego hasła.

Przykład:

#!/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

Skrypt verify

Skrypt inicjujący wykonanie właściwego kodu weryfikującego hasła.

Przykład:

#!/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

Kod modyfikujący hasło

Informacja

Wszelkie zmienne zadeklarowane w pliku manifest.json dostępne są poprzez zmienne środowiskowe. Oprócz nich, funkcjonuje zmienna specjalna account_new_secret, dostępna tylko w skrypcie modyfikującym hasło. Zmienna inicjowana jest wartością wygenerowaną automatycznie przez Fudo PAM.

Przykład odwołania się do zmiennej:

import os

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

Przykład kodu w języku Python zmieniającego hasło do serwisu Redmine za pomocą 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()

Informacja

Prawidłowo wykonany kod powinien na wyjściu zwrócić wartość 0. Każda inna wartość będzie zinterpretowana jako zmiana/weryfikacja wykonana niepomyślnie.

Tematy pokrewne: