2023年3月31日金曜日

Powershellから言語パックを英語削除する方法

Powershellから言語パックを英語削除する方法

概要

なぜか定期的に復活する言語パックをスクリプトで削除する方法を紹介します

環境

  • Windows 10

スクリプト

  • vim remove_us_langpack.ps1
$LangList = Get-WinUserLanguageList
$MarkedLang = $LangList | Where-Object {$_.LanguageTag -eq "en-US"}
if($MarkedLang -ne $null) {
    $LangList.Remove($MarkedLang)
    Set-WinUserLanguageList -LanguageList $LangList -Force
}

C:\Users\username 配下に置くと実行が楽になります

実行方法

コマンドプロンプトを起動して以下を実行します

  • powershell -NoProfile -ExecutionPolicy Unrestricted .\remote_us_langpack.ps1

最後に

ダブルクリック実行する場合は C:\Users\username\remove_us_langpack.ps1 のショートカットを作成しプロパティからリンク先を

powershell -ExecutionPolicy RemoteSigned -File C:\Users\username\remove_us_langpack.ps1

にしてショートカットをダブルクリックすれば OK です

参考サイト

2023年3月29日水曜日

証明書の内容をbase64化してシェルスクリプト内で使用する方法

証明書の内容をbase64化してシェルスクリプト内で使用する方法

概要

タイトルの通りです

環境

  • Ubuntu 18.04

追記: こっちのほうがシンプル

(コメントありがとうございます)

curl -XPOST 'http://localhost:8080/cert' -H 'Content-Type: application/json' \
-d @- << EOF
{
  "certificate": "$(base64 -w 0 /path/to/certs/fullchain.pem)",
  "key": "$(base64 -w 0 /path/to/certs/privkey.pem)"
}
EOF

サンプルコード

curl -XPOST 'http://localhost:8080/cert' -H 'Content-Type: application/json' \
-d @- << EOF
{
  "certificate": "$(cat /path/to/certs/fullchain.pem | base64 | tr -d '\n')",
  "key": "$(cat /path/to/certs/privkey.pem | base64 | tr -d '\n')"
}
EOF

base64 後に改行コードをなくすのがポイントです

2023年3月28日火曜日

pytest-covでファイルや関数を無視する方法

pytest-covでファイルや関数を無視する方法

概要

カバレッジの計算からは外したい場合などがあると思います
その方法をいろいろ紹介します

環境

  • Ubuntu 18.04
  • Python 3.10.2
  • pytest-cov 2.10.1

.coveragerc

[report]
exclude_lines =
    if __name__ == .__main__.:
    pragma: no cover
omit =
    path/to/file/mymodule.py
    path/to/file/mymodule2.py

ちょっと解説

if __name__ はメイン関数の実行をテスト対象にしたくない場合に使います
if __name__ 配下でコールされているメソッドやクラスはすべてテストのカバレッジ対象から外れます

pragma: no cover は除外したい関数があるときに便利です
関数の名前の横にコメントで # pragma: no cover とすることでその関数をカバレッジの対象から除外できます
この方法の場合大本の関数を除外してもその関数内でコールされている他の関数は対象になるのですべて除外したい場合はすべてにコメントを入れる必要があります

omit を使ってファイルを指定するとそのファイル自体をすべて除外することができます
ファイルは複数指定することが可能です

参考サイト

2023年3月27日月曜日

redis-server で Warning: Could not create server TCP listening socket #:6379: nodename nor servname provided, or not known

redis-server で Warning: Could not create server TCP listening socket #:6379: nodename nor servname provided, or not known

概要

Mac 上の Redis で bind するアドレスを 127.0.0.1 以外に変更しようとした際に発生したので対処方法を紹介します

環境

  • macOS 11.7.4
  • redis-server 7.0.9

解決方法: bind に必ず 127.0.0.1 を付与する

  • vim /usr/local/etc/redis.conf
bind 0.0.0.0 127.0.0.1

おまけ: プロテクションモードを無効にする

  • CONFIG SET protected-mode no

これをしないと celery.flower で DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. が発生します

最後に

redis のバージョンを 7 に上げたら発生したので最新バージョンの仕様なのかもしれません

2023年3月24日金曜日

Macにfaceswapをインストールする方法

Macにfaceswapをインストールする方法

概要

Intel Mac に faceswap をインストールしてみました
GPU はオンボードなので CUDA は使えない環境ですがインストールできるようです

環境

  • macOS 11.7.4
  • プロセッサ 1.7 GHz デュアルコアIntel Core i7
  • Anaconda Navigator 2.4.0
    • Python 3.8.16
  • faceswap 216ef387636eb7b84819c1b77d9a2f631ed97ab5 (2023/03/22 時点)

XQuartz のインストール

  • brew install xquartz

Mac を再起動します

Anaconda のインストール

  • brew install anaconda

環境の作成

  • Anaconda Navigator を開く
  • 左メニューの Environments を開く
  • 下の Create を選択
    • Name -> faceswap
    • Python -> 3.8.16
  • 作成された環境を Open Terminal で開く

faceswap のインストール

  • git clone --depth 1 https://github.com/deepfakes/faceswap.git
  • cd faceswap
  • python setup.py
INFO     Running without root/admin privileges
INFO     The tool provides tips for installation and installs required python packages
INFO     Setup in Darwin 20.6.0
INFO     Installed Python: 3.8.16 64bit
INFO     Running in Conda
INFO     Running in a Virtual Environment
INFO     Encoding: UTF-8
INFO     Installed pip: 23.0.1
INFO     AMD Support:
         This version is deprecated and will be removed from a future update.
         Nvidia Users MUST answer 'no' to this option.
Enable AMD Support? [y/N] N
Enable  Docker? [y/N] N
INFO     Docker Disabled
Enable  CUDA? [Y/n] n
INFO     Skipping ROCm checks as not enabled
INFO     Faceswap config written to: /Users/username/faceswap/config/.faceswap
Please ensure your System Dependencies are met
Continue? [y/N] y
Traceback (most recent call last):
  File "setup.py", line 1615, in <module>
    Install(ENV)
  File "setup.py", line 968, in __init__
    self._packages.get_required_packages()
  File "setup.py", line 447, in get_required_packages
    with open(requirements_file, encoding="utf8") as req:
FileNotFoundError: [Errno 2] No such file or directory: '/Users/username/faceswap/requirements/requirements_None.txt'

amd, docker, cuda はすべて No にします
エラーになるので手動で必要なものをインストールします
また CPU をバックグランドとして動作させるように設定ファイルを変更します

  • conda install tk
  • pip install -r ./requirements/requirements_cpu.txt
  • vim config/.faceswap
{"backend": "cpu"}

起動

  • python faceswap.py gui

GUI が起動しない場合は XQuartz がちゃんとインストールされているか、その後 Mac を再起動したか確認してください

また必要なライブラリがしっかりインストールされているかと環境がちゃんと Anaconda 経由で起動したターミナル上で実行されているか確認してください

最後に

Intel Mac に faceswap をインストールしてみました
M1 Mac だと別のインストール方法になりそうです

基本は Python が動作する環境であれば動くはずです
次回は faceswap の簡単な使い方を紹介します

参考サイト

2023年3月23日木曜日

isortでmulti_line_outputが効かないときの対処方法

isortでmulti_line_outputが効かないときの対処方法

概要

multi_line_output は import 文を複数行にわけてフォーマットしてくれるオプションです
なぜか効かないケースがあったので対処方法を紹介します

環境

  • Python 3.10.2
  • isort 5.12.0

.isort.cfg

[settings]
multi_line_output = 3
include_trailing_comma = true

コマンド実行

  • pipenv run isort --force-grid-wrap=2 hoge.py

ポイントは --force-grid-wrap=2 を付与して実行する点です
2列に強制するオプションですが multi_line_output と組み合わせることで強制的に改行を行わせることができます

うまく動作しない場合には試してみてください
なお --force-grid-wrap オプションは CLI の引数のみ提供されているので設定ファイルには記載できません

2023年3月22日水曜日

MySQLのAES_ENCRYPTを SQLAlchemy で使う方法

MySQLのAES_ENCRYPTを SQLAlchemy で使う方法

概要

前回 Python 側で暗号化してデータを保存する方法を紹介しました
今回は MySQL 自体が提供する暗号化用の関数 aes_encrypt を sqlalchemy から使ってみました

環境

  • macOS 11.7.4
  • Python 3.10.2
  • Flask 2.2.3
  • Flask-SQLAlchemy 3.0.3
  • Flask-marshmallow 0.14.0
  • Flask-Migrate 4.0.4
  • SQLAlchemy 2.0.7

サンプルコード

  • vim app.py
from flask import (Flask,
                   request,
                   jsonify)
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from sqlalchemy import cast
from sqlalchemy.dialects.mysql import CHAR


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
migrate = Migrate(app, db)
ma = Marshmallow(app)
key = "xxx"


class EncryptedUser(db.Model):

    id = db.Column(db.Integer, primary_key=True, autoincrement="auto")
    name = db.Column(db.String(50), nullable=False)
    password = db.Column(db.String(50), nullable=False)

    debug_password = db.column_property(
        cast(
            db.func.aes_decrypt(
                db.func.unhex(password), db.func.unhex(db.func.sha2(key, 512))
            ),
            CHAR(charset="utf8"),
        )
    )

    def __init__(self, name, password):
        self.name = name
        self.password = db.func.hex(
            db.func.aes_encrypt(
                password, db.func.unhex(db.func.sha2(key, 512))
            )
        )


class EncryptedUserSchema(ma.Schema):

    class Meta:
        fields = ("id", "name", "password", "debug_password")


@app.route('/insert')
def insert():
    name = request.args.get('name', 'default_user')
    password = request.args.get('password', 'default_password')
    user = EncryptedUser(name=name, password=password)
    db.session.add(user)
    db.session.commit()
    return jsonify({'status': 'ok'})


@app.route('/get')
def get():
    users = EncryptedUserSchema(many=True).dump(EncryptedUser.query.all())
    return jsonify({'users': users})

解説

MySQL 側の関数を直接使う場合には db.func を通してコールします

保存する場合に db.func.aes_encrypt を使い暗号化し取得する場合には db.func.aes_decrypt を使って復号化します
機能はどちらも SQLAlchemy の機能なので上記らのメソッドを扱う場合は marshmallow 側ではなく db.Model 側で扱う必要があります

今回はデバッグとして column_property を使って取得専用のフィールドを定義しています
aes_decrypt 後は Python で扱えるように文字列に変換する必要があります
convert 関数でもいいのですが db.func.convert が使えないの (using utf8 が使えない) ので今回は cast 関数を使っています
また cast 関数だけ直接 SQLAlchemy パッケージを参照していますがうまくやれば db.func からでも参照できるかもしれません

SQL の場合には

当然 SQL を直接コールすることもできます
select の場合には convert or cast 関数どちらかが選択できます

  • insert
insert into encrypted_user values (null, 'hawk', hex(AES_ENCRYPT('fuga', UNHEX(SHA2('xxx',512)))));
  • select
select convert(AES_DECRYPT(unhex(password), UNHEX(SHA2('xxx',512))) USING utf8) from encrypted_user;

or

select CAST(aes_decrypt(unhex(password), unhex(sha2('xxx',512))) AS CHAR CHARACTER SET utf8) from encrypted_user;

最後に

MySQL の暗号化機能を SQLAlchemy で扱う方法を紹介しました
データベース側の機能を使うことで Python 側で暗号化復号化を考える必要がないのとカラムの型も単純な文字列として定義できるのでコードはシンプルになります

レスポンススキーマ側ではすでに復号化されたデータを扱うことになるのでシンプルになります

2023年3月21日火曜日

SQLAlchemyのEncriptedTypeを使ってみた

SQLAlchemyのEncriptedTypeを使ってみた

概要

sqlalchemy-utils という拡張に EncryptedType があったので使ってみました
データを暗号化して保存したい場合に便利です

今回は Flask 環境下で使用しています

環境

  • macOS 11.7.4
  • Python 3.10.2
  • Flask 2.2.3
  • Flask-SQLAlchemy 3.0.3
  • Flask-marshmallow 0.14.0
  • Flask-Migrate 4.0.4
  • SQLAlchemy 2.0.7
  • SQLAlchemy-Utils 0.40.0
  • cryptography 39.0.2

サンプルコード

  • vim app.py
from flask import (Flask,
                   request,
                   jsonify)
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from cryptography.fernet import Fernet
from sqlalchemy_utils import EncryptedType

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
migrate = Migrate(app, db)
ma = Marshmallow(app)
# 暗号化キーはアプリ起動ごとに変わる
key = Fernet.generate_key()


class EncryptedUser(db.Model):

    id = db.Column(db.Integer, primary_key=True, autoincrement="auto")
    name = db.Column(db.String(50), nullable=False)
    password = db.Column(EncryptedType(db.String, key), nullable=True)

    def __init__(self,
                 name: str,
                 password: str):
        cipher_suite = Fernet(key)
        self.name = name
        # 文字列のpasswordをバイトに変換しencryptで暗号化、戻っていた暗号化データのバイトを文字列に変換してデータベースに保存
        self.password = cipher_suite.encrypt(bytes(password, "utf-8")).decode("utf-8")


class EncryptedUserSchema(ma.Schema):

    class Meta:
        fields = ("id", "name", "password")

    def decode_password(self, obj) -> str:
        cipher_suite = Fernet(key)
        # 暗号化された文字列データをバイトに変換しdecryptで復号化、戻ってきたバイトデータを文字列にしてレスポンスで返却
        return cipher_suite.decrypt(bytes(obj.password, "utf-8")).decode("utf-8")
        # 暗号化したデータのままレスポンスに含めたい場合は以下の通り
        # return bytes(obj.password, "utf-8").decode("utf-8")

    password = ma.Method("decode_password")


@app.route('/insert')
def insert():
    name = request.args.get('name', 'default_user')
    password = request.args.get('password', 'default_password')
    user = EncryptedUser(name=name, password=password)
    db.session.add(user)
    db.session.commit()
    return jsonify({'status': 'ok'})


@app.route('/get')
def get():
    users = EncryptedUserSchema(many=True).dump(EncryptedUser.query.all())
    return jsonify({'users': users})

ちょっと解説

アプリのコードやマイグレーションのコード、レスポンススキーマのコードがすべて含まれています

EncryptedType カラムとして定義した場合にはコンストラクタを定義して該当のカラムの暗号化方法を定義します
今回は単純な共通鍵での暗号化 (AES) を行います
暗号化する場合は cipher_suite.encrypt を使います
今回は Flask アプリで扱うので最終的な入力や出力はすべて文字列ですが暗号化/復号化で扱うデータはすべてバイトになるので変換してあげます
復号化する場合は cipher_suite.decrypt を使います
MySQL から取得した暗号化データは文字列になっているのでバイトに変換し復号化したあとで再度レスポンス用に文字列に変換してあげましょう

MySQL 上での実態は blob 型として保存されています
当然データは鍵で暗号化されているので文字列に変換してもどんなデータ化はわからないようになっています

% mysql -u root test -e "show create table encrypted_user \G"                     
*************************** 1. row ***************************
       Table: encrypted_user
Create Table: CREATE TABLE `encrypted_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `password` blob,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

% mysql -u root test -e "select convert(password using utf8) from encrypted_user \G"
*************************** 1. row ***************************
convert(password using utf8): hKC9sQXHgSYEw7NSLbMVFJkIwhlCBP6P0K+EvByrmkgGuXzR6oGV52WoqFv4MOb+wzaZPxTMTCW8shqbo6F7kO3aAvJqR8a45zhu6fQkws7WAH0YLQhDCUbePcE2AowwE+5LuCEIOeVbCaKvqCxhFA==

鍵は Fernet.generate_key() で生成しています
今回はアプリ起動ごとに鍵が変わるような仕組みになっているので実際は一度鍵を生成したらその鍵を使い回すような仕組みにしましょう

マイグレーションスクリプトの修正

Flask-Migrate が EncryptedType に完全に対応していないようなのでマイグレーションスクリプトを手動で修正します
先頭に import 文を 1 行追加してあげるだけになります

  • FLASK_APP=app pipenv run flask db init
  • FLASK_APP=app pipenv run flask db migrate -m "Initial migration."
  • vim migrations/versions/147b8d1c4486_initial_migration.py
import sqlalchemy_utils

マイグレーション

実際にマイグレーションしてテーブルが作成されることを確認しましょう

  • FLASK_APP=app pipenv run flask db upgrade

動作確認

アプリを起動しデータを登録して確認します
動作確認は復号化されたデータで取得できるのでわかりづらいですがデータベースではしっかりデータが暗号化されていることが確認できると思います

  • pipenv run flask run
  • curl 'localhost:5000/insert?password=hoge'
  • curl 'localhost:5000/get'

最後に

SQLAlchemy-Utils の EncryptedType を使ってみました
内部的には blob で扱うタイプの暗号化ツールになります
今回は Python 側で暗号化してデータベースに登録するような流れですがデータベース自体に暗号化するような機能もある場合があるので用途に合わえて使い分けられるといいかなと思います

おまけ: 固定の鍵を生成する方法

import base64
keystr = 'JYZdCbdf7WSCRHgq7LjUMMYV18EbVIq0'
key = base64.urlsafe_b64encode(keystr.encode())

参考サイト

2023年3月20日月曜日

(celery) flower にベーシック認証をつける方法

(celery) flower にベーシック認証をつける方法

概要

flower は celery の管理ツールです
今回は flower にベーシック認証を付与する方法を紹介します
なお今回は docker で起動します

環境

  • Ubuntu 18.04
  • docker 20.10.7
  • Python 3.10.2
  • flower 1.2

ベーシック認証用の引数を付与して起動

--basic_auth オプションを使うだけです

  • docker run -p 5555:5555 --rm mher/flower celery --broker=redis://172.17.0.1:6379 flower --address=0.0.0.0 --pt=5555 --basic_auth=user1:password1

動作確認

これで localhost:5555 にアクセスするとベーシック認証が発動するのが確認できると思います

最後に

引数で簡単に指定できるのはいいのですが平文なので何ともという感じです

もしこれが嫌な場合は nginx などのリバースプロキシ配下で flower を起動する方法があるのでそれを使う感じになるかなと思います

参考サイト

2023年3月17日金曜日

iPhoneXR のバッテリー交換

iPhoneXR のバッテリー交換

概要

手順はすでにネット上にいろいろとありますが自分でもやってみたので感想も含めて紹介します

一番大変なのは画面を開けるところで二番目にたいへんなのはバッテリーの両面テープを剥がすところでした

学んだ教訓

バッテリーを剥がすときは無理に剥がさずにゆっくり慎重に剥がさないとバッテリー裏のケース背面に配線されている無線充電用のケーブルをぼろぼろにしてしまうということがわかかった

道具

  • 交換用バッテリー (1500円)
  • バッテリー固定用両面テープ (600円)
  • Y字ドライバー0.6mm (500円)
  • プラスドライバー
  • 星型ドライバー
  • 吸盤、ピック、テコ
  • ピンセット

交換時のバッテリー最大容量

  • 82%

「設定」 >「バッテリー」と選択してから、「バッテリーの状態と充電」をタップ

電源オフ

オフにしてから作業しましょう

画面を開く

まずは星型ドライバーを使って画面の固定を解除します

次に画面を開きます
正直これが一番たいへんです
吸盤で持ち上げてピックを差し込めれば勝ちです
吸盤が弱すぎて全然画面が持ち上がらない場合はカッターのような細いものをフレームと画面の間に挟んでテコの原理で持ち上げてもいいです
ただしフレームが傷つく可能性があるので慎重にやりましょう

プレートを外す

画面が邪魔なので外します
コネクタを外すためにプレートを外す必要があります
Y字ドライバーとプラスドライバーを使って外していきましょう

また今回は面倒なので画面は外しません

コネクタを外す

バッテリーのコネクタを外しましょう

正直画面が接続された状態でも頑張ればバッテリーを交換できるので慣れている人は画面を外さなくてもできます
だたしバッテリーのコネクタがプレートの下にあるのでプレートを外すのは必須です

バッテリーを外す

両面テープを引っ張って剥がします
ピンセットでぐるぐるして引っ張ると剥がしやすいです

これが2番目に大変でした
というのも自分の場合はテープが切れました
おそらく古い iPhone だとテープ自体の強度が劣化しておりひっぱって剥がせないと思います

しかたないのでヘラを使ってテコの原理で少しづつ剥がしていきました
半分くらい剥がれたところであとは手を使いました
が剥がして裏面を見たら大変なことになっていました
バッテリー裏に張っている細いケーブル?っぽいのが切れているのと裏面の塗装がヘラで剥がれまくっていました

正直ヘラは本当にはじめだけで少しでも浮いたらあとは手で剥がしたほうがいいと思いました

バッテリー裏ケーブル切断による影響は

不明ですがおそらく無線充電ができなくなっている感じかなと
基本はすべて問題なく使えています
iPhone の左下あたりに続くケーブルっぽいですが左下には何もインタフェースがないから影響がないのかもしれません
(もしかすると eSIM の可能性もあるかも)

バッテリーを交換する

新品のバッテリーは両面テープを貼ってから固定しましょう
今回はちゃんとした両面テープを使いましたが市販の両面テープでもいいかもしれません

あとは逆をたどる

あとは逆をたどるだけです
結構ネジが多いのでなくさないようにするのとちゃんと外したところに戻すようにしましょう

シーラントグルーについて

画面と本体は水没対策としてシーラントグルーという接着剤でくっついています
一度画面と本体を開けるとシーラントグルーがなくなってしまいます
今回は面倒だったのでシーラントグルーは組み立て時に使いませんでしたがシーラントグルーだけでも売ってるので画面を戻すときに使っても OK です
その場合は剥がれたシーラントグルーが iPhone に残っているのでしっかり剥がしてから新しいシーラントグルーを使いましょう

最後に

昔 iPhone5 のバッテリーを交換したときに比べるとかんり面倒な感じになったような気がします
新品のバッテリーだけ購入することもできますが両面テープやらドライバは付属していないケースが多いので素直に修理キット的なやつを購入したほうがいいかもしれません

ちなみにバッテリーは正規ではないので設定のバッテリーで「不明な部品」と出続けます

一番大変だったのはやはり画面を開けるところだったかなと思います

2023年3月16日木曜日

MinIOでOneloginのOpenIDConnectを使って認証する方法

MinIOでOneloginのOpenIDConnectを使って認証する方法

概要

MinIO にはデフォルトでOpenIDConnectに接続するための機能が実装されています
今回は Onelogin を使って OpenIDConnect 認証できるようにしてみました

環境

  • Ubuntu 18.04
  • docker 20.10.7
  • minio RELEASE.2022-10-24T18-35-07Z

注意事項

gateway モードでは使えません

流れ

  1. MinIO 起動
  2. MinIO にグループのポリシーを追加
  3. Onelogin 側の設定
  4. ログイン

MinIO の起動

まずは MinIO を起動します
今回は docker を使います

ポイントは MINIO_IDENTITY_OPENID_ で始まる環境変数でここに Onelogin の認証情報を登録します

MINIO_IDENTITY_OPENID_CLAIM_NAME は groups という値にしています
これは Onelogin から送られてきたトークンに含まれる groups というフィールドをもとに MinIO 内で使用するポリシーを選択するために使用されます
特定のグループに対するポリシー情報の登録は後ほど行います

リダイレクトURLは http://localhost:9100/oauth_callback を設定します
MinIO には oauth_callback というリダイレクト用の URI が用意されいます
Onelogin の場合 localhost 以外のコールバック URL を指定する場合は https 化が必須なので注意しましょう

docker run --rm -p 9100:9100 -p 9000:9000 --name minio-s3 \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=xxx" \
-v ~/minio/data:/data \
-e "MINIO_IDENTITY_OPENID_CLAIM_NAME=groups" \
-e "MINIO_IDENTITY_OPENID_CONFIG_URL=https://xxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration" \
-e "MINIO_IDENTITY_OPENID_CLIENT_ID=xxxx" \
-e "MINIO_IDENTITY_OPENID_CLIENT_SECRET=xxxx" \
-e "MINIO_IDENTITY_OPENID_SCOPES=groups email openid profile" \
-e "MINIO_IDENTITY_OPENID_REDIRECT_URI=http://localhost:9100/oauth_callback" \
quay.io/minio/minio:RELEASE.2022-10-24T18-35-07Z server /data --console-address ":9100"

MinIO にグループのポリシーを追加

MinIO が起動できたらグループに対する MinIO 上で割り当てるポリシーを作成します

ポリシーの割り当てには MinIO 用の mc という CLI ツールを使います

先にポリシー用のファイルを作成します

  • vim allaccess.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

あとはこれと mc コマンドを使って起動した MinIO にポリシーを登録します

  • docker run --name mc -it --entrypoint=/bin/sh minio/mc

mc コマンドコール用のコンテナが起動したらファイルを送ります

  • docker cp allaccess.json mc:/

そしてポリシーを登録します

  • mc alias set myminio http://172.17.0.1:9000 admin password
  • mc admin policy add myminio admin allaccess.json

上記の場合 1. 先程起動した MinIO を myminio という名前で登録 2. myminio の admin グループに allaccess.json のポリシーを登録という流れになります

なのでもし Onelogin から送られてくる groups 情報に admin ではなく別のグループ名が含まれている場合は admin の部分を適当なグループ名に変更してポリシー登録してください

Onelogin 側の設定

Onelogin 側ではコールバック用の URL を追加するくらいかなと思います

詳しい設定方法や認証情報の確認方法は過去に紹介しているのでそちらを参照してください

動作確認ログイン

localhost:9100 にアクセスするとログインボタンが表示されます
クリックすると Onelogin のログイン画面に遷移するのでログインしましょう

トラブルシューティング

おそらく一番目にするのは Policy claim missing from the JWT token, credentials will not be generated というエラーかなと思います
これは Onelogin から送られてきたトークン情報に policy というフィールドがないから MinIO 内で使用するポリシー情報を割り当てられませんというエラーになります

この policy という値をコントロールするのが MINIO_IDENTITY_OPENID_CLAIM_NAME になります
Onelogin では policy フィールドはトークンに含まれていません
なので groups フィールドを使うために MINIO_IDENTITY_OPENID_CLAIM_NAME=gropus にしています

IdP から送られてくるトークン情報をデバッグする方法は以下のコマンドで実行することで確認できます

  • mc admin trace -v -a myminio

WebIdentityToken というフィールドがJSONトークン情報になります
トークン情報はエンコードされているのでデコードして確認してください

最後に

MinIO で OpenIDConnect に接続する方法を紹介しました
IdP 側が送信するトークン情報に応じて対応が少し異なるので注意が必要です
IdP 側から直接ポリシー情報を送信することもできるようです

参考サイト

2023年3月15日水曜日

Promethues の Basic 認証を追加する方法

Promethues の Basic 認証を追加する方法

概要

タイトルの通りです
web.yml に認証情報を記載して Prometheus を起動させるだけです
今回は docker コンテナで Promethues は動作させます

環境

  • Ubuntu 18.04
  • docker 20.10.7
  • Python 3.10.2
  • bcrypt 4.0.1

bcrypt パスワードの生成

ベーシック認証用のパスワードは bcrypt で暗号化された文字列が必要です

公式にもありますが bcrypt は Python のライブラリを使って生成しました

  • pip install bcrypt
  • python -c 'import bcrypt; hashed_password=bcrypt.hashpw("passw0rd!".encode("utf-8"), bcrypt.gensalt()); print(hashed_password)'

上記の平文パスワードの部分を適宜変更すれば OK です

web.yml の作成

パスワード情報は web.yml に記載します

  • vim web.yml
basic_auth_users:
    admin: $2b$12$BB48JzIK5bfRo44g4GOAPe3/q6sTaLW3pbvugPlUZVuYg8LOW4sOK

起動する

作成した web.yml を使って起動します
web.yml を指定するオプションは web.config.file です

  • docker run -d -p 9090:9090 -v $(pwd):/prometheus-data prom/prometheus --config.file=/prometheus-data/prometheus.yml --web.config.file=/prometheus-data/web.yml

prometheus.yml は必須なので適当にこちらから作成してください (ノードエクスポータなどの内容は仮で大丈夫です)

動作確認

localhost:9090 にアクセスするとブラウザのベーシック認証が発動するのが確認できると思います

API をコールするときもベーシック認証が必要になっていることを CLI で確認しましょう

  • curl --head -u admin http://localhost:9090/graph

最後に

今回は紹介しませんが Alertmanager でも同じ歩法でベーシック認証を付与することができます

参考サイト

2023年3月14日火曜日

emacsでvue3(volar)の開発環境を整える

emacsでvue3(volar)の開発環境を整える

概要

emacs で vue3 の Language Server である volar と連携してみました
vue2 までの vetur + vls の設定ではなく volar + vue-language-server の設定になります
全然情報がなかったので備忘録として残しておきます

環境

  • macOS 11.7.4
  • npm 9.5.0
  • volar 1.2.0
  • emacs 28.2
    • lsp-mode 20230310.1855

インストール

  • npm install -g @volar/vue-language-server

これで vue-language-server コマンドが使えるようになります

emacs 設定

  • vim .emacs.d/site-list/init.el
(require 'lsp-mode)
(lsp-workspace-folders-add "/path/to/nuxt-app")
(add-hook 'vue-mode-hook #'lsp)

注意事項: 現在のlsp-modeのバージョンだと必ず lsp-workspace-folders-add にプロジェクトのパスを追加しなければならない

https://github.com/emacs-lsp/lsp-mode/issues/3559

上記にあるように lsp-volar--activate-p というプロジェクトをアクティベートする関数にバグがあるため手動でプロジェクトのパスを設定する必要があります

プロジェクトが追加になった場合は面倒ですが追記しましょう

動作確認

/path/to/nuxt-app/app.vue を emacs で開くと vetur ではなく volar が起動するはずです
server_id が vue-semantic-server になっていれば volar になっています

うまく volar が起動せず vue-language-server を認識せず vls などを起動しようとしている場合は lsp-workspace-folders-add が追加されているか確認してください

最後に

emacs + vue3 (volar) の設定方法を紹介しました

おそらくまだ lsp-volar には修正が入ると思います
素直に vscode を使ったほうがいいのだろうか、、

参考サイト

2023年3月13日月曜日

Nuxt で全ページに対して共通の処理をさせたい場合

Nuxt で全ページに対して共通の処理をさせたい場合

概要

例えば認証やロギングなど全ページで共通の処理をさせたい場合があります
Nuxt のミドルウェア機能を基本的には使いますが今回はルーティングに特化したミドルウェアの方法を紹介します

環境

  • macOS 11.7.4
  • nodejs 19.7.0
  • npx 9.5.0
  • yarn 1.22.19

プロジェクト作成

過去に紹介しているのでこちらを参考に作成しましょう

プラグインディレクトリの作成

今回はプラグインとして追加するのでプラグイン用のディレクトリを作成します

  • mkdir nuxt-app/plugins
  • touch nuxt-app/plugins/logging.ts

addRouteMiddleware を追加する

今回は addRouteMiddleware という機能を使って各ページに共通の処理を追加したいと思います

  • vim nuxt-app/logging.ts
export default defineNuxtPlugin(() => {
  addRouteMiddleware('logging', (to, from) => {
    if (to.path === '/nothing') {
      // false を return することで 404 ページを表示する
      return false
    } else {
      // 共通の処理
      console.log('nothing ページ以外で実行する共通処理を記載します')
      console.log(`現在のパスは${to.path}です`)
    }
  }, { global: true })
})

global: true を設定することですべてのページでミドルウェアが呼ばれます
to.path で移動先にパスを取得できるのでパスに合わせて実行する処理/しない処理を切り分けることもできます

navigateTo を使う場合に DomException が発生するケースがある

実際に次に遷移するページの描画は行われておりその最中に navigateTo が呼び出されると DomExecption が発生することがあるようです
https://github.com/nuxt/nuxt/issues/13350 or https://github.com/nuxt/nuxt/issues/12735

回避策もあるようですが難しい場合は navigateTo の前に location.replace('/') という感じで入れるもの苦肉の策としてはありかなと思います

最後に

Nuxt で各ページに共通する処理を実行する方法を紹介しました
認証などではよく使うパターンかなと思います

参考サイト

2023年3月10日金曜日

sshでアクセス可能なサーバのポートにlocalhostでアクセスする方法

sshでアクセス可能なサーバのポートにlocalhostでアクセスする方法

概要

所謂SSHのポートフォワーディングです

コマンド

  • ssh -L 5000:host01:5000 host01

説明

host01 の 5000 番ポートを localhost:5000 としてアクセスすることができます

後ろの host01 はリレーサーバの指定もできるので host02 とかにすると

  • localhost -> host02 -> host01

という経路でポートフォワーディングすることもできます

ターミナルでログイン状態の場合に localhost でアクセスできるのでターミナルをログアウトするとポートフォワーディングも終了して localhost でのアクセスはできなくなります

2023年3月8日水曜日

Quasar ですべてのページにアクセスする前に特定の処理をさせる方法

Quasar ですべてのページにアクセスする前に特定の処理をさせる方法

概要

例えば認証をつけたい場合に使えます
前回のアプリケーションをベースに変更していきます

環境

  • macOS 11.7.4
  • nodejs 19.6.0
    • yarn 1.22.19
    • quasar 2.0.0
  • Python 3.10.9
    • Flask 2.2.3
    • Flask-CORS 3.0.10
    • authlib 1.2.0

axios のリクエストを管理する (src/boot/axios.js)

バックエンドとやり取りする api をアプリケーション全体で管理するようにします

  • vim src/boot/axios.js
import { boot } from 'quasar/wrappers'
import axios from 'axios'

const api = axios.create({ 
  baseURL: 'http://localhost:5000',
  withCredentials: true
})

export default boot(({ app }) => {
  app.config.globalProperties.$axios = axios
  app.config.globalProperties.$api = api
})

export { axios, api }

ルーティングの設定 (src/router/routes.js)

認証が必要なページと不要なページを設定します
認証が必要なページは meta.requiresAuth を設定します

  • vim src/router/routes.js
const routes = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue'), meta: { requiresAuth: true } }
    ]
  },
  {
    path: '/login',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/LoginPage.vue') }
    ]
  },
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue')
  }
]

export default routes

beforeEach を router に追加する (src/router/index.js)

Quasar の beforeEach という機能を使うことですべてのページに対して共通して実施したいことを定義できます

先程各ページに付与した meta.requiresAuth が有効なページの場合に認証済みかどうかをバックエンドに聞きに行くような処理を追加します

バックエンド側にはあとで /verify というエンドポイントを追加して認証が完了しているかチェックする機能を追加します

/verify でエラーが返ってきた場合は Quasar のログインページを表示します

  • vim src/router/index.js
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
import { api } from 'boot/axios'

export default route(function (/* { store, ssrContext } */) {
  const createHistory = process.env.SERVER
    ? createMemoryHistory
    : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)

  const Router = createRouter({
    scrollBehavior: () => ({ left: 0, top: 0 }),
    routes,
    history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE)
  })

  // ここを追加する
  Router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.requiresAuth)) {
      // 認証が必要なページはバックエンドに問い合わせる
      api.get('/verify')
        .then((response) => {
          console.log(response.data)
          next()
        })
        .catch((error) => {
          console.log(error)
          next('/login')
        })
    } else {
      // 認証が不要なページはそのまま表示する
      next()
    }
  })

  return Router
})

注意事項

本当は catch したところでバックエンドのログインページに飛ばしてあげてもいいのですがそうすると無限ループになってしまうのでそうならないように認証が不要なログインページを用意してあげる必要があります

ログイン用のページ (src/pages/LoginPage.vue)

このページには認証がありません
ログイン/ログアウトボタンとログインしたあとで取得できるユーザ情報を表示しています

  • vim src/pages/LoginPage.vue
<template>
  <div>
    <h1>Login Test Page</h1>
    <div>{{ data }}</div>
    <button @click="login">Login</button>
    <button @click="logout">Logout</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { api } from 'boot/axios'

const data = ref('nodata')

const fetchUser = async(value) => {
  api.get('/user')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

const login = async(value) => {
    window.location.href = 'http://localhost:5000/login';
}

const logout = async(value) => {
  api.get('/logout')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

onMounted(() => {
  fetchUser()
})
</script>

バックエンドの処理

基本は前回と同じですが verify メソッドを追加しています
verify ではセッション(クッキー)に値が設定されているかをチェックしています

  • vim app.py
from typing import Any, Optional
from flask import (Flask,
                   url_for,
                   session,
                   redirect)
from flask_cors import CORS
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = "xxxx"
app.config["ONELOGIN_CLIENT_ID"] = "xxxx"
app.config["ONELOGIN_CLIENT_SECRET"] = "xxxx"
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
CORS(app,
     supports_credentials=True,
     max_age=300)

oauth = OAuth(app)
oauth.register(
  name="onelogin",
  server_metadata_url="https://xxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration",
  client_kwargs={"scope": "openid email profile"}
)


def get_session() -> Optional[Any]:
    return session.get("user")


def has_session() -> bool:
    user = get_session()
    if user is None:
        return False
    return True


@app.route("/user")
def user():
    if not has_session():
        return "Not logged in yet."
    return f"Login successful. => {get_session()}"


@app.route("/verify")
def verify():
    if not has_session():
        return "Not logged in yet.", 400
    return "Already logged in."


@app.route("/login")
def login():
    redirect_uri = url_for("auth", _external=True)
    return oauth.onelogin.authorize_redirect(redirect_uri)


@app.route("/callback")
def auth():
    token = oauth.onelogin.authorize_access_token()
    session["user"] = token["userinfo"]
    return redirect("http://localhost:8080/#/login")


@app.route("/logout")
def logout():
    session.pop("user", None)
    return "Logout successful."

動作確認

  • quasar dev
  • pipenv run flask run

で localhost:8080/#/ にアクセスすると強制的にログインページに飛ばされるのが確認できます
そして OpenIDConnect でログイン後には localhost:8080/#/ に無事アクセスできることが確認できると思います

最後に

Quasar で全ページに対して行う共通処理を実現する方法を紹介しました
特定のページにのみ実施したい場合には meta 情報を使って beforeEach で判断するようなテクニックがあります

ログアウトするとアプリケーション内のセッションは消えますが OpenIDConnect を提供している Idp 側ではまだセッションが残っている場合があります
ログアウト時に Idp 側のセッションもログアウトにする方法があるので興味がある方は調べてみてください

参考サイト

2023年3月7日火曜日

QuasarでOpenIDConnect認証連携する方法

QuasarでOpenIDConnect認証連携する方法

概要

Quasar で OpenIDConnect 認証する方法を紹介します
バックエンドに Flask アプリケーションを使って連携します
今回連携する OpenIDConnect 先は Onelogin になります

環境

  • macOS 11.7.4
  • nodejs 19.6.0
    • yarn 1.22.19
    • quasar 2.0.0
  • Python 3.10.9
    • Flask 2.2.3
    • Flask-CORS 3.0.10
    • authlib 1.2.0

Quasarプロジェクトの作成

こちらを参考に作成しましょう
error @achrinza/node-ipc@9.2.5: The engine "node" is incompatible with this module. が発生する場合は

  • yarn config set ignore-engines true

を設定しましょう

Quasar 側のログインテストページの作成

  • vim src/pages/LoginPage.vue
<template>
  <div>
    <h1>Login Test Page</h1>
    <button @click="login">Login</button>
    <button @click="logout">Logout</button>
    <div>{{ data }}</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const data = ref('nodata')

const api = axios.create({ 
  baseURL: 'http://localhost:5000',
  withCredentials: true
})

const fetchUser = async(value) => {
  api.get('/user')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

const login = async(value) => {
    window.location.href = 'http://localhost:5000/login';
}

const logout = async(value) => {
  api.get('/logout')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

onMounted(() => {
  fetchUser()
})
</script>

またルーティングを追加しましょう

  • vim src/router/routes.js
const routes = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') }
    ]
  },
  {
    path: '/login',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/LoginPage.vue') }
    ]
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue')
  }
]

export default routes

起動は

  • quasar dev

で行います
localhost:8080/#/login にアクセスすると OpenIDConnect のログインテスト用のページが表示されます

バックエンドの Flask アプリをまだ作成していないので動作はしません

Flask でバックエンドアプリの作成

flask-cors, authlib を使うのでインストールしましょう

  • pipenv install flask-cors authlib

今回 OpenIDConnect との連携はすべて Flask 側で行います
認証が成功して Flask 側で ID トークンを受け取ったらそれを session (cookies) に保存して SPA 側と共有します

from flask import (Flask,
                   url_for,
                   session,
                   redirect)
from flask_cors import CORS
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = "xxx"
app.config["ONELOGIN_CLIENT_ID"] = "xxxx"
app.config["ONELOGIN_CLIENT_SECRET"] = "xxxx"
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
CORS(app,
     supports_credentials=True,
     max_age=300)

oauth = OAuth(app)
oauth.register(
  name="onelogin",
  server_metadata_url="https://xxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration",
  client_kwargs={"scope": "openid email profile"}
)


@app.route("/login")
def login():
    redirect_uri = url_for("auth", _external=True)
    return oauth.onelogin.authorize_redirect(redirect_uri)


@app.route("/user")
def user():
    user = session.get("user")
    if user is None:
        return "Not logged in yet."
    return f"Login successful. => {user}"


@app.route("/callback")
def auth():
    token = oauth.onelogin.authorize_access_token()
    session["user"] = token["userinfo"]
    return redirect("http://localhost:8080/#/login")


@app.route("/logout")
def logout():
    session.pop("user", None)
    return "Logout successful."

起動しましょう

  • pipenv run flask run

これでバックエンドは localhost:5000 で起動します
当然ですが Quasar は localhost:8080 で起動しバックエンドは localhost:5000 で起動しているので CORS の設定が必要です

ポイント

ログインはSPA側からバックエンドにリダイレクトする

OpenIDConnet のログインページへのリダイレクトは Flask 側で行います
なので SPA 側からログインページを表示したい場合はまずバックエンドの /login URI へリダイレクトする必要があります

SPA からリダイレクトをせずに Flask から直接ログインページへリダイレクトしようとすると CORS エラーになるので注意が必要です

SPA側とバックエンド側の認証情報の共有はセッション(cookies)を使う

クッキー情報はドメインごとに管理されます
しかし今回はドメインが異なるので異なるドメイン間でクッキーを共有する必要があります

Quasar 側では withCredentials: true を設定することで axios リクエスト時にバックエンドにクッキー情報を送信できます

Flask 側では CORS(app, supports_credentials=True, max_age=300) という感じで supports_credentials=True にすることで送信された異なるドメインから送信されたクッキーを受け入れることができます

セキュリティ的な面を考慮する

今回はクッキーの Secure 属性と HTTP-Only 属性のみ有効にしています
あとは CORS 側の設定で max_age を設定して共有する有効期限を定めています

そもそもクッキーを使わない方法もあるようなのでプロダクションではセキュリティ麺を考慮した設定が実装の追加が必要になるかなと思います

最後に

Quasar に OpenIDConnect を使った認証機能を追加してみました
基本はバックエンド側のアプリが必要になると思います (調べてみると SPA オンリーでもできるっぽいですが)

SPA はモダンなのですが考えることが多いのが厄介です
そういうのも含めると普通に Web アプリケーションとして作成するのがいいのかもしれません