Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

RLSを用いたマルチテナント実装 for Django

RLSを用いたマルチテナント実装 for Django
by Takayuki Shimizukawa

複数のテナント(チーム・組織)向けにサービスを提供するシステムで、テナント相互の情報を分離して扱う、複数のマルチテナントアーキテクチャが考案されています。「各プログラマが努力して実装する」戦略でも実現はできますが、プログラミングミスや設定間違いによるデータ混濁が高確率で発生します。このトークでは、マルチテナントアーキテクチャにおけるデータ分割アプローチのひとつ「共有アプローチ」をDjangoとPostgresのRow Level Security (RLS) の組合せで安全に実現する方法を紹介します。またこの方法のメリット、デメリットを紹介します。
https://djangocongress.jp/

Related Books

Free with a 30 day trial from Scribd

See all
  • Be the first to comment

RLSを用いたマルチテナント実装 for Django

  1. 1. Takayuki Shimizukawa 2021/7/3(土) DjangoCongress JP 2021 RLSを用いたマルチテナント実装 for Django 1
  2. 2. @shimizukawa (清水川) ● BeProud 取締役 / IT Architect ○ 受託開発(Webアプリ / 機械学習 / 数理最適化) ○ 自社サービス(connpass / PyQ / TRACERY) ○ Python研修(Python基礎、Django、Pandas、その他) ● 一般社団法人PyCon JP Association 会計理事 ○ PyCon JP 年次イベントの見守り ○ Python Boot Camp 主催 ● Sphinx (コミッター休業中) おまえ誰よ / Who are you 2
  3. 3. 1版: 2010/5/28 2版: 2018/2/26 3版: 2021/7/30 1版: 2013/09/12 2版: 2017/10/20 2018/02/23 2020/02/27 1版: 2012/03/27 2版: 2015/02/27 3版: 2018/06/12 執筆・翻訳した書籍 3 NEW 年
  4. 4. このトークで紹介すること マルチテナントアーキテクチャにおけるデータ分割アプロー チのひとつ「共有アプローチ」を、PostgresのRow Level Security (RLS)を用いてDjangoで安全に実現する方法を紹介 します。 またこの方法のメリット、デメリットを紹介します。 4
  5. 5. アジェンダ ● マルチテナント概要と課題 ● RLS: Row Level Security ● DjangoではRLSどうなるの? ● Djangoデモ ● マルチテナントの開発と運用 ● まとめ ● 参考資料 5
  6. 6. マルチテナントの概要と課題 6
  7. 7. マルチテナントの概要 マルチテナントは、複数のテナント(チーム・組織)向けにサービスを提供 するシステムです。 ● マルチテナント ○ 1つのシステムに複数の顧客 ○ 顧客同士はデータが隔離されている ○ 顧客同士は存在を知らない ● 対義語: シングルテナント ○ 1つのシステムに1つの顧客 ● それ以外: ○ 複数の組織/グループがサービス上共存している 7
  8. 8. Multi-Tenant Magic: Under the Covers of the Force.com data Architecture より 8 シングルテナントアーキテクチャ ユーザー企業ごとにデータベースを用 意すると、データベースごとに管理者 が必要となるため多くのシステム管理 者が必要となり、またインフラの利用 効率をあげることも難しい。 マルチテナントアーキテクチャ 全ユーザーが1つのインフラを共有する ため管理者が少なくて済み、インフラ の稼働効率も高くなる
  9. 9. マルチテナントの課題 マルチテナントによって、運用時の費用を下げたい。 しかし導入においては、テナント相互の情報分離、つまりデータ混濁が発生 しないと保証されていることが大事です。 「各プログラマが努力して実装する」戦略でもマルチテナントを実現できま すが、努力だけでデータ混濁を避けるのはとても難しいです。 9 小 難易度 大 大 運用コスト 小 図は Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する より引用
  10. 10. データ混濁 提供するデータに期待しないデータが混ざり込むこと。 ある顧客データのなかに別の顧客データが混ざりこむ等。 ● 個人情報が他の会社に見えちゃった ● 売上額の集計値に他の会社の売上が混ざる マルチテナントアーキテクチャは、データ混濁を避けるため の戦略 10
  11. 11. マルチテナントアーキテクチャ データ混濁を避ける、複数のマルチテナントアーキテクチャが考案されています 。 マルチテナント SaaS パターン - Azure SQL Database | Microsoft Docs による分 類では、マルチテナントは以下の3つのモデルに分けられる。 1. スタンドアロンアプリ 2. テナント単位データベース 3. シャードマルチテナント 図は Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する より引用 11
  12. 12. マルチテナントアーキテクチャ 1. スタンドアロンアプリ(DB分離、アプリ分離) ○ DB・アプリもテナント毎に用意、ユーザーには極めて柔軟 ○ 運用コストはもっともかかる 2. テナント単位データベース(DB分離/準分離、アプリ共有) ○ DBは分けつつ、アプリは共有 ○ 運用は、アプリが分かれてない分楽 ○ (清水川所感:Web記事、スライド等ではこのパターンが多い) 3. シャードマルチテナント(DB共有、アプリ共有) ○ 同じDB・アプリのリソースを利用 ○ 最も効率的で「真のマルチテナント」と呼ばれる ○ (今日紹介するのはこの方式) 12
  13. 13. DB分割アプローチ DB層でデータ混濁を避けるための、3つのDB分割アプローチ 1. 分離(isolated) ○ DBを分ける(≒ シングルテナントDB) 2. 準分離(semi-isolated) ○ DBは1つ、スキーマ/名前空間を分ける 3. 共有(shared) ○ DB/スキーマを分けない 13
  14. 14. DB準分離アプローチの特徴 ● 1つのDBインスタンス内に、テナント別スキーマ ○ ただし、一部のテーブルはPublicスキーマに配置 ● 仕組みが複雑で、通常運用が基本大変 ● アプリからは、スキーマ指定でDBにアクセス ○ Django拡張いくつかあり 採用例は多いが、保守停止時間はテナント数に比例 ● マイグレーション 30秒 x 5,000テナント = 42時間 ● 例: つらくないマルチテナンシーを求めて: 全て見せま す! SmartHR データベース移行プロジェクトの裏側 DB準分離アプローチ 14
  15. 15. DB共有アプローチ DB共有アプローチの特徴 ● DB、スキーマが全体で1つ ● 仕組みがシンプルで、通常運用が基本楽 ● WHERE句にTenantIDを指定必須 ○ Django拡張はあまりない 採用を避ける理由 ● ミスると、データ混濁が発生 ○ ORM実装で、WHERE句付け忘れ ○ 保守で生SQLを書いて、WHERE句付け忘れ ● 直感に反する(分かれてないので、危なそう) 15
  16. 16. DB分割アプローチ, Pros, Cons 5000テナントの場合の例 (django-easy-tenants · PyPIを元に追記) 16
  17. 17. メリット ● テーブルがスキーマ単位で分かれている(データ隔離) ○ データ混濁が発生しにくい、複数スキーマのデータが混在しない ● テナント個別の制御がしやすい ○ バックアップ、リストアが容易 ○ 負荷が高いテナントがあるとき、DB分離に切り替えやすい デメリット ● テーブルがスキーマ単位で分かれている(同じテーブルがたくさん) ○ 各スキーマにマイグレーションが必要、時間はテナント数に比例 ● テナント個別の制御が必要 ○ 管理ユーザーが、テナントそれぞれに必要 ○ 管理コストがテナントの数だけかかる DB準分離アプローチのメリデメ 17 by @kashew_nuts
  18. 18. メリット ● スキーマはひとつ、テーブル構造もひとつ ● テナント個別の制御が不要 ○ 管理ユーザーは、テナントをまたいで管理しやすい ○ 大抵はマルチテナントを忘れられるため、通常の実装 & 管理コストが低い デメリット ● テナント個別の制御がしづらい ○ バックアップ・リストア ○ 特定テナントだけをメンテナンス ○ 個別カスタマイズ ■ 保守運用が死ぬので、どのアプローチでもやめましょう!JSON型カラムで回避 ● ミスった時のデータ混濁が致命的 ○ RLS: Row Level Security (行レベルアクセス制御)を導入しましょう! 18 DB共有アプローチのメリデメ by @shimizukawa
  19. 19. DB準分離 vs DB共有 19 django-tenantsのような拡張も公開 されてますし! コードレベルで変更が必要な箇所は 分かってます! 準分離はテナント数が増えた場合の運用コス トが大きい! DB共有なら省リソース・省コストで、RLSを 使えば安全に実現できるはず! (真のマルチテナント、やってみたいなー)
  20. 20. RLS: Row Level Security
  21. 21. RLS: Row Level Securityとは ● Row Level Security = 行レベルのアクセス制御 ● データベース層でデータ混濁を回避する技術 ○ SQL実行時に、アクセス権限のある行だけが返される ○ WHERE句の指定は不要 ○ アクセス権限は、DB接続ユーザー または ロール で決まる RLSを使えば、 「シャードマルチテナント(真のマルチテナント)」 を安全に実現できる 21
  22. 22. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 SELECT * FROM customers; RLS利用時は権限のある行のみ取得 22 SQL実行時に、WHERE句を指定しなくても、アクセス権限の ある行だけが返される。 tenant_id =2 の全行 つまり、アプリ各所でのSQL書き換えが不要 ■User ■Role ■RLS
  23. 23. 23 どういう仕組み?
  24. 24. db=# CREATE TABLE rls (id SERIAL PRIMARY KEY, tenant_id INTEGER); db=> INSERT INTO rls (tenant_id) VALUES (1),(2),(3),(1),(2),(3); db=> SELECT * FROM rls; id | tenant_id ----+----------- 1 | 1 2 | 2 ... 6 | 3 (6 rows) 実験用テーブル rls を作る 24 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3
  25. 25. db=# CREATE ROLE “1”; -- tenant_id=1 の行のみアクセスできるROLE(予定) db=# SET ROLE “1”; db=> SELECT * FROM rls; ERROR: permission denied for table customers db=# RESET ROLE; -- 接続ユーザー権限に戻る 新規のロール "1" を作る 25 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK NG ■User ■Role ■RLS https://www.postgresql.org/docs/13/sql-set-role.html USER でも ROLE でも可能ですが、ROLE で制御する 理由は29スライド付近で説明します。 PostgreSQLのロール - Qiita
  26. 26. db=# GRANT ALL ON ALL TABLES IN SCHEMA public TO ”1”; db=# GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ”1”; db=# SET ROLE “1”; db=# SELECT * FROM rls; 1 | 1 2 | 2 ... 6 | 3 (6 rows) db=# RESET ROLE; -- 接続ユーザー権限に戻る "1" に全権限を付ける 26 全権限があるので、全行見えてます rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK OK ■User ■Role ■RLS https://www.postgresql.org/docs/13/sql-grant.html
  27. 27. db=# ALTER TABLE rls ENABLE ROW LEVEL SECURITY; db=# SET ROLE “1”; db=# SELECT * FROM rls; id | tenant_id ----+----------- (0 rows) db=# RESET ROLE; -- 接続ユーザー権限に戻る rls テーブルのRLSを有効化 27 SELECT権限はありますが、 RLSによって制限されました rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK OKだが 1行も見 えない ■User ■Role ■RLS https://www.postgresql.org/docs/13/ddl-rowsecurity.html
  28. 28. db=# CREATE POLICY foo ON rls USING(tenant_id::text = current_user); db=# SET ROLE “1”; db=# SELECT * FROM rls; id | tenant_id ----+----------- 1 | 1 4 | 1 (2 rows) rls テーブルのRLSポリシーを設定 28 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role SELECT権限がありますが、 RLSポリシーによって 対象行が制限されました OK OK ■User ■Role ■RLS https://www.postgresql.org/docs/13/ddl-rowsecurity.html
  29. 29. DjangoではRLSどうなるの? 29
  30. 30. tenant=2 で ログイン 2 dbで接続 セッションでのSELECT 30 全行 ■User ■Role ■RLS customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 Postgres users “db” User “1” User “2” User “3” User SELECT * FROM customers;
  31. 31. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 Postgres users “db” User “1” User “2” User “3” User セッション+RLSでのSELECT 31 CREATE POLICY foo ON customers USING(tenant_id::text = session_user); 接続Userで制御 ■User ■Role ■RLS SELECT * FROM customers; tenant_id =2 の全行 “2” で接続 ログインユーザー毎に、 DjangoからDBへ接続する ユーザーを切り替えるのは難しい
  32. 32. tenant=2 で ログイン 2 tenant_id =2 の全行 Postgres users “db” User “1” Role “2” Role “3” Role dbで接続 SET ROLE “2”; SELECT * FROM customers; RLS+Roleを介したSELECT(Role密結合) 32 CREATE POLICY foo ON customers USING(tenant_id::text = current_user); CREATE ROLE “1”; GRANT ALL ON TABLES ... TO “1”; GRANT ALL ON SCHEMAS ... TO “1”; -- 2, 3, 以下繰り返し customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 Roleで制御 ■User ■Role ■RLS ログインユーザーのHTTPリクエスト毎に、 Roleを切り替えるMiddlewareを用意 DjangoからDBへの接続は、 常に1つのuser/passwordを使う
  33. 33. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 Postgres users “db” User “tenantuser” Role “1” Role “2” Role “3” Role dbで接続 SET ROLE “2”; SELECT * FROM customers; RLS+Roleを介したSELECT(Role疎結合) 33 CREATE POLICY foo ON customers USING(tenant_id::text = current_user); CREATE ROLE “tenantuser”; GRANT ALL ON TABLES … TO “tenantuser”; GRANT ALL ON SCHEMAS … TO “tenantuser”; GRANT “tenantuser” TO "1"; -- 以下繰り返し Roleで制御 ■User ■Role ■RLS テナントのRoleは、代理ロール ”tenantuser” 経由でテー ブルへのアクセスが許可される tenant_id =2 の全行
  34. 34. Django でRLSを使うには(まとめ) 34 初期化 1. 代理ROLEを作成し、全テーブルのGRANT ALLを設定 2. テナント制御するテーブルにRLS有効化&ポリシー設定 都度 1. CREATE ROLE -- テナント追加時 2. SET ROLE -- テナント判別し、ROLEを設定 ■User ■Role ■RLS
  35. 35. 1 & 2. 初期化 35 db=# CREATE ROLE “tenantuser”; db=# GRANT ALL ON ALL TABLES IN SCHEMA public TO ”tenantuser”; db=# GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ”tenantuser”; ※ 初期化は、後から追加されたテーブルにも個別適用が必要 ■User ■Role ■RLS -- for <table> in <tables_to_enable_rls>: db=# ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; db=# CREATE POLICY <name> ON <table> USING(tenant_id::text = current_user); 1. 代理ROLEを作成し、全テーブルのGRANT ALLを設定 2. テナント制御するテーブルにRLS有効化&ポリシー設定
  36. 36. 3. CREATE ROLE -- テナント追加時 36 ■User ■Role ■RLS from django.db.models.signals import post_save def on_create_tenant(sender, instance, created, **kwargs): if created: # レコード追加時 tenant_id = instance.tenant_id with connection.cursor() as cursor: cursor.execute(f'CREATE ROLE "{tenant_id}"') cursor.execute(f'GRANT "tenantuser" TO "{tenant_id}"') # DBにレコードが追加された後に実行する post_save.connect(on_create_tenant, sender=models.Tenant) https://docs.djangoproject.com/ja/3.2/topics/signals/
  37. 37. 4. SET ROLE -- Middlewareでテナント判別 37 ■User ■Role ■RLS https://docs.djangoproject.com/ja/3.2/topics/http/middleware/ class RlsMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): tenant_id = getattr(request.user, 'tenant_id', None) if tenant_id: # テナントに所属しないスーパー管理者はROLE設定不要 with connection.cursor() as cursor: cursor.execute(f'SET ROLE "{tenant_id}" ') response = self.get_response(request) return response ※ Middlewareを通らない処理では個別に SET ROLE が必要
  38. 38. 4. SET ROLE -- Middleware以外でもテナント設定 38 ■User ■Role ■RLS https://django-tenants.readthedocs.io/en/latest/use.html#tenant_context @contextmanager def tenant_context(tenant_id: int): with connection.cursor() as cursor: cursor.execute(f'SET ROLE "{tenant_id}" ') Yield cursor.execute(f'RESET ROLE') # バッチや非同期呼び出し等、Middlewareを経由しない場合の呼び出し def some_command(tenant_id): with tenant_context(tenant_id): # SET ROLE される objs = DjangoModel.objects.all()
  39. 39. デモ 39
  40. 40. マルチテナントの開発と運用 40
  41. 41. 各RDBMSのRLS対応状況 2021/05 調査時点 ● PostgreSQL 9.5 (2016) 以降 ○ https://www.postgresql.org/docs/9.5/ddl-rowsecurity.html 設定方法などのサンプルコード付き ● SQL Server 2016 (13.x) 以降 ○ https://docs.microsoft.com/ja-jp/sql/relational-databases/security/row-level-security?view=sql-server-ver15 ● Oracle 10g Release 2 (10.2) (2005) 以降 ○ https://docs.oracle.com/cd/B19306_01/network.102/b14267/intro.htm ● MySQL ○ 8.0.25 (2021-05-11) では非対応 ● MariaDB ○ 10.5.10 (2021-05-07) では非対応 ● SQLite ○ 3.36.0 (2021-06-18) 非対応 41
  42. 42. シングルテナント < マルチテナント ● 分離 = シングルテナントの場合 ○ データ混濁について考える必要はない ● 準分離(スキーマ分離)アプローチにした場合 ○ 共有するテーブルと分離するテーブルを管理し、専用のマイグレーション手順を用意 ● 共有アプローチにした場合 ○ 複数テナントを前提にしたテーブル設計が必要、RLSの導入が必要 開発コスト 42 DB分離アプローチ DB共有アプローチ
  43. 43. テナント数の増加に合わせて変化 ● 運用では、テナント数と管理対象が比例する ● DBやアプリのインスタンス毎に担当者が必要 運用コスト 43 少 テナント数 多 大 DB数 小 大 運用・改修コスト 小 大 リソース 小
  44. 44. ● パフォーマンス測定を行い、N+1等は解消しておく ○ RLSにより負荷は上がるため、顕著になってしまう ● 作り方によってはINDEXが効かない等の問題がある ○ ROLE名によるPOLICY適用時に、フルスキャンが発生等 ● コネクションをテナント別に用意しない(できない) ○ そもそもDBの接続数上限を100や1000にするのは厳しい(デフォルトは10) ○ Postgresは、コネクション生成コストが高いため、都度生成は負荷が高い ○ データ混濁を避けたいとしても、コネクションは共有するしかない ○ (なお、Djangoではテナント別コネクション利用がそもそも難しい) ● Citus / Hyperscale の利用を検討する ○ 特定カラムの値(tenant_id等)をキーに、ノード分散によって負荷分散できる RLS利用時の負荷対策 44
  45. 45. ● ROLE設定を忘れないようにする ○ 今回のデモではMiddlewareで自動設定した ○ 管理コマンド等Middlewareを通らないケースがあるので、そこは実装で努力 ○ ROLE設定を忘れると、「全テナントのデータが漏洩する」 ■ SET ROLEを忘れたら全部見れない方が安全 -> 今後の課題 ● バックアップをテナント単位でやりたい ○ DB全体のバックアップで。リストア方法は要検討 ● PITR(Point In Time Recovery)をテナント単位でやりたい ○ DB内の特定レコード群だけを巻き戻す、のは難しい。 ○ DB準分離ならスキーマ単位で独立のため、多少楽(publicスキーマのデータとの不整合に注意) ● 高負荷テナントのリソースを隔離をしたい ○ Citus / Hyperscale の利用を検討する ■ 特定カラムの値(tenant_id等)をキーに、ノード分散によって負荷分散できる RLS/DB共有のデメリット対策 45
  46. 46. まとめ RLSのメリット ● コンピューターリソースを無駄なく最大限活用できる ● マイグーレーション時間が線形増加しない ● テナント個別の特別対応をしない、という潔さ RLSのデメリット ● クエリ負荷の上昇は避けられない ● テナント個別の特別対応が難しい ○ バックアップ、リストア、負荷の高いテナントの分離 46
  47. 47. 参考資料 マルチテナント ● マルチテナント SaaS パターン - Azure SQL Database | Microsoft Docs ● Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する IBM Developer Works (WebArchive) ● Multi-Tenant Data Architecture Microsoft (WebArchive) ● つらくないマルチテナンシーを求めて: 全て見せます! SmartHR データベース移行プロジェクトの裏側 / builderscon 2018 - Speaker Deck RLS ● Django + RLS でマルチテナント - 清水川のScrapbox /デモコード https://github.com/shimizukawa/django-pg-rls ● Using Postgres Row-Level Security in Python and Django ● 行レベルのセキュリティ - SQL Server ● PostgreSQL の行レベルのセキュリティを備えたマルチテナントデータの分離 | Amazon Web Services ブログ ● Row Level Securityはマルチテナントの銀の弾丸になりうるのか / Row Level Security is silver bullet for multitenancy? - Speaker Deck ● PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring) Citus (Hyperscale) ● Azure Database for PostgreSQL Hyperscale(Citus)しらべてみた - Qiita ● チュートリアル:マルチテナント データベースを設計する - Hyperscale (Citus) - Azure Database for PostgreSQL | Microsoft Docs 47
  48. 48. Special Thanks! -- Review頂いたみなさん BeProud slack ● kashew, delhi09, susumuis, tell-k, mtb_beta, altnight django-ja Discord ● hirokiky, tokibito 48
  49. 49. and… Thanks for all attendees!! by @shimizukawa 49

    Be the first to comment

  • MGoto1

    Jul. 3, 2021
  • t2y

    Jul. 3, 2021
  • uokada

    Jul. 4, 2021
  • syuichitsuji

    Jul. 4, 2021

RLSを用いたマルチテナント実装 for Django by Takayuki Shimizukawa 複数のテナント(チーム・組織)向けにサービスを提供するシステムで、テナント相互の情報を分離して扱う、複数のマルチテナントアーキテクチャが考案されています。「各プログラマが努力して実装する」戦略でも実現はできますが、プログラミングミスや設定間違いによるデータ混濁が高確率で発生します。このトークでは、マルチテナントアーキテクチャにおけるデータ分割アプローチのひとつ「共有アプローチ」をDjangoとPostgresのRow Level Security (RLS) の組合せで安全に実現する方法を紹介します。またこの方法のメリット、デメリットを紹介します。 https://djangocongress.jp/

Views

Total views

3,029

On Slideshare

0

From embeds

0

Number of embeds

693

Actions

Downloads

4

Shares

0

Comments

0

Likes

4

×