skydum

個人的な作業記録とか備忘録代わりのメモ

オフライン環境でデスクトップ共有

オフライン環境でデスクトップ共有を行いたい

サーバの操作画面の共有と他の作業中の画面の共有を比較的簡単かつ操作を教えなくてもできるような簡単な方法で共有したい場合の方法について調べたのでメモ代わりに記載。 今回の要件だとVNCが一番楽だった(サーバーにVNC導入済み)のでVNCを採用しましたが、Jitsi Meetは別の用途でも利用できるのでおフラン環境での構築手順のテストと導入した後の不具合がないことを確認して導入を行いたい。

検討対象

Jitsi Meet

どんな事ができるのか

  • メリット

    • ビデオ会議システム
    • dockerで比較的簡単に構築が可能
    • アカウント管理などは無い
    • 使い方は最近よくあるビデオ会議システムと変わらない
    • ブラウザだけで操作が完結し、クライアント側に特別なソフトのインストールが不要
  • デメリット

    • サーバーが必要(接続するユーザー数によっては高スペックを求められる?)
    • どれ位のサーバークライアント間でトラフィックが流れるのか要調査
    • セキュリティについては別途検討が必要

構築方法

上の構築手順で進めるとLet's Encryptで証明書を発行する必要があり、証明書を発行して設定をしなければマイクなどが利用できない状態となる。 オフライン環境で証明書の運用は少し面倒なので証明書を利用せずに接続時に警告が出るが無視して利用する。 ミーティングに参加した時に「あなたは切断されました」と出てうまく接続できないときは以下の設定を.envに追加する。 PROXY経由でWebSocketが予期せずに閉じられた場合に発生するエラーとなるので、明示的に無効化する。

ENABLE_XMPP_WEBSOCKET=0

使い方

VNC(色々な種類があるが一纏めにしてVNCとする)

どんなことができるのか

  • メリット
    • どんなOSでも導入が可能
    • 使い方が簡単
    • サーバー側で頑張ればHTTPでお話し(VNCに接続)ができるようになる
  • デメリット
    • 共有したいPC or サーバにVNCの導入が必要
    • 導入するには管理者権限が必要
    • 音声の転送は不可
    • 同時に数人であれば問題はないと思うけれども、数十人とかになるとサーバのスペックが高くないと辛いため、回避策としてTreeVNC http://www.cr.ie.u-ryukyu.ac.jp/software/TreeVNC.htmlというのがあるらしい(動作未確認)

構築方法

検索すればいくらでも構築方法が出てくるので飛ばします。

使い方

  • VNCサーバを起動して接続用パスワードを設定
  • VNC VIEWERを使ってサーバのIPアドレス:ポート番号で接続
    • 複数人から一つのVNCサーバに接続したいときは必要なだけサーバのIPアドレス:ポート番号で接続する

RealVNC日本語インストール版に付属のVNC VIEWERをつかってVNCサーバに接続すると2個目のセッションが接続されると1個目のセッションが切断される症状が出たので、クライアント側は別のVNC VIEWERを利用した方がいい。 UltraVNCのVNC VIEWERの場合2個目のセッションも問題なく接続ができた。

vscodeのMarket Placeから任意の拡張機能の任意のバージョンをダウンロードする

vscodeのMarket Placeから指定した拡張機能の古いバージョンがダウンロードできない

  • vscodeのMaket PlaceにあるVersion Historyは直近10個程しか古いバージョンへのリンクがないので、古いバージョンがほしいときに困ることがある。
  • ダウンロードできる方法がないか調べてみた。

古い拡張機能のダウンロード

vscodeのMakert Placeから任意の拡張の陰萎のバージョンの拡張機能をダウンロードするには以下の手順でURLを生成するとダウンロードすることができる。

URLの生成

  1. Version Historyからリンクされているどれか任意のファイルのURLをメモ帳などにコピーしつつ、どのバージョンでもいいのでファイルをダウンロードする。

  2. 今回は2022.15.12631011をダウンロード。

  3. ダウンロードされたファイル名
    ms-python.python-2022.15.12631011.vsix
  4. ダウンロードしたファイルのURL
    https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2022.15.12631011/vspackage

  5. ダウンロードしたファイル名を.と-の所で以下のように4分割する

  6. URLを置き換える

の様にすると任意のバージョンの拡張機能がダウンロードできる。

CentOS7 + vscode(remote development)でpythonのデバッガーが動かなくなった

CentOS7 + vscode(Remote Development) + python3.6.8でデバッガーが動作しない

vscodeのバージョンと一緒にpython拡張機能をv2022,8.1に上げた所、以下の画面でF5を押してもブレイクポイントで止まらないし、デバッガーも起動しない。

調べてみたvscodeで利用しているpython拡張機能のバージョンが上がるとpython 3.6.8のバージョンでは動作しなくなるらしい。

回避策として拡張機能のバージョンを下げると動作するようになるらしいので、python拡張機能のバージョンを下げる。

python(拡張機能)のバージョンを下げる

  • CentOS7 + python3.6.8の場合python(拡張機能)のバージョンがv2022.8.1まで下げた所デバッグ機能が動作するようになったのでバージョンの下げ方を記載する。

バージョンダウンの方法

  1. 拡張機能の一覧(ctrl+shift+x)を開いてpythonを選択。

  2. アンインストールの横のvを選択するとメニュー現れるので、その中から別バージョンをインストールを選択

  3. バージョンを選択する画面が現れるので、2022.8.1を選択
    バージョン番号の後ろの数字が2022.15.12631011の様な表示のものはプレリリースバージョン?の為、安定しているもののほうがいい場合は避けること。

  4. 最新版以外のバージョンをインストールした場合、最新版への更新を促す表示が出るので更新ボタンを押さないように注意してください。

  5. バージョンを下げると問題なくデバッグができるようになった。

拡張機能の自動更新を停止する

  • 拡張機能の自動更新を全て停止しても問題がない場合は拡張機能の一覧(ctrl+shift+x)から以下のように自動更新をなしにすることができます。

MySQLのprocesslistをコンソールに常時表示する

MySQLのprocesslistをコンソールに常時表示する

  • SQLAlchemy使ったテストをしている時にMySQLのセッション情報を確認をする事があり、その時に毎回コンソールから入ってコマンドを打つのが面倒だったので確認するシェルスクリプトを作ったのでメモ

事前準備

MySQLへ接続するための設定をファイルに書き出す

  • login-pathにはシェルスクリプトで接続先の名称として利用する名前を付ける
    今回はprocesslist
  • MySQLに接続するためのユーザーはshowuser
  • 設定ファイルにパスワードも書き出す(パスワードは暗号化されます)
$ mysql_config_editor set --login-path=processlist --user=showuser --password
Enter password:  ← パスワードを設定する

設定の確認

  • 設定が完了するとホームディレクトリに.mylogin.cnfというファイルができる
  • ファイルの中身は暗号化されているので見ることは出来ないがコマンドで中身を確認することができる
$ mysql_config_editor print --all
admin@ubuntu:~$ mysql_config_editor print --all
[processlist]
user = "showprocess"
password = *****

シェルスクリプトの作成

  • showprocess.sh
#!/bin/bash

while true
do
    clear
    /usr/bin/mysql --login-path=processlist -Dperformance_schema -e "select * from INFORMATION_SCHEMA.PROCESSLIST;"

    # ユーザー名もパスワードもスクリプト内に直書きで困らないなら上の行をコメントアウトして以下でも可
    /usr/bin/mysql -u showprocess -pPASSWORD -Dperformance_schema -e "select * from INFORMATION_SCHEMA.PROCESSLIST;"
 
    sleep 1
done
  • showprocess.sh

シェルスクリプトの実行

$ chmod 700 showprocess.sh
$ ./showprocesslist.sh
+------+-----------------+-----------+--------------------+---------+------+------------------------+----------------------------------------------+
| ID   | USER            | HOST      | DB                 | COMMAND | TIME | STATE                  | INFO                                         |
+------+-----------------+-----------+--------------------+---------+------+------------------------+----------------------------------------------+
| 1361 | showprocess     | localhost | performance_schema | Query   |    0 | executing              | select * from INFORMATION_SCHEMA.PROCESSLIST |
|    5 | event_scheduler | localhost | NULL               | Daemon  | 2698 | Waiting on empty queue | NULL                                         |
+------+-----------------+-----------+--------------------+---------+------+------------------------+----------------------------------------------+

Flask-Loginで認証が必要なAPIを作成する

Flask-Loginで認証が必要なAPIを作成する

Flask-Loginを使うと簡単にユーザーのログイン管理ができるのでとても便利

Flask-Loginの特徴

  • メリット
    • 簡単にログインが必要なAPIを作成できる
  • デメリット
    • ユーザーの情報はセッションcookieに保存されるので、サーバー側からユーザーをログアウトする事ができない
      ユーザーのcookieが盗まれた場合にcookieを無効化するにはSECRET_KEYを変更するしかないが、実用上は問題はないと思う

Flask-Loginのログイン処理に利用するメソッドなど

Flask-Loginのインストール

$ pip install flask-login

Flask-Loginを利用するための手順

  1. app.config["SECRET_KEY"]の作成
  2. login_managerの作成
  3. class User(UserMixin)の作成
  4. /login APIの作成
  5. ログインしていない場合エラーとなる/user APIとユーザー情報を読み込むuser_loaderの作成
  6. /logout APIの作成

1. app.config["SECRET_KEY"]の作成

  • Flask-LoginはFlask側ではユーザーのセッション情報を保持しない
  • ユーザーの情報はセッション cookieに保存されるのでcookieを改竄されるとログインができてしまうため、
    改ざんされないように暗号化をするのに利用する為のSECRET_KEYを作成して設定する
  • SECRET_KEYを毎回ランダムな値とした場合cookieからユーザー情報を復元するremenber meという機能が利用できなくなる
    • cookieの値をSECRET KEYと設定された値を使ってハッシュ化して作成しているので、SECRET_KEYを変更するとcookieの値と一致しなくなるため
# ランダムな文字列を生成する
# python -c 'import secrets; print(secrets.token_hex())'
app.config["SECRET_KEY"] = "ランダムな文字列"

2. login_managerの作成

  • Flask-LoginがユーザーIDからユーザー情報を復元する方法や、ログインする処理などをFlaskと連携するために利用するオブジェクトを作成する
from flask_login import LoginManager
from flask import Flask

app = Flask(__name__)
login_manager = LoginManager()
login_manager.init_app(app)

3. class User(UserMixin)の作成

  • Flask-Loginがユーザーを管理する際に利用するクラスを作成する
  • Userクラスに入れた情報はAPIの中でcurrent_userとして取得ができるようになるので、後で利用したい情報があればこのクラスに入れておく
  • 今回はユーザーを一意に特定できるuser_idとuser_nameを指定する
class User(UserMixin):
    def __init__(self, user_id: str) -> None:
        """user_idにはユーザーを一意に特定できる値を指定し、その他の項目は任意で設定する

        Args:
            user_id (str): ユーザーを一意に特定できる値
        """
        self.user_id = user_id

    def get_id(self):
        """このメソッドは必要に応じて定義する必要があり、デフォルトではself.idの値が返されるので、
        self.id以外の値を返したい場合はget_idのメソッドを上書きする
        self.get_idで返す値はユーザーを一意に特定できる値を返すこと
        仮に単一の項目では一意の値とならない場合は複数項目をTuple(ハッシュ化可能な値)で返すと良い

        def __init__(self, user_id:str, group_id:str):
            self.user_id = user_id
            self.group_id = group_id

        def get_id(self):
            return (self.user_id, self.grop_id)
        """
        return self.user_id

4. /login APIの作成

  • /loginにアクセスされた際に呼ばれるエンドポイント
  • getのurlパラメーターとしてuserName, passwordが渡される
  • user_authenticationに渡されたuserName, passwordに一致するデータを渡すと何らかのデータベース等から探して一致するデータあった場合、一致するデータを返す
    必要に応じてuser_authenticationの中身を書き換えると良い
# ここに定義されているuserIdはUSER_DATABASEないで一意の値となっていること
USER_DATABASE = [
    {"userId": "test-user", "password": "PASSWORD", "userName": "foo bar"},
    {"userId": "test-user2", "password": "PASSWORD2", "userName": "foo2 bar2"},
]
@app.route("/login", methods=["GET"])
def login():
    """GETのパラメータとして渡されたuserNameとpasswordと一致するデータを
    USER_DATABASEから探して一致するデータあればログインを許可。
    USER_DATABASEから取得したデータをレスポンスとして返す

    Raises:
        BadLoginReuquestError: userName, passwordの片方または両方が指定されていない場合
        LoginFailureError: USRE_DATABASEに一致するユーザーが存在しない場合

    Returns:
        Dict[str, str]: {"userId": ユーザーを一意に特定できる値, "userName": ユーザー名}
    """

    user_name = request.args.get("userName", None)
    password = request.args.get("password", None)

    if not all([user_name, password]):
        raise BadLoginReuquestError()

    auth_user = user_authentication(user_name, password)  # type: ignore

    if auth_user == {}:
        raise LoginFailureError()

    res = {"userId": auth_user["userId"], "userName": auth_user["userName"]}

    # 後でログインが必要なページにアクセスした際にユーザーを一意に特定できる値をUserオブジェクトに設定する
    # Userクラスに指定したuser_idの値がcookieに設定され、後で呼び出されるuser_loaderでuser_idが利用される
    user = User(auth_user["userId"])
    login_user(user, remember=True)

    return jsonify(res), 200
def user_authentication(user_name: str, password: str) -> Dict[str, str]:
    """user_nameとパスワードと一致するデータをUSER_DATABASEから探して一致するデータがあれば
    取得したユーザーのデータを返す

    Args:
        user_name (str): ユーザー名
        password (str): パスワード

    Returns:
        Dict[str, str]: 見つかったユーザー情報を返す、見つからない時は空のDictを返す

    Return Examples:
          match: {"uesrId": "test-user", "userName": "foo bar"}
       no match: {}
    """
    auth = [
        {"userId": value["userId"], "userName": value["userName"]}
        for value in USER_DATABASE
        if user_name == value["userName"] and password == value["password"]
    ]

    if auth == []:
        return {}

    return auth[0]

5. ログインしていない場合エラーとなる/user APIの作成

  • Flaskのエンドポイントに@login_requiredをつけるとログインしていない場合エラーとなるAPIを作ることができる
  • Flask内部での処理の順番
    1. /user にアクセスが来る
    2. @login_manager.user_loaderが呼び出される
    3. @login_manager.user_loaderの中でuser_idの値が渡されるので、user_idをもとにユーザー情報を取得して返す
    4. @login_manager.ueer_loaderからのレスポンスがNoneの場合はログインをしていないとして@login_manager.unauthorized_handlerが呼び出される
@app.route("/user", methods=["GET"])
@login_required
def show_user_data():
    """現在ログインしているユーザーの情報を返す"""
    user_name = current_user.user_name  # type: ignore
    user_id = current_user.user_id  # type: ignore

    res = {"userId": user_id, "userName": user_name}
    return jsonify(res), 200
  • @login_manager.user_loader
@login_manager.user_loader
def load_user(user_id: str):
    """ログインが必要なAPIにアクセスした際にcookieからuser_idを取得して
    ユーザーの情報を検索してAPIにユーザーの情報を返す
    見つからない時はNoneを返すこと

    Args:
        user_id (str): ユーザーを一意に特定できる値

    Returns:
        Union[User, None]: ユーザー情報を見つけた時はUserオブジェクトを見つからない時はNone
    """
    user_data = [value for value in USER_DATABASE if user_id == value["userId"]]

    if user_data == []:
        return None

    user = User(user_data[0]["userId"])

    return user
  • @login_manager.unauthorized_handler
@login_manager.unauthorized_handler
def unatuhorized_handler():
    error_msg = {"errorMessage": "ログインが必須のページです", "statusCode": 403}
    return jsonify(error_msg), 403

6. logout APIの作成

  • ユーザーのログアウトを行うにはlogout_user()を呼び出すとブラウザのcookieが削除される
@app.route("/logout", methods=["GET"])
def logout():
    """ユーザーをログアウトする"""
    logout_user()

    return jsonify({}), 200

Flaskのエラー処理

  • Flaskで例外が発生するとHTTPExceptionを基底とするエラーが発生するのでエラーハンドラーにHTTPExceptionを登録するとエラー処理がやりやすい
  • 他にもAPIで独自にExceptionを作成して@app.errorhandlerに登録するとエラー処理が一箇所で済むので見通しが良い
from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def http_error(e):
    """Flaskのエラー
    """
    error_msg = {"errorMessage": e.description, "statusCode": e.code}

    return jsonify(error_msg), e.code
  • /loginの所にあるLoginFailureErrorは以下のよう例外クラスを作成すると便利
  • API中で発生したAuthenticationError,BadLoginReuquestError,LoginFailureErrordef authentication_error(e)の所に飛んでくる
class AuthenticationError(Exception):
    """アプリケーション内で利用する基底のエラークラス"""

    msg = ""
    status_code = 500


class BadLoginReuquestError(AuthenticationError):
    msg = "userNameとpasswordの指定は必須です"
    status_code = 400


class LoginFailureError(AuthenticationError):
    msg = "userNameまたはpasswordが誤っています"
    status_code = 401
  • エラーハンドラーの作成
@app.errorhandler(AuthenticationError)
def authentication_error(e):
    error_msg = {"errorMessage": e.msg, "statusCode": e.status_code}
    return jsonify(error_msg), e.status_code

APIのソースとサンプルのリクエス

### LOGIN OK
GET http://127.0.0.1:5000/login?userName=foo%20bar&password=PASSWORD

### GET /user
GET http://127.0.0.1:5000/user

### GET /logout
GET http://127.0.0.1:5000/logout
import traceback
from typing import Dict

from flask import Flask, jsonify, request
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

# 日本語の文字化け対策
app.config["JSON_AS_ASCII"] = False

app.config["SECRET_KEY"] = "9abecf1b701c38448a21bbba5bad84d7c6e7e0255961a9e891d42efa8c989dcc"

login_manager = LoginManager()
login_manager.init_app(app)


# ここに定義されているuserIdはUSER_DATABASEないで一意の値となっていること
USER_DATABASE = [
    {"userId": "test-user1", "password": "PASSWORD", "userName": "foo bar"},
    {"userId": "test-user2", "password": "PASSWORD2", "userName": "foo2 bar2"},
]


class AuthenticationError(Exception):
    """アプリケーション内で利用する基底のエラークラス"""

    msg = ""
    status_code = 500


class BadLoginReuquestError(AuthenticationError):
    msg = "userNameとpasswordの指定は必須です"
    status_code = 400


class LoginFailureError(AuthenticationError):
    msg = "userNameまたはpasswordが誤っています"
    status_code = 401


@app.errorhandler(AuthenticationError)
def authentication_error(e):
    error_msg = {"errorMessage": e.msg, "statusCode": e.status_code}
    return jsonify(error_msg), e.status_code


def page_not_found(e):
    return "404 Not found", 404


@app.errorhandler(HTTPException)
def http_error(e):
    """Flaskのエラー

    Args:
        e (_type_): _description_

    Returns:
        _type_: _description_
    """
    error_msg = {"errorMessage": e.description, "statusCode": e.code}

    return jsonify(error_msg), e.code


@app.errorhandler(Exception)
def error_handler(e):
    info = traceback.format_exc()

    if not getattr(e, "args"):
        error_msg = {"errorMessage": "error", "traceBack": info, "statusCode": 500}
    elif getattr(e, "args") and isinstance(e.args, tuple):
        error_msg = {"errorMessage": e.args[0], "traceBack": info, "statusCode": 500}
    else:
        error_msg = {"errorMessage": "error", "traceBack": info, "statusCode": 500}

    return jsonify(error_msg), 500


@login_manager.unauthorized_handler
def unatuhorized_handler():
    error_msg = {"errorMessage": "ログインが必須のページです", "statusCode": 403}
    return jsonify(error_msg), 403


class User(UserMixin):
    def __init__(self, user_id: str) -> None:
        """user_idにはユーザーを一意に特定できる値を指定し、その他の項目は任意で設定する

        Args:
            user_id (str): ユーザーを一意に特定できる値
        """
        self.user_id = user_id

    def get_id(self):
        """このメソッドは必要に応じて定義する必要があり、デフォルトではself.idの値が返されるので、
        self.id以外の値を返したい場合はget_idのメソッドを上書きする
        self.get_idで返す値はユーザーを一意に特定できる値を返すこと
        仮に単一の項目では一意の値とならない場合は複数項目をTuple(ハッシュ化可能な値)で返すと良い

        def __init__(self, user_id:str, group_id:str):
            self.user_id = user_id
            self.group_id = group_id

        def get_id(self):
            return (self.user_id, self.grop_id)
        """
        return self.user_id


def user_authentication(user_name: str, password: str) -> Dict[str, str]:
    """user_nameとパスワードと一致するデータをUSER_DATABASEから探して一致するデータがあれば
    取得したユーザーのデータを返す

    Args:
        user_name (str): ユーザー名
        password (str): パスワード

    Returns:
        Dict[str, str]: 見つかったユーザー情報を返す、見つからない時は空のDictを返す

    Return Examples:
          match: {"uesrId": "test-user", "userName": "foo bar"}
       no match: {}
    """
    auth = [
        {"userId": value["userId"], "userName": value["userName"]}
        for value in USER_DATABASE
        if user_name == value["userName"] and password == value["password"]
    ]

    if auth == []:
        return {}

    return auth[0]


@login_manager.user_loader
def load_user(user_id: str):
    """user_idにはcookieから取得したclass Userのget_idで指定した値が入ってくる
    ログインが必要なAPIにアクセスした際にcookieからuser_idを取得して
    ユーザーの情報を検索してAPIにユーザーの情報を返す
    見つからない時はNoneを返すこと

    Args:
        user_id (str): ユーザーを一意に特定できる値

    Returns:
        Union[User, None]: ユーザー情報を見つけた時はUserオブジェクトを見つからない時はNone
    """
    user_data = [value for value in USER_DATABASE if user_id == value["userId"]]

    if user_data == []:
        return None

    user = User(user_data[0]["userId"])

    return user


@app.route("/login", methods=["GET"])
def login():
    """GETのパラメータとして渡されたuserNameとpasswordと一致するデータを
    USER_DATABASEから探して一致するデータあればログインを許可。
    USER_DATABASEから取得したデータをレスポンスとして返す

    Raises:
        BadLoginReuquestError: userName, passwordの片方または両方が指定されていない場合
        LoginFailureError: USRE_DATABASEに一致するユーザーが存在しない場合

    Returns:
        Dict[str, str]: {"userId": ユーザーを一意に特定できる値, "userName": ユーザー名}
    """

    user_name = request.args.get("userName", None)
    password = request.args.get("password", None)

    if not all([user_name, password]):
        raise BadLoginReuquestError()

    auth_user = user_authentication(user_name, password)  # type: ignore

    if auth_user == {}:
        raise LoginFailureError()

    res = {"userId": auth_user["userId"], "userName": auth_user["userName"]}

    # 後でログインが必要なページにアクセスした際にユーザーを一意に特定できる値をUserオブジェクトに設定する
    # Userクラスに指定したuser_idの値がcookieに設定され、後で呼び出されるuser_loaderでuser_idが利用される
    user = User(auth_user["userId"])
    login_user(user, remember=True)

    return jsonify(res), 200


@app.route("/logout", methods=["GET"])
def logout():
    """ユーザーをログアウトする"""
    logout_user()

    return jsonify({}), 200


@app.route("/user", methods=["GET"])
@login_required
def show_user_data():
    """現在ログインしているユーザーの情報を返す"""
    user_name = current_user.user_name  # type: ignore
    user_id = current_user.user_id  # type: ignore

    res = {"userId": user_id, "userName": user_name}
    return jsonify(res), 200


if __name__ == "__main__":
    app.run()

ldapsearchの使い方

ldapsearchの使い方

利用するテスト用のLDAPサーバ

  • 2022/07/21時点のデータでテスト
  • FreeIPAのhttps://www.freeipa.org/page/Demo
    • hostname: ipa.demo1.freeipa.org
    • user: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org
    • password: Secret123

接続テスト

  • 接続に成功した場合は以下のようなレスポンスが返って来ます

  • -h 接続するホスト名を指定

  • -D 認証情報を指定
  • -w パスワードを指定
    • -wの時はパスワードを指定する必要があるが、-Wにすると後で入力する対話モードになる
$ ldapsearch -h ipa.demo1.freeipa.org -D 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org' -w 'Secret123'
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# compat, demo1.freeipa.org
dn: cn=compat,dc=demo1,dc=freeipa,dc=org
objectClass: extensibleObject
cn: compat

# users, compat, demo1.freeipa.org
dn: cn=users,cn=compat,dc=demo1,dc=freeipa,dc=org
objectClass: extensibleObject
cn: users

# helpdesk, users, compat, demo1.freeipa.org
dn: uid=helpdesk,cn=users,cn=compat,dc=demo1,dc=freeipa,dc=org
objectClass: posixAccount
objectClass: ipaOverrideTarget
objectClass: top
gecos: Test Helpdesk
cn: Test Helpdesk
uidNumber: 613800004
gidNumber: 613800004
loginShell: /bin/sh
homeDirectory: /home/helpdesk
ipaAnchorUUID:: OlNJRDpTLTEtNS0yMS0xMDkyMTUyMjQ1LTI4MDExNTgzMDAtMzk4MzA2Njc3LT
 EwMDQ=
uid: helpdesk

# manager, users, compat, demo1.freeipa.org
dn: uid=manager,cn=users,cn=compat,dc=demo1,dc=freeipa,dc=org
objectClass: posixAccount
objectClass: ipaOverrideTarget
objectClass: top
gecos: Test Manager
cn: Test Manager
uidNumber: 613800001
gidNumber: 613800001
loginShell: /bin/sh
homeDirectory: /home/manager
ipaAnchorUUID:: OlNJRDpTLTEtNS0yMS0xMDkyMTUyMjQ1LTI4MDExNTgzMDAtMzk4MzA2Njc3LT
 EwMDE=
uid: manager


以下省略

# search result
search: 2
result: 0 Success

# numResponses: 1865
# numEntries: 1864
  • パスワードが間違っている場合はエラーの場合以下のような表示が出ます
ldap_bind: Invalid credentials (49)

ldapsearchで検索

単一項目で検索

  • objectClass: posixAccountと一致するデータを検索
$ ldapsearch -h ipa.demo1.freeipa.org -D 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org' -w 'Secret123' -b "dc=demo1,dc=freeipa,dc=org" "(objectClass=posixAccount)"
  • 12件のデータが返ってくる
# search result
search: 2
result: 0 Success

# numResponses: 13
# numEntries: 12

OR検索

  • objectClass: posixAccountと一致するデータを検索
$ ldapsearch -h ipa.demo1.freeipa.org -D 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org' -w 'Secret123' -b "dc=demo1,dc=freeipa,dc=org" "(|(uid=manager)(uid=admin))"
  • 6件のデータが返ってくる
# search result
search: 2
result: 0 Success

# numResponses: 7
# numEntries: 6

AND検索

  • uid: manager and cn: Test Manager
$ ldapsearch -h ipa.demo1.freeipa.org -D 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org' -w 'Secret123' -b "dc=demo1,dc=freeipa,dc=org" "(&(uid=manager)(cn=Test Manager))"
# search result
search: 2
result: 0 Success

# numResponses: 4
# numEntries: 3

python3 + ldap3 + Active Directory(LDAP)で認証を行う

Active Directory + python3でユーザー認証を行う

環境

構築した環境

Windows Server 2019

項目名 設定内容
IPv4アドレス 192.168.0.155/24
IPv6アドレス ネットワークのタブでチェックを外して未使用状態に変更
Active Directory ad.example.com

作成したユーザーとグループ

ユーザー名 所属グループ名 パスワード
test-user Domain Users, TestGroups PASSWORD
test-user2 TestSecondGroups PASSWORD
グループ名 所属グループ名
TestGroups Remote Desktop Users
TestSecondGroups
コンピューター名 備考欄
WIN10LTS Windows10をActive Directoryドメイン参加

登録ユーザー

  • test-user

  • test-user2

  • TestGroups

  • TestSecondGroups

Windows Server 2019上でユーザーの登録内容の確認

  • PowerShelleを起動して以下のコマンドを入力
PS C:\Users\Administrator> Get-ADUser -Filter { SamAccountName -eq "test-user" } -Properties *


AccountExpirationDate                : 
accountExpires                       : 9223372036854775807
AccountLockoutTime                   : 
AccountNotDelegated                  : False
AllowReversiblePasswordEncryption    : False
AuthenticationPolicy                 : {}
AuthenticationPolicySilo             : {}
BadLogonCount                        : 0
badPasswordTime                      : 0
badPwdCount                          : 0
CannotChangePassword                 : True
CanonicalName                        : ad.example.com/Users/FirstName LastName イニシャル.
Certificates                         : {}
City                                 : 
CN                                   : FirstName LastName イニシャル.
codePage                             : 0
Company                              : 
CompoundIdentitySupported            : {}
Country                              : 
countryCode                          : 0
Created                              : 2022/07/19 20:44:21
createTimeStamp                      : 2022/07/19 20:44:21
Deleted                              : 
Department                           : 
Description                          : 
DisplayName                          : FirstName LastName イニシャル.
DistinguishedName                    : CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com
Division                             : 
DoesNotRequirePreAuth                : False
dSCorePropagationData                : {2022/07/19 20:44:21, 1601/01/01 9:00:00}
EmailAddress                         : 
EmployeeID                           : 
EmployeeNumber                       : 
Enabled                              : True
Fax                                  : 
GivenName                            : LastName
HomeDirectory                        : 
HomedirRequired                      : False
HomeDrive                            : 
HomePage                             : 
HomePhone                            : 
Initials                             : イニシャル
instanceType                         : 4
isDeleted                            : 
KerberosEncryptionType               : {}
LastBadPasswordAttempt               : 
LastKnownParent                      : 
lastLogoff                           : 0
lastLogon                            : 0
LastLogonDate                        : 
LockedOut                            : False
logonCount                           : 0
LogonWorkstations                    : 
Manager                              : 
MemberOf                             : {CN=TestGroups,CN=Users,DC=ad,DC=example,DC=com}
MNSLogonAccount                      : False
MobilePhone                          : 
Modified                             : 2022/07/19 20:44:21
modifyTimeStamp                      : 2022/07/19 20:44:21
msDS-User-Account-Control-Computed   : 0
Name                                 : FirstName LastName イニシャル.
nTSecurityDescriptor                 : System.DirectoryServices.ActiveDirectorySecurity
ObjectCategory                       : CN=Person,CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
ObjectClass                          : user
ObjectGUID                           : 1248700d-e14c-4589-8408-b44298854cbb
objectSid                            : S-1-5-21-4206833561-140562442-4113086211-1610
Office                               : 
OfficePhone                          : 
Organization                         : 
OtherName                            : 
PasswordExpired                      : False
PasswordLastSet                      : 2022/07/19 20:44:21
PasswordNeverExpires                 : True
PasswordNotRequired                  : False
POBox                                : 
PostalCode                           : 
PrimaryGroup                         : CN=Domain Users,CN=Users,DC=ad,DC=example,DC=com
primaryGroupID                       : 513
PrincipalsAllowedToDelegateToAccount : {}
ProfilePath                          : 
ProtectedFromAccidentalDeletion      : False
pwdLastSet                           : 133027046616261250
SamAccountName                       : test-user
sAMAccountType                       : 805306368
ScriptPath                           : 
sDRightsEffective                    : 15
ServicePrincipalNames                : {}
SID                                  : S-1-5-21-4206833561-140562442-4113086211-1610
SIDHistory                           : {}
SmartcardLogonRequired               : False
sn                                   : FirstName
State                                : 
StreetAddress                        : 
Surname                              : FirstName
Title                                : 
TrustedForDelegation                 : False
TrustedToAuthForDelegation           : False
UseDESKeyOnly                        : False
userAccountControl                   : 66048
userCertificate                      : {}
UserPrincipalName                    : test-user@ad.example.com
uSNChanged                           : 28775
uSNCreated                           : 28767
whenChanged                          : 2022/07/19 20:44:21
whenCreated                          : 2022/07/19 20:44:21




PS C:\Users\Administrator> 

Active DirectoryからLDAPで検索

ldapsearchでLDAPを検索してみる

  • CentOS 7ならyum install openldap-clientを入れると利用できる
    • 何のパッケージに入っているのかわからないときはyum provides ldapsearchみたいにすると良い

接続法 その1

  • ldapsearchを使って接続する場合はPowerShellの結果に含まれるDistinguishedNameを利用する
DistinguishedName                    : CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com
  • -h は接続先のIPアドレス
  • -D はActive Direcotryへ接続するユーザー名
  • -w はActive Directoryへ接続するユーザーのパスワード

  • 成功した場合は以下のようなレスポンスが返ってくる

$ ldapsearch -h 192.168.0.155 -D 'CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com' -w "PASSWORD"
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object
text: 0000208D: NameErr: DSID-03100220, problem 2001 (NO_OBJECT), data 0, best
 match of:
        ''


# numResponses: 1
  • パスワードを間違えた場合やユーザーが見つからないなとの場合以下のようなエラーが発生する
$ ldapsearch -h 192.168.0.155 -D 'CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com' -w "PASSWORDZ"
ldap_bind: Invalid credentials (49)
        additional info: 80090308: LdapErr: DSID-0C090439, comment: AcceptSecurityContext error, data 52e, v4563

接続法 その2

  • こちらのほうがわかりやすくて良いかも
$ ldapsearch -h 192.168.0.155 -D 'test-user@ad.example.com' -w 'PASSWORD'

python3 + ldap3でLDAPで認証

# pip install ldap3

python3 + ldap3で認証テスト

  • ConnectionでLDAPのサーバへ接続する
    • user=ドメイン名\ユーザー名
    • password=パスワード
    • authentication=NTML(NTLM形式で認証を行う)
    • auto_bind=True(サーバへ接続を自動的に行う)
    • read_ony=読み込み専用モードで接続する
  • conn.extend.standard.who_am_i
    • 接続したユーザーが誰かをLDAPのサーバに問い合わせる
    • 成功した場合u:AD-SERVER\test-userの様な値が返ってくる
from ldap3 import ALL, NTLM, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPBindError


SERVER = "192.168.0.155"
USERNAME = "ad.example.com\\test-user"
PASSWORD = "PASSWORD"


try:
    server = Server(SERVER, get_info=ALL)

    # サーバーへの接続に失敗した場合Raiseする
    # connには接続に使用した情報が全て含まれている
    conn = Connection(server, user=USERNAME, password=PASSWORD, authentication=NTLM, auto_bind=True, read_only=True)

    # 接続に成功するとauthにu:AD-SERVER\test-userが入る
    auth = conn.extend.standard.who_am_i()

    # u:AD-SERVER\test-user
    print(auth)

    # ldap://192.168.0.155:389 - cleartext - user: ad.example.com\test-user - not lazy - bound - open - <local: 192.168.0.50:63201 - remote: 192.168.0.155:389> - tls not started - listening - SyncStrategy - internal decoder
    print(conn)

    # DSA info (from DSE):
    # Supported LDAP versions: 3, 2
    # Naming contexts:
    #     DC=ad,DC=example,DC=com
    #     CN=Configuration,DC=ad,DC=example,DC=com
    #     CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    #     DC=DomainDnsZones,DC=ad,DC=example,DC=com
    #     DC=ForestDnsZones,DC=ad,DC=example,DC=com
    # Supported controls:
    #     1.2.840.113556.1.4.1338 - Verify name - Control - MICROSOFT
    #     1.2.840.113556.1.4.1339 - Domain scope - Control - MICROSOFT
    #     1.2.840.113556.1.4.1340 - Search options - Control - MICROSOFT
    #     1.2.840.113556.1.4.1341 - RODC DCPROMO - Control - MICROSOFT
    #     1.2.840.113556.1.4.1413 - Permissive modify - Control - MICROSOFT
    #     1.2.840.113556.1.4.1504 - Attribute scoped query - Control - MICROSOFT
    #     1.2.840.113556.1.4.1852 - User quota - Control - MICROSOFT
    #     1.2.840.113556.1.4.1907 - Server shutdown notify - Control - MICROSOFT
    #     1.2.840.113556.1.4.1948 - Range retrieval no error - Control - MICROSOFT
    #     1.2.840.113556.1.4.1974 - Server force update - Control - MICROSOFT
    #     1.2.840.113556.1.4.2026 - Input DN - Control - MICROSOFT
    #     1.2.840.113556.1.4.2064 - Show recycled - Control - MICROSOFT
    #     1.2.840.113556.1.4.2065 - Show deactivated link - Control - MICROSOFT
    #     1.2.840.113556.1.4.2066 - Policy hints [DEPRECATED] - Control - MICROSOFT
    #     1.2.840.113556.1.4.2090 - DirSync EX - Control - MICROSOFT
    #     1.2.840.113556.1.4.2204 - Tree deleted EX - Control - MICROSOFT
    #     1.2.840.113556.1.4.2205 - Updates stats - Control - MICROSOFT
    #     1.2.840.113556.1.4.2206 - Search hints - Control - MICROSOFT
    #     1.2.840.113556.1.4.2211 - Expected entry count - Control - MICROSOFT
    #     1.2.840.113556.1.4.2239 - Policy hints - Control - MICROSOFT
    #     1.2.840.113556.1.4.2255 - Set owner - Control - MICROSOFT
    #     1.2.840.113556.1.4.2256 - Bypass quota - Control - MICROSOFT
    #     1.2.840.113556.1.4.2309
    #     1.2.840.113556.1.4.2330
    #     1.2.840.113556.1.4.2354
    #     1.2.840.113556.1.4.319 - LDAP Simple Paged Results - Control - RFC2696
    #     1.2.840.113556.1.4.417 - LDAP server show deleted objects - Control - MICROSOFT
    #     1.2.840.113556.1.4.473 - Sort Request - Control - RFC2891
    #     1.2.840.113556.1.4.474 - Sort Response - Control - RFC2891
    #     1.2.840.113556.1.4.521 - Cross-domain move - Control - MICROSOFT
    #     1.2.840.113556.1.4.528 - Server search notification - Control - MICROSOFT
    #     1.2.840.113556.1.4.529 - Extended DN - Control - MICROSOFT
    #     1.2.840.113556.1.4.619 - Lazy commit - Control - MICROSOFT
    #     1.2.840.113556.1.4.801 - Security descriptor flags - Control - MICROSOFT
    #     1.2.840.113556.1.4.802 - Range option - Control - MICROSOFT
    #     1.2.840.113556.1.4.805 - Tree delete - Control - MICROSOFT
    #     1.2.840.113556.1.4.841 - Directory synchronization - Control - MICROSOFT
    #     1.2.840.113556.1.4.970 - Get stats - Control - MICROSOFT
    #     2.16.840.1.113730.3.4.10 - Virtual List View Response - Control - IETF
    #     2.16.840.1.113730.3.4.9 - Virtual List View Request - Control - IETF
    # Supported extensions:
    #     1.2.840.113556.1.4.1781 - Fast concurrent bind - Extension - MICROSOFT
    #     1.2.840.113556.1.4.2212 - Batch request - Extension - MICROSOFT
    #     1.3.6.1.4.1.1466.101.119.1 - Dynamic Refresh - Extension - RFC2589
    #     1.3.6.1.4.1.1466.20037 - StartTLS - Extension - RFC4511-RFC4513
    #     1.3.6.1.4.1.4203.1.11.3 - Who am I - Extension - RFC4532
    # Supported features:
    #     1.2.840.113556.1.4.1670 - Active directory V51 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.1791 - Active directory LDAP Integration - Feature - MICROSOFT
    #     1.2.840.113556.1.4.1935 - Active directory V60 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.2080 - Active directory V61 R2 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.2237 - Active directory W8 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.800 - Active directory - Feature - MICROSOFT
    # Supported SASL mechanisms:
    #     GSSAPI, GSS-SPNEGO, EXTERNAL, DIGEST-MD5
    # Schema entry:
    #     CN=Aggregate,CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    # Other:
    # domainFunctionality:
    #     7
    # forestFunctionality:
    #     7
    # domainControllerFunctionality:
    #     7
    # rootDomainNamingContext:
    #     DC=ad,DC=example,DC=com
    # ldapServiceName:
    #     ad.example.com:win-2fechokc8i8$@AD.EXAMPLE.COM
    # isGlobalCatalogReady:
    #     TRUE
    # supportedLDAPPolicies:
    #     MaxPoolThreads
    #     MaxPercentDirSyncRequests
    #     MaxDatagramRecv
    #     MaxReceiveBuffer
    #     InitRecvTimeout
    #     MaxConnections
    #     MaxConnIdleTime
    #     MaxPageSize
    #     MaxBatchReturnMessages
    #     MaxQueryDuration
    #     MaxDirSyncDuration
    #     MaxTempTableSize
    #     MaxResultSetSize
    #     MinResultSets
    #     MaxResultSetsPerConn
    #     MaxNotificationPerConn
    #     MaxValRange
    #     MaxValRangeTransitive
    #     ThreadMemoryLimit
    #     SystemMemoryLimitPercent
    # serverName:
    # CN=WIN-2FECHOKC8I8,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=example,DC=com
    # schemaNamingContext:
    #     CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    # isSynchronized:
    #     TRUE
    # highestCommittedUSN:
    #     28797
    # dsServiceName:
    #     CN=NTDS Settings,CN=WIN-2FECHOKC8I8,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=example,DC=com
    # dnsHostName:
    # WIN-2FECHOKC8I8.ad.example.com
    # defaultNamingContext:
    #     DC=ad,DC=example,DC=com
    # currentTime:
    #     20220719123933.0Z
    # configurationNamingContext:
    #     CN=Configuration,DC=ad,DC=example,DC=com#
    print(server.info)

    entories = conn.extend.standard.paged_search(
        "dc=ad,dc=example,dc=com",
        "(objectclass=person)",
        attributes=["cn", "displayName", "description"],
        paged_size=100,
    )

    # generatorが返ってくる
    print(entories)

    list_entries = list(entories)

    for entry in list_entries:
        if entry.get("attributes", None) is None:
            continue

        # {'cn': 'FirstName LastName イニシャル.', 'description': ['説明'], 'displayName': 'FirstName LastName イニシャル.'}
        # {'cn': 'admin', 'displayName': 'admin', 'description': []}
        # {'cn': 'WIN10LTS', 'displayName': [], 'description': []}
        # {'cn': 'krbtgt', 'description': ['キー配布センター サービス アカウント'], 'displayName': []}
        # {'cn': 'WIN-2FECHOKC8I8', 'displayName': [], 'description': []}
        # {'cn': 'Guest', 'description': ['コンピューター/ドメインへのゲスト アクセス用 (ビルトイン アカウント)'], 'displayName': []}
        # {'cn': 'Administrator', 'description': ['コンピューター/ドメインの管理用 (ビルトイン アカウント)'], 'displayName': []}
        print(entry["attributes"])
except LDAPBindError as e:
    # 認証エラーが発生した場合ここに来る
    # automatic bind not successful - invalidCredentials
    print(e)

python3 + ldap3でLDAPを検索

  • ConnectionでLDAPのサーバに接続した後でconn.extend.standard.paged_searchを使って検索を行う
    この時に利用できる条件などはldapsearchで利用できる検索条件と同等なため、毎回pythonを実行するよりldapsearchで条件を決めてからpythonに組み込んだほうが楽だと思う。
  • attibutes=["*"]としているので取得できるすべての項目が取得される。
    attributes=["cn", "displayName", "description"]の様にすると記載した3個の条件に一致する項目だけが取得できる。
from ldap3 import ALL, NTLM, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPBindError

SERVER = "192.168.0.155"
USERNAME = "ad.example.com\\test-user"
PASSWORD = "PASSWORD"

try:
    server = Server(SERVER, get_info=ALL)
    conn = Connection(
        server,
        user=USERNAME,
        password=PASSWORD,
        authentication=NTLM,
        auto_bind=True,
        read_only=True,
    )

    entories = conn.extend.standard.paged_search(
        "dc=ad,dc=example,dc=com",
        "(&(objectclass=person)(sAMAccountName=test-user))",
        attributes=["*"],
        paged_size=100,
    )

    for entry in entories:
        if entry.get("attributes", None) is None:
            continue

        print(entry["attributes"])
except LDAPBindError as e:
    print(e)

ldapsearchでの検索方法

ldapsearchでActive Directoryを検索

まずは全検索

MaxPageSize - この値は、返される各オブジェクトのサイズに依存して、1 つの検索結果で返されるオブジェクトの最大数を制御します。 この数を超える可能性がある検索を実行するには、クライアントがページ検索コントロールを指定する必要があります。 返される結果を、MaxPageSize 値より大きいグループにグループ化します。 要約すると、MaxPageSize は、1 つの検索結果で返されるオブジェクトの数を制御します。

既定値: 1,000

  • 検索コマンド
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com"

検索結果

$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com"
# extended LDIF
#
# LDAPv3
# base <cn=Users,dc=ad,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
...省略

# FirstName LastName \E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB., Users, ad.example.com

...省略
  • FiresName Lastnameの所で# FirstName LastName \E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB., Users, ad.の様に表示されている部分はデコードすると「イニシャル」になる

  • bytes.fromhex(r"\E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB".replace("\\", "")).decode('utf-8')

単一項目で絞って検索する

  • objectclassがpersonの物を検索
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "objectclass=person"

複数項目(AND)で絞って検索する

  • objectclassがperson and sAMAccountNameがtest-user
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "(&(objectclass=person)(sAMAccountName=test-user))"

複数項目(OR)で絞って検索

  • UserPrincipalNameがtest-user or UserPrincipalNameがtest-user2のものを検索
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "(|(UserPrincipalName=test-user@ad.example.com)(UserPrincipalName=test-user2@ad.example.com))"