yucatio@システムエンジニア

趣味で作ったものいろいろ

Firestoreで文字列を正規表現で制限するセキュリティルール

Firestoreのセキュリティルールでは、文字列を正規表現で制限することができます。悪意のあるユーザにおかしな値を登録されないようアプリと同じ制限をかけておきましょう。

String.matches(regex)

String.matchesを使うと、フィールドに登録される値が 指定された正規表現を満たす場合にのみ、DBに登録することができます。 記述はこのようになります。

// 電話番号(ハイフンなし)
allow create, update: if request.resource.data.tel.matches("^0[0-9]{9,10}$")
// メールアドレスっぽい文字列
// size()も組み合わせて使いましょう
allow create, update: if request.resource.data.email.matches("^[-a-zA-Z0-9_\\.]+@[-a-zA-Z0-9_\\.]+$") &&
                         request.resource.data.email.size() <= 255

String.matches()の使用例

例として参加登録フォームを作ってみます。

f:id:yucatio:20211026132400p:plain:w350

データ構造はこのようになっています。

f:id:yucatio:20211026150453p:plain

電話番号は0とそれに続く数字が9個か10個です。メールアドレスは半角英数と、記号は-_.が使えます。@マークは1つだけです。 セキュリティルールの全体はこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /applications/{application} {
      function validApplication(docData) {
        return docData.tel.matches("^0[0-9]{9,10}$") &&
               docData.email.matches("^[-a-zA-Z0-9_\\.]+@[-a-zA-Z0-9_\\.]+$") &&
               docData.email.size() <= 255;
      }
      allow create, update: if validApplication(request.resource.data)
    }
  }
}

正規表現に合うように登録してみます。

f:id:yucatio:20211026132400p:plain:w350

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211026141753p:plain

電話番号を12桁で登録してみます。

f:id:yucatio:20211026141917p:plain:w350

エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211017172609p:plain

次に、メールアドレスに使えない文字列を使ってみます。

f:id:yucatio:20211026143959p:plain:w350

こちらもエラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211017172609p:plain

リンク

正規表現はこちらのものを参考にしました。 qiita.com

Firestoreで数値の範囲を制限するセキュリティルール

Firestoreのセキュリティルールで数値の範囲を制限することができます。悪意のあるユーザにより思わぬ値を登録されてしまい、誤動作を起こすことを防げます。

フィールドの大小比較を使う

数値の範囲を指定するには、フィールドの値を大小比較します。 例えば、"age"(年齢)フィールドを18以上65以下に制限するには以下のように記述します。

allow create, update: if request.resource.data.age >= 18 &&
                         request.resource.data.age <= 65

数値の範囲の使用例

例として参加登録フォームを作ってみます。

f:id:yucatio:20211024160316p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211024172031p:plain

年齢を必須かつ18以上65以下という条件を、セキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /applications/{application} {
      function validApplication(docData) {
        return docData.age is int && 
               docData.age >= 18 &&
               docData.age <= 65;
      }
      allow create, update: if validApplication(request.resource.data)
    }
  }
}

18歳で登録してみます。

f:id:yucatio:20211024161032p:plain:w300

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211024164702p:plain

-(マイナス)30歳で登録してみると、エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211024163343p:plain:w300

f:id:yucatio:20211017172609p:plain

以上でFirestoreのセキュリティルールで数値の範囲を制限することができました。

Firestoreで文字列の長さを制限するセキュリティルール

Firestoreのセキュリティルールで文字列の長さを制限することができます。悪意のあるユーザによりとても長い文字列が登録されてしまい、結果過大な請求が発生するのを防ぐためにも、文字列の長さを制限しておくのはおすすめです。

string.size()

文字列のstring.size()メソッドを使用することで文字列の長さを取得できます。これと大小比較を組み合わせてルールを作成します。 例えば、"text"フィールドを16文字以下に制限するには以下のように記述します。

allow create, update: if request.resource.data.text.size() <= 16

String.size()の使用例

例としてTODOアプリを考えます。入力はタスク名とステータス(実施状況)です。

f:id:yucatio:20211017171414p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211017230600p:plain:w350

タスク名を必須かつ16文字以下という条件を、セキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todo} {
      function validTodo(docData) {
        return docData.text.size() > 0 &&
               docData.text.size() <= 16;
      }
      
      allow read: if true;

      allow create, update: if validTodo(request.resource.data);
    }
  }
}

6文字のタスク名で登録してみます。

f:id:yucatio:20211018121053p:plain:w250

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211018121104p:plain

長いタスク名(18文字)で登録してみると、エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211018120759p:plain

以上でFirestoreのセキュリティルールで文字列の長さを制限することができました。

FirestoreでEnum型風ルールを作成する

Firestoreではセキュリティルールでフィールドの型を指定できます。 フィールドの型にはboolintstringなどがあります。 しかしenum型は存在しません。

Firebaseのセキュリティルールでは、string型のフィールドに対して登録できる文字列を制限することができます。 この機能を使うことでenum型風のフィールドを作成することができます。

String.matches(regex)

String.matchesを使うと、フィールドに登録される値が 指定された正規表現を満たす場合にのみ、DBに登録することができます。 記述はこのようになります。

allow create, update: if request.resource.data.status.matches("^(TODO|IN_PROGRESS|DONE)$")

String.matches(regex)の使用例

例としてTODOアプリを考えます。入力はタスク名とステータス(実施状況)です。

f:id:yucatio:20211017171414p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211017230600p:plain:w350

status(ステータス)には"TODO"(未着手)か"IN_PROGRESS"(作業中)か"DONE"(完了)のみ指定できます。

これをセキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todo} {
      function validTodo(docData) {
        return docData.status.matches("^(TODO|IN_PROGRESS|DONE)$")
      }
      
      allow read: if true;
      allow create, update: if validTodo(request.resource.data);
    }
  }
}

このセキュリティルールでステータスが"TODO"のタスクを登録します。 Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211017172045p:plain

もし"IN_PROGRESS"を"DOING"と間違えてしまった場合、"Missing or insufficient permissions."のエラーが出ます。DBを確認すると登録ができていないことが確認できます。

f:id:yucatio:20211017172609p:plain
google chromeのコンソール

最後に、念のため正規表現があっているか確かめておきましょう。

https://jex.im/regulex/#!flags=&re=%5E(TODO%7CIN_PROGRESS%7CDONE)%24

f:id:yucatio:20211017171755p:plain

大丈夫そうですね。

MUI(Material-UI)のアイコンにフチドリをつける

MUI(Material-UI)のアイコンにフチドリをつけます。

f:id:yucatio:20211017140609p:plain

通常のCSSのスタイルと同様にstrokestrokeWidthを指定します。strokeOpacitystrokeLinejoinなども指定できます。

コード

<StarIcon
  sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}}
/>

ソースコード

import React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import StarIcon from '@mui/icons-material/Star';
import FavoriteIcon from '@mui/icons-material/Favorite';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import AppleIcon from '@mui/icons-material/Apple';
import BedtimeIcon from '@mui/icons-material/Bedtime';
import CloudIcon from '@mui/icons-material/Cloud';
import { blue, grey, red, pink, yellow } from '@mui/material/colors';

function App() {
  return (
    <Box sx={{p:2}}>
      <Typography variant="h6">通常</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500]}} />
        <FavoriteIcon sx={{color: pink[500]}} />
        <AddCircleIcon sx={{color: blue[500]}} />
        <AppleIcon sx={{color: red[500]}} />
        <BedtimeIcon sx={{color: yellow[600]}} />
        <CloudIcon sx={{color: grey[500]}} />
      </Stack>

      <Typography variant="h6">フチドリあり</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <FavoriteIcon sx={{color: pink[500], stroke: pink[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <AddCircleIcon sx={{color: blue[500], stroke: blue[800], strokeWidth:4, strokeOpacity: 0.2}} />
        <AppleIcon sx={{color: red[500], stroke:red[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <BedtimeIcon sx={{color: yellow[600], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <CloudIcon sx={{color: grey[500], stroke:grey[800], strokeWidth: 4, strokeOpacity: 0.2}} />
      </Stack>
    </Box>
  );
}
export default App;

環境

  • Material-UI: 5.0.4
  • React: 11.4.1

PythonでSetのSetを作成する

PythonでSetのSetを作成しようとするとエラーになります。

my_set = set()
my_set.add({1, 2, 3})
# => TypeError: unhashable type: 'set'

Pythonでは、Setの各要素はハッシュ可能(hashable)でなければいけません。 Pythonの組み込み型でハッシュ可能なのは、int, str, tuple, frozensetです。list. dict, setはunhashableハッシュ不可能(unhashable)です。

ハッシュ可能についてはこちらの記事を参考にしました。

qiita.com

frozensetを使う

pythonでsetのsetを作成するには、frozensetを使用します。frozensetはイミュータッブルでハッシュ可能な型です。

my_set = set()

my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({4, 5}))

print(my_set)
#=> {frozenset({1, 2, 3}), frozenset({4, 5})}

エラーなく動作しました。

もう少し挙動を確認してみます。

my_set = set()

# setにsetを追加する
my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({1, 2}))
my_set.add(frozenset({2, 1}))  # 集合としては1つ上と同じ
my_set.add(frozenset({5, 4, 3}))
my_set.add(frozenset({4, 3, 5, 3, 4}))  # 集合としては1つ上と同じ
my_set.add(frozenset({4}))
my_set.add(frozenset({10, 2, 4, 7, 2}))
# addの回数は7回

print(len(my_set))
# => 5

print(my_set)
# => {frozenset({1, 2}), frozenset({10, 2, 4, 7}), frozenset({3, 4, 5}), frozenset({1, 2, 3}), frozenset({4})}
# {1, 2} と {2, 1} は同一のものなので、setには片方が登録されている
# {5, 4, 3} と {5, 4, 3, 3, 4} は同一のものなので、setには片方が登録されている

print({1, 2} in my_set)
# => True
# setとの比較は可能

print({3, 5, 4} in my_set)
# => True
# {3, 4, 5}と同一のものとして判定される

print({2} in my_set)
# => False
# 2が含まれる要素もあるが、my_setの要素としては一致しないのでFalseとなる

以上で、setのsetの作成と、動作確認ができました。

再帰を使わない順列生成 : 1から9の数字を1回ずつ使ってできる数を、小さい方から順に列挙する(アルゴリズム編)

1から9の数字を1回ずつ使ってできる数を、小さい方から順に列挙するアルゴリズムです。

次に小さい数を求めるアルゴリズム

ある数Nが与えられたとき、その数に使用されている数字を使用してできる数で、Nの次に大きい数を生成します。

N10^iの桁を、n[i]で表します

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

n[i]n[j]を入れ替えます

n[0]からn[i-1]を逆順にします

この説明だとよくわからないと思うので、具体的な数字でアルゴリズムを確かめてみましょう。

アルゴリズムを適用した例

N=179836542 とします。

N10^iの桁を、n[i]で表します

以下のようになります。

n[0] = 2  // 一の位 (10^0)
n[1] = 4  // 十の位 (10^1)
n[2] = 5  // 百の位 (10^2)
n[3] = 6  // 千の位 (10^3)
n[4] = 3  // 万の位 (10^4)
n[5] = 8  // 十万の位 (10^5)
n[6] = 9  // 百万の位 (10^6)
n[7] = 7  // 千万の位 (10^7)
n[8] = 1  // 億の位 (10^8)

f:id:yucatio:20200326160139p:plain

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

小さい桁から見て行ったとき、3 < 6のとき初めて数が小さくなります。このときn[4] < n[3]なので、i = 4です。

f:id:yucatio:20200326160231p:plain

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

10^4より下の桁で3より大きい数が初めて出てくるのは4です。n[1] = 4なので、j = 1です。

f:id:yucatio:20200326160423p:plain

n[i]n[j]を入れ替えます

34を入れ替えます。入れ替えた後も、n[0]からn[i-1]までは昇順に並んでいますね。

f:id:yucatio:20200326160434p:plain

n[0]からn[i-1]を逆順にします

入れ替えた結果です。

f:id:yucatio:20200326160448p:plain

以上で、179836542から、次に大きい数である、179842356を得ることができました。

小さい方から列挙するアルゴリズム

1から9の数字を1回ずつ使ってできる数の中で、一番小さい数は123456789です。 ここから、上記アルゴリズムを使用して、次に小さい数を求めます。 さらにそこから得られた数に対して上記アルゴリズムを適用します。

これを繰り返すことによって順列を取り出すことができます。

手順の、

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます

ここで該当するiがなくなったら終了です。

数字が重複しているとき

数が重複しているとき、も上記アルゴリズムはうまく動作します。

例えばN=652331の場合を考えます。

N10^iの桁を、n[i]で表します

f:id:yucatio:20200330085020p:plain

n[i]を桁小さい方からみていき、初めてn[i] < n[i-1]となるiを求めます こちらは上記と同じで、

n[0] = 1  // 一の位 (10^0)
n[1] = 3  // 百の位 (10^2)
n[2] = 3  // 千の位 (10^3)
n[3] = 2  // 万の位 (10^4)
n[4] = 5  // 十万の位 (10^5)
n[5] = 6  // 百万の位 (10^6)

2 < 3のとき初めて数が小さくなります。このときn[3] < n[2]なので、i = 3です。

f:id:yucatio:20200330085047p:plain

n[0]からn[i-1]まで順番に見ていき、n[i]より初めて大きくなるものを求めます。求めたものをn[j]とします。

2より下の桁で2より大きい数で、その中で最小なのは3です。3は2つありますが、桁が小さい方を選択します。桁が小さい方は n[1] = 3なので、j = 1です。

f:id:yucatio:20200330085113p:plain

n[i]n[j]を入れ替えます 32を入れ替えます。

f:id:yucatio:20200330085150p:plain

n[0]からn[i-1]を逆順にします

f:id:yucatio:20200330085217p:plain

以上で、652331から、次に大きい数である、653123を得ることができました。

応用 : n個の数のうちk個を使用する組み合わせ

上記のアルゴリズムは、n個の数を全て使用する場合の順列を列挙しました。 n個の数のち、k個を使用する場合のアルゴリズムは以下をご覧ください。

★記事作成中