mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-14 22:45:47 +00:00
OAuth2 (#32)
* make auth method asyncs * oauth2 demo support * full coverage * update docs
This commit is contained in:
@ -19,10 +19,14 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Type
|
||||
import logging
|
||||
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateUser
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
from ahriman.models.user import User
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
@ -45,6 +49,8 @@ class Auth:
|
||||
:param configuration: configuration instance
|
||||
:param provider: authorization type definition
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
self.allow_read_only = configuration.getboolean("auth", "allow_read_only")
|
||||
self.allowed_paths = set(configuration.getlist("auth", "allowed_paths"))
|
||||
self.allowed_paths.update(self.ALLOWED_PATHS)
|
||||
@ -53,6 +59,17 @@ class Auth:
|
||||
self.enabled = provider.is_enabled
|
||||
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
|
||||
|
||||
@property
|
||||
def auth_control(self) -> str:
|
||||
"""
|
||||
This workaround is required to make different behaviour for login interface.
|
||||
In case of internal authentication it must provide an interface (modal form) to login with button sends POST
|
||||
request. But for an external providers behaviour can be different: e.g. OAuth provider requires sending GET
|
||||
request to external resource
|
||||
:return: login control as html code to insert
|
||||
"""
|
||||
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
|
||||
"""
|
||||
@ -62,11 +79,33 @@ class Auth:
|
||||
"""
|
||||
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
|
||||
if provider == AuthSettings.Configuration:
|
||||
from ahriman.core.auth.mapping_auth import MappingAuth
|
||||
return MappingAuth(configuration)
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
return Mapping(configuration)
|
||||
if provider == AuthSettings.OAuth:
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
return OAuth(configuration)
|
||||
return cls(configuration)
|
||||
|
||||
def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
|
||||
@staticmethod
|
||||
def get_users(configuration: Configuration) -> Dict[str, User]:
|
||||
"""
|
||||
load users from settings
|
||||
:param configuration: configuration instance
|
||||
:return: map of username to its descriptor
|
||||
"""
|
||||
users: Dict[str, User] = {}
|
||||
for role in UserAccess:
|
||||
section = configuration.section_name("auth", role.value)
|
||||
if not configuration.has_section(section):
|
||||
continue
|
||||
for user, password in configuration[section].items():
|
||||
normalized_user = user.lower()
|
||||
if normalized_user in users:
|
||||
raise DuplicateUser(normalized_user)
|
||||
users[normalized_user] = User(normalized_user, password, role)
|
||||
return users
|
||||
|
||||
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
|
||||
"""
|
||||
validate user password
|
||||
:param username: username
|
||||
@ -76,20 +115,20 @@ class Auth:
|
||||
del username, password
|
||||
return True
|
||||
|
||||
def is_safe_request(self, uri: Optional[str], required: UserAccess) -> bool:
|
||||
async def is_safe_request(self, uri: Optional[str], required: UserAccess) -> bool:
|
||||
"""
|
||||
check if requested path are allowed without authorization
|
||||
:param uri: request uri
|
||||
:param required: required access level
|
||||
:return: True in case if this URI can be requested without authorization and False otherwise
|
||||
"""
|
||||
if not uri:
|
||||
return False # request without context is not allowed
|
||||
if required == UserAccess.Read and self.allow_read_only:
|
||||
return True # in case if read right requested and allowed in options
|
||||
if not uri:
|
||||
return False # request without context is not allowed
|
||||
return uri in self.allowed_paths or any(uri.startswith(path) for path in self.allowed_paths_groups)
|
||||
|
||||
def known_username(self, username: str) -> bool: # pylint: disable=no-self-use
|
||||
async def known_username(self, username: Optional[str]) -> bool: # pylint: disable=no-self-use
|
||||
"""
|
||||
check if user is known
|
||||
:param username: username
|
||||
@ -98,7 +137,7 @@ class Auth:
|
||||
del username
|
||||
return True
|
||||
|
||||
def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool: # pylint: disable=no-self-use
|
||||
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool: # pylint: disable=no-self-use
|
||||
"""
|
||||
validate if user has access to requested resource
|
||||
:param username: username
|
||||
|
@ -17,17 +17,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateUser
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
from ahriman.models.user import User
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
class MappingAuth(Auth):
|
||||
class Mapping(Auth):
|
||||
"""
|
||||
user authorization based on mapping from configuration file
|
||||
:ivar salt: random generated string to salt passwords
|
||||
@ -44,26 +43,7 @@ class MappingAuth(Auth):
|
||||
self.salt = configuration.get("auth", "salt")
|
||||
self._users = self.get_users(configuration)
|
||||
|
||||
@staticmethod
|
||||
def get_users(configuration: Configuration) -> Dict[str, User]:
|
||||
"""
|
||||
load users from settings
|
||||
:param configuration: configuration instance
|
||||
:return: map of username to its descriptor
|
||||
"""
|
||||
users: Dict[str, User] = {}
|
||||
for role in UserAccess:
|
||||
section = configuration.section_name("auth", role.value)
|
||||
if not configuration.has_section(section):
|
||||
continue
|
||||
for user, password in configuration[section].items():
|
||||
normalized_user = user.lower()
|
||||
if normalized_user in users:
|
||||
raise DuplicateUser(normalized_user)
|
||||
users[normalized_user] = User(normalized_user, password, role)
|
||||
return users
|
||||
|
||||
def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
|
||||
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
|
||||
"""
|
||||
validate user password
|
||||
:param username: username
|
||||
@ -84,15 +64,15 @@ class MappingAuth(Auth):
|
||||
normalized_user = username.lower()
|
||||
return self._users.get(normalized_user)
|
||||
|
||||
def known_username(self, username: str) -> bool:
|
||||
async def known_username(self, username: Optional[str]) -> bool:
|
||||
"""
|
||||
check if user is known
|
||||
:param username: username
|
||||
:return: True in case if user is known and can be authorized and False otherwise
|
||||
"""
|
||||
return self.get_user(username) is not None
|
||||
return username is not None and self.get_user(username) is not None
|
||||
|
||||
def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool:
|
||||
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool:
|
||||
"""
|
||||
validate if user has access to requested resource
|
||||
:param username: username
|
||||
@ -100,6 +80,5 @@ class MappingAuth(Auth):
|
||||
:param context: URI request path
|
||||
:return: True in case if user is allowed to do this request and False otherwise
|
||||
"""
|
||||
del context
|
||||
user = self.get_user(username)
|
||||
return user is not None and user.verify_access(required)
|
113
src/ahriman/core/auth/oauth.py
Normal file
113
src/ahriman/core/auth/oauth.py
Normal file
@ -0,0 +1,113 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import aioauth_client # type: ignore
|
||||
|
||||
from typing import Optional, Type
|
||||
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
|
||||
|
||||
class OAuth(Mapping):
|
||||
"""
|
||||
OAuth user authorization.
|
||||
It is required to create application first and put application credentials.
|
||||
:ivar client_id: application client id
|
||||
:ivar client_secret: application client secret key
|
||||
:ivar provider: provider class, should be one of aiohttp-client provided classes
|
||||
:ivar redirect_uri: redirect URI registered in provider
|
||||
:ivar scopes: list of scopes required by the application
|
||||
"""
|
||||
|
||||
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.OAuth) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param configuration: configuration instance
|
||||
:param provider: authorization type definition
|
||||
"""
|
||||
Mapping.__init__(self, configuration, provider)
|
||||
self.client_id = configuration.get("auth", "client_id")
|
||||
self.client_secret = configuration.get("auth", "client_secret")
|
||||
# in order to use OAuth feature the service must be publicity available
|
||||
# thus we expect that address is set
|
||||
self.redirect_uri = f"""{configuration.get("web", "address")}/user-api/v1/login"""
|
||||
self.provider = self.get_provider(configuration.get("auth", "oauth_provider"))
|
||||
# it is list but we will have to convert to string it anyway
|
||||
self.scopes = configuration.get("auth", "oauth_scopes")
|
||||
|
||||
@property
|
||||
def auth_control(self) -> str:
|
||||
"""
|
||||
:return: login control as html code to insert
|
||||
"""
|
||||
return """<a class="nav-link" href="/user-api/v1/login" title="login via OAuth2">login</a>"""
|
||||
|
||||
@staticmethod
|
||||
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:
|
||||
"""
|
||||
load OAuth2 provider by name
|
||||
:param name: name of the provider. Must be valid class defined in aioauth-client library
|
||||
:return: loaded provider type
|
||||
"""
|
||||
provider: Type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
|
||||
try:
|
||||
is_oauth2_client = issubclass(provider, aioauth_client.OAuth2Client)
|
||||
except TypeError: # what if it is random string?
|
||||
is_oauth2_client = False
|
||||
if not is_oauth2_client:
|
||||
raise InvalidOption(name)
|
||||
return provider
|
||||
|
||||
def get_client(self) -> aioauth_client.OAuth2Client:
|
||||
"""
|
||||
load client from parameters
|
||||
:return: generated client according to current settings
|
||||
"""
|
||||
return self.provider(client_id=self.client_id, client_secret=self.client_secret)
|
||||
|
||||
def get_oauth_url(self) -> str:
|
||||
"""
|
||||
get authorization URI for the specified settings
|
||||
:return: authorization URI as a string
|
||||
"""
|
||||
client = self.get_client()
|
||||
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri)
|
||||
return uri
|
||||
|
||||
async def get_oauth_username(self, code: str) -> Optional[str]:
|
||||
"""
|
||||
extract OAuth username from remote
|
||||
:param code: authorization code provided by external service
|
||||
:return: username as is in OAuth provider
|
||||
"""
|
||||
try:
|
||||
client = self.get_client()
|
||||
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)
|
||||
client.access_token = access_token
|
||||
|
||||
print(f"HEEELOOOO {client}")
|
||||
user, _ = await client.user_info()
|
||||
username: str = user.email
|
||||
return username
|
||||
except Exception:
|
||||
self.logger.exception("got exception while performing request")
|
||||
return None
|
@ -63,11 +63,12 @@ class InitializeException(Exception):
|
||||
base service initialization exception
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, details: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param details: details of the exception
|
||||
"""
|
||||
Exception.__init__(self, "Could not load service")
|
||||
Exception.__init__(self, f"Could not load service: {details}")
|
||||
|
||||
|
||||
class InvalidOption(Exception):
|
||||
|
@ -46,12 +46,12 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
|
||||
if logger is not None:
|
||||
for line in result.splitlines():
|
||||
logger.debug(line)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.output is not None and logger is not None:
|
||||
for line in e.output.splitlines():
|
||||
logger.debug(line)
|
||||
raise exception or e
|
||||
return result
|
||||
|
||||
|
||||
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||
|
@ -30,10 +30,12 @@ class AuthSettings(Enum):
|
||||
web authorization type
|
||||
:cvar Disabled: authorization is disabled
|
||||
:cvar Configuration: configuration based authorization
|
||||
:cvar OAuth: OAuth based provider
|
||||
"""
|
||||
|
||||
Disabled = auto()
|
||||
Configuration = auto()
|
||||
OAuth = auto()
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[AuthSettings], value: str) -> AuthSettings:
|
||||
@ -46,6 +48,8 @@ class AuthSettings(Enum):
|
||||
return cls.Disabled
|
||||
if value.lower() in ("configuration", "mapping"):
|
||||
return cls.Configuration
|
||||
if value.lower() in ('oauth', 'oauth2'):
|
||||
return cls.OAuth
|
||||
raise InvalidOption(value)
|
||||
|
||||
@property
|
||||
|
@ -37,6 +37,7 @@ class Counters:
|
||||
:ivar failed: packages in failed status count
|
||||
:ivar success: packages in success status count
|
||||
"""
|
||||
|
||||
total: int
|
||||
unknown: int = 0
|
||||
pending: int = 0
|
||||
|
@ -34,6 +34,7 @@ class InternalStatus:
|
||||
:ivar repository: repository name
|
||||
:ivar version: service version
|
||||
"""
|
||||
|
||||
architecture: Optional[str] = None
|
||||
packages: Counters = field(default=Counters(total=0))
|
||||
repository: Optional[str] = None
|
||||
|
@ -35,6 +35,7 @@ class User:
|
||||
:ivar password: hashed user password with salt
|
||||
:ivar access: user role
|
||||
"""
|
||||
|
||||
username: str
|
||||
password: str
|
||||
access: UserAccess
|
||||
@ -42,16 +43,18 @@ class User:
|
||||
_HASHER = sha512_crypt
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[User], username: Optional[str], password: Optional[str]) -> Optional[User]:
|
||||
def from_option(cls: Type[User], username: Optional[str], password: Optional[str],
|
||||
access: UserAccess = UserAccess.Read) -> Optional[User]:
|
||||
"""
|
||||
build user descriptor from configuration options
|
||||
:param username: username
|
||||
:param password: password as string
|
||||
:param access: optional user access
|
||||
:return: generated user descriptor if all options are supplied and None otherwise
|
||||
"""
|
||||
if username is None or password is None:
|
||||
return None
|
||||
return cls(username, password, UserAccess.Read)
|
||||
return cls(username, password, access)
|
||||
|
||||
@staticmethod
|
||||
def generate_password(length: int) -> str:
|
||||
@ -70,7 +73,10 @@ class User:
|
||||
:param salt: salt for hashed password
|
||||
:return: True in case if password matches, False otherwise
|
||||
"""
|
||||
verified: bool = self._HASHER.verify(password + salt, self.password)
|
||||
try:
|
||||
verified: bool = self._HASHER.verify(password + salt, self.password)
|
||||
except ValueError:
|
||||
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy
|
||||
return verified
|
||||
|
||||
def hash_password(self, salt: str) -> str:
|
||||
@ -79,6 +85,10 @@ class User:
|
||||
:param salt: salt for hashed password
|
||||
:return: hashed string to store in configuration
|
||||
"""
|
||||
if not self.password:
|
||||
# in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider
|
||||
# when we do not store any password here
|
||||
return ""
|
||||
password_hash: str = self._HASHER.hash(self.password + salt)
|
||||
return password_hash
|
||||
|
||||
|
@ -48,11 +48,11 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
|
||||
|
||||
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||
"""
|
||||
retrieve authorized username
|
||||
retrieve authenticated username
|
||||
:param identity: username
|
||||
:return: user identity (username) in case if user exists and None otherwise
|
||||
"""
|
||||
return identity if self.validator.known_username(identity) else None
|
||||
return identity if await self.validator.known_username(identity) else None
|
||||
|
||||
async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool:
|
||||
"""
|
||||
@ -62,7 +62,7 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
|
||||
:param context: URI request path
|
||||
:return: True in case if user is allowed to perform this request and False otherwise
|
||||
"""
|
||||
return self.validator.verify_access(identity, permission, context)
|
||||
return await self.validator.verify_access(identity, permission, context)
|
||||
|
||||
|
||||
def auth_handler(validator: Auth) -> MiddlewareType:
|
||||
@ -78,7 +78,7 @@ def auth_handler(validator: Auth) -> MiddlewareType:
|
||||
else:
|
||||
permission = UserAccess.Write
|
||||
|
||||
if not validator.is_safe_request(request.path, permission):
|
||||
if not await validator.is_safe_request(request.path, permission):
|
||||
await aiohttp_security.check_permission(request, permission, request.path)
|
||||
|
||||
return await handler(request)
|
||||
|
@ -61,6 +61,7 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
||||
|
||||
GET /status-api/v1/status get web service status itself
|
||||
|
||||
GET /user-api/v1/login OAuth2 handler for login
|
||||
POST /user-api/v1/login login to service
|
||||
POST /user-api/v1/logout logout from service
|
||||
|
||||
@ -92,5 +93,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
||||
|
||||
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
|
||||
|
||||
application.router.add_get("/user-api/v1/login", LoginView)
|
||||
application.router.add_post("/user-api/v1/login", LoginView)
|
||||
application.router.add_post("/user-api/v1/logout", LogoutView)
|
||||
|
@ -34,9 +34,11 @@ class IndexView(BaseView):
|
||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||
|
||||
architecture - repository architecture, string, required
|
||||
authorized - alias for `not auth_enabled or auth_username is not None`
|
||||
auth_enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||
auth_username - authorized user id if any, string. None means not authorized
|
||||
auth - authorization descriptor, required
|
||||
* authenticated - alias to check if user can see the page, boolean, required
|
||||
* control - HTML to insert for login control, HTML string, required
|
||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||
* username - authenticated username if any, string, null means not authenticated
|
||||
packages - sorted list of packages properties, required
|
||||
* base, string
|
||||
* depends, sorted list of strings
|
||||
@ -74,24 +76,27 @@ class IndexView(BaseView):
|
||||
"status_color": status.status.bootstrap_color(),
|
||||
"timestamp": pretty_datetime(status.timestamp),
|
||||
"version": package.version,
|
||||
"web_url": package.web_url
|
||||
"web_url": package.web_url,
|
||||
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
|
||||
]
|
||||
service = {
|
||||
"status": self.service.status.status.value,
|
||||
"status_color": self.service.status.status.badges_color(),
|
||||
"timestamp": pretty_datetime(self.service.status.timestamp)
|
||||
"timestamp": pretty_datetime(self.service.status.timestamp),
|
||||
}
|
||||
|
||||
# auth block
|
||||
auth_username = await authorized_userid(self.request)
|
||||
authorized = not self.validator.enabled or self.validator.allow_read_only or auth_username is not None
|
||||
auth = {
|
||||
"authenticated": not self.validator.enabled or self.validator.allow_read_only or auth_username is not None,
|
||||
"control": self.validator.auth_control,
|
||||
"enabled": self.validator.enabled,
|
||||
"username": auth_username,
|
||||
}
|
||||
|
||||
return {
|
||||
"architecture": self.service.architecture,
|
||||
"authorized": authorized,
|
||||
"auth_enabled": self.validator.enabled,
|
||||
"auth_username": auth_username,
|
||||
"auth": auth,
|
||||
"packages": packages,
|
||||
"repository": self.service.repository.name,
|
||||
"service": service,
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPFound, HTTPUnauthorized, Response
|
||||
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized, Response
|
||||
|
||||
from ahriman.core.auth.helpers import remember
|
||||
from ahriman.web.views.base import BaseView
|
||||
@ -28,6 +28,33 @@ class LoginView(BaseView):
|
||||
login endpoint view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
OAuth2 response handler
|
||||
|
||||
In case if code provided it will do a request to get user email. In case if no code provided it will redirect
|
||||
to authorization url provided by OAuth client
|
||||
|
||||
:return: redirect to main page
|
||||
"""
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
|
||||
code = self.request.query.getone("code", default=None)
|
||||
oauth_provider = self.validator
|
||||
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
|
||||
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||
|
||||
if not code:
|
||||
return HTTPFound(oauth_provider.get_oauth_url())
|
||||
|
||||
response = HTTPFound("/")
|
||||
username = await oauth_provider.get_oauth_username(code)
|
||||
if await self.validator.known_username(username):
|
||||
await remember(self.request, response, username)
|
||||
return response
|
||||
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
login user to service
|
||||
@ -44,7 +71,7 @@ class LoginView(BaseView):
|
||||
username = data.get("username")
|
||||
|
||||
response = HTTPFound("/")
|
||||
if self.validator.check_credentials(username, data.get("password")):
|
||||
if await self.validator.check_credentials(username, data.get("password")):
|
||||
await remember(self.request, response, username)
|
||||
return response
|
||||
|
||||
|
@ -49,8 +49,9 @@ async def on_startup(application: web.Application) -> None:
|
||||
try:
|
||||
application["watcher"].load()
|
||||
except Exception:
|
||||
application.logger.exception("could not load packages")
|
||||
raise InitializeException()
|
||||
message = "could not load packages"
|
||||
application.logger.exception(message)
|
||||
raise InitializeException(message)
|
||||
|
||||
|
||||
def run_server(application: web.Application) -> None:
|
||||
|
Reference in New Issue
Block a user