end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

openam 12 における 持続cookie の設定

forgerock社のdocumentによれば、上記urlで 「iPSPCookie」「 DProPCookie」によるページ内検索。 または、「2.3.18. Hints for the Persistent Cookie Module」を読めば、よいのかも。

ただ、ssoadm コマンドでは

set-realm-svc-attrs -e sso -s iPlanetAMAuthService \
  -a "iplanet-am-auth-persistent-cookie-mode=true"
set-realm-svc-attrs -e sso -s iPlanetAMAuthService \
  -a "iplanet-am-auth-persistent-cookie-time=5184000"

により持続cookie を有効化した記憶がありますが、 「iplanet-am-auth-persistent-cookie-mode=true」に該当する画面はどこだろう?

clang-format による java source code 整形

clang-format は、コマンドラインでも使用できるようですので、メモ

https://clang.llvm.org/docs/ClangFormat.html

https://clang.llvm.org/docs/ClangFormatStyleOptions.html

install from source code もできますが...

以下の手順で、src からinstallできますが、makeに数時間や数GB(5GB超?)を要する為、 私が使用する centos8 on virtualboxでは、build できませんでした。

$ wget https://github.com/llvm/llvm-project/archive/llvmorg-10.0.1.tar.gz
$ tar -xvf llvmorg-10.0.1.tar.gz
$ cd llvm-project-llvmorg-10.0.1
$ mkdir build
$ cd build
$ cmake -G "Unix Makefiles" ../llvm
$ make

install from package

$ sudo yum install clang*
$ /usr/bin/clang-format --version
clang-format version 9.0.1 (Red Hat 9.0.1-2.module_el8.2.0+309+0c7b6b03)

formatting java source code

以下は、foo.java を indent幅=4 で、上書き(-i)するものです。

clang-format -i -style="{IndentWidth: 4}" ./foo.java

その他、標準入力でsource code を受け取ることもできます。 以下は、「Seasar2徹底入門 - SAStruts/S2JDBC対応 - 翔泳社」のサンプルコードを 整形した例です。

https://www.shoeisha.co.jp/book/detail/9784798121505

$ cat foo.java | clang-format --assume-filename="foo.java" -style="{IndentWidth: 4}"
package org.seasar.sastruts.example.action;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.seasar.fisshplate.preview.FPPreviewUtil;
import org.seasar.fisshplate.template.FPTemplate;
import org.seasar.sastruts.example.fpao.HelloFpao;
import org.seasar.sastruts.example.fpao.HelloFpao.HelloDto;
import org.seasar.struts.annotation.Execute;
import org.seasar.struts.util.ResponseUtil;

/**
 * Fisshplateのサンプルです。
 *
 * @author Naoki Takezoe
 */
public class ExcelAction {
    @Resource protected HelloFpao helloFpao;

    /**
     * Fisshplateのサンプル一覧を表示します。
     */
    @Execute(validator = false)
    public String index() {
        return "index.jsp";
    }

    /**
     * バインド変数のサンプルです。
     */
    @Execute(validator = false)
    public String helloWorld() throws Exception {
        InputStream in =
            getClass().getResourceAsStream("/excel/HelloWorld.xls");
        FPTemplate template = new FPTemplate();

        Map<String, String> map = new HashMap<String, String>();
        map.put("name", "たけぞう");

        HSSFWorkbook wb = template.process(in, map);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        wb.write(out);

        ResponseUtil.download("HelloWorld.xls", out.toByteArray());

        return null;
    }
   :
    /**
     * プレビュー機能のサンプルです。
     */
    @Execute(validator = false)
    public String preview() throws Exception {
        InputStream template =
            getClass().getResourceAsStream("/excel/Foreach.xls");
        InputStream data =
            getClass().getResourceAsStream("/excel/Foreach_Preview.xls");

        HSSFWorkbook wb = FPPreviewUtil.getWorkbook(template, data);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        wb.write(out);

        ResponseUtil.download("Foreach_Preview.xls", out.toByteArray());

        return null;
    }
}
$ 

logging.config for python の 設定fileを yaml で記載

python の logging には、様々な設定が可能ですが、とりあえずは、以下で動作します。

# -*- coding: utf-8 -*-

import logging.config
import yaml

log_conf = './log_conf.yaml'

def main():
    logging.config.dictConfig(yaml.load(open(log_conf).read(),
                                        Loader=yaml.SafeLoader))

    logger = logging.getLogger('mainLogger')
    logger.error('hogehoge')

if __name__ == "__main__":
    main()
version: 1

formatters:
  mainFormatter:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    datefmt: '%Y/%m/%d %I:%M:%S'

handlers:
  file:
    class: logging.handlers.RotatingFileHandler
    level: INFO
    formatter: mainFormatter
    filename: antlr.log
    maxBytes: 1000
    backupCount: 1

loggers:
  mainLogger:
    level: INFO
    handlers: [file]
    propagate: no

root:
  level: INFO
  handlers: []

IdP by OpenAM - SP by OpenAM による SAML連携

SAMLシングル・サインオン構成において、 IdP(Identity Provider)にのみ OpenAM を用い、 SP(Service Provider)にOpenAM以外(SalesforceやG-Suite、cybozu)を用いた例は、 インターネット上によく見かけます。

一方、SP に OpenAMを用いた例は、あまり見かけない為、 今回、主に SPや SP側WebAgentの設定について記載しています。

f:id:end0tknr:20200822094024p:plain

0.今回 構成のポイント

https://resources.us.safenetid.com/help/ForgeRock%20OpenAM/Index.htm

上記urでは、SP に OpenAM を用い、 Authentication Modules (SAML)や Authentication Chain を 設定し、 SP にあるログイン画面を表示しないようにしています。(下図)

f:id:end0tknr:20200822094045p:plain

当初、上記urlの設定に倣おうとしましたが、上手くSAML連携できなかった為、 今回は、Auth Moduleや Auth Chain を用いず、 主に SAMLトラストサークルと、ログイン画面URLの設定を行うことで IdPのログイン画面をdefault表示するようにしていますので、 SPのログイン画面もURLを直接指定することで表示できる状態にしています。

1. 事前準備

ここでは、SPやSP側WebAgentのサーバインスタンスの追加から、 ミドルウェアやOpenAMのインストールまでを簡単に記載します。

1.1. OpenAM SP - sp.end0tknr.com

1.1.1. サーバインスタンス

AWS EC2

t2.small、ディスク=50GB、OS=AmazonLinux1

hostname

$ sudo vi /etc/sysconfig/network
   HOSTNAME=sp1.end0tknr.com

(ロードバランサ下での冗長構成を想定し、上記としてます)

/etc/hosts

$ sudo vi /etc/hosts
  $PRIVATE_IP sp1.end0tknr.com

( $PRIVATE_IPはAWSにより決定される任意のものです)

ALB(ELB)

ホスト名=sp.end0tknr.com
TARGET= sp1.end0tknr の port:8080

1.1.2. Java

version 1.8.0を利用していますが、別のバージョンでも問題ないと思います。

$ sudo yum install java-1.8.0-openjdk-devel.x86_64
$ sudo alternatives --config java
$ /usr/bin/java -version
openjdk version "1.8.0_252"

1.1.3. tomcat

少々、古めのOpenAM 13を使用する為、tomcat7を使用しています

$ cd /opt
$ sudo wget https://ftp.tsukuba.wide.ad.jp/software/apache/tomcat/tomcat-7/v7.0.105/bin/apache-tomcat-7.0.105.tar.gz
$ sudo tar -xvf apache-tomcat-7.0.105.tar.gz
$ sudo ln -s apache-tomcat-7.0.105 tomcat

$ sudo useradd tomcat
$ sudo chown -R tomcat:tomcat /opt/tomcat
$ sudo chown -R tomcat:tomcat /opt/apache-tomcat-7.0.105

$ sudo mkdir /opt/sso  ## openamの設定用dir
$ sudo chown tomcat:tomcat /opt/sso

$ sudo vi /etc/init.d/tomcat
  #!/bin/bash
  # description: Tomcat7 Start Stop Restart
  # processname: tomcat7
  # chkconfig: 234 20 80
  CATALINA_HOME=/opt/tomcat

  case $1 in
  start)
      sh $CATALINA_HOME/bin/startup.sh
  ;;
  stop)
      sh $CATALINA_HOME/bin/shutdown.sh
  ;;
  restart)
      sh $CATALINA_HOME/bin/shutdown.sh
      sh $CATALINA_HOME/bin/startup.sh
  ;;
  esac

1.1.4. OpenAM

OpenAM ver.12 を使用したかったのですが、 ForgeRock社のライセンス変更?により入手できない為、 OpenAM Community Edition ver.13 を使用しています。

$ sudo /etc/init.d/tomcat stop

$ sudo su -
# cd /opt/tomcat/webapps
# wget https://github.com/OpenIdentityPlatform/OpenAM/releases/download/13.0.0/OpenAM-13.0.0.war
# mv OpenAM-13.0.0.war sso.war
# chown tomcat:tomcat sso.war

$ sudo /etc/init.d/tomcat start

https://sp.end0tknr.com/sso へブラウザでアクセス

- 「設定オプション」画面にて「カスタム設定の新しい設定の作成」リンクをクリック

- 「カスタム設定オプション 手順1:一般」にて
  「amadmin」のpasswordを<ないしょ>に設定

- 「カスタム設定オプション 手順2:サーバ設定」にて
   サーバURL     : https://sp.end0tknr.com:443
   Cookieドメイン: .end0tknr.com
   ロケール      : en_US
   設定dir       : /opt/sso
   
- 「カスタム設定オプション 手順3:設定データストア設定」にて
   設定データストア  : OpenAM
   ポート            : 50389
   管理者ポート      : 4444
   JMXポート         : 1689
   暗号化鍵          : root4HEMS
   ルートサフィックス: dc=openam,dc=forgerock,dc=org

- 「カスタム設定オプション 手順4:ユーザーデータストア設定」にて
  「OpenAMのユーザデータストア」を選択

- 「カスタム設定オプション 手順5:サイト設定」に
   ロードバランサの背後に配備され「ない」として次へ
   ※「ロードバランサの背後に配備される」でも問題ないと思います。

- 「カスタム設定オプション 手順6:デフォルトのポリシーエージェントユーザー」にて
   パスワード : <ないしょ>

「設定の作成」をクリックし、しばらく経過すると「設定が完了しました」と
表示される為、「ログインに進む」をクリックでログインできれば、完了。

1.1.5.テスト用ユーザの作成

ブラウザでOpenAMにログインし、IdPにて、Top Level Realm -> Data Stores -> 「対象」順に画面遷移後、ユーザを追加して下さい。

1.2. OpenAM SP側WebAgent - sp-agt.end0tknr.com

1.2.1. サーバインスタンス

AWS EC2

t2.small、ディスク=50GB、OS=AmazonLinux1

hostname

$ sudo vi /etc/sysconfig/network
   HOSTNAME=sp-agt1.end0tknr.com

(ロードバランサ下での冗長構成を想定し、上記としてます)

/etc/hosts

$ sudo vi /etc/hosts
  $PRIVATE_IP sp-agt1.end0tknr.com

( $PRIVATE_IPはAWSにより決定される任意のものです)

ALB(ELB)

ホスト名=sp-agt.end0tknr.com
TARGET= sp-agt1.end0tknr の port:8080

1.2.2. apache httpd

$ sudo yum install httpd24
$ /usr/sbin/httpd -v
Server version: Apache/2.4.43 (Amazon)

$ sudo chkconfig --add httpd
$ sudo chkconfig httpd on
$ sudo chkconfig --list httpd
httpd    0:off  1:off   2:on    3:on    4:on    5:on    6:off

$ sudo /etc/init.d/httpd start

1.2.3. WebAgent (Servie Provider側)

先程、installした Service Provider (OpenAM)へブラウザでログインし、 以下の設定を行って下さい。

エージェントプロファイルを作成。
Top Level Realm -> Agents -> Web で遷移した画面で、
エージェントの「新規」をクリックし、「新しいWeb」の画面にて以下を入力

  名前            : idp_to_sp_by_saml
  パスワード      : ないしょ
  設定            : 集中
  サーバー URL    : https://sp.end0tknr.com:443/sso
  エージェント URL: https://sp-agt.end0tknr.com:443/

同じ IdPにて、OpenAM上でAuthorization Policyを作成。
Top Level Realm -> Authorization -> Policy Setsへ移動。
iPlanetAMWebAgentServiceの右側のEdit(鉛筆)アイコンをクリック。
Add a New policyで新しいポリシーを作成する。
「New Policy」の画面にて以下を入力。

  Name        : idp_to_sp_by_saml
  Description : null
  Resources   : *://*:*/*

更に「Subjects」タブへ移動し、TypeをAuthenticated Usersに変更。

1.2.4. WebAgent

※yum install httpd で adduser された apache を su - 可能にする(以下)
# su - apache
This account is currently not available.
# vi /etc/passwd
apache:x:48:48:Apache:/var/www:/bin/bash  # CHANGE from /sbin/nologin

$ sudo su -
$ mkdir /etc/httpd/openam
$ chown apache:apache /etc/httpd/openam

## 以下は、web agent の installerが httpd.conf を書き換える為に実施
$ chown -R apache:apache /etc/httpd/conf

$ su - apache
$ cd /etc/httpd/openam
$ wget https://github.com/OpenIdentityPlatform/OpenAM-Web-Agents/releases/download/4.1.1/Apache_v24_Linux_64bit_4.1.1.zip
$ unzip Apache_v24_Linux_64bit_4.1.1.zip

$ echo "ないしょ" > web_agents_passwd

$ cd /etc/httpd/openam/web_agents/apache24_agent/bin
$ ./agentadmin --help
$ ./agentadmin --i   ## 「i」は、インストールを意味します

  Configuration file [/opt/apache/conf/httpd.conf]: /etc/httpd/conf/httpd.conf
  Existing OpenSSOAgentBootstrap.properties file: NULL
  OpenAM server URL: https://sp.end0tknr.com/sso
  Agent URL: https://sp-agt.end0tknr.com
  Agent Profile name: idp_to_sp_by_saml
  Agent realm/organization name: [/]: NULL
  The path and name of the password file: /etc/httpd/openam/web_agents_passwd


$ sudo /etc/init.d/httpd stop
$ sudo /etc/init.d/httpd start

2. SAML設定

ここからが、本題の SAML設定です

2.1. SAMLトラストサークル作成

OpenAM Service Provider へブラウザで amadminでログインし 「FEDERATION」→「連携タブ」→「新規」ボタンをクリックして下さい。 「トラストサークルの作成」画面にて「名前」【のみ】入力し、 「了解」をクリックすることで 【空】のトラストサークルを作成します。

f:id:end0tknr:20200822100955p:plain

f:id:end0tknr:20200822101008p:plain

f:id:end0tknr:20200822101017p:plain

2.2. SAMLトラストサークルへのエンティティ追加 (ServiceProvider)

「Top Level Realm」→「Create SAMLv2 Providers」→ 「Create Hosted Service Provider」で遷移後、 「このサーバー上に SAMLv2 サービスプロバイダを作成します」画面で、 以下に記載のように設定することで、 先程作成したトラストサークルへServiceProviderが追加されます。

f:id:end0tknr:20200822101924p:plain

f:id:end0tknr:20200822101935p:plain

項目 内容
このプロバイダのメタデータはありますか? いいえ
メタデータURL https://sp.end0tknr.com:443/sso
トラストサークル 既存のトラストサークルに追加します
既存のトラストサークル ★先程作成したトラストサークル名を選択下さい
IdPのデフォルト属性マッピングを使用しますか はい

2.3.SAMLトラストサークルへのエンティティ追加 (Identity Provider)

「Top Level Realm」→「Create SAMLv2 Providers」→ 「Register Remote Identity Provider」で遷移後、 「SAMLv2 リモートアイデンティティープロバイダを作成します」画面で、 以下に記載のように設定することで、 先程作成したトラストサークルへIdentity Providerが追加されます。

f:id:end0tknr:20200822103147p:plain

f:id:end0tknr:20200822103201p:plain

項目 内容
メタデータ ファイルはどこに存在しますか? URL
メタデータが配置されているURL https://idp.end0tknr.com/sso/saml2/jsp/exportmetadata.jsp?realm=/
トラストサークル 既存のトラストサークルに追加します
既存のトラストサークル ★先程作成したトラストサークル名を選択下さい

2.4. SAMLトラストサークル作成と、エンティティ追加(IdP側)

先程までに行ったSP側と、ほぼ同様の手順で、IdP側(idp.end0tknr.com)へも SAMLトラストサークル作成と、エンティティ追加を行って下さい。 IdP側のトラストサークルに追加するServiceProviderエンティティのメタデータURLは、 例えば、次のものです。

https://sp.end0tknr.com/sso/saml2/jsp/exportmetadata.jsp?realm=/

2.5. SAMLエンティティ(ServiceProvider)の修正 表明処理における自動連携設定

ここでは、IdPとSP間での、ユーザID(uid)の連携を設定します。

まず、SAMLトラストサークル設定画面にあるエンティティプロバイダ(SP)のリンクから、 設定画面へ遷移して下さい。

f:id:end0tknr:20200822104246p:plain

次に「SP」の「表明処理」画面で以下のように設定し、完了です。

項目 内容
自動連携 有効 はい
自動連携 属性 uid

f:id:end0tknr:20200822104832p:plain

2.6. WebAgent設定

SP側 WebAgentへのアクセスで、IdP側ログイン画面へリダイレクトする設定を行います。 まず、「Top Level Realm」→「Agents」で画面遷移して下さい。

f:id:end0tknr:20200822110230p:plain

f:id:end0tknr:20200822110241p:plain

「グローバル」タブにて以下の赤線のように設定を行って下さい。

項目 内容
エージェント通知 URL https://idp.end0tknr.com:443/UpdateAgentCacheServlet?shortcircuit=false
エージェント通知 URI プレフィックス https://idp.end0tknr.com:443/amagent
CDSSO のエージェントルート URL https://idp.end0tknr.com:443/
完全修飾ドメイン名 FQDN デフォルト end0tknr.com

f:id:end0tknr:20200822111432p:plain

f:id:end0tknr:20200822112127p:plain

「OpenAMサービス」タブタブにて以下の赤線のような設定を【戦闘に】追加して下さい。

項目 内容
OpenAM ログイン URL https://sp.end0tknr.com/sso/saml2/jsp/spSSOInit.jsp?metaAlias=/sp&idpEntityID=https://idp.end0tknr.com:443/sso
OpenAM ログアウト URL https://sp.end0tknr.com/sso/UI/Logout?realm=/

f:id:end0tknr:20200822113353p:plain

以上で、設定完了で、SP側WebAgentへブラウザでアクセスすると、IdPのログイン画面が表示され、 認証成功すると、SP側WebAgent の画面が表示されるかと思います。

unittest by selenium for python のオレオレ sample

参考url https://www.seleniumqref.com/

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import copy
import getopt
import os
import random
import re

# chromedriver の入手先-> http://chromedriver.chromium.org/getting-started
from selenium import webdriver

from selenium.webdriver.common.by   import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support    import expected_conditions
from selenium.webdriver.support    import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

import sys
import time
import unittest

conf = {"chrome_driver": os.getcwd() + '\\chromedriver.exe',
        "chrome_options" :[
            #"--headless",
            "--enable-logging=False",
            "--ignore-certificate-errors", #以下、3行はSSLエラー対策らしい
            "--disable-extensions",
            "--disable-print-preview"]}

class FormReConstruct(unittest.TestCase):
    # test前 処理
    def setUp(self):
        chopt = webdriver.ChromeOptions()
        for option_tmp in conf["chrome_options"]:
            chopt.add_argument( option_tmp )
        self.driver = webdriver.Chrome(options = chopt,
                                       executable_path=conf["chrome_driver"])
        self.driver.implicitly_wait(10)

    # test後 処理
    def tearDown(self):
        self.driver.close()

    def test_form_urls_1(self):
    test_url = "http://cent80.a5.jp:8080/contact2/catalog/main/form.shtml"

        with self.subTest(test_url=test_url):
            self.subtest_input_all_valid(test_url)      # 全項目に正しい値を入力
            self.subtest_input_text(test_url)           # 各textへの入力test
            self.subtest_input_text_zipcode(test_url)   # 郵便番号→住所変換test
            self.subtest_input_radio(test_url)          # radioのnull test
            self.subtest_select_build_plan(test_url)    # 選択による他項目の表示切替え test
      
      
    def subtest_input_all_valid(self,test_url):
        self.init_input_page(test_url)

        #入力内容に全く問題なければ、.submitBtn のelement に .js-disabled がない
        submit_btn = self.driver.find_element_by_class_name("submitBtn")
        self.assertEqual("submitBtn", submit_btn.get_attribute("class"))
        #上記の処理までにvalidatioとしてはOKだが、おまけでsubmitをclick
        submit_btn.click()


    def init_input_page(self,test_url):
        retry_limit = 5
        while retry_limit:
            if self.init_input_page_sub(test_url):
                return True
            retry_limit -= 1
            print("retry init_input_page_sub()", retry_limit)
        return False

    def init_input_page_sub(self,test_url):
        self.driver.get(test_url) # 対象urlへaccess
        
        # onbeforeunload イベントにより
        # 「ページを離れようとしています。よろしいですか?」のalertが
        # 表示される場合の処理
        try:
            alert = self.driver.switch_to.alert
            alert.accept()
        except:
            pass

        # pageの全要素が読み込まれるまでwait
        # refer to https://qiita.com/uguisuheiankyo/items/cec03891a86dfda12c9a
        try:
            WebDriverWait(self.driver, 10).until(EC.presence_of_all_elements_located)
        except:
            print("timeout",presence_of_all_elements_located)
            return False

        # 氏名等の入力前にcatalog選択し、その後、初期値として、全項目を入力
        if self.pre_input_all_valid(test_url) == False:
            return False
        if self.input_all_valid(test_url) == False:
            return False
        return True

    def subtest_input_text(self,test_url):
        # 現行のvalidationは、/request/js/common.js に定義されてます
        elm_names = {}
        elm_names["name_f"] = elm_names["name_s"] = \
            [{"value":"",                             "valid":False },
             {"value": random_str(10,["katakana_h"]), "valid":False },
             {"value": random_str(10,["hiragana","katakana_z","kanji","kishuizon"]),
              "valid":True },
             {"value": random_str(11,["hiragana","katakana_z","kanji","kishuizon"]),
              "valid":False}]
        elm_names["name_fk"] = elm_names["name_sk"] = \
            [{"value":"",                             "valid":False },
             {"value": random_str(10,["katakana_z"]), "valid":True  },
             {"value": random_str(11,["katakana_z"]), "valid":False },
             # TODO フリガナは、全角数字やアルファベットを許さず
             # valudationが厳しい
             {"value": random_str(10,["katakana_z","number_z","alphabet_z"]),
              "valid":False },
             {"value": random_str(10,["katakana_z","number_z"]), "valid":False },
             {"value": random_str(10,["katakana_z"]),            "valid":True} ]
        elm_names["zip_f"] = [{"value":"",               "valid":False},
                              {"value":random_str(7,[]), "valid":False},
                              {"value":random_str(8,[]), "valid":False}]
        elm_names["add_1"] = [{"value":"",                  "valid":False},
                              {"value": random_str(100,["number_z","alphabet_z",
                                                        "hiragana","kanji","kishuizon"]),
                               "valid":True },
                              {"value": random_str(101,["number_z","alphabet_z",
                                                        "hiragana","kanji","kishuizon"]),
                               "valid":False },
                              {"value": random_str(50,[]), "valid":False }]
        elm_names["add_2"] = [{"value":"",                  "valid":False},
                              {"value": random_str(100,["number_z","alphabet_z",
                                                        "hiragana","kanji","kishuizon"]),
                               "valid":True },
                              {"value": random_str(101,["number_z","alphabet_z",
                                                        "hiragana","kanji","kishuizon"]),
                               "valid":False },
                              {"value": random_str(50,[]), "valid":True }]
        elm_names["tel1"] =  [{"value":"",                 "valid":False},
                              {"value": random_str(11,[]), "valid":False},
                              {"value":"09012345678",      "valid":True }]
        elm_names["email1"] = [{"value":"",                 "valid":False},
                               {"value": random_str(11,[]), "valid":False}]
        elm_names["hp_inquire"] = [{"value":"",                   "valid":True },
                                   {"value": random_str(1500,[]), "valid":False}]
        
        for elm_name in elm_names.keys():
            for test_case in elm_names[elm_name]:
                print(elm_name, test_case["value"], test_case["valid"] )

                # 画面の初期状態として、全項目に値を入力
                self.init_input_page(test_url)

                # 次にtest_caseにある値を入力
                form_elms = self.driver.find_elements_by_name(elm_name)

                form_elms[0].clear()
                form_elms[0].send_keys(test_case["value"] + "\t")
                time.sleep(0.5)

                submit_btn = self.driver.find_element_by_class_name("submitBtn")
                time.sleep(1)

                if test_case["valid"]==False:
                    self.assertEqual("submitBtn js-disabled",
                                     submit_btn.get_attribute("class"),
                                     elm_name +" "+ test_case["value"])
                else:
                    self.assertEqual("submitBtn",
                                     submit_btn.get_attribute("class"),
                                     elm_name +" "+ test_case["value"])

    # "build_plan" により、他項目の表示/非表示が切り替わるtest
    def subtest_select_build_plan(self,test_url):
        self.init_input_page(test_url)
        
        plan_elms      = self.driver.find_elements_by_name("build_plan")
        plan_size_elms = self.driver.find_elements_by_name("build_plan_size")
        plan_city_elms = self.driver.find_elements_by_name("build_plan_city")
        plan_area_elms = self.driver.find_elements_by_name("build_plan_area")

        test_cases = \
            [{"plan":"お選びください",      "size":False,"city":False,"area":False},
             {"plan":"あり(現住所と同じ)","size":True, "city":False,"area":False},
             {"plan":"あり(現住所と違う)","size":True, "city":True, "area":False},
             {"plan":"なし(購入希望)",    "size":True, "city":False,"area":True}]

        for test_case in test_cases:
            plan_elms[0].send_keys(test_case["plan"])
            time.sleep(2)
            self.assertEqual(test_case["size"], plan_size_elms[0].is_displayed())
            self.assertEqual(test_case["city"], plan_city_elms[0].is_displayed())
            self.assertEqual(test_case["area"], plan_area_elms[0].is_displayed())

        
    def subtest_input_radio(self,test_url):
        elm_defs = [{"elm_name":"sex",  "valid":True }, 
                    {"elm_name":"bunjo","valid":True }] #任意項目
        for elm_def in elm_defs:
            self.init_input_page(test_url)

            form_elms = self.driver.find_elements_by_name(elm_def["elm_name"])
            for form_elm in form_elms:
                elm_name = elm_def["elm_name"]
                
                # radioはseleniumでnull化できない為、javascriptにより値をclear
                if form_elm.is_selected():
                    js_str = "".join(["for( elm of document.getElementsByName('",
                                      elm_name,
                                      "')) elm.checked = false;"])
                    self.driver.execute_script(js_str)
                    
                submit_btn = self.driver.find_element_by_class_name("submitBtn")

            if elm_def["valid"]==False:
                self.assertEqual("submitBtn js-disabled",
                                 submit_btn.get_attribute("class"),
                                 elm_name)
            else:
                self.assertEqual("submitBtn",
                                 submit_btn.get_attribute("class"),
                                 elm_name)


    # 郵便番号→住所 自動変換のtest
    def subtest_input_text_zipcode(self,test_url):
        # google日本語apiにより住所へ変換していますが、
        # 新しい過ぎる?郵便番号は変換できないようです
        zipcodes = [{"zip_f":"100-0005", "todouhuken":"東京都","add_1":"千代田区丸の内"},
                    {"zip_f":"1000005",  "todouhuken":"東京都","add_1":"千代田区丸の内"},
                    {"zip_f":"150-0002", "todouhuken":"東京都","add_1":"渋谷区渋谷"},
                    {"zip_f":"105-8566", "todouhuken":"",      "add_1":""}, # ※1
                    {"zip_f":"999-9999", "todouhuken":"",      "add_1":""} ]

        # まず、対象urlへaccessし、全項目に正しく入力
        self.init_input_page(test_url)
        
        for zipcode in zipcodes:
            zip_elms  = self.driver.find_elements_by_name("zip_f")
            pref_elms = self.driver.find_elements_by_name("todouhuken")

            add_elms  = self.driver.find_elements_by_name("add_1")
            zip_elms[0].clear()
            pref_elms[0].send_keys("お選びください")
            add_elms[0].clear()

            zip_elms[0].send_keys(zipcode["zip_f"])
            btn_elms  = self.driver.find_elements_by_css_selector("a.addressBtn")
            btn_elms[0].click()
            time.sleep(2)

            self.assertEqual(zipcode["todouhuken"],pref_elms[0].get_attribute("value"))
            self.assertEqual(zipcode["add_1"],     add_elms[0].get_attribute("value") )


    def pre_input_all_valid(self,test_url):
        # catalog一覧が表示されるまでwait
        selector_str = '.hmSetList__list>li'
        WebDriverWait(self.driver,10).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR,selector_str)
            ))

        # catalog一覧内からrandomに選択
        li_elms = self.driver.find_elements_by_css_selector(selector_str)
        li_pos = random.randint(0, len(li_elms)-1)
        try:
            li_elms[li_pos].click()
        except:
            print("fail click ",selector_str)
            return False
        
        # 「お受け取り先の入力へ」がclick可能になるまでwait
        x_path_str = "//span[text()='お受け取り先の入力へ']"
        WebDriverWait(self.driver,10).until(
            EC.element_to_be_clickable((By.XPATH,x_path_str)
            ))

        span_elms = self.driver.find_elements_by_xpath(x_path_str)
        try:
            span_elms[0].click()
        except:
            print("fail click ",x_path_str)
            return False
        return True


    def input_all_valid(self,test_url):

        for elm_name in form_type_a_all_valid.keys():
            try:
                WebDriverWait(self.driver,10).until(
                    EC.element_to_be_clickable((By.NAME,elm_name)
                    ))
            except:
                print("timeout element_to_be_clickable",elm_name)
                return False

            elm_def = form_type_a_all_valid[elm_name]
            form_elms = self.driver.find_elements_by_name(elm_name)
            
            if form_elms[0].get_attribute("type")=="select-one":
                form_elms[0].send_keys(elm_def["value"])
                
            elif form_elms[0].get_attribute("type")=="radio":
                for form_elm in form_elms:
                    if form_elm.get_attribute("value")==elm_def["value"]:
                        form_elm.click()
                        form_elm.click()
                        # 親要素の選択
                        # form_elm.find_element_by_xpath("..").click()
                        break
            elif form_elms[0].get_attribute("type")=="checkbox" :
                for form_elm in form_elms:
                    if form_elm.get_attribute("value")==elm_def["value"]:
                        try:
                            form_elm.click()
                        except:
                            print("fail click ",
                                  form_elms[0].get_attribute("type"),
                                  elm_name )
                            return False
                        break
            else:
                # focos out時にvalidationされる場合もある為、タブも追加で押下げ
                form_elms[0].clear()
                form_elms[0].send_keys(elm_def["value"]+"\t")

        return True

                
form_type_a_all_valid = \
    {"name_f"           :{"value":"テスト髙橋"                      },
     "name_s"           :{"value":"テスト①太郎"                    },
     "name_fk"          :{"value":"タカハシ"                        },
     "name_sk"          :{"value":"イチタロウ"                      },
     "sex"              :{"value":"女"                              },
     "age"              :{"value":"26~30歳"                        },
     "job"              :{"value":"会社員(事務系)"                },
     "zip_f"            :{"value":"100-0005"                        },
     "todouhuken"       :{"value":"東京都"                          },
     "add_1"            :{"value":"千代田区丸の内"                  },
     "add_2"            :{"value":"1丁目 東京駅の住所です"         },
     "tel1"             :{"value":"03-1234-5678"                    },
     "email1"           :{"value":"test-user-01@example.com"        },
     "build_clas2"      :{"value":"新築"                            },
     "build_plan"       :{"value":"あり(現住所と同じ)"            },
     "build_plan_size"  :{"value":"25坪以下(約82平方メートル以下)"},
     #build_plan_size, build_plan_city, build_plan_area は
     #build_planの値により表示/非表示が切り替わります
     #"build_plan_city":{"type":"text"},
     #"build_plan_area":{"type":"select-one"}
     "build_time"       :{"value":"半年以内"                                        },
     "interest1"        :{"value":"土地情報の紹介・斡旋、土地購入に関するアドバイス"},
     "interest2"        :{"value":"プランニングのご相談"                            },
     "interest3"        :{"value":"ファイナンシャルプランナーによる家計アドバイス"  },
     "interest4"        :{"value":"法律家、税理士、会計士等へのご相談"              },
     "interest5"        :{"value":"工場見学会や建築現場・ご入居者様宅の見学会"      },
     "build_budget"     :{"value":"1500万円以下"                                    },
     "soil_budget"      :{"value":"1500万円以下"                                    },
     # bunjo の項目は、選択したcatalogによっては表示されない
     "bunjo"            :{"value":"希望する"                                        },
     "hp_inquire"       :{"value":"これはテストのご意見です。\n改行を入れています。"}}

def random_str(char_len,char_groups):

    chars = \
        {"number_h"   : "0123456789",
         "number_z"   : "0123456789",
         "alphabet_h" : "abcdefghij",
         "alphabet_z" : "abcdefghij",
         "symbol_h"   : "~!@#$%^&*(",
         "space"      : "       ",
         "hiragana"   : "あいうえおぎゃぎょー",
         "katakana_z" : "アイウエオギャギョー",
         "katakana_h" : "アイウエオャギョー",
         "kanji"      : "日本東京都港区虎ノ門",
         "kishuizon"  : "㈱髙﨑①Ⅰα㍻㋿─○"}
    
    str_source = "";
    if len(char_groups) > 0:
        for char_group in char_groups:
            str_source += chars[char_group]
    else:
        for char_group in chars.keys():
            str_source += chars[char_group]
        
    # 元となる文字列が短い場合、random.sample()が失敗する為
    while len(str_source) <= char_len:
        str_source += str_source
            
    random_str = "".join(random.sample(str_source, char_len))
    return random_str


if __name__ == "__main__":
    unittest.main()

input type=radioは selenium for python でclearできない為、javascriptを使用しましょう

つまり、以下の execute_script() で OK です

def subtest_input_radio(self,test_url):
    for elm_name in ["build_time"]:
        self.driver.get(test_url)      # 対象urlへaccess
        self.input_all_valid(test_url) # まず全項目に正しく入力

        form_elms = self.driver.find_elements_by_name(elm_name)
        for form_elm in form_elms:
            if form_elm.is_selected():
                js_str = "".join(["for( elm of document.getElementsByName('",
                                  elm_name,
                                  "')) elm.checked = false;"])
                self.driver.execute_script(js_str)
                
        self.driver.find_element_by_id("submit").click()
        time.sleep(3)
    
        # 入力内容に問題なければ、#formErrorにerror msgなしor次の画面へ遷移
        err_msg_elms = self.driver.find_elements_by_class_name("errWrap")
        self.assertNotEqual("",
                         err_msg_elms[0].text,
                         elm_name +" not selected")

python で、asciiやかな、漢字、機種依存文字を含むランダム文字列を生成

とあるweb入力formのseleniumによる自動テストを作成する際に役立つかと思い。

次の通りかと思います。

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import random

def main():
    make_random_str(30,["number","kanji","kishuizon"])
    
def make_random_str(char_len,char_groups):
    chars = \
        {"number"   : "0123456789",
         "alphabet" : "abcdefghij",
         "symbol"   : "~!@#$%^&*(",
         "space"    : " \t  \t  \t  ",
         "hiragana" : "あいうえおぎゃぎょー",
         "katakana" : "アイウエオギャギョー",
         "hankata"  : "アイウエオャギョー",
         "kanji"    : "日本東京都港区虎ノ門",
         "kishuizon": "㈱髙﨑①Ⅰα㍻㋿─○"}

    str_source = "";
    for char_group in char_groups:
        str_source += chars[char_group]

    # 元となる文字列が短い場合、random.sample()が失敗する為
    while len(str_source) <= char_len:
        str_source += str_source

    print( "".join(random.sample(str_source, char_len)) )


if __name__ == "__main__":
    main()

↑こう書くと、↓こう表示されます

0東﨑○㋿α5京門日本﨑都東23㈱94虎髙718区─ノ6Ⅰ①

python における byte文字列<->unicode文字列の変換は、encode("unicode-escape") / decode("unicode-escape")

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

def main():
    str_org = "あいうえお"
    print(str_org)
    bytes = str_org.encode("unicode-escape")
    print(bytes)
    str_new = bytes.decode("unicode-escape")
    print(str_new)

if __name__ == "__main__":
    main()

↑こう書くと、↓こう表示されます

あいうえお
b'\\u3042\\u3044\\u3046\\u3048\\u304a'
あいうえお

keystore / keytools for java オレオレ入門

サーバ証明書作成の際、これまで、openssl を使用してきましたが、 keystore / keytools for java は、殆ど利用したことがない為、メモ。

keystore / keytools for java とは

鍵fileや証明書fileを複数保管できるfileで、keystoreのfile全体もpasswordで暗号化されます。

keystore file は、 keytools for java コマンドで作成/管理します。

keystore の JCEKS とは

keystore の file形式には、 JKS(Java KeyStore), JCEKS(Java Cryptography Extension KeyStore), PKCS#12 があります。

このうち、JCEKS は、JKS の独自代替keystoreで、Triple DES により強力に暗号化されています。

SSOのプロダクトであるOpenAMは、過去、JKS形式をdefaultとしていましたが OpenAM ver13.5より JCEKS を default としています。

keytools for java の コマンド例

JKS 形式 の keypair 作成 と、JKS->JKS インポート

# echo -n "keypass"   > /tmp/keypass       ## 作成するkey用
# echo -n "storepass" > /tmp/storepass     ## keysotre   用

## keypair(有効期間=10年、エイリアス名=SAML)を keystore.jks へ作成
$ keytool -genkeypair -keyalg RSA -keysize 2048 -validity 3650 -alias saml \
    -dname "CN=sso.end0tknr.com,OU=sso,O=end0tknr,L=Tokyo,C=JP" \
    -keystore /tmp/keystore.jks \
    -keypass `cat /tmp/keypass` -storepass `cat /tmp/storepass`

## import用に更に作成
$ keytool -genkeypair -keyalg RSA -keysize 2048 -validity 3650 -alias saml \
    -dname "CN=sso.end0tknr.com,OU=sso,O=end0tknr,L=Tokyo,C=JP" \
    -keystore ~/tmp/keystore_2.jks \
    -keypass `cat ~/tmp/keypass` -storepass `cat ~/tmp/storepass`

## keystore_2.jks -> keystore.jks で import
$ keytool -importkeystore \
        -srckeystore  ~/tmp/keystore_2.jks -srcstoretype  jks \
        -destkeystore ~/tmp/keystore.jks   -deststoretype jks \
        -srcstorepass  `cat ~/tmp/storepass` \
        -deststorepass `cat ~/tmp/storepass`
  Existing entry alias saml exists, overwrite? [no]:  no
  Enter new alias name  (RETURN to cancel import for this entry):  saml2
  Enter key password for <saml>
  Entry for alias saml successfully imported.

## keystore.jks の内容確認
$ keytool -v -list -keystore ~/tmp/keystore.jks -storetype jks \
             -storepass `cat ~/tmp/storepass`
Keystore type: jks
Keystore provider: SUN

Your keystore contains 2 entries

Alias name: saml
Creation date: Aug 10, 2020
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=sso.end0tknr.com, OU=sso, O=end0tknr, L=Tokyo, C=JP
Issuer: CN=sso.end0tknr.com, OU=sso, O=end0tknr, L=Tokyo, C=JP
Serial number: 7bed8067
Valid from: Mon Aug 10 10:52:11 JST 2020 until: Thu Aug 08 10:52:11 JST 2030
Certificate fingerprints:
     MD5:  2C:D8:DB:4C:C9:89:47:96:0E:A5:01:13:42:F5:7E:1B
     SHA1: C7:74:5B:D1:EC:FC:BD:96:9C:BD:34:3B:19:F3:E4:86:DE:2C:93:E4
     SHA256: AC:9E:DB:DC:0C:2E:22:FC:18:FC:6F:5E:FF:5A:7E:01:18:F6:97:~
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3

(おまけ) JKS の OpenAMへの配備

参考 https://www.identityfusion.com/how-to-configure-openam-signing-keys/

$ sudo /opt/openam/admin/sso/bin/ampassword --encrypt /tmp/keypass > /tmp/.keypass
$ sudo /opt/openam/admin/sso/bin/ampassword --encrypt /tmp/storepass > /tmp/.storepass

$ rm /tmp/keypass /tmp/storepass 
$ sudo mv /tmp/keystore.jks /tmp/.keypass /tmp/.storepass /usr/share/tomcat8/sso/sso/
$ sudo chown tomcat:tomcat /usr/share/tomcat8/sso/sso/keystore.jks \
                           /usr/share/tomcat8/sso/sso/.keypass \
               /usr/share/tomcat8/sso/sso/.storepass 

JCEKS 形式 の keypair 作成 と、JKS->JCEKS インポート

## keypair(有効期間=10年、エイリアス名=SAML3)を keystore.jceks へ作成
$ keytool -genkeypair -keyalg RSA -keysize 2048 -validity 3650 -alias saml3 \
       -dname "CN=sso.end0tknr.com,OU=sso,O=end0tknr,L=Tokyo,C=JP" \
       -keystore ~/tmp/keystore.jceks -storetype jceks \
       -keypass `cat ~/tmp/keypass` -storepass `cat ~/tmp/storepass`

## keystore.jks -> keystore.jceks で import
$ keytool -importkeystore \
        -srckeystore  ~/tmp/keystore.jks   -srcstoretype  jks \
        -destkeystore ~/tmp/keystore.jceks -deststoretype jceks \
        -srcstorepass  `cat ~/tmp/storepass` \
        -deststorepass `cat ~/tmp/storepass`

  Importing keystore /home/end0tknr/tmp/keystore.jks to /home/end0tknr/tmp/keystore.jceks...
  Enter key password for <saml>
  Entry for alias saml successfully imported.
  Enter key password for <saml2>
  Entry for alias saml2 successfully imported.

## keystore.jks の内容確認
$ keytool -v -list -keystore ~/tmp/keystore.jceks -storetype jceks \
             -storepass `cat ~/tmp/storepass`
Keystore type: JCEKS
Keystore provider: SunJCE

Your keystore contains 3 entries

Alias name: saml
Creation date: Aug 10, 2020
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=sso.end0tknr.com, OU=sso, O=end0tknr, L=Tokyo, C=JP
Issuer: CN=sso.end0tknr.com, OU=sso, O=end0tknr, L=Tokyo, C=JP
Serial number: 7bed8067
Valid from: Mon Aug 10 10:52:11 JST 2020 until: Thu Aug 08 10:52:11 JST 2030
Certificate fingerprints:
     MD5:  2C:D8:DB:4C:C9:89:47:96:0E:A5:01:13:42:F5:7E:1B
     SHA1: C7:74:5B:D1:EC:FC:BD:96:9C:BD:34:3B:19:F3:E4:86:DE:2C:93:E4
     SHA256: AC:9E:DB:DC:0C:2E:22:FC:18:FC:6F:5E:FF:5A:7E:01:18:F6:97:~
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3
   :

yum install httpd で adduser された apache を su - 可能にする

以下の通り、/etc/passwd で「 /sbin/nologin」を「 /bin/bash 」とすれば、OK

# su - apache
This account is currently not available.
# vi /etc/passwd
apache:x:48:48:Apache:/var/www:/bin/bash  # CHANGE from /sbin/nologin

AWS Certificate Manager(ACM)で、既存のSSL証明書をimport

忘れるので、メモ。

証明書 & 秘密鍵 & 中間証明書を用意した上で、以下の画面遷移の通り。

ちなみに、証明書チェーン = 中間証明書 です

STEP1/4

f:id:end0tknr:20200804212350p:plain

STEP2/4

f:id:end0tknr:20200804212417p:plain

STEP3/4

f:id:end0tknr:20200804212452p:plain

STEP4/4

f:id:end0tknr:20200804212504p:plain

install postgres 12.3 from source code to centos8

前準備 : yum install systemd-devel

centos8ですので、自動起動には systemd (systemctl)を使用します。 postgresでは、systemd による自動起動を行う場合、 configure時に「--with-systemd」を指定します。

更に「--with-systemd」は「systemd-devel」を必要とします。

$ sudo yum install systemd-devel

上記を行わないと、以下のようなerrorとなります。

$ ./configure --with-systemd
  :
configure: error: header file <systemd/sd-daemon.h> is required for systemd support

download source code , build , install ...

wget https://ftp.postgresql.org/pub/source/v12.3/postgresql-12.3.tar.gz
tar -xvf postgresql-12.3.tar.gz
cd postgresql-12.3
less INSTALL

./configure --with-systemd
make
make check
$ su
# make install
# adduser postgres
# mkdir /usr/local/pgsql/data
# chown postgres /usr/local/pgsql/data
# su - postgres
$ /usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data
$ /usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start
$ /usr/local/pgsql/bin/createdb test
$ /usr/local/pgsql/bin/psql test

systemd (systemctl)による自動起動

参考url https://www.postgresql.jp/document/12/html/server-start.html

$ sudo vi /etc/systemd/system/postgresql.service

[Unit]
Description=PostgreSQL database server
Documentation=man:postgres(1)

[Service]
Type=notify
User=postgres
ExecStart=/usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=0

[Install]
WantedBy=multi-user.target

$ sudo systemctl enable postgresql
Created symlink /etc/systemd/system/multi-user.target.wants/postgresql.service → /etc/systemd/system/postgresql.service.
$ sudo systemctl start postgresql

JAVA_HOME設定の為、readlinkとdirnameで yum installしたjavaの場所を調べる

よく忘れるので、メモ

$ sudo yum install java-1.8.0-openjdk-devel.x86_64
$ sudo alternatives --config java
$ /usr/bin/java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)

$ sudo alternatives --config java

$ dirname $(readlink $(readlink $(which java)))
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.amzn2.0.1.x86_64/jre/bin

$ export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.amzn2.0.1.x86_64