Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auth client #5

Merged
merged 1 commit into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 289 additions & 0 deletions client/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
"""Auth Client"""

import uuid
from typing import Optional
import grpc
from passlib.hash import pbkdf2_sha256
from client.protocol import ProtocolClient as CurpClient
from api.xline.xline_command_pb2 import Command, RequestWithToken, CommandResponse
from api.xline.rpc_pb2_grpc import AuthStub
from api.xline.auth_pb2 import UserAddOptions, Permission
from api.xline.rpc_pb2 import (
AuthEnableRequest,
AuthEnableResponse,
AuthDisableRequest,
AuthDisableResponse,
AuthStatusRequest,
AuthStatusResponse,
AuthenticateRequest,
AuthenticateResponse,
AuthUserAddRequest,
AuthUserAddResponse,
AuthUserGetRequest,
AuthUserGetResponse,
AuthUserListRequest,
AuthUserListResponse,
AuthUserDeleteRequest,
AuthUserDeleteResponse,
AuthUserChangePasswordRequest,
AuthUserChangePasswordResponse,
AuthUserGrantRoleRequest,
AuthUserGrantRoleResponse,
AuthUserRevokeRoleRequest,
AuthUserRevokeRoleResponse,
AuthRoleAddRequest,
AuthRoleAddResponse,
AuthRoleGetRequest,
AuthRoleGetResponse,
AuthRoleListRequest,
AuthRoleListResponse,
AuthRoleDeleteRequest,
AuthRoleDeleteResponse,
AuthRoleGrantPermissionRequest,
AuthRoleGrantPermissionResponse,
AuthRoleRevokePermissionRequest,
AuthRoleRevokePermissionResponse,
)

PermissionType = Permission.Type

PERM_READ = Permission.Type.READ
PERM_WRITE = Permission.Type.WRITE
PERM_READWRITE = Permission.Type.READWRITE


class AuthClient:
"""
Client for Auth operations.

Attributes:
name: Name of the AuthClient, which will be used in CURP propose id generation.
curp_client: The client running the CURP protocol, communicate with all servers.
token: The auth token.
"""

name: str
curp_client: CurpClient
auth_client: AuthStub
token: Optional[str]

def __init__(self, name: str, curp_client: CurpClient, channel: grpc.Channel, token: Optional[str]) -> None:
self.name = name
self.curp_client = curp_client
self.auth_client = AuthStub(channel=channel)
self.token = token

async def auth_enable(self) -> AuthEnableResponse:
"""
Enables authentication.
"""
request_with_token = RequestWithToken(token=self.token, auth_enable_request=AuthEnableRequest())
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_enable_response

Check warning on line 82 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L80-L82

Added lines #L80 - L82 were not covered by tests

async def auth_disable(self) -> AuthDisableResponse:
"""
Disables authentication.
"""
request_with_token = RequestWithToken(token=self.token, auth_disable_request=AuthDisableRequest())
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_disable_response

Check warning on line 90 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L88-L90

Added lines #L88 - L90 were not covered by tests

async def auth_status(self) -> AuthStatusResponse:
"""
Gets authentication status.
"""
request_with_token = RequestWithToken(token=self.token, auth_status_request=AuthStatusRequest())
er = await self.handle_req(req=request_with_token, use_fast_path=True)
return er.auth_status_response

Check warning on line 98 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L96-L98

Added lines #L96 - L98 were not covered by tests

async def authenticate(self, name: str, password: str) -> AuthenticateResponse:
"""
Process an authentication request, and return the auth token.
"""
res: AuthenticateResponse = await self.auth_client.Authenticate(

Check warning on line 104 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L104

Added line #L104 was not covered by tests
AuthenticateRequest(name=name, password=password)
)
return res

Check warning on line 107 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L107

Added line #L107 was not covered by tests

async def user_add(self, name: str, password: str = "", no_password: bool = True) -> AuthUserAddResponse:
"""
Add an user.
"""
if name == "":
msg = "user name is empty"
raise Exception(msg)

Check warning on line 115 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L114-L115

Added lines #L114 - L115 were not covered by tests
need_password = not no_password
if need_password and password == "":
msg = "Password is required but not provided"
raise Exception(msg)

Check warning on line 119 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L118-L119

Added lines #L118 - L119 were not covered by tests

hashed_password = ""
if need_password:
hashed_password = self.hash_password(password)
password = ""

Check warning on line 124 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L123-L124

Added lines #L123 - L124 were not covered by tests

req = AuthUserAddRequest(
name=name,
password=password,
hashedPassword=hashed_password,
options=UserAddOptions(no_password=no_password),
)
request_with_token = RequestWithToken(token=self.token, auth_user_add_request=req)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_user_add_response

async def user_get(self, name: str) -> AuthUserGetResponse:
"""
Gets the user info by the user name.
"""
request_with_token = RequestWithToken(token=self.token, auth_user_get_request=AuthUserGetRequest(name=name))
er = await self.handle_req(req=request_with_token, use_fast_path=True)
return er.auth_user_get_response

async def user_list(self) -> AuthUserListResponse:
"""
Lists all users.
"""
request_with_token = RequestWithToken(token=self.token, auth_user_list_request=AuthUserListRequest())
er = await self.handle_req(req=request_with_token, use_fast_path=True)
return er.auth_user_list_response

async def user_delete(self, name: str) -> AuthUserDeleteResponse:
"""
Deletes the user by the user name.
"""
request_with_token = RequestWithToken(
token=self.token, auth_user_delete_request=AuthUserDeleteRequest(name=name)
)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_user_delete_response

async def user_change_password(self, name: str, password: str) -> AuthUserChangePasswordResponse:
"""
Change password for an user.
"""
if password == "":
msg = "password is empty"
raise Exception(msg)

Check warning on line 168 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L167-L168

Added lines #L167 - L168 were not covered by tests

hashed_password = self.hash_password(password)

request_with_token = RequestWithToken(
token=self.token,
auth_user_change_password_request=AuthUserChangePasswordRequest(name=name, hashedPassword=hashed_password),
)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_user_change_password_response

async def user_grant_role(self, user: str, role: str) -> AuthUserGrantRoleResponse:
"""
Grant role for a user.
"""
request_with_token = RequestWithToken(
token=self.token, auth_user_grant_role_request=AuthUserGrantRoleRequest(user=user, role=role)
)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_user_grant_role_response

async def user_revoke_role(self, name: str, role: str) -> AuthUserRevokeRoleResponse:
"""
Revoke role for a user.
"""
request_with_token = RequestWithToken(
token=self.token, auth_user_revoke_role_request=AuthUserRevokeRoleRequest(name=name, role=role)
)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_user_revoke_role_response

async def role_add(self, name: str) -> AuthRoleAddResponse:
"""
Adds role.
"""
if name == "":
msg = "role name is empty"
raise Exception(msg)

Check warning on line 205 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L204-L205

Added lines #L204 - L205 were not covered by tests

request_with_token = RequestWithToken(token=self.token, auth_role_add_request=AuthRoleAddRequest(name=name))
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_role_add_response

async def role_get(self, role: str) -> AuthRoleGetResponse:
"""
Gets role.
"""
request_with_token = RequestWithToken(token=self.token, auth_role_get_request=AuthRoleGetRequest(role=role))
er = await self.handle_req(req=request_with_token, use_fast_path=True)
return er.auth_role_get_response

async def role_list(self) -> AuthRoleListResponse:
"""
Lists role.
"""
request_with_token = RequestWithToken(token=self.token, auth_role_list_request=AuthRoleListRequest())
er = await self.handle_req(req=request_with_token, use_fast_path=True)
return er.auth_role_list_response

async def role_delete(self, role: str) -> AuthRoleDeleteResponse:
"""
Deletes role.
"""
request_with_token = RequestWithToken(
token=self.token, auth_role_delete_request=AuthRoleDeleteRequest(role=role)
)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_role_delete_response

async def role_grant_permission(
self, name: str, perm_type: PermissionType, key: bytes, range_end: bytes | None = None
) -> AuthRoleGrantPermissionResponse:
"""
Grants role permission.
"""
req = AuthRoleGrantPermissionRequest(
name=name, perm=Permission(permType=perm_type, key=key, range_end=range_end)
)
request_with_token = RequestWithToken(token=self.token, auth_role_grant_permission_request=req)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_role_grant_permission_response

async def role_revoke_permission(
self, role: str, key: bytes, range_end: bytes | None = None
) -> AuthRoleRevokePermissionResponse:
"""
Revoke role permission.
"""
req = AuthRoleRevokePermissionRequest(role=role, key=key, range_end=range_end)
request_with_token = RequestWithToken(token=self.token, auth_role_revoke_permission_request=req)
er = await self.handle_req(req=request_with_token, use_fast_path=False)
return er.auth_role_revoke_permission_response

async def handle_req(self, req: RequestWithToken, use_fast_path: bool) -> CommandResponse:
"""
Send request using fast path or slow path.
"""
propose_id = self.generate_propose_id()
cmd = Command(request=req, propose_id=propose_id)

if use_fast_path:
er, _ = await self.curp_client.propose(cmd, True)
return er
else:
er, asr = await self.curp_client.propose(cmd, False)
if asr is None:
msg = "sync_res is always Some when use_fast_path is false"
raise Exception(msg)

Check warning on line 275 in client/auth.py

View check run for this annotation

Codecov / codecov/patch

client/auth.py#L274-L275

Added lines #L274 - L275 were not covered by tests
return er

def generate_propose_id(self) -> str:
"""Generate propose id with the given prefix."""
propose_id = f"{self.name}-{uuid.uuid4()}"
return propose_id

@staticmethod
def hash_password(password: str) -> str:
"""Generate hash of the password."""
ROUNDS = 200000
SALTSIZE = 16
hashed_password = pbkdf2_sha256.using(rounds=ROUNDS, salt_size=SALTSIZE).hash(password)
return hashed_password
15 changes: 12 additions & 3 deletions client/client.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
"""Xline Client"""

from __future__ import annotations
import grpc
from client.protocol import ProtocolClient
from client.kv import KvClient
from client.auth import AuthClient


class Client:
"""
Xline client

Attributes:
kv: Kv client
kv_client: Kv client
auth_client: Auth client
"""

kv_client: KvClient
auth_client: AuthClient

def __init__(self, kv: KvClient) -> None:
def __init__(self, kv: KvClient, auth: AuthClient) -> None:
self.kv_client = kv
self.auth_client = auth

@classmethod
async def connect(cls, addrs: list[str]) -> Client:
"""
New `Client`
"""
protocol_client = await ProtocolClient.build_from_addrs(addrs)
# TODO: Load balancing
channel = grpc.aio.insecure_channel(addrs[0])
LingKa28 marked this conversation as resolved.
Show resolved Hide resolved
# TODO: Acquire the auth token

kv_client = KvClient("client", protocol_client, "")
auth_client = AuthClient("client", protocol_client, channel, "")
LingKa28 marked this conversation as resolved.
Show resolved Hide resolved

return cls(kv_client)
return cls(kv_client, auth_client)
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"grpcio",
"grpcio-tools",
"pytest-asyncio",
"passlib",
]

[project.urls]
Expand Down Expand Up @@ -130,6 +131,8 @@ ignore = [
"I001",
# Allow verable shadow
"A003",
# Allow const use upercase
"N806",
]
unfixable = [
# Don't touch unused imports
Expand Down
Loading
Loading