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

[ IMPR ] hashed passwords #237

Merged
merged 11 commits into from
Aug 6, 2024
Prev Previous commit
Next Next commit
user and oauth normalized + required crud
Winston-Hsiao committed Aug 3, 2024
commit d6971d0808e788d0ef837dfb76432a203e1bb0ee
19 changes: 12 additions & 7 deletions store/app/crud/users.py
Original file line number Diff line number Diff line change
@@ -42,10 +42,15 @@ async def _create_user_from_email(self, email: str, password: str) -> User:
await self._add_item(user, unique_fields=["email"])
return user

async def _create_user_from_auth_key(self, auth_key: str, email: str) -> User:
user = await self._create_user_from_email(email)
key = OAuthKey.create(auth_key, user.id)
await self._add_item(key, unique_fields=["user_token"])
async def _create_user_from_oauth(self, email: str, provider: str, token: str) -> User:
user = User.create(email=email, password=None)
if provider == "github":
user.github_id = token
elif provider == "google":
user.google_id = token
await self._add_item(user, unique_fields=["email"])
oauth_key = OAuthKey.create(user_id=user.id, provider=provider, token=token)
await self._add_item(oauth_key, unique_fields=["user_token"])
return user

@overload
@@ -71,7 +76,7 @@ async def get_user_from_github_token(self, token: str, email: str) -> User:
user = await self._get_user_from_auth_key(auth_key)
if user is not None:
return user
return await self._create_user_from_auth_key(auth_key, email)
return await self._create_user_from_oauth(email, "github", auth_key)

async def delete_github_token(self, github_id: str) -> None:
await self._delete_item(await self._get_oauth_key(github_auth_key(github_id), throw_if_missing=True))
@@ -81,7 +86,7 @@ async def get_user_from_google_token(self, token: str, email: str) -> User | Non
user = await self._get_user_from_auth_key(auth_key)
if user is not None:
return user
return await self._create_user_from_auth_key(auth_key, email)
return await self._create_user_from_oauth(email, "google", auth_key)

async def delete_google_token(self, google_id: str) -> None:
await self._delete_item(await self._get_oauth_key(google_auth_key(google_id), throw_if_missing=True))
@@ -126,7 +131,7 @@ async def delete_api_key(self, token: APIKey | str) -> None:

async def test_adhoc() -> None:
async with UserCrud() as crud:
await crud._create_user_from_email(email="ben@kscale.dev")
await crud._create_user_from_email(email="ben@kscale.dev", password="examplepas$w0rd")


if __name__ == "__main__":
47 changes: 31 additions & 16 deletions store/app/model.py
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@

import time
from datetime import datetime, timedelta
from typing import Literal, Self
from typing import Optional, Self, Set, Literal

from pydantic import BaseModel
from pydantic import BaseModel, EmailStr

from store.settings import settings
from store.store.app.utils.password import hash_password
@@ -33,28 +33,38 @@ class RobolistBaseModel(BaseModel):
class User(RobolistBaseModel):
"""Defines the user model for the API.

Users are defined by their email and hashed_password. This is the
simplest form of authentication, and is used for users who sign up with
their email and password.
Users are defined by their id and email (both unique).
Hashed password is set if user signs up with email and password, and is
left empty if the user signed up with Google or Github OAuth.
"""

email: str
hashed_password: str
permissions: set[UserPermission] | None = None
email: EmailStr
hashed_password: Optional[str] = None
permissions: Optional[Set[UserPermission]] = None
created_at: int
updated_at: int
email_verified_at: int | None = None
email_verified_at: Optional[int] = None
github_id: Optional[str] = None
google_id: Optional[str] = None

@classmethod
def create(cls, email: str, password: str) -> Self:
def create(
cls,
email: str,
password: Optional[str] = None,
github_id: Optional[str] = None,
google_id: Optional[str] = None,
) -> Self:
now = int(time.time())
hashed_pw = hash_password(password) if password else None
return cls(
id=new_uuid(),
email=email,
hashed_password=hash_password(password),
permissions=None,
hashed_password=hashed_pw,
created_at=now,
updated_at=now,
github_id=github_id,
google_id=google_id,
Comment on lines +66 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it advisable to keep these fields here? what are we going to use them for? in theory we could look this up from OAuth if we need to (although it would be kind of gross), i'm just not sure what we would use these IDs for once we have already retrieved the email. apologies if you'd addressed this already in the doc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point,

only case I could see now is if someone wanted to unlink an OAuth provider. We have their user data already loaded (they're a signed in user and want to set a password and unlink the provider without deleting/resetting their account and all their listings/artifacts/data), we can then backtrace and search the OAuthKey based on their linked/associated OAuthProvider (that we have stored on their user object) otherwise we'd have to somehow persist the OAuth token past sign up and in cookies or something. Then we'd do the CRUD to delete the key and set their provider to empty string, also for consistency in the user model if a hashed_password value is not set it is assumed they have a populated github_id or google_id so having the 2 string fields exist could be useful for some validation checks and in terms of db storage space it's pretty light and not much of a concern.

Copy link
Contributor Author

@Winston-Hsiao Winston-Hsiao Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving in for now, resolved everything else and need approval for merge.

But can revisit this in the future if it really seems redundant/unnecessary we can remove them to keep the user model more lean.

)

def update_timestamp(self) -> None:
@@ -66,13 +76,18 @@ def verify_email(self) -> None:

class OAuthKey(RobolistBaseModel):
"""Keys for OAuth providers which identify users."""

user_id: str
user_token: str
provider: str
token: str

@classmethod
def create(cls, user_token: str, user_id: str) -> Self:
return cls(id=new_uuid(), user_id=user_id, user_token=user_token)
def create(cls, user_id: str, provider: str, token: str) -> Self:
return cls(
id=new_uuid(),
user_id=user_id,
provider=provider,
token=token
)


APIKeySource = Literal["user", "oauth"]