single-shot auth using headers

This commit is contained in:
Devaev Maxim 2019-04-27 05:16:00 +03:00
parent 3476f52da9
commit 493d160a6e
7 changed files with 46 additions and 25 deletions

View File

@ -47,33 +47,40 @@ class AuthManager:
) -> None: ) -> None:
self.__internal_service = get_auth_service_class(internal_type)(**internal_kwargs) self.__internal_service = get_auth_service_class(internal_type)(**internal_kwargs)
get_logger().info("Using internal login service %r", self.__internal_service.PLUGIN_NAME) get_logger().info("Using internal auth service %r", self.__internal_service.PLUGIN_NAME)
self.__external_service: Optional[BaseAuthService] = None self.__external_service: Optional[BaseAuthService] = None
if external_type: if external_type:
self.__external_service = get_auth_service_class(external_type)(**external_kwargs) self.__external_service = get_auth_service_class(external_type)(**external_kwargs)
get_logger().info("Using external login service %r", self.__external_service.PLUGIN_NAME) get_logger().info("Using external auth service %r", self.__external_service.PLUGIN_NAME)
self.__internal_users = internal_users self.__internal_users = internal_users
self.__tokens: Dict[str, str] = {} # {token: user} self.__tokens: Dict[str, str] = {} # {token: user}
async def login(self, user: str, passwd: str) -> Optional[str]: async def authorize(self, user: str, passwd: str) -> bool:
if user not in self.__internal_users and self.__external_service: if user not in self.__internal_users and self.__external_service:
service = self.__external_service service = self.__external_service
else: else:
service = self.__internal_service service = self.__internal_service
if (await service.login(user, passwd)): ok = (await service.authorize(user, passwd))
if ok:
get_logger().info("Authorized user %r via auth service %r", user, service.PLUGIN_NAME)
else:
get_logger().error("Got access denied for user %r from auth service %r", user, service.PLUGIN_NAME)
return ok
async def login(self, user: str, passwd: str) -> Optional[str]:
if (await self.authorize(user, passwd)):
for (token, token_user) in self.__tokens.items(): for (token, token_user) in self.__tokens.items():
if user == token_user: if user == token_user:
return token return token
token = secrets.token_hex(32) token = secrets.token_hex(32)
self.__tokens[token] = user self.__tokens[token] = user
get_logger().info("Logged in user %r via login service %r", user, service.PLUGIN_NAME) get_logger().info("Logged in user %r", user)
return token return token
else: else:
get_logger().error("Access denied for user %r from login service %r", user, service.PLUGIN_NAME)
return None return None
def logout(self, token: str) -> None: def logout(self, token: str) -> None:

View File

@ -82,11 +82,11 @@ except ImportError:
from aiohttp.helpers import AccessLogger # type: ignore # pylint: disable=ungrouped-imports from aiohttp.helpers import AccessLogger # type: ignore # pylint: disable=ungrouped-imports
_ATTR_KVMD_USER = "kvmd_user" _ATTR_KVMD_AUTH_INFO = "kvmd_auth_info"
def _format_P(request: aiohttp.web.BaseRequest, *_, **__) -> str: # type: ignore # pylint: disable=invalid-name def _format_P(request: aiohttp.web.BaseRequest, *_, **__) -> str: # type: ignore # pylint: disable=invalid-name
return (getattr(request, _ATTR_KVMD_USER, None) or "-") return (getattr(request, _ATTR_KVMD_AUTH_INFO, None) or "-")
AccessLogger._format_P = staticmethod(_format_P) # type: ignore # pylint: disable=protected-access AccessLogger._format_P = staticmethod(_format_P) # type: ignore # pylint: disable=protected-access
@ -148,6 +148,9 @@ _ATTR_EXPOSED_METHOD = "exposed_method"
_ATTR_EXPOSED_PATH = "exposed_path" _ATTR_EXPOSED_PATH = "exposed_path"
_ATTR_SYSTEM_TASK = "system_task" _ATTR_SYSTEM_TASK = "system_task"
_HEADER_AUTH_USER = "X-KVMD-User"
_HEADER_AUTH_PASSWD = "X-KVMD-Passwd"
_COOKIE_AUTH_TOKEN = "auth_token" _COOKIE_AUTH_TOKEN = "auth_token"
@ -156,12 +159,23 @@ def _exposed(http_method: str, path: str, auth_required: bool=True) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response: async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
try: try:
if auth_required: if auth_required:
user = request.headers.get(_HEADER_AUTH_USER, "")
passwd = request.headers.get(_HEADER_AUTH_PASSWD, "")
token = request.cookies.get(_COOKIE_AUTH_TOKEN, "") token = request.cookies.get(_COOKIE_AUTH_TOKEN, "")
if token:
if user:
user = valid_user(user)
setattr(request, _ATTR_KVMD_AUTH_INFO, "%s (xhdr)" % (user))
if not (await self._auth_manager.authorize(user, valid_passwd(passwd))):
raise ForbiddenError("Forbidden")
elif token:
user = self._auth_manager.check(valid_auth_token(token)) user = self._auth_manager.check(valid_auth_token(token))
if not user: if not user:
setattr(request, _ATTR_KVMD_AUTH_INFO, "- (token)")
raise ForbiddenError("Forbidden") raise ForbiddenError("Forbidden")
setattr(request, _ATTR_KVMD_USER, user) setattr(request, _ATTR_KVMD_AUTH_INFO, "%s (token)" % (user))
else: else:
raise UnauthorizedError("Unauthorized") raise UnauthorizedError("Unauthorized")

View File

@ -28,7 +28,7 @@ from .. import get_plugin_class
# ===== # =====
class BaseAuthService(BasePlugin): class BaseAuthService(BasePlugin):
async def login(self, user: str, passwd: str) -> bool: async def authorize(self, user: str, passwd: str) -> bool:
raise NotImplementedError # pragma: nocover raise NotImplementedError # pragma: nocover
async def cleanup(self) -> None: async def cleanup(self) -> None:

View File

@ -44,6 +44,6 @@ class Plugin(BaseAuthService):
"file": Option("/etc/kvmd/htpasswd", type=valid_abs_path_exists, unpack_as="path"), "file": Option("/etc/kvmd/htpasswd", type=valid_abs_path_exists, unpack_as="path"),
} }
async def login(self, user: str, passwd: str) -> bool: async def authorize(self, user: str, passwd: str) -> bool:
htpasswd = passlib.apache.HtpasswdFile(self.__path) htpasswd = passlib.apache.HtpasswdFile(self.__path)
return htpasswd.check_password(user, passwd) return htpasswd.check_password(user, passwd)

View File

@ -69,7 +69,7 @@ class Plugin(BaseAuthService):
"timeout": Option(5.0, type=valid_float_f01), "timeout": Option(5.0, type=valid_float_f01),
} }
async def login(self, user: str, passwd: str) -> bool: async def authorize(self, user: str, passwd: str) -> bool:
session = self.__ensure_session() session = self.__ensure_session()
try: try:
async with session.request( async with session.request(

View File

@ -39,16 +39,16 @@ async def test_ok__htpasswd_service(tmpdir) -> None: # type: ignore
htpasswd.save() htpasswd.save()
async with get_configured_auth_service("htpasswd", file=path) as service: async with get_configured_auth_service("htpasswd", file=path) as service:
assert not (await service.login("user", "foo")) assert not (await service.authorize("user", "foo"))
assert not (await service.login("admin", "foo")) assert not (await service.authorize("admin", "foo"))
assert not (await service.login("user", "pass")) assert not (await service.authorize("user", "pass"))
assert (await service.login("admin", "pass")) assert (await service.authorize("admin", "pass"))
htpasswd.set_password("admin", "bar") htpasswd.set_password("admin", "bar")
htpasswd.set_password("user", "bar") htpasswd.set_password("user", "bar")
htpasswd.save() htpasswd.save()
assert (await service.login("admin", "bar")) assert (await service.authorize("admin", "bar"))
assert (await service.login("user", "bar")) assert (await service.authorize("user", "bar"))
assert not (await service.login("admin", "foo")) assert not (await service.authorize("admin", "foo"))
assert not (await service.login("user", "foo")) assert not (await service.authorize("user", "foo"))

View File

@ -73,7 +73,7 @@ async def test_ok(auth_server_port: int, kwargs: Dict) -> None:
("auth_plus_basic" if kwargs.get("user") else "auth"), ("auth_plus_basic" if kwargs.get("user") else "auth"),
) )
async with get_configured_auth_service("http", url=url, **kwargs) as service: async with get_configured_auth_service("http", url=url, **kwargs) as service:
assert not (await service.login("user", "foobar")) assert not (await service.authorize("user", "foobar"))
assert not (await service.login("admin", "foobar")) assert not (await service.authorize("admin", "foobar"))
assert not (await service.login("user", "pass")) assert not (await service.authorize("user", "pass"))
assert (await service.login("admin", "pass")) assert (await service.authorize("admin", "pass"))