понедельник, 20 февраля 2017 г.

Django Active Directory user backend

В статье Django аутентификация через NGINX kerberos (Active Directory) я описал как использовать прозрачную (sso) аутентификацию.
Django RemoteUserBackend работает так как надо, но мне понадобилось для создаваемого пользователя добавлять из Active Directory дополнительную информацию, а именно ФИО и email. Схема получилась такая:
1) Пользователь первый раз заходит на сайт example.com
2) RemoteUserBackend создает учетную запись пользователя (user.username) в Django
3) ... и дополнительно загружает информацию с использованием класса ADRemoteUserBackend

Ниже привожу код LDAPRemoteUserBackend.py с комментариями, что и куда добавлять.


"""
ADRemoteUserBackend добавляет фамилию, имя и email из Active Directory. Используется
как расширение RemoteUserBackend.
У меня данный LDAPRemoteUserBackend.py находится в папке back, там же где и settings.py
Для аутентификации надо убрать RemoteUserBackend и добавить в settings.py:
 
AUTHENTICATION_BACKENDS = [
    #'django.contrib.auth.backends.RemoteUserBackend',
    'back.LDAPRemoteUserBackend.ADRemoteUserBackend',
    'django.contrib.auth.backends.ModelBackend',
]
 
"""

from django.contrib.auth.backends import RemoteUserBackend
from django.conf import settings as s
from ldap3 import Server, Connection, SIMPLE, SYNC, ASYNC, SUBTREE, ALL


"""
Если созданному пользователю надо добавить права superuser (например, пользователь admin), то
 
python manage.py shell
 
from django.contrib.auth.models import User
user = User.objects.get(username=admin)
user.is_staff = True
user.is_superuser = True
user.save()
"""

# Можно прописать следующие Константы в settings.py, а можно и здесь (чтобы не засорять settings.py)
# DNS имя сервера Active Directory
s.AD_SERVER = "srv.example.com"
# Пользователь (логин) в Active Directory - нужно указать логин в AD в формате 'EXAMPLE\aduser' или 'aduser@example.com'
s.AD_USER = 'EXAMPLE\userad'
s.AD_PASSWORD = 'FADSfjkb,.^%'
s.AD_SEARCH_TREE = 'dc=exampe,dc=com'


class ADRemoteUserBackend(RemoteUserBackend):
    def clean_username(self,username):
        clean_username=username.split('@')[0]
        return clean_username

    def configure_user(self,user):
        #Если нужно пускать пользователей в админку, то раскоментировать следующую строку
        #user.is_staff=True

        #connect to ldap server
        server = Server(s.AD_SERVER)
        conn = Connection(server, user=s.AD_USER, password=s.AD_PASSWORD)
        conn.bind()
        AD_FILTER = '(&(objectCategory=Person)(sAMAccountName='+user.username+'))'
        # get user Common-Name, email, first name, last name, отчество, full Name - attributes ['cn', 'mail', 'givenName', 'sn', 'initials', 'displayName']
        conn.search(s.AD_SEARCH_TREE, AD_FILTER, SUBTREE, attributes =['cn', 'mail', 'givenName', 'sn', 'initials', 'displayName'])

        for entry in conn.entries:
            if entry.mail:
                user.email = entry.mail
            try:
                displayName = entry.displayName.value
            except:
                displayName = entry.cn.value
            if displayName:
                displayName = displayName.split(" ")
            else:
                displayName = ""
            if (displayName[1] and displayName[2]):
                user.first_name = displayName[1] + " " + displayName[2]
            elif (displayName[1]):
                user.first_name = displayName[1]
            if displayName[0]:
                user.last_name = displayName[0]
        #close LDAP Connection
        conn.unbind()
        user.save()

Как используя python получить информацию из Active Directory

Во многих локальных сетях используется Microsoft Active Directory.
Используя python с библиотекой ldap3 можно получить информацию для выгрузки в вебпроекты или просто посмотреть нужную информацию (например, когда под пользователем последний раз подключались).

Библиотека python ldap3 - http://ldap3.readthedocs.io
Установка:
pip install ldap3

Использование:
- Ниже привожу код, пример использования. В коде есть поясняющие комментарии.


from ldap3 import Server, Connection, SIMPLE, SYNC, ASYNC, SUBTREE, ALL

# домен - example.com
# DNS имя сервера Active Directory
AD_SERVER = 'srv.example.com'
# Пользователь (логин) в Active Directory - нужно указать логин в AD 

# в формате 'EXAMPLE\aduser' или 'aduser@example.com'
AD_USER = 'EXAMPLE\aduser'
AD_PASSWORD = 'FDfashv,.@#'
AD_SEARCH_TREE = 'dc=example,dc=com'

server = Server(AD_SERVER)
conn = Connection(server,user=AD_USER,password=AD_PASSWORD)
conn.bind()
# в ответ должно быть - True

# Поиск в Active Directory
# примеры ldap фильтров можно посмотреть здесь -
# https://social.technet.microsoft.com/wiki/contents/articles/8077.active-directory-ldap-ru-ru.aspx
# Я в нижеследующем фильтре:
# - исключаю всеx отключенных пользователей (!(UserAccountControl:1.2.840.113556.1.4.803:=2))
# - добавляю только тех пользователей у которых заполнено имя и фамилия
# - и вывожу атрибуты - attributes
# Все возможные атрибуты Active Directory можно посмотреть здесь -
# https://msdn.microsoft.com/en-us/library/ms675090%28v=vs.85%29.aspx
conn.search(AD_SEARCH_TREE,'(&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(givenName=*)(sn=*))',
    SUBTREE,
    attributes =['cn','proxyAddresses','department','sAMAccountName', 'displayName', 'telephoneNumber', 'ipPhone', 'streetAddress',
    'title','manager','objectGUID','company','lastLogon']
    )
# после этого запроса в ответ должно быть - True

# можно посмотреть на результат
print(conn.entries)
# или вывести только Common-Name - cn
for entry in conn.entries:
    print(entry.cn)

# Найти пользователя с логином admin (sAMAccountName=admin) и показать информацию по нему
conn.search(AD_SEARCH_TREE,'(&(objectCategory=Person)(sAMAccountName=admin))', SUBTREE,
    attributes =['cn','proxyAddresses','department','sAMAccountName', 'displayName', 'telephoneNumber', 'ipPhone', 'streetAddress',
    'title','manager','objectGUID','company','lastLogon']
    )

conn.entries

воскресенье, 19 февраля 2017 г.

Ember file upload to Django

Ни на сайте emberjs.com, ни в еще какой документации я так и не нашел описания - как загрузить файл с использование Ember js. И действительно, средствами Ember js - никак. Поэтому нужно использовать либо js XMLHttpRequest, либо jQuery ajax.

Я разрабатываю - фронтэнд на  Ember js, бакэнд на Django. Мне надо загрузить файлы на бакэнд.
Порядок загрузки файла на фронэнде следующий:

1) Находится файл на странице:
let fileUpload = document.getElementById('file-field').files[0]; 

2) Создается formData и в нее добавляются все необходимые для отправки данные, в том числе и файл:
let formData = new FormData();
formData.append('creator', creator_id);
formData.append('attachment', fileUpload); 

3) Собранная  formData отсылается (POST) на url бакэнда с добавлением дополнительных заголовков.

Сначала пример POST запроса с использованием XMLHttpRequest

//1
let fileUpload = document.getElementById('file-field').files[0];
//2
let formData = new FormData();
formData.append('creator', creator_id);
formData.append('attachment', fileUpload); 
//3
let req = new XMLHttpRequest();
req.open('POST', url);
let jwtoken = this.get('session.data.authenticated.access_token');
req.setRequestHeader('Authorization', 'Bearer ' + jwtoken);
req.send(formData)



А теперь пример отправки файла с использованием jQuery ajax, взятого из рабочего кода:

route.js

  actions: {
    saveFile() {
      let model = this.controller.get('model');
      let creator_id = this.get('session.data.authenticated.user.id');
      let fileUpload = document.getElementById('file-field').files[0];
 
      let host = this.store.adapterFor('application').get('host');
      let url = host + '/api/task/taskfiles/';
 
      let formData = new FormData();
      formData.append('task', model.get('task.id'));
      formData.append('creator', creator_id);
      formData.append('attachment', fileUpload);
 
      let jwtoken = this.get('session.data.authenticated.access_token');
 
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(request) {
          request.setRequestHeader('Authorization', 'Bearer ' + jwtoken);
        },
        data: formData,
        processData: false,
        contentType: false
      })
        .then((response)=>{
          console.log(response);
          this.store.findRecord('task/taskfile', response.id);
          this.transitionTo('task.taskfiles');
        })
        .catch(response => {
          //console.log(response);
          if (response.responseJSON.attachment) {
            this.controller.set('errorMessage', 'File do not selected');
          }
        });
 
    },

template.hbs

  <h3>Adding a new file</h3>
 
  <p>{{errorMessage}}</p>
  <input type='file' id='file-field' class="form-control" name="file"/>
  <button class="btn btn-success form-control" type="submit" {{action 'saveFile' model}}>{{t "Save"}}</button>

На Ember фронэнтде разобрались как отсылать.
Теперь настройка Django бакэнда, а точнее Django-rest-framework, для приема файла

В serializers.py все стандартно

class TaskFileSerializer(serializers.ModelSerializer):
    class Meta:
        model = TaskFile
        fields = ('id', 'creator', 'task','attachment')


А во views.py надо добавить:

from rest_framework.parsers import MultiPartParser, FormParser
 
TaskFileViewSet(viewsets.ModelViewSet):
    queryset = TaskFile.objects.all()
    serializer_class = TaskFileSerializer
    parser_classes = (MultiPartParser, FormParser)


Вроде и не сложно.

Ember document title

В Ember js для html head title я использую - 
https://github.com/kimroen/ember-cli-document-title

Поддерживает как статические, так и динамические title

Пример:

- В route - tasks добавляю статический title

// front/app/tasks/route.js 
export default Ember.Route.extend({
  title: 'Задачи',
});


- В route - application добавляю динамический title

// front/app/application/route.js
export default Ember.Route.extend({
  title: function(tokens) {
    return tokens.join(' - ') + ' Название сайта';
  },

});

суббота, 18 февраля 2017 г.

Django rest framework SessionAuthentication и JSONWebTokenAuthentication

Столкнулся с такой ситуацией:
- Опубликовал Ember фронтэнд на Django бакэнде. GET запросы проходят, а вот при POST запросе вылетает ошибка:

POST http://localhost:8000/api/tasks/ 403 (Forbidden)
Error: Adapter operation failed

Смотрю в отладчике хрома network responce:

{"detail":"CSRF Failed: CSRF token missing or incorrect."}

Ничего не понимаю. При разработке Ember фронтэнда POST запросы с localhost:4200 на localhost:8000 пролетают без проблем, а тут при POST запросе с localhost:8000 на localhost:8000 вылетает ошибка связанная с CSRF. Смотрю в Django debug, сравниваю POST запросы с localhost:4200 и localhost:8000 - в обоих случаях передается в request header - authorization - access_token, а вот втором случае дополнительно в request попадают Cookie и Session data. Ну и что с того, что дополнительная информация падает? Смотрю настройки settings.py бакэнда для REST_FRAMEWORK - DEFAULT_AUTHENTICATION_CLASSES прописано:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
}


и тут до меня доходит, что при POST запросах с localhost:4200 на localhost:8000 используется только  'rest_framework_jwt.authentication.JSONWebTokenAuthentication' , а при POST запросах с localhost:8000 на localhost:8000 сначала попадая на 'rest_framework.authentication.SessionAuthentication' проходит сразу аутентификация, потому что имеются session data, и до аутентификации JSONWebTokenAuthentication дело не доходит. Поэтому я взял и поменял порядок аутентификации - поставил сначала JSONWebTokenAuthentication, а потом SessionAuthentication:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',    

    ),
}


Теперь все POST запросы проходят! Это тот случай, когда порядок имеет значание!

пятница, 17 февраля 2017 г.

Ember Django deploy v2

Ну, неугомонный я, хочется как попроще. Я уже сделал один вариант публикации Ember фронэнда на Django бакэнде. Теперь еще один вариант.

Структура разработки Django - Ember сайта такая:

SITENAME (это название сайта)
    |
    |-back (это Django проект, здесь внути и будет находится static)
    |
    |-front (это Ember проект)


1) На Django бакэнде добавить:

в settings.py

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]
STATIC_URL = '/static/'



в urls.py

from .views import index

urlpatterns = [
    ...

    url(r'^$', index, name='index'),
    url(r'^(?P<path>.*)/$', index),
]


во views.py

from django.shortcuts import render

def index(request, path=''):
    response = render(request, 'index.html')
    return response



2) На Ember фронтэнде

Просто запускаем в папке front:

$ ember serve --output-path ../back/static/

Используя ключ --output-path ../back/static/ можно заменить путь вывода скомпиленного фронэнда (по умолчанию папка dist).

После запуска фронтэнда, видим, что в папке back создалась папка static и внутри лежит файл index.html и папка assets (в ней лежат js скрипты и css фронтэнда).
Нам нужен файл index.html, копируем его в back/templates, открываем его и видим строки, которые ссылаются на assets

<link rel="stylesheet" href="/assets/vendor.css">
<link rel="stylesheet" href="/assets/front.css">


<script src="/assets/vendor.js"></script>
<script src="/assets/front.js"></script>


Т.к. django не знает ничего про папку assets, зато знает где находится static (в котором теперь есть assets), то надо просто добавить перед assets - static. Вот что должно получится:

<link rel="stylesheet" href="/static/assets/vendor.css">
<link rel="stylesheet" href="/static/assets/front.css">


<script src="/static/assets/vendor.js"></script>
<script src="/static/assets/front.js"></script>


Теперь и на бакэнде (localhost:8000) можно наблюдать фронтэнд.

Эти же настройки можно использовать не только при разработке, но и при деплое, используя команду:

$ ember build -prod --output-path ../back/static/
Ну и скопировать и отредактировать index.html, как было описано выше.

Можно добавить в файл ember-cli-build.js

  var app = new EmberApp(defaults, {
    fingerprint: {
      prepend: '/static/'
    }
  });


Строка prepend: '/static/' позволит не добавлять префикс static в index.html.
Теперь, после выполнения команды

$ ember build -prod --output-path ../back/static/

в index.html уже будут строки похожие на такие

    <link rel="stylesheet" href="/static/assets/ember-app-cd507f10f1eb6d96c8c44db034f28a4f.css">
    <script src="/static/assets/ember-app-03dac61f48f7ccdae5ae0aa2db976ec8.js"></script>


и, теперь, достаточно будет скопировать index.html в templates без всяких изменений.


На мой взгляд, второй вариант получился проще.

Чтобы на фронтэнде при запуске не вспоминать путь --output-path ../back/static/ я сделал shell скрипт в папке front:

- Создаю файл run.sh
$ vi run.sh

- Добавляю туда следующие строки:

#!/bin/bash
ember serve --output-path ../back/static/


- Делаю файл run.sh исполняемым

$ chmod +x run.sh

Теперь можно запускать фронтэнд  командой
./run.sh

четверг, 16 февраля 2017 г.

Публикуем (deploy) Ember фронтэнд в Django бакэнде

Разработка это, конечно, интересно, но приходит время деплоя (публикации фронтэнда). Перед публикацией на хостинге надо проверить работу на локальном компьютере.
Как и куда поместить данные из фронтэнда в бакэнд я сейчас расскажу.
Если непонятна структура связки Django - Ember, то смотрите статью про Создание среды для разработки веб приложения под Django framework и Ember js

Итак, делаем тестовый деплой:

1. На Django бакэнде

Приложение frontend можно и не создавать, но с ним элегантней смотрится (понятно, что и откуда берется).

- Создаем приложение - frontend:
$ python manage.py startapp frontend

-  В back/back/settings.py в INSTALLED_APPS добавляю приложение 'frontend' и сразу добавляю путь к статике фронэнда:

INSTALLED_APPS = [
    ...

    #local apps
    'frontend.apps.FrontendConfig',
]

 
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "frontend/templates"),
]



- создаем back/frontend/urls.py

# back/frontend/urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),

    url(r'^(?P<path>.*)/$', index),
]


- в back/back/urls.py добавляем ссылку на  'frontend.urls' 

# back/back/urls.py

from django.conf.urls import url, include
...

 
urlpatterns = [
    url(r'^', include('frontend.urls')),
    ...
]


- в back/frontend/views.py создаем функцию index  которая будем выводить index.html на главную страницу нашего Django бакэнда:

# back/frontend/views.py

from django.shortcuts import render


def index(request, path=''):
    response = render(request, 'index.html')
    return response


- создаем папку templates в back/frontend/

- в back/frontend/templates/ создаем для проверки  index.html
- в back/frontend/templates/index.html добавляем строку

<h1>Главная страница Django бакэнда</h1>

- Запускаем django бакэнд
$ python manage.py runserver

и смотрим http://localhost:8000/ должна быть надпись: Главная страница Django бакэнда


2. На Ember фронтэнде

переходим в папку ember фронэнда - у меня называется front

- добавляю в файл front/ember-cli-build.js префикс 'static/'

//front/ember-cli-build.js
 
var EmberApp = require('ember-cli/lib/broccoli/ember-app');

module.exports = function(defaults) {
  var app = new EmberApp(defaults, {
    fingerprint: {
      prepend: 'static/'
    }
  });
  return app.toTree();
};


Смысл в том, что в Django файлы статики ищутся в 'static' ,а в Ember создается для статики папка assets, я пошел по простому пути и добавил префикс 'static/'

- теперь выполняем команду:

$ ember build -prod

все благополучно "собралось" в папку front/dist

- содержимоем папки front/dist копирую в back/frontend/templates/

и запустив в консоли Django бакэнд -
$ python manage.py runserver
смотрю в браузере http://localhost:8000/.

Таким образом, "собрав" исходники на фронэнде и опубликовав их на бакэнде можно посмотреть, предварительно, как это все работает на одном сервере, т.е. теперь не нужно запускать фронэнд.

Например, можно отключить CORS на бакэнде
CORS_ORIGIN_ALLOW_ALL = False
и посмотреть на результат.


Есть еще и второй вариант Ember Django deploy v2

среда, 15 февраля 2017 г.

Ember - Django session authentication

Имеется:
- Бакэнд Django.
- Фронтэнд Ember.js


Задача: При входе в Ember (фронтэнд) автоматически проходить аутентификацию (ember simple authentication), если в Django (бакэнд) аутентификацию уже прошла, т.е. получить access_token в Ember для имеющейся пользовательской Django сессии.

Зачем это нужно?
1) Если на бакэнде пользователь уже авторизован, то на фронэнде повторно проходить аутентификацию уже не нужно.
2) В Django реализована аутентификаци при помощи REMOTE_USER - https://docs.djangoproject.com/en/dev/howto/auth-remote-user/ (это когда используется kerberos аутентификация) и, в таком случае, зачем заставлять пользователя проходить повторную аутентификацию на фронэнде? Если простыми словами: пользователь входит в Windows -> запускает браузер и в браузере уже не нужно проходить повторную аутентификацию. Ранее, я уже описывал как сделать Django аутентификацию через NGINX kerberos (Active Directory) , поэтому повторяться здесь не буду.


У меня Django бакэнд лежит в папке back, соответсвенно настройки settings.py лежат в back/back/settings.py . Подробно про создание среды разработки Django Ember

На бакэнэде нужно дополнительно установить:

$ pip install djangorestframework
$ pip install djangorestframework-jwt
$ pip install django-cors-headers


и настроить settings.py:

- в INSTALLED_APPS добавляю 'rest_framework' и 'corsheaders'

INSTALLED_APPS = (
   ...
    # external apps
    'rest_framework',
    'corsheaders',
)


- в MIDDLEWARE_CLASSES добавляю 'corsheaders.middleware.CorsMiddleware',

MIDDLEWARE_CLASSES = [
   ...
   'corsheaders.middleware.CorsMiddleware',
   'django.middleware.common.CommonMiddleware',
   ...
]


- добавляю новую конфигурацию для REST_FRAMEWORK

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
# разрешаю подключение к api всем, ограничивать подключения буду во views
'rest_framework.permissions.AllowAny',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
# аутентификацию делаю на уровне сессий (для прямых веб подключений) и с использованием token для фронтэнда
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',

'rest_framework.authentication.SessionAuthentication', 
),
'PAGE_SIZE': 100,
}


Для REST_FRAMEWORK аутентификация на первом месте обязательно должна быть JSONWebTokenAuthentication. Это очень важно, потому что сначала делается попытка пройти аутентификацию по jwt, а если не получается, то идет аутентификация по сессии. И если поставить сначала аутентификацию по сессии, то на фронтэнде аутентификация будет выполнена по SessionAuthentication и при POST запросе будет вылетать ошибка, т.к. аутентификация по сессии использует CSRF.  Вообщем-то, когда будет все отлажено и запущено, можно вообще убрать аутентификацию SessionAuthentication.


- для CORS разрешаю все запросы во время разработки (DEBUG = True, а значит получится и CORS_ORIGIN_ALLOW_ALL = True), а также разрешаю принимать cookie в в заголоках - CORS_ALLOW_CREDENTIALS = True:

CORS_ORIGIN_ALLOW_ALL = DEBUG
 CORS_ALLOW_CREDENTIALS = True

- для djangorestframework-jwt (JSON Web Token) добавляю настройки:

import datetime

JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'back.views.jwt_response_payload_handler',
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
# default JWT_EXPIRATION_DELTA - 5 minutes
# JWT_EXPIRATION_DELTA = datetime.timedelta(seconds=300)
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=12),
}


Мне не хватает для разработки 5 минут активного токена jwt, которые стоят по умолчанию, поэтому я сделал 12 часов, не надо передергивать постояно. Надо понимать, что быстрое "протухание" токена сделано с целью безопасности, поэтому на рабочих проектах надо поставить оптимальное время.

- на этом настройки settings.py закончены, сохраняю.

Т.к. я сослался в настройках на  'JWT_RESPONSE_PAYLOAD_HANDLER': 'back.views.jwt_response_payload_handler', то надо теперь создать
back/back/views.py
и добавить функцию, которая меняет название ключа 'token' на 'access_token'. (по умолчанию djangorestframework-jwt выдает в json ответе ключ 'token', а ember-simple-auth хочет видеть 'access_token'):


def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'access_token': token,
    }


Чтобы не прыгать от настроек к настройкам, добавлю сразу в back/back/views.py функцию отдачи jwt ключа по POST запросу, который будет создаваться при наличии пользовательской сессии.

Привожу полный back/back/views.py

from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from rest_framework import status


#получаю пользователя из сессии

def get_user_from_session(session_key):
    try:
        session = Session.objects.get(session_key = session_key)
        uid = session.get_decoded().get('_auth_user_id')
        return User.objects.get(pk = uid)
    except:
        return None


@api_view(['POST'])
def SessionIdJSONWebToken(request):
    if request.method == 'POST':
        session_key = request.session.session_key
        user = get_user_from_session(session_key)
        if user:
            #Создаю jwt токен для отдачи
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            username = user.username
            response_data = jwt_response_payload_handler(token, user, request)
            return Response(response_data)
        else:

            #если нет пользовательской сесии выдаю ошибку 401
            response_data = {"error":"User session not found."}
            return Response(response_data, status=status.HTTP_401_UNAUTHORIZED)

#добавляю дополнительно user.id и user.username в jwt json ответ, 

# чтобы на фронтэнде через сессии можно было посмотреть 
# авторизованного пользователя без дополнительных запросов
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id','username',)


def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'access_token': token,
        'user': UserSerializer(user, context={'request': request}).data,
    }



Добавляю в back/back/urls.py


from django.conf.urls import url, include
from django.contrib import admin

from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
from .views import SessionIdJSONWebToken


 

urlpatterns = [
    #url(r'^', include('front.urls')),
    url(r'^admin/', admin.site.urls),
    url(r'^api/auth/login/', obtain_jwt_token),
    url(r'^api/auth/verify-token/', verify_jwt_token),
    url(r'^api/auth/token-sessionid/', SessionIdJSONWebToken),
]



Проверяем отдачу 'access_token' на бакэнде.
- Если еще не создан супер пользователь, то создаем:
$ cd back
$ python manage.py createsuperuser

затем запускаю Django бакэнд:
$ python manage.py runserver

Захожу в админку - http://localhost:8000/admin/
После успешного входа иду по адресу - http://localhost:8000/api/auth/token-sessionid/ , где вижу
"detail": "Method \"GET\" not allowed."
все правильно, принимаются только POST запросы.

Ввожу в поле Content пустые фигурные скобки {} и нажимаю кнопку "POST". Получаю json ответ на экране

HTTP 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "user": {
        "id": 2,
        "username": "hairetdin"
    },
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhhaXJldGRpbiIsInVzZXJfaWQiOjIsImVtYWlsIjoiIiwiZXhwIjoxNDg3MTQ1MDExfQ.kcl2TKs3pCd8nk8DIme9tzzWW7UUuy6JM-P21yly7Ig"
}

Почему запрос не через curl? Вроде проще, а нет. В консоли нет пользовательской сессии. Можно попробывать и посмотреть

$ curl -X POST http://localhost:8000/api/auth/token-sessionid/
{"error":"User session not found."}


Проверяем json token при аутентификации - http://localhost:8000/api/auth/login/ и верификации - http://localhost:8000/api/auth/verify-token/. Соответсвенно ввожу логин и пароль или в поле Token вставляю 'access_token' из предыдущей страницы.
Или в командной строке:
$ curl -X POST -d "username=admin&password=парольсуперпользователя" http://127.0.0.1:8000/api/auth/login/


С бакэндом закончили, переходим к фронтэнду.

Фронтэнд - Ember js. Детали описывать не буду, если что-то непонятно, то Подробно про создание среды разработки Django Ember

$ cd front

- Устанавливаю EDA (Ember-Django-Adapter)
$ ember install ember-django-adapter

- Устанавливаю для аутентификации, авторизации - ember-simple-auth
$ ember install ember-simple-auth

- Устанавливаю ember-cli-js-cookie, чтобы можно было вызывать cookie 
$ ember install ember-cli-js-cookie

В настройках front/config/environment.js указываю API_HOST бакэнда и путь для ember-simple-auth:

if (environment === 'development') { 
  ENV.APP.API_HOST = 'http://localhost:8000';
  ENV['ember-simple-auth'] = {
    serverAuthEndpoint: ENV.APP.API_HOST + '/api/auth/'
  }
... 
}

- Хоть и ведутся какие-то неправильные разговоры про pods, я его все равно использую и далее планирую использовать, мне нравится когда каждое приложение лежит в отдельной папке. Поэтому, назначаю по умолчанию pods структуру в файле front/.ember-cli

{
  "usePods": true,

}

- Создаю аутентификатор
$ ember g authenticator django

Аутентификатор создался по пути front/app/authenticators/django.js
Вношу в front/app/authenticators/django.js следующий код:

//начало front/app/authenticators/django.js
import Ember from 'ember';
import Base from 'ember-simple-auth/authenticators/base';
import ENV from '../config/environment';
import Cookies from 'ember-cli-js-cookie';


function isSecureUrl(url) {
  var link  = document.createElement('a');
  link.href = url;
  link.href = link.href;
  return link.protocol === 'https:';
}

export default Base.extend({

  init() {
    //var apiHost = ENV.APP.API_HOST;
    var apiAuthentication = ENV['ember-simple-auth'] || {};
    this.serverAuthEndpoint = apiAuthentication.serverAuthEndpoint;

  },

  authenticate(identification,password) {

    return new Ember.RSVP.Promise((resolve, reject) => {
      let remoteResponse;
      if (!(identification && password)) {
        remoteResponse = this.requestBackendSession();
      } else {
        const data = { username: identification, password: password };
        let host = ENV.APP.API_HOST;
        let login_url = host + '/api/auth/login/';
        //let login_url = this.serverAuthEndpoint + 'login/';
        remoteResponse = this.makeRequest(login_url, data);
      }
      remoteResponse
      .then((response) => {
        Ember.run(() => {
          console.log('django authenticate response:',response);
          resolve(response);
        });
      }, (xhr /*, status, error */) => {
        Ember.run(() => {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  restore(data) {
    return new Ember.RSVP.Promise((resolve, reject) => {
      console.log('authenticator django restore data', data);
      let host = ENV.APP.API_HOST;
      let verifyTokenUrl = host + '/api/auth/verify-token/';
      let access_token = {token:data.access_token};
      this.makeRequest(verifyTokenUrl, access_token).then((response) => {
        Ember.run(() => {
          resolve(response);
        });
      }, (xhr /*, status, error */) => {
        Ember.run(() => {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },


  invalidate(/* data */) {
    function success(resolve) {
      resolve();
    }
    return new Ember.RSVP.Promise((resolve /*, reject */) => {
      //let logout_url = this.serverAuthEndpoint + 'logout/';
      let host = ENV.APP.API_HOST;
      let logout_url = host + '/api/auth/logout/';
      this.makeRequest(logout_url, {}).then((/* response */) => {
        Ember.run(() => {
          success(resolve);
        });
      }, (/* xhr, status, error */) => {
        Ember.run(() => {
          success(resolve);
        });
      });
    });
  },

  makeRequest(url, data) {
    if (!isSecureUrl(url)) {
      Ember.Logger.warn('Credentials are transmitted via an insecure connection - use HTTPS to keep them secure.');
    }

    return new Ember.RSVP.Promise((resolve, reject) => {
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(request) {
          request.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
        },
        data: data,
      })
      .then((response) => {
        if (response.status === 400) {
          response.json().then((json) => {
            reject(json);
          });

        } else if (response.status > 400) {
          reject(response);
        } else {
          resolve(response);
        }
      }).catch((err) => {
        reject(err);
      });
    });
  },

  requestBackendSession(){

    let host = ENV.APP.API_HOST;
    let url = host + '/api/auth/token-sessionid/';
    let csrftoken = Cookies.get('csrftoken');
    return new Ember.RSVP.Promise((resolve, reject) => {
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(xhr/*, settings*/) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        },
        xhrFields: {
            withCredentials: true
        },
      })
      .then((response) => {
        if (response.status === 400) {
          response.json().then((json) => {
            reject(json);
          });

        } else if (response.status > 400) {
          reject(response);
        } else {
          resolve(response);
        }
      }).catch((err) => {
        reject(err);
      });
    });
  }
});

//конец front/app/authenticators/django.js


- Генерирую ресурс и контроллер  application:

$ ember g resource application
$ ember g controller application

- Добавляю в темплейт front/app/application/template.hbs следующий код:

{{!-- front/app/application/template.hbs --}}
<div class="menu">
  {{#if session.isAuthenticated}}
    {{session.data.authenticated.user.username}}
    <a href {{action 'invalidateSession'}}>Выход</a>
  {{else}}
    {{#link-to 'login'}}Вход{{/link-to}}
  {{/if}}
</div>
<div class="main">
  {{outlet}}
</div>


- Добавляю в контроллер front/app/application/controller.js следующий код:

// front/app/application/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),





  sessionAuthenticated: Ember.observer('session.isAuthenticated', function () {
    //reload application if session isAuthenticated
    window.location.href = '/';
  }),

  actions: {
    invalidateSession() {
      this.get('session').invalidate();
    }
  }
});



- Добавляю в роут front/app/application/route.js следующий код:

// front/app/application/route.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Ember.Route.extend(ApplicationRouteMixin,{
  session: Ember.inject.service('session'),
  beforeModel: function(){
    if (!(this.get('session.isAuthenticated'))){
      //try to authenticate with backend's session, if session is not authenticated
      this.get('session').authenticate('authenticator:django');
    }
  },

});


Вообщем-то на этом этапе уже можно наблюдать результат:
- Запускаем бакэнд - python manage.py runserver
- Запускаем фронтэнд - ember s
На бакэнде проходим аутентификацию на странице http://localhost:8000/admin/login/, а на странице фронтэнда - http://localhost:4200/ смотрим результат в консоли (кнопка F12):

django authenticate response: Object {user: Object, access_token: ....

а еще выводится ошибка

Error: There is no route named login

это потому, что в темплейте front/app/application/template.hbs имеется ссылка {{#link-to 'login'}}Вход{{/link-to}} на роут login, а такого роута пока нет. Исправим эту ошибку.
- Создаю роут и контроллер для login
$ ember g route login
$ ember g controller login

- Редактирую login темлейт front/app/login/template.hbs:

{{!-- front/app/login/template.hbs --}}
<form {{action 'authenticate' on='submit'}}>
  <label for="identification">Пользователь</label>
  {{input id='identification' placeholder='Enter Login' value=identification}}
  <label for="password">Пароль</label>
  {{input id='password' placeholder='Enter Password' type='password' value=password}}
  <button type="submit">Вход</button>
  {{#if errorMessage}}
    <p>{{errorMessage}}</p>
  {{/if}}
</form>


- Редактирую login контроллер front/app/login/controller.js:

// front/app/login/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),

  actions: {
    authenticate() {
      let { identification, password } = this.getProperties('identification', 'password');

      this.get('session').authenticate('authenticator:django', identification, password).
      then(() => {
       console.log('Успешна аутентификация с токеном: ' + this.get('session.data.authenticated.access_token'));
      }, (err) => {
       alert('Ошибка токена: ' + err.responseText);
      }).
      catch((reason) => {
      this.set('errorMessage', reason.error || reason);
      });
    }
  }
});


Теперь в http://localhost:4200/ ошибок нет и возможна аутентификация через фронэнд, если на бакэнде аутентификации не было.


А вот авторизатор я изобретать не буду, использую стандартный.
Создаю авторизатор:
$ ember g authorizer django

и добавлю в него (front/app/authorizers/django.js) следующие строки

// front/app/authorizers/django.js
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();


Теперь нужен еще адаптер, который будет использовать созданный django авторизатор

$ ember g adapter application

и добавляю в него (front/app/application/adapter.js) следующий код:

// front/app/application/adapter.js
import DRFAdapter from '../adapters/drf';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default DRFAdapter.extend(DataAdapterMixin , {
  authorizer: 'authorizer:django'
});





django-oscar tinymce 4 filebrowser

Задача: в дашборде django-oscar загружать изображения 1. Установка django-filebrowser-no-grappelli - Открываем проект, загружаем виртуа...