Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BANG-468: Use keycloak oauth #48

Merged
merged 3 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, Union

import aiohttp_jinja2
from aiohttp import web
from aiohttp.web_exceptions import HTTPFound
from aiohttp_security import remember
from aiopg.sa.engine import Engine
from aiopg.sa.result import RowProxy
from aiohttp.web import Application
from aiohttp_security.abc import AbstractAuthorizationPolicy
from dynaconf import settings
from passlib.hash import sha256_crypt
from sqlalchemy import and_, func, not_

Expand All @@ -14,6 +19,31 @@
from typing import Optional


async def get_login_context(error: str | None = None) -> Dict[str, Union[str | bool]]:
use_oauth = getattr(getattr(settings, 'OAUTH', None), 'IS_USED', False)
only_oauth = getattr(getattr(settings, 'OAUTH', None), 'ONLY_OAUTH', False)
oauth_sign_in_title = getattr(getattr(settings, 'OAUTH', None), 'SIGN_IN_TITLE', '')
context = {
'context': '',
'use_oauth': use_oauth,
'only_oauth': only_oauth,
'oauth_sign_in_title': oauth_sign_in_title,
}
if error:
context['error'] = error
return context


async def oauth_on_login(request: web.Request, user_data: dict) -> web.Response:
await remember(request, HTTPFound('/zbs/switches'), 'admin')
return HTTPFound('/zbs/switches')


@aiohttp_jinja2.template('users/login.html')
async def oauth_on_error(request: web.Request) -> Dict[str, Union[str | bool]]:
return await get_login_context(error='OAUTH failed')


async def check_credentials(db_engine: Engine, username: str, password: str) -> bool:
"""Производит аутентификацию пользователя."""
async with db_engine.acquire() as conn:
Expand Down
20 changes: 14 additions & 6 deletions auth/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union

import aiohttp_jinja2
from aiohttp.abc import StreamResponse
from aiohttp.web import HTTPFound, View, Response
from aiohttp_security import forget, remember
from dynaconf import settings
from marshmallow.exceptions import ValidationError
from multidict import MultiDictProxy

from auth.auth import check_credentials
from auth.auth import check_credentials, get_login_context
from auth.schemes import LoginPostRequestSchema

if TYPE_CHECKING:
Expand All @@ -17,12 +18,16 @@

class LoginView(View):
@aiohttp_jinja2.template('users/login.html')
async def get(self, error: Optional[str] = None) -> Dict[str, str]:
return {'context': ''}
async def get(self, error: Optional[str] = None) -> Dict[str, Union[str | bool]]:
return await get_login_context()

@aiohttp_jinja2.template('users/login.html')
async def error(self) -> Dict[str, str]:
return {'context': '', 'error': 'Authorization failed'}
async def error(self) -> Dict[str, Union[str | bool]]:
return await get_login_context('Authorization failed')

@aiohttp_jinja2.template('users/login.html')
async def only_oauth_error(self) -> Dict[str, Union[str | bool]]:
return await get_login_context('Classic login is forbidden')

async def authorise(
self, response_location: Response, login: str, password: str,
Expand All @@ -33,6 +38,9 @@ async def authorise(
return await self.error()

async def post(self) -> StreamResponse:
only_oauth = getattr(getattr(settings, 'OAUTH', None), 'ONLY_OAUTH', False)
if only_oauth:
return await self.only_oauth_error()
response_location = HTTPFound('/zbs/switches')
form_data = await self.request.post()
validated_data = self.validate_form_data(form_data)
Expand Down
17 changes: 16 additions & 1 deletion its_on/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pathlib

from aiohttp import web
from aiohttp_oauth2 import oauth2_app
from aiohttp_security import setup as setup_security
from aiohttp_security import SessionIdentityPolicy
import aiohttp_cors
Expand All @@ -16,7 +17,7 @@
from dynaconf import settings
import uvloop

from auth.auth import DBAuthorizationPolicy
from auth.auth import DBAuthorizationPolicy, oauth_on_login, oauth_on_error
from its_on.cache import setup_cache
from its_on.db_utils import init_pg, close_pg
from its_on.middlewares import setup_middlewares
Expand All @@ -42,6 +43,20 @@ def init_app(
) -> web.Application:
app = web.Application(loop=loop)

if settings.OAUTH.IS_USED:
app.add_subapp(
'/oauth/',
oauth2_app(
client_id=settings.OAUTH.CLIENT_ID,
client_secret=settings.OAUTH.CLIENT_SECRET,
authorize_url=settings.OAUTH.AUTHORIZE_URL,
token_url=settings.OAUTH.TOKEN_URL,
on_login=oauth_on_login,
on_error=oauth_on_error,
json_data=False,
),
)

app['config'] = settings

if not redis_pool:
Expand Down
7 changes: 7 additions & 0 deletions its_on/templates/users/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<div class="container">
<h2>ITS ON</h2>
<br>
{%- if not only_oauth -%}
<form class="form-horizontal" action="/zbs/login" method="post">
<div class="form-group">
<label class="control-label col-sm-2" for="email">Login:</label>
Expand All @@ -31,6 +32,12 @@ <h2>ITS ON</h2>
</div>
</div>
</form>
{%- endif -%}
{%- if use_oauth -%}
<div class="form-group">
<a href="/oauth/auth" class="col-sm-offset-2 btn btn-default"> {{ oauth_sign_in_title }} </a>
</div>
{%- endif -%}
{%- if error is defined -%}
<div class="alert alert-danger" role="alert">{{ error }}</div>
{%- endif -%}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ aiohttp-cors==0.7.0
aiohttp-jinja2==1.5.0
aiohttp-security==0.4.0
aiohttp-session==2.8.0
aiohttp-oauth2==0.0.5
aiodns==2.0.0
aiopg==1.0.0
aioredis==1.3.1
Expand Down
8 changes: 8 additions & 0 deletions settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ default:
port: 8081
database:
dsn: postgresql://bestdoctor:bestdoctor@localhost:5432/its_on
oauth:
is_used: false
only_oauth: false
client_id: '@none'
client_secret: '@none'
authorize_url: '@none'
token_url: '@none'
sign_in_title: 'Sign in with "Bestdoctor ID"'
enable_db_logging: false
cache_url: redis://127.0.0.1:6379/1
cache_ttl: 300
Expand Down
Loading