mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 23:37:18 +00:00
oauth2 demo support
This commit is contained in:
parent
c4e7f63d7c
commit
b6950ba554
@ -20,13 +20,18 @@ libalpm and AUR related configuration.
|
|||||||
|
|
||||||
## `auth` group
|
## `auth` group
|
||||||
|
|
||||||
Base authorization settings.
|
Base authorization settings. `OAuth2` provider requires `aioauth-client` library to be installed.
|
||||||
|
|
||||||
* `target` - specifies authorization provider, string, optional, default `disabled`. Allowed values are `disabled`, `configuration`.
|
* `target` - specifies authorization provider, string, optional, default `disabled`. Allowed values are `disabled`, `configuration`, `oauth`.
|
||||||
* `allow_read_only` - allow requesting read only pages without authorization, boolean, required.
|
* `allow_read_only` - allow requesting read only pages without authorization, boolean, required.
|
||||||
* `allowed_paths` - URI paths (exact match) which can be accessed without authorization, space separated list of strings, optional.
|
* `allowed_paths` - URI paths (exact match) which can be accessed without authorization, space separated list of strings, optional.
|
||||||
* `allowed_paths_groups` - URI paths prefixes which can be accessed without authorization, space separated list of strings, optional.
|
* `allowed_paths_groups` - URI paths prefixes which can be accessed without authorization, space separated list of strings, optional.
|
||||||
|
* `client_id` - OAuth2 application client ID, string, required in case if `oauth2` is used.
|
||||||
|
* `client_secret` - OAuth2 application client secret key, string, required in case if `oauth2` is used.
|
||||||
* `max_age` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
|
* `max_age` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
|
||||||
|
* `oauth_provider` - OAuth2 provider class name as is in `aioauth-client` (e.g. `GoogleClient`, `GithubClient` etc), string, required in case if `oauth2` is used.
|
||||||
|
* `oauth_redirect_uri` - full URI for OAuth2 redirect, must point to `/user-api/v1/login`, e.g. `https://example.com/user-api/v1/login`, string, required in case if `oauth2` is used.
|
||||||
|
* `oauth_scopes` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. `https://www.googleapis.com/auth/userinfo.email` for `GoogleClient` or `user:email` for `GithubClient`, space separated list of strings, required in case if `oauth2` is used.
|
||||||
* `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand).
|
* `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand).
|
||||||
|
|
||||||
## `auth:*` groups
|
## `auth:*` groups
|
||||||
@ -35,6 +40,7 @@ Authorization mapping. Group name must refer to user access level, i.e. it shoul
|
|||||||
|
|
||||||
Key is always username (case-insensitive), option value depends on authorization provider:
|
Key is always username (case-insensitive), option value depends on authorization provider:
|
||||||
|
|
||||||
|
* `OAuth` - by default requires only usernames and ignores values. But in case of direct login method call (via POST request) it will act as `Mapping` authorization method.
|
||||||
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `create-user` subcommand.
|
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `create-user` subcommand.
|
||||||
|
|
||||||
## `build:*` groups
|
## `build:*` groups
|
||||||
|
@ -13,6 +13,7 @@ optdepends=('breezy: -bzr packages support'
|
|||||||
'darcs: -darcs packages support'
|
'darcs: -darcs packages support'
|
||||||
'gnupg: package and repository sign'
|
'gnupg: package and repository sign'
|
||||||
'mercurial: -hg packages support'
|
'mercurial: -hg packages support'
|
||||||
|
'python-aioauth-client: web server with OAuth2 authorization'
|
||||||
'python-aiohttp: web server'
|
'python-aiohttp: web server'
|
||||||
'python-aiohttp-jinja2: web server'
|
'python-aiohttp-jinja2: web server'
|
||||||
'python-aiohttp-security: web server with authorization'
|
'python-aiohttp-security: web server with authorization'
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>ahriman
|
<h1>ahriman
|
||||||
{% if authorized %}
|
{% if auth.authorized %}
|
||||||
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
||||||
<img src="https://img.shields.io/badge/repository-{{ repository }}-informational" alt="{{ repository }}">
|
<img src="https://img.shields.io/badge/repository-{{ repository }}-informational" alt="{{ repository }}">
|
||||||
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
||||||
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
{% if not auth_enabled or auth_username is not none %}
|
{% if not auth.enabled or auth.username is not none %}
|
||||||
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
|
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
|
||||||
<i class="fa fa-plus"></i> Add
|
<i class="fa fa-plus"></i> Add
|
||||||
</button>
|
</button>
|
||||||
@ -70,7 +70,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if authorized %}
|
{% if auth.authorized %}
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
<tr data-package-base="{{ package.base }}">
|
<tr data-package-base="{{ package.base }}">
|
||||||
<td data-checkbox="true"></td>
|
<td data-checkbox="true"></td>
|
||||||
@ -100,19 +100,19 @@
|
|||||||
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
|
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if auth_enabled %}
|
{% if auth.enabled %}
|
||||||
{% if auth_username is none %}
|
{% if auth.username is none %}
|
||||||
<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>
|
{{ auth.control|safe }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="/user-api/v1/logout" method="post">
|
<form action="/user-api/v1/logout" method="post">
|
||||||
<button class="btn btn-link" style="text-decoration: none">logout ({{ auth_username }})</button>
|
<button class="btn btn-link" style="text-decoration: none">logout ({{ auth.username }})</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if auth_enabled %}
|
{% if auth.enabled %}
|
||||||
{% include "build-status/login-modal.jinja2" %}
|
{% include "build-status/login-modal.jinja2" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
1
setup.py
1
setup.py
@ -106,6 +106,7 @@ setup(
|
|||||||
"Jinja2",
|
"Jinja2",
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
"aiohttp_jinja2",
|
"aiohttp_jinja2",
|
||||||
|
"aioauth-client",
|
||||||
"aiohttp_session",
|
"aiohttp_session",
|
||||||
"aiohttp_security",
|
"aiohttp_security",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
|
@ -55,6 +55,17 @@ class Auth:
|
|||||||
self.enabled = provider.is_enabled
|
self.enabled = provider.is_enabled
|
||||||
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
|
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 authorization 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
|
@classmethod
|
||||||
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
|
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
|
||||||
"""
|
"""
|
||||||
@ -66,6 +77,9 @@ class Auth:
|
|||||||
if provider == AuthSettings.Configuration:
|
if provider == AuthSettings.Configuration:
|
||||||
from ahriman.core.auth.mapping import Mapping
|
from ahriman.core.auth.mapping import Mapping
|
||||||
return Mapping(configuration)
|
return Mapping(configuration)
|
||||||
|
if provider == AuthSettings.OAuth:
|
||||||
|
from ahriman.core.auth.oauth import OAuth
|
||||||
|
return OAuth(configuration)
|
||||||
return cls(configuration)
|
return cls(configuration)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
106
src/ahriman/core/auth/oauth.py
Normal file
106
src/ahriman/core/auth/oauth.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#
|
||||||
|
# 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 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")
|
||||||
|
self.redirect_uri = configuration.get("auth", "oauth_redirect_uri")
|
||||||
|
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) -> str:
|
||||||
|
"""
|
||||||
|
extract OAuth username from remote
|
||||||
|
:param code: authorization code provided by external service
|
||||||
|
:return: username as is in OAuth provider
|
||||||
|
"""
|
||||||
|
client = self.get_client()
|
||||||
|
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)
|
||||||
|
client.access_token = access_token
|
||||||
|
|
||||||
|
user, _ = await client.user_info()
|
||||||
|
username: str = user.email
|
||||||
|
return username
|
@ -63,11 +63,12 @@ class InitializeException(Exception):
|
|||||||
base service initialization exception
|
base service initialization exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, details: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
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):
|
class InvalidOption(Exception):
|
||||||
|
@ -46,12 +46,12 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
|
|||||||
if logger is not None:
|
if logger is not None:
|
||||||
for line in result.splitlines():
|
for line in result.splitlines():
|
||||||
logger.debug(line)
|
logger.debug(line)
|
||||||
|
return result
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.output is not None and logger is not None:
|
if e.output is not None and logger is not None:
|
||||||
for line in e.output.splitlines():
|
for line in e.output.splitlines():
|
||||||
logger.debug(line)
|
logger.debug(line)
|
||||||
raise exception or e
|
raise exception or e
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||||
|
@ -37,6 +37,7 @@ class Counters:
|
|||||||
:ivar failed: packages in failed status count
|
:ivar failed: packages in failed status count
|
||||||
:ivar success: packages in success status count
|
:ivar success: packages in success status count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
total: int
|
total: int
|
||||||
unknown: int = 0
|
unknown: int = 0
|
||||||
pending: int = 0
|
pending: int = 0
|
||||||
|
@ -34,6 +34,7 @@ class InternalStatus:
|
|||||||
:ivar repository: repository name
|
:ivar repository: repository name
|
||||||
:ivar version: service version
|
:ivar version: service version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
architecture: Optional[str] = None
|
architecture: Optional[str] = None
|
||||||
packages: Counters = field(default=Counters(total=0))
|
packages: Counters = field(default=Counters(total=0))
|
||||||
repository: Optional[str] = None
|
repository: Optional[str] = None
|
||||||
|
@ -35,6 +35,7 @@ class User:
|
|||||||
:ivar password: hashed user password with salt
|
:ivar password: hashed user password with salt
|
||||||
:ivar access: user role
|
:ivar access: user role
|
||||||
"""
|
"""
|
||||||
|
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
access: UserAccess
|
access: UserAccess
|
||||||
@ -42,16 +43,18 @@ class User:
|
|||||||
_HASHER = sha512_crypt
|
_HASHER = sha512_crypt
|
||||||
|
|
||||||
@classmethod
|
@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
|
build user descriptor from configuration options
|
||||||
:param username: username
|
:param username: username
|
||||||
:param password: password as string
|
:param password: password as string
|
||||||
|
:param access: optional user access
|
||||||
:return: generated user descriptor if all options are supplied and None otherwise
|
:return: generated user descriptor if all options are supplied and None otherwise
|
||||||
"""
|
"""
|
||||||
if username is None or password is None:
|
if username is None or password is None:
|
||||||
return None
|
return None
|
||||||
return cls(username, password, UserAccess.Read)
|
return cls(username, password, access)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_password(length: int) -> str:
|
def generate_password(length: int) -> str:
|
||||||
@ -70,7 +73,10 @@ class User:
|
|||||||
:param salt: salt for hashed password
|
:param salt: salt for hashed password
|
||||||
:return: True in case if password matches, False otherwise
|
:return: True in case if password matches, False otherwise
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
verified: bool = self._HASHER.verify(password + salt, self.password)
|
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
|
return verified
|
||||||
|
|
||||||
def hash_password(self, salt: str) -> str:
|
def hash_password(self, salt: str) -> str:
|
||||||
@ -79,6 +85,10 @@ class User:
|
|||||||
:param salt: salt for hashed password
|
:param salt: salt for hashed password
|
||||||
:return: hashed string to store in configuration
|
: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)
|
password_hash: str = self._HASHER.hash(self.password + salt)
|
||||||
return password_hash
|
return password_hash
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
|
|
||||||
GET /status-api/v1/status get web service status itself
|
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/login login to service
|
||||||
POST /user-api/v1/logout logout from 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("/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/login", LoginView)
|
||||||
application.router.add_post("/user-api/v1/logout", LogoutView)
|
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:
|
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||||
|
|
||||||
architecture - repository architecture, string, required
|
architecture - repository architecture, string, required
|
||||||
authorized - alias for `not auth_enabled or auth_username is not None`
|
auth - authorization descriptor, required
|
||||||
auth_enabled - whether authorization is enabled by configuration or not, boolean, required
|
* authorized - alias to check if user can see the page, boolean, required
|
||||||
auth_username - authorized user id if any, string. None means not authorized
|
* control - HTML to insert for login control, HTML string, required
|
||||||
|
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||||
|
* username - authorized username if any, string, null means not authorized
|
||||||
packages - sorted list of packages properties, required
|
packages - sorted list of packages properties, required
|
||||||
* base, string
|
* base, string
|
||||||
* depends, sorted list of strings
|
* depends, sorted list of strings
|
||||||
@ -74,24 +76,27 @@ class IndexView(BaseView):
|
|||||||
"status_color": status.status.bootstrap_color(),
|
"status_color": status.status.bootstrap_color(),
|
||||||
"timestamp": pretty_datetime(status.timestamp),
|
"timestamp": pretty_datetime(status.timestamp),
|
||||||
"version": package.version,
|
"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)
|
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
|
||||||
]
|
]
|
||||||
service = {
|
service = {
|
||||||
"status": self.service.status.status.value,
|
"status": self.service.status.status.value,
|
||||||
"status_color": self.service.status.status.badges_color(),
|
"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 block
|
||||||
auth_username = await authorized_userid(self.request)
|
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 = {
|
||||||
|
"authorized": 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 {
|
return {
|
||||||
"architecture": self.service.architecture,
|
"architecture": self.service.architecture,
|
||||||
"authorized": authorized,
|
"auth": auth,
|
||||||
"auth_enabled": self.validator.enabled,
|
|
||||||
"auth_username": auth_username,
|
|
||||||
"packages": packages,
|
"packages": packages,
|
||||||
"repository": self.service.repository.name,
|
"repository": self.service.repository.name,
|
||||||
"service": service,
|
"service": service,
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# 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.core.auth.helpers import remember
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
@ -28,6 +28,33 @@ class LoginView(BaseView):
|
|||||||
login endpoint view
|
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):
|
||||||
|
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||||
|
|
||||||
|
if code is None:
|
||||||
|
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:
|
async def post(self) -> Response:
|
||||||
"""
|
"""
|
||||||
login user to service
|
login user to service
|
||||||
|
@ -49,8 +49,9 @@ async def on_startup(application: web.Application) -> None:
|
|||||||
try:
|
try:
|
||||||
application["watcher"].load()
|
application["watcher"].load()
|
||||||
except Exception:
|
except Exception:
|
||||||
application.logger.exception("could not load packages")
|
message = "could not load packages"
|
||||||
raise InitializeException()
|
application.logger.exception(message)
|
||||||
|
raise InitializeException(message)
|
||||||
|
|
||||||
|
|
||||||
def run_server(application: web.Application) -> None:
|
def run_server(application: web.Application) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user