# Djangoの深~いパーミッション管理の話 @hirokiky http://slides.hirokiky.org/pyconjp2017.html --- ## 対象の方 * Pythonを使う人 * Djangoを使う人 * チュートリアルやった * 自分でアプリ作った ??? 中級者向けの内容になります。 「これが答えです」ではなくて、共有したい * こういう問題あるよね * ライブラリー使ってみたよ * ライブラリー作ってみたよ --- ## 権限管理の悩み * Viewのif文で権限の判定つらい * Templateでも判定必要 * 自前のデコレーターで管理が破綻 * DjangoのPermissionは使わない ??? * アクセスできるかどうかのチェックなどをView関数内でやる * 自前のデコレーターで色々作ったりする * 毎回頑張っている感じがする * CustomのUserModelはつかってもDjango自体のPermissionとかはあまり使わない * DB管理でやりたくないとか、色々 * Admin内でたまに使うくらい --- ## Agenda * 第1部: Djangoでの権限管理の定石紹介 * 第2部: 検討したライブラリー * 第3部: こんなライブラリー作った --- class: small-image ## 自己紹介 * @hirokiky * [BeProud Inc](http://beproud.jp/) で[PyQ](https://pyq.jp/)を作っています ![BeProud](images/beproud.png) --- ## PyQという製品作っています ![PyQ](images/pyq_lp.png) --- ## PyQは本気でプログラミングを学べる * Pythonをブラウザだけで動かして学ぶ * プロフェッショナルになれるレベルで学ぶ * Webや機械学習、データ分析も学べる --- ## PyQリリースと成長 2017年4月にリリースして順調に成長 * コンテンツ拡充(機械学習など) * 機能拡張 * JupyterNotebook対応 * 他色々な対応 --- ## 製品の成長と設計 ![製品の成長](images/pyconjp2017/growth.png) --- ## 製品の成長と設計 * まずは作ることが大切 * 初期は小さい設計で良い * 成長に合わせて設計も成長させる --- ## 設計の是正 ![設計の是正](images/pyconjp2017/growth2.png) ??? PyQは例えばバックエンドのインフラの設計などをドラスティックに変えている。 --- class: middle ### 第一部 ## Djangoでの権限管理の定石紹介 --- class: wide ## Lv1 Viewでif文 ```python def premium_articles(request): if request.user.plan.code != PREMIUM: return HttpResponseForbidden( "プレミアム会員限定です", ) ... ``` --- class: wide ## Lv1 プロパティを使う ```python class User(AbstructUser): ... @cached_property def is_premius(self): return self.plan.code != PREMIUM ``` --- class: wide ## Lv1 Viewでif文 ```python def premium_articles(request): if not request.user.is_premium: return HttpResponseForbidden( "プレミアム会員限定です", ) ... ``` --- class: wide ## Lv2 デコレーターでやる場合 ```python def premium_required(f): @wraps(f) def _wrapped(request, *args, **kwargs): if not request.user.is_premium: return HttpResponseForbidden( "プレミアム会員限定です" ) return f(request, *args, **kwargs) return _wrapped ``` --- class: wide ## デコレーターを適用 ```python @premium_required def premium_articles(request): ... ``` --- class: wide ## 変わる問題 プレミアム会員専用だった機能が、スタンダード会員でも見れるように ```python # @is_premium もう使えない def premium_articles(self): ... ``` --- class: wide ## Lv2 凝ったデコレータ ```python def subscription_required(*plan_codes): def _dec(f): @wraps(f) def _wrapped(request, *args, **kwargs): if request.user.plan.code not in plan_codes: return HttpResponseForbidden(...) return f(request, *args, **kwargs) return _wrapped return _dec ``` --- class: wide ## Lv2 凝ったデコレータ ```python @subscription_required(PREMIUM_PLAN, STANDARD_PLAN) def premium_articles(request): ... ``` --- ## Lv2までの自作の問題 * 仕様の変更で壊れやすい * デコレーターが肥大化する、増える * 結局View、Templateにも処理書いちゃう ??? * 仕様変更で適応してるデコレータ、判定している部分すべて変える必要あり * なんか色々やるデコレーターが色々増える * 結局 `if user.is_premius` みたいな処理を書いちゃう --- class: middle ## 「リクエストの情報」と「権限」が密結合なのが問題 --- ## 「リクエストの情報」 * ユーザー user である * プレミアムプランに契約している --- ## 「権限」 * プレミアム記事を見れる * チームを管理できる --- class: middle ## パーミッション作るお! --- ## 「パーミッション」を文字列で * `Set[str]` * プレミアム記事を読める == `"view_premius_article"` --- class: wide ## Lv3 パーミッションでやる ```python class User(AbstractUser): def get_permissions(): permissions = set() if user.is_premium(): permissions.add("view_premium_articles") permissions.add("view_special_feature") return permissions def has_permission(permission): return permission in self.get_permissions() ``` --- class: wide ## Lv3 パーミッションでやる場合 ```python @user_permission_required("view_premium_articles") def premium_articles(request): ... ``` --- ## Lv3 オブジェクトは? 記事 Articleがあるとき * Article.premium: プレミアム会員用 * not .premium: スタンダード、プレミアム会員用 * 無料会員はすべて読めない --- class: wide ## Lv3 許可設定 ```python class Article(models.Model): def has_permission(self, user, permission): permissions = set() if self.premium and user.is_premium: permissions.add("view") if not self.premium: if user.is_premium or user.is_standard: permissions.add("view") return permission in permissions ``` --- class: wide ## Lv3 View内で使う ```python def premium_article_detail(request, id): article = get_object_or_404(Article, id=id) if not article.has_permission(user, "view"): return HttpResponseForbidden(...) ``` --- ## パーミッションで扱う利点 仕様の変更に強くなる * 「お試しユーザー」ができた * 「キャンペーン利用ユーザー」ができた * 「未ログインも読める記事」ができた --- ## Modelだけ変える ```python def get_permissions(user): ... if user.during_campain: permissions.remove("...") ... ``` --- ## 自作パーミッション問題 * 独自パーミッション管理部分の仕様が複雑になる --- ## おさらい * Lv1: Viewでif文 * Lv2: デコレータを作って使う * Lv3: パーミッション管理する仕組み --- class: middle ## でも複雑なまま。。。 --- class: middle ## うまいことやってくれるライブラリーはよ。。。 --- class: middle ### 第2部 ## ライブラリーでやろう --- ## 検討したライブラリー * DjangoのPermission * django-guardian * django-rules https://djangopackages.org/grids/g/perms/ --- class: wide ## DjangoのPermission ```python content_type = ContentType.objects.get_for_model(BlogPost) permission = Permission.objects.get( codename='change_blogpost', content_type=content_type, ) user.user_permissions.add(permission) ``` --- class: wide ## DjangoのPermission ```python @permission_required('myapp.change_blogpost') def change_blog_post(request, blog_id): ... ``` --- ## DjangoのPermissionがあわなかった点 * DBで管理したくない * User <-> Modelのパーミッションだけ * パーミッションを持つ条件変えたら?? ??? あとあんまりスマートじゃない --- ## django-guardian 一番人気らしい。 DjangoのPermissionベース。 --- class: wide ## django-guardian ```python from guardian.shortcuts import assign_perm assign_perm('view_task', joe, task) joe.has_perm('view_task', task) # True ``` --- ## django-guardian良さそうな点 * シンプルに書けそう * イケてる機能 * Modelが持てるパーミッション一覧 * 権限持ってるデータだけfilter --- ## guardian合わなかった点 イケてる版DjangoのPermissionだったこと * DBで管理したくない * User <-> Modelのパーミッションだけ * パーミッションを持つ条件変えたら?? --- class: wide ## django-rules ```python >>> @rules.predicate >>> def is_book_author(user, book): ... return book.author == user ... >>> rules.add_rule('can_edit_book', is_book_author) >>> rules.add_rule('can_delete_book', is_book_author)
``` --- class: wide ## django-rulesで権限チェック ```python >>> adrian = User.objects.get(username='adrian') >>> rules.test_rule('can_edit_book', adrian, guidetodjango) ``` --- ## django-rules良さそうな点 * DB使わずにできる * デコレータ、Templateもある * predicate & predicateなどで新しいpredicateを作れる --- ## django-rulesは良さそう かなり良さそう。 権限の一覧性が悪そう? (あんまりちゃんと試せていない) --- class: middle ## でももっとうまくできそう --- class: midlle ## ライブラリー作ろう! 良いアイディアがあったのでライブラリーも作ってみた。 PyQ向けに色々設計考える => ライブラリーだこれ --- class: middle ### 第3部 ## ライブラリー作った --- ## 求めていたもの * 「リクエストの情報」と「権限」分離 * DBで管理しない * User<->Model以外のグローバルな権限 * View、Templateどこでも使える --- ## 作ったもの [django-keeper](https://github.com/hirokiky/django-keeper/) --- class: middle ## 例えば記事の著者しか編集できないView --- class: wide ```python from keeper.security import Allow from keeper.operators import Everyone from keeper.operators import IsUser class Article: author = models.ForeginKey("myapp.User") def __acl__(self): return [ (Allow, Everyone, 'view'), (Allow, IsUser(self.author), 'edit'), ] ``` --- class: wide ## keeperを使った解法 ```python from keeper.views import keeper @keeper( 'edit' Article, lambda request, article_id: {'id': article_id} ) def article_edit(request, article_id): article = request.k_context ... ``` --- ## keeperを使った解法 * ACLを設定する * アクション・誰に・どの権限 * Viewには `@keeper` をつけるだけ * オブジェクトの取得もできる --- ![keeper概念図](images/pyconjp2017/acl.png) --- ## OperatorsとPermissions 「リクエストの情報」と「権限」の分離 ![OperatorsとPermissions](images/pyconjp2017/op_perm.png) --- ## Operatorって? `Callable[[HttpRequest], bool]` です。 --- ## Permissionって? `str` です。 --- ## デフォルトのOperators * keeper.operators.Everyone * keeper.operators.Authenticated * keeper.operators.IsUser * keeper.operators.Staff --- ## デフォルトのOperatros ```python return [ (Allow, Everyone, "view"), (Allow, Authenticated, "comment"), (Allow, IsUser(self.author), ("edit", "delete")), (Allow, IsStaff, ("edit", "delete")), ] ``` --- ## 追加のOperatorを作る ```python class IsPlan(Authenticated): def __init__(self, plan_code): self.plan_code = plan_code def __call__(self, request): if not super().__call__(request): return False return ( request.user.plan.code == self.plan_code ) ``` --- ## 追加のOperatorを使う ```python from myapp.operators import IsPlan class Article(models.Model): def __acl__(self): return [ (Allow, IsPlan(PLAN_PREMIUM), 'view'), (Allow, IsPlan(PLAN_STANDARD), 'view'), ] ``` --- ```python from myapp.operators import IsPlan class Article(models.Model): premium = models.BooleanField() def __acl__(self): if self.premium: return [ (Allow, IsPlan(PLAN_PREMIUM), 'view') ] else: return [ (Allow, IsPlan(PLAN_PREMIUM), 'view'), (Allow, IsPlan(PLAN_STANDARD), 'view'), ] ``` ??? 例えば仕様変更があった時 --- ## ACLの利点 * 権限をもつ場合を俯瞰しやすい * 「リクエストの情報」判定のロジックはOperator * 「権限」はPermission --- class: wide ## テンプレートでの利用方法 ```django {% load keeper %} {% has_permission article 'edit' as can_edit %} {% if can_edit %}
編集
{% endif %}
{{ article.title }}
{{ article.body }}
``` --- class: wide ## グローバルのACL設定 ```python class Root: def __acl__(self): return [ (Allow, Authenticated, 'post_article'), ] ``` --- ## グローバル権限を使う ```python @keeper('post_article') def article_post(request): pass ``` --- ## デモアプリ紹介 ユーザー、契約、チームなど含むアプリでdjango-keeperを使ったデモ。 https://github.com/hirokiky/django-keeper/tree/master/demo/ --- ## django-keeperの良い所 * DBを使わない * ACLを使うので見通しが良い * User<->Modelに依存しないこと * 「リクエストの情報」「権限」を分離する --- ## django-keeperの悪い所 * まだまだベータ感があります。。 --- ## 全体のまとめ * Djangoでの権限管理の定石 * 検討したライブラリー * 作ったライブラリー --- ## PyQブース出展中です ![PyQブース出展中](images/pyconjp2017/pyq_booth.png) [CC BY 2.0 PyConJP](https://www.flickr.com/photos/pyconjp/29843581315/) --- class: middle ## おわり