* make auth method asyncs

* oauth2 demo support

* full coverage

* update docs
This commit is contained in:
2021-09-12 21:41:38 +03:00
committed by GitHub
parent 1b29b5773d
commit d19deb57e7
39 changed files with 695 additions and 251 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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: