Суперпользователь в web-приложении на Python и Starlette
programming
Суперпользователь в web-приложении на Python и Starlette, процесс разработки которого мы детально и вдумчиво изучаем в этом цикле статей, — это пользователь из группы Администраторы. Он имеет разрешения на все предусмотренные интерфейсом приложения действия без каких-либо ограничений. Учётная запись суперпользователя создаётся администратором сервера при развёртывании приложения в сети, для этого необходимо предусмотреть наиболее простой и быстрый способ регистрации первого пользователя. В этом выпуске цикла я разработаю интерактивную консольную программу, выполняющую это действие на раз-два.
Техническое задание
Необходимо разработать консольную программу, которая в интерактивном режиме запрашивала бы у пользователя компьютера данные для создания учётной записи, осуществляла по мере ввода проверку этих данных по заданным критериям и, если эта проверка завершилась успешно, создавала бы в базе данных web-приложения учётную запись пользователя, участника группы Администраторы, и привязанный к этой учётной записи аккаунт.
Учётная запись и аккаунт пользователя будут храниться в базе данных PostgreSQL в подготовленных на предыдущем этапе разработки таблицах. Чтобы создать их, необходимо запросить у пользователя — администратора web-сервера, следующие данные:
-
имя пользователя;
-
адрес электронной почты, к которому будет привязан аккаунт;
-
пароль, с которым пользователь сможет войти в сервис.
Поскольку соответствующие поля таблиц базы данных, в которых эти данные будут впоследствии храниться, имеют определённые параметры, вводимые при создании пользователя значения должны быть проверены по следующим критериям:
-
Имя пользователя должно состоять из букв латинского или русского алфавитов, цифр, может содержать дефис, знак подчёркивания и точку, должно содержать не менее 3 и умещаться в 16 символов, начинаться с буквы любого из двух алфавитов. Имя пользователя должно быть уникальным, не допускается создание двух учётных записей с одним именем пользователя;
-
Адрес электронной почты должен соответствовать общепринятой форме адреса электронной почты, содержать в себе знак
@
и доменное имя почтового сервера, умещаться в 128 символов. Адрес электронной почты должен быть уникальным, два разных пользователя не могут иметь один адрес электронной почты, кроме этого, адрес электронной почты не должен числиться в полеswap
любого уже существующего аккаунта; -
Пользователь, создающих учётную запись, должен придумать пароль самостоятельно и осознанно, ввести его как минимум дважды без ошибок и опечаток. Длина пароля не ограничивается, посимвольный состав пароля не ограничивается.
Проверку данных по этим простым требованиям необходимо предусмотреть в алгоритмах разрабатываемой программы.
Запросы в базу данных
Имя пользователя и адрес электронной почты должны быть уникальными. Это значит, что при создании учётной записи необходимо предусмотреть проверку вводимых администратором сервера значений на предмет наличия в базе данных уже существующих записей, содержащих совпадающие до знака значения. Для этого потребуется соединение с базой данных и соответствующий запрос на каждом шаге. Запросы к базе данных я реализую с помощью двух вспомогательных функций, одна для проверки имени пользователя, вторая — адреса электронной почты. Хранить эти вспомогательные функции я буду в уже созданном модуле pg.py
каталога auth
, в данном случае имя модуля соответствуют функционалу содержащихся в нём инструментов. Запускаю терминал, вхожу в корневой каталог приложения и открываю этот модуль в текстовом редакторе Vim.
$ vim webapp/auth/pg.py
Начнём с необходимых этому модулю процедур импорта. В рамках этой программы мне потребуется выразить текущие дату и время в терминах Питона, для этого подключаю объект datetime одноимённого модуля стандартной библиотеки.
from datetime import datetime
Соединение с базой данных я предполагаю реализовать с помощью уже разработанной ранее вспомогательной функции get_conn, импортирую её.
from ..common.pg import get_conn
Начнём с имени пользователя. Получив от администратора сервера имя пользователя, программа должна подключиться к базе данных и проверить наличие в ней уже существующих записей, содержащих это имя пользователя. Это действие можно без труда реализовать следующим кодом:
## функция в качестве аргументов получает данные
## из файла настроек для подключения к базе
## и имя пользователя — строку
async def check_username(config, username):
## создаю соединение с базой данных приложения
conn = await get_conn(config)
## отправляю в базу данных запрос соответствующей формы
res = await conn.fetchrow(
'SELECT username FROM users WHERE username = $1', username)
## закрываю соединение с базой данных
await conn.close()
## преобразую полученное из базы данных значение
## в булеву форму и возвращаю этот объект
return bool(res)
Вызов функции check_username
будет расположен в другом модуле, о нём мы поговорим подробно чуть позже.
Адрес электронной почты я планирую проверять аналогичной вспомогательной функцией, вот как выглядит её код:
## функция получает в качестве аргументов данные
## из файла настроек и адрес электронной почты — строку
async def check_address(config, address):
## создаю переменную res и присваиваю ей значение False
res = False
## создаю соединение к базе данных
conn = await get_conn(config)
## проверяю существование аккаунта с полученным адресом
## в поле address и значение поля user_id этого аккаунта
account = await conn.fetchrow(
'SELECT address, user_id FROM accounts WHERE address = $1', address)
## проверяю существование аккаунта с полученным адресом
## в поле swap
swap = await conn.fetchrow(
'SELECT swap FROM accounts WHERE swap = $1', address)
## закрываю соединение с базой данных
await conn.close()
## проверяю значения в переменных account и swap
if (account and account.get('user_id')) or swap:
## если условие выполняется, изменяю значение
## переменной res на противоположное
res = True
## возвращаю переменную res
return res
Как видно из кода, логика действий достаточно проста. Следует запрос к базе данных и проверка полученных данных по обозначенным в техническом задании критериям, в данном случае проверяется только наличие уже существующих записей базы данных.
В результате работы разрабатываемой программы в базе данных должны появиться данные созданных учётной записи и аккаунта пользователя, такого результата можно достичь следующим кодом:
## функция получает в качестве аргументов
## данные файла настроек приложения,
## имя пользователя создаваемой учётной записи,
## адрес электронной почты создаваемого аккаунта,
## пароль пользователя для создаваемой учётной записи,
## имя группы, участником которой будет создаваемый пользователь
async def create_user(config, username, address, passwd, group):
## создаю переменную now, в ней хранятся текущие дата и время
now = datetime.utcnow()
## подключаюсь к базе данных приложения
conn = await get_conn(config)
## создаю в базе данных учётную запись нового пользователя,
## содержащую переданные в параметрах значения
user_id = await create_user_record(conn, username, passwd, group, now)
## обновляю (или создаю) аккаунт с заданным адресом электронной почты
await update_account(conn, address, user_id, now)
## закрываю соединение с базой данных
await conn.close()
Здесь следует обратить внимание, что вспомогательные функции create_user_record и update_account, вызовы которых я использовал в этом коде, были разработаны на предыдущем этапе проектирования.
Сохраняю изменения в файл. Вызовы разработанных только что функций я буду использовать в исполняемом модуле разрабатываемой консольной программы, об этом чуть позже...
Проверка имени пользователя по составу
Уже в ближайшей перспективе, получив от администратора сервера имя пользователя создаваемой учётной записи, программе предстоит проверка полученного значения — состава полученной строки. Такую проверку без труда можно организовать с помощью самого обычного регулярного выражения. Хранить это регулярное выражения я планирую в модуле attri.py
каталога auth
, этот модуль уже существует, открываю его в текстовом редакторе.
:edit webapp/auth/attri.py
В соответствии с заявленными выше требованиями, предъявляемыми к имени пользователя, проверять полученные данные я буду по следующему регулярному выражению:
USERNAME_PATTERN = r'^[A-ZА-ЯЁa-zа-яё][A-ZА-ЯЁa-zа-яё0-9\-_.]{2,15}$'
Вписываю эту процедуру верхней строчкой в открытый в текстовом редакторе файл.
Сохраняю изменения в файл. Все необходимые вспомогательные инструменты готовы, остаётся сформулировать код исполняемого модуля разрабатываемой программы.
Исполняемый модуль программы
Исполняемый модуль разрабатываемой программы будет храниться в корневом каталоге приложения, рядом с базовым каталогом, имя ему даю соответствующее — create_root.py
. Открываю этот файл в уже активном окне текстового редактора Vim, в его командном режиме.
:edit create_root.py
Этот модуль будет иметь собственную логику, в которой объединит все разработанные в предыдущем выпуске этого блога и только что вспомогательные функции из модуля pg.py
. Начнём с процедур импорта. Поскольку вспомогательные функции являются корутинами, их вызов должен быть оформлен соответственно — для этого потребуются инструменты asyncio. Кроме этого, необходимо будет обеспечить скрытый от глаз навязчивого наблюдателя ввод пароля, инструменты модуля getpass позволят реализовать эту возможность. Введённое имя пользователя нужно будет проверить по регулярному выражению, эту возможность можно реализовать с помощью модуля re. Все три модуля из стандартной библиотеки Питона. Подключаю.
import asyncio
import getpass
import re
На предыдущем этапе разработки я установил в виртуальное окружение модуль validate_email, с помощью его инструментов я проверю, соответствует ли введённый пользователем адрес общепринятой форме адреса электронной почты. Подключаю.
from validate_email import validate_email
Для подключения к базе данных приложения мне потребуются данные из файла настроек, которые хранятся в модуле dirs.py
базового каталога, в объекте с именем settings
. Подключаю.
from webapp.dirs import settings
Из модуля attri.py
каталога auth
мне потребуется класс groups и регулярное выражение для проверки имени пользователя по составу образующих его символов. Подключаю.
from webapp.auth.attri import groups, USERNAME_PATTERN
Три разработанные только что вспомогательные функции из модуля pg.py
тоже будут необходимы, в рамках исполняемого модуля я буду использовать их вызовы с заданными вводом пользователя параметрами. Подключаю.
from webapp.auth.pg import check_address, check_username, create_user
Интерактивный режим ввода вывода мне помогут реализовать несколько строковых констант следующего содержания.
USERNAME = 'Enter your username: '
MESSAGE = '''Username must be from 3 to 16 symbols
(latin letters, russian letters including `ё`,
numbers, dots, hyphens, underscores)
and start with any letter, latin or russian.
'''
EMAIL = 'Enter your email address: '
PASSWORD = 'Enter your password: '
Алгоритм программы предполагает организацию ввода данных пользователем в три этапа. На первом этапе запрашивается имя пользователя. Это действие я реализую с помощью ещё одной вспомогательной функции.
def get_username():
pattern = re.compile(USERNAME_PATTERN)
username = input(USERNAME)
while True:
if not pattern.match(username):
print(MESSAGE)
username = input(USERNAME)
continue
if asyncio.run(check_username(settings, username)):
print('This name is already registered. Try again.\n')
username = input(USERNAME)
continue
return username
Функция get_username
запрашивает у пользователя в бесконечном цикле желаемый username, проверяет его по регулярному выражению, затем по базе данных, каждый раз возвращая пользователя к вводу, если были введены неверные данные. В итоге, когда пользователь всё-таки введёт верные данные, функция вернет полученное из ввода значение.
На втором этапе ввода программа запрашивает у пользователя адрес электронной почты, опять в бесконечном цикле, и сопровождая проверкой по общепринятой для адреса электронной почты форме, а затем по наличию адреса в существующих аккаунтах базы данных. Для этого мне потребуется ещё одна функция — get_email
, и вот как будет выглядеть её код.
def get_email():
address = input(EMAIL)
while True:
if not validate_email(address):
print('This is not a valid email address.\n')
address = input(EMAIL)
continue
if asyncio.run(check_address(settings, address)):
print('This email address cannot be registered. Try another.\n')
address = input(EMAIL)
continue
return address
И, наконец, третий этап ввода данных предполагает ввод и подтверждение желаемого пароля. Это действие я реализую с помощью функции get_passwd
, вот её код.
def get_passwd():
phrase = getpass.getpass(PASSWORD)
while True:
confirm = getpass.getpass('Confirm the password: ')
if confirm != phrase:
print('Passwords must match!\n')
phrase = getpass.getpass(PASSWORD)
continue
return phrase
Эта функция проверяет совпадение пароля, запрошенного и введённого дважды, и в этом случае опять работает бесконечный цикл.
Имея все эти только что разработанные инструменты, я могу без труда организовать исполнение асинхронной функции create_user
, создав с помощью соответствующего инструмента asyncio необходимый для вызова асинхронных функций event loop. Эту процедуру дописываю в самый конец файла в стандартной идиоме Питона, вот как выглядит код.
if __name__ == '__main__':
asyncio.run(create_user(
settings, get_username(), get_email(), get_passwd(), groups.root))
Обращаю ваше внимание, что предложенный код я не выдумал усилием своего бурного воображения, а последовательно отладил, тестируя код после каждой внесённой правки. В рамках текстового блога продемонстрировать процесс отладки представляется довольно сложной задачей, поэтому предлагаю поверить мне на слово. Пришло время протестировать разработанное решение и попробовать создать пару учётных записей.
Создаём суперпользователя web-приложения
Запустить только что разработанную программу можно в терминале с активным виртуальным окружением следующей командой.
$ python create_root.py
Программа в интерактивном режиме запросит все необходимые ей данные, на снимке экрана далее показан результат исполнения команды с последовательным вводом данных.
Как видно, пароль при вводе на терминале не отобразился. Программа отработала в штатном режиме, сбоев и отчётов об ошибок не последовало. Смею предположить, что учётная запись пользователя avm4 в результате этого вызова успешно создана и хранится в базе данных.
На стадии разработки проекта ограничиваться созданием одного суперпользователя не стоит, следует проверить все варианты ввода, в том числе повторный ввод имени и адреса, которые уже точно существуют в базе данных. Внимание на снимок экрана, на вторую попытку создания суперпользователя.
Как видно на снимке экрана выше, программа не позволила создать второго пользователя с именем avm4 и не приняла уже существующий в базе данных адрес электронной почты. Конечно же стоит провести более тщательное тестирование, попробовать разные имена с разным количеством и составом образующих символов, попробовать вместо адреса электронной почты ввести какую-нибудь абракадабру, попробовать ввести не совпадающие пароли. При тестировании стоит проверить все возможные и невозможные варианты ввода. В рамках этой демонстрации я ограничусь только двумя показанными примерами.
В процессе тестирования, когда учётные записи пользователей в базе данных уже созданы, будет очень полезно подключиться к базе данных в консоли psql и посмотреть на содержимое таблиц с помощью самых обычных и простых SQL-запросов. Вот как примерно это выглядит в моём терминале.
Как видно на снимке экрана выше, в результате предпринятых действий в базе данных появились две учётные записи пользователей из группы Администраторы, к каждой учётной записи привязан аккаунт с адресом электронной почты, в каждой учётной записи пароль хранится в зашифрованном виде. Текущую версию кода приложения можно найти в моём профиле на github.com по этой короткой ссылке. Все цели этой демонстрации достигнуты.
Подводим промежуточный итог
В приложении появился пользователь — главное действующее лицо, и все дальнейшие усилия разработки на последующих этапах будут направлены на программирование инструментов, обеспечивающих пользователю вход в сервис посредством ввода логина и пароля в web-интерфейсе сайта. Это достаточно объёмная задача, и решать я её начну с построения собственного сервиса пользовательских аватар. Продолжение следует...