Yura YuLife

ITエンジニアの覚え書き。

Django を AWS ECS(FARGATE) + ELB で動かすときの ALLOWED_HOSTS の設定

めちゃくちゃ苦戦したのでメモ。

困ったこと

Django のアプリを ECS(FARGATE) 上で動かすと Invalid HTTP_HOST header: '10.0.X.Y'. You may need to add '10.0.X.Y' to ALLOWED_HOSTS. みたいなエラーが出て、 ELB のヘルスチェックで落ちる。

だからといって、 ALLOWED_HOSTS = ['*'] みたいにするのはセキュリティ的にも気持ち的にもやりたくない。

解決策

Stack overflow に同じ問題にぶち当たっている人がいた。

stackoverflow.com

通常の ALLOWED_HOSTS の設定の下に、以下を追加すれば良いみたい。

try:
    resp = requests.get('http://169.254.170.2/v2/metadata')
    data = resp.json()
    container_meta = data['Containers'][0]
    EC2_PRIVATE_IP = container_meta['Networks'][0]['IPv4Addresses'][0]
    ALLOWED_HOSTS.append(EC2_PRIVATE_IP)
except requests.exceptions.RequestException:
    pass

これで当該のエラーが出なくなって、タスクが再起動されまくる悪夢から解放されました!

参考URL

Djangoでユーザがグループに所属しているかを判定する

Djangoで、特定のグループに所属しているユーザのみにリンクやページを表示する場合に、以下のような方法があります。

テンプレートタグを用いる方法

テンプレートタグを用いると、ユーザの所属するグループによってページ内の情報やリンクを出しわけることができます。

使い方

以下のin_group.pyというスクリプトを、アプリ内のtemplatetags/ディレクトリに格納。

hoge/templatetags/in_group.py

from django.template import Library
register = Library()

@register.filter
def in_group(user, team_name):
    if user.groups.filter(name=team_name).exists():
        return True
    else:
        return False

テンプレート内でin_groupを呼び出します。

{% load in_group %}

{% if user|in_group:"developer" %}
<!-- developerというグループに所属するユーザ用のリンク -->
<a href="#">開発者向けページ</a>
{% else %}
<!-- その他のユーザ用のリンク -->
<a href="#">権限を申請</a>
{% endif %}

if文と組み合わせることで、特定のグループのユーザのみに情報を表示したり、隠したりすることができます。

user_passes_testを利用する方法

上記のテンプレートタグを利用する方法では、リンクや情報を特定のグループのユーザのみに表示することはできますが、特定ページへのアクセスを制限することはできません。

そこで、user_passes_testというデコレータを利用することで、特定の条件を満たしたユーザのみがページにアクセスできるようにします。

使い方

views.py

from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import render

@login_required
@user_passes_test(lambda user: user.groups.filter(name='developer').exists())
def developer_page(request):
    u'''ログイン済みでdeveloperグループに所属するユーザのみアクセス可能なページ'''
    return render(request, 'developer.html', {})

user_passes_testの条件を満たさないユーザはログインページへ飛ばされます。スタッフ権限を持たないユーザが管理サイトへログインしようとした際の挙動と同じですね。

参考URL

Django 1.8にAxesを追加する

連続ログイン失敗時にアカウントをロックする機能を追加できるプラグインdjango-axesのインストール手順です。

環境

django-axes のインストール

$ pip install django-axes

settings.pyの編集

vi settings.py

INSTALLED_APPS = (
    # 以下の1行を追記
    'axes',

)

MIDDLEWARE_CLASSES = (
    # 以下の1行を追記
    'axes.middleware.FailedLoginMiddleware'
)

# ログイン失敗は連続5回まで
AXES_LOGIN_FAILURE_LIMIT = 5
# ログインを連続失敗した場合は24時間アカウントロック
AXES_COOLOFF_TIME = 24
# AXES_LOGGERはデフォルトでは'axes.watch_login'となっているので、
# ログイン失敗をログ出力するためには、
# 'axes.watch_login'というロガーを作成するか
# 独自のロガーインスタンスにて行う
AXES_LOGGER = 'custom_logger'

urls.pyの編集

vi urls.py

from django.conf.urls import include, url
from django.contrib.auth import views as auth_views
from axes.decorators import watch_login

urlpatterns = [
    url(r'^$', hoge_page),
    url(r'^hoge/', include('hoge.urls')),

    ...

    # ログイン画面でwatch_loginデコレータを設定
    url(r'^login/$', watch_login(auth_views.login),
        {'template_name': 'auth/login.html'}, name='login'),
]

templateの編集

vi auth/login.html

<!-- ログインフォームのPOST先URLを以下のように設定 -->
<!-- url 'django.contrib.auth.views.login' とするとエラーを吐くので注意 -->
<form role="form" method="post" action="{% url 'login' %}?next={{ next }}">
  {% csrf_token %}
  {{ form.non_field_errors }}
  <div>
    <input type="text" placeholder="ユーザ名" id="id_username" name="username" maxlength="254" required autofocus>
  </div>
  <div>
    <input type="password" placeholder="パスワード" id="id_password" name="password" required>
  </div>
  <input type="submit" value="ログイン">
</form>

マイグレーションの実行

$ ./manage.py migrate

マイグレーションを実行すると、管理サイト内にAxesというAppとAccess attempts, Access logsという項目が表示されます。

参考サイト

djangoのペジネータで、現在のページの前後数ページのみをリンクとして表示する

Djangoのペジネータを使ってリンクを表示するときに、Google検索みたいに前後数ページのリンクを表示する方法です。

f:id:yurayur:20150911184131p:plain

Googleの場合は前5ページ、後4ページを表示していますね。

環境

ペジネータで前後数ページを表示

views.py

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def some_page(request):
    some_list = SomeObject.objects.all()
    page = request.GET.get('page')  # 現在表示中のページを取得

    # 1ページに10件ずつ表示
    paginator = Paginator(some_list, 10)
    try:
        paged_list = paginator.page(page)
    except PageNotAnInteger:
        paged_list = paginator.page(1)
    except EmptyPage:
        paged_list = paginator.page(paginator.num_pages)

    return render(request, 'some_page.html', {
        'paged_list ': paged_list
    })

some_page.html

<!-- ペジネータここから -->
{{ paged_list.start_index }}件目〜{{ paged_list.end_index }}件目を表示中 (全{{ paged_list.paginator.count }}件)</h4>
<div class="paginator">
  {% if paged_list.has_previous %}
    <a href="?page={{ paged_list.previous_page_number }}">前へ</a> 
  {% endif %}

  {% for page in paged_list.paginator.page_range %}
    <!-- 現在のページへはリンクを貼らない -->
    {% if page == paged_list.number %}
      <strong>{{ page }}</strong> 
    <!-- 現在表示しているページの前後5ページへのリンクを表示 -->
    {% elif paged_list.number|add:"-5" <= page and paged_list.number|add:"5" >= page %}
      <a href="?page={{ page }}">{{ page }}</a> 
    {% endif %}
  {% endfor %}

  {% if paged_list.has_next %}
    <a href="?page={{ paged_list.next_page_number }}">次へ</a>
  {% endif %}
</div>
<!-- ペジネータここまで -->

こんな感じで表示されます。

f:id:yurayur:20150911190050p:plain

Djangoの初回マイグレーション時に relation "auth_user" does not exist というエラーが発生する場合

環境

初回マイグレーション時のエラー

Djangoのプロジェクトをコピーしてきて、$ python manage.py migrate しようとすると、以下のようなエラーが発生した。

Synchronizing apps without migrations:
  Creating tables...
    Creating table app1_model1
    Creating table app2_model2

    ...

    Running deferred SQL...
Traceback (most recent call last):
  File "./manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)

  ...

django.db.utils.ProgrammingError: relation "auth_user" does not exist

どうも、自分で作ったModelの中でdjango.contrib.auth.models.Userへのリレーションを張っているのに、Userのテーブルがまだ作られていないことがマズいらしい。

ということで、先にauthマイグレーションを行うことで、この問題を回避できます。

$ python manage.py migrate auth
$ python manage.py migrate