mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-08-27 03:49:57 +00:00
OAuth2 (#32)
* make auth method asyncs * oauth2 demo support * full coverage * update docs
This commit is contained in:
@ -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