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

Authorization #109

Closed
wants to merge 4 commits into from
Closed
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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ Note: at this stage of development, it is preferable to install from sources (se
Clone this repository and install the needed plugins:

```bash
pip install -e . --no-deps
pip install -e plugins/login
pip install -e plugins/auth
pip install -e plugins/contents
pip install -e plugins/kernels
pip install -e plugins/terminals
pip install -e plugins/lab
pip install -e plugins/jupyterlab
pip install -e plugins/nbconvert
pip install -e . --no-deps && \
pip install -e plugins/login && \
pip install -e plugins/auth && \
pip install -e plugins/contents && \
pip install -e plugins/kernels && \
pip install -e plugins/terminals && \
pip install -e plugins/lab && \
pip install -e plugins/jupyterlab && \
pip install -e plugins/nbconvert && \
pip install -e plugins/yjs

# you should also install the latest FPS:
Expand Down
74 changes: 38 additions & 36 deletions plugins/auth/fps_auth/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
import httpx
from httpx_oauth.clients.github import GitHubOAuth2 # type: ignore
from fastapi import Depends, Response, HTTPException, status
from sqlalchemy.orm import sessionmaker, Session # type: ignore

from fastapi_users.authentication import CookieAuthentication, BaseAuthentication # type: ignore
from fastapi_users.db import SQLAlchemyUserDatabase # type: ignore
from fastapi_users import FastAPIUsers, BaseUserManager # type: ignore
from starlette.requests import Request

from fps.logging import get_configured_logger # type: ignore

from .config import get_auth_config
from .db import secret, get_user_db
from .models import User, UserDB, UserCreate, UserUpdate
from .db import engine, secret, get_user_db, UserTable
from .models import User, UserDB, UserCreate, UserUpdate, Role

logger = get_configured_logger("auth")

session: Session = sessionmaker(bind=engine)()

class NoAuthAuthentication(BaseAuthentication):
def __init__(self, name: str = "noauth"):
Expand All @@ -41,7 +44,7 @@ async def get_login_response(self, user, response, user_manager):

noauth_authentication = NoAuthAuthentication(name="noauth")
cookie_authentication = CookieAuthentication(
secret=secret, cookie_secure=get_auth_config().cookie_secure, name="cookie" # type: ignore
secret=secret, cookie_secure=get_auth_config().cookie_secure, name="token" # type: ignore
)
github_cookie_authentication = GitHubAuthentication(secret=secret, name="github")
github_authentication = GitHubOAuth2(
Expand All @@ -52,6 +55,11 @@ async def get_login_response(self, user, response, user_manager):
class UserManager(BaseUserManager[UserCreate, UserDB]):
user_db_model = UserDB

async def on_after_request_verify(self, user: UserDB, token: str, request: Optional[Request] = None):
super().on_after_request_verify(user, token, request)
user.connected = True
await self.user_db.update(user)

async def on_after_register(self, user: UserDB, request: Optional[Request] = None):
for oauth_account in user.oauth_accounts:
if oauth_account.oauth_name == "github":
Expand All @@ -61,14 +69,15 @@ async def on_after_register(self, user: UserDB, request: Optional[Request] = Non
f"https://api.github.com/user/{oauth_account.account_id}"
)
).json()

user.anonymous = False

user.username = r["login"]
user.anonymous = False
user.name = r["name"]
user.color = None
user.avatar = r["avatar_url"]
user.workspace = "{}"
user.settings = "{}"
user.avatar_url = r["avatar_url"]
user.is_verified = True

if oauth_account.oauth_name == "token":
user.hashed_password = uuid4()

await self.user_db.update(user)

Expand All @@ -78,33 +87,41 @@ def get_user_manager(user_db=Depends(get_user_db)):


async def get_enabled_backends(auth_config=Depends(get_auth_config)):
if auth_config.mode == "noauth" and not auth_config.collaborative:
if auth_config.mode == "noauth":
return [noauth_authentication, github_cookie_authentication]
else:
return [cookie_authentication, github_cookie_authentication]
return [github_cookie_authentication, cookie_authentication]


fapi_users = FastAPIUsers(
get_user_manager,
[noauth_authentication, cookie_authentication, github_cookie_authentication],
[github_cookie_authentication, noauth_authentication, cookie_authentication],
User,
UserCreate,
UserUpdate,
UserDB,
)


async def create_guest(user_db, auth_config=Depends(get_auth_config)):
async def create_guest(user_db, auth_config):
global_user = await user_db.get_by_email(auth_config.global_email)
user_id = str(uuid4())
guest = UserDB(
id=user_id,
anonymous=True,
email=f"{user_id}@jupyter.com",
username=f"{user_id}@jupyter.com",
hashed_password="",
email=f"{user_id}@jupyter.com",
role=Role.READ,
anonymous=True,
connected=False,
name=None,
color=None,
avatar_url=None,
workspace=global_user.workspace,
settings=global_user.settings,
hashed_password="",
is_superuser=False,
is_active=True,
is_verified=False,
)
await user_db.create(guest)
return guest
Expand All @@ -118,35 +135,20 @@ async def current_user(
optional=True, get_enabled_backends=get_enabled_backends
)
),
user_db=Depends(get_user_db),
user_db: SQLAlchemyUserDatabase = Depends(get_user_db),
user_manager: UserManager = Depends(get_user_manager),
auth_config=Depends(get_auth_config),
):
active_user = user

if auth_config.collaborative:
if not active_user and auth_config.mode == "noauth":
active_user = await create_guest(user_db)
if not active_user and auth_config.mode == "token":
users = session.query(UserTable).filter(UserTable.hashed_password == token).all()
if len(users) > 0 and users[0].hashed_password == token:
active_user = users[0]
await cookie_authentication.get_login_response(
active_user, response, user_manager
)

elif not active_user and auth_config.mode == "token":
global_user = await user_db.get_by_email(auth_config.global_email)
if global_user and global_user.hashed_password == token:
active_user = await create_guest(user_db)
await cookie_authentication.get_login_response(
active_user, response, user_manager
)
else:
if auth_config.mode == "token":
global_user = await user_db.get_by_email(auth_config.global_email)
if global_user and global_user.hashed_password == token:
active_user = global_user
await cookie_authentication.get_login_response(
active_user, response, user_manager
)

if active_user:
return active_user

Expand Down
2 changes: 1 addition & 1 deletion plugins/auth/fps_auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class AuthConfig(PluginModel):
mode: Literal["noauth", "token", "user"] = "token"
token: str = str(uuid4())
collaborative: bool = False
global_email: str = "guest@jupyter.com"
global_email: str = "jovyan@jupyter.com"
cookie_secure: bool = (
False # FIXME: should default to True, and set to False for tests
)
Expand Down
17 changes: 10 additions & 7 deletions plugins/auth/fps_auth/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase # type: ignore
from fastapi_users.db import SQLAlchemyBaseOAuthAccountTable # type: ignore
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base # type: ignore
from sqlalchemy import Boolean, String, Text, Column # type: ignore
from sqlalchemy import Boolean, String, Text, Enum, Column # type: ignore
import sqlalchemy # type: ignore
import databases # type: ignore
from fps.config import get_config # type: ignore

from .config import AuthConfig
from .models import (
UserDB,
Role
)

auth_config = get_config(AuthConfig)
Expand Down Expand Up @@ -43,14 +44,16 @@


class UserTable(Base, SQLAlchemyBaseUserTable):
anonymous = Column(Boolean, default=True, nullable=False)
username = Column(String(length=32), nullable=False, unique=True)
email = Column(String(length=32), nullable=False, unique=True)
username = Column(String(length=32), nullable=True, unique=True)
role = Column(Enum(Role), nullable=False)
anonymous = Column(Boolean, nullable=False)
connected = Column(Boolean, nullable=False)
name = Column(String(length=32), nullable=True)
color = Column(String(length=32), nullable=True)
avatar = Column(String(length=32), nullable=True)
workspace = Column(Text(), nullable=False)
settings = Column(Text(), nullable=False)
avatar_url = Column(String(length=32), nullable=True)
workspace = Column(Text(), nullable=True)
settings = Column(Text(), nullable=True)


class OAuthAccount(SQLAlchemyBaseOAuthAccountTable, Base):
Expand All @@ -69,5 +72,5 @@ class OAuthAccount(SQLAlchemyBaseOAuthAccountTable, Base):
user_db = SQLAlchemyUserDatabase(UserDB, database, users, oauth_accounts)


def get_user_db():
def get_user_db() -> SQLAlchemyUserDatabase:
yield user_db
37 changes: 24 additions & 13 deletions plugins/auth/fps_auth/models.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
from enum import Enum
from uuid import uuid4
from typing import Optional

from pydantic import BaseModel
from fastapi_users import models # type: ignore


class Role(Enum):
ADMIN = 1
READ = 2
WRITE = 3
RUN = 4

class JupyterUser(BaseModel):
username: str = f"{uuid4()}@jupyter.com"
email: str = f"{uuid4()}@jupyter.com"
role: Role = Role.READ
anonymous: bool = True
username: str = ""
connected: bool = False
name: Optional[str] = None
color: Optional[str] = None
avatar: Optional[str] = None
workspace: str = "{}"
settings: str = "{}"

avatar_url: Optional[str] = None
workspace: Optional[str] = "{}"
settings: Optional[str] = "{}"

class User(models.BaseUser, models.BaseOAuthAccountMixin, JupyterUser):
pass


class UserCreate(models.BaseUserCreate):
anonymous: bool = True
username: Optional[str] = None
name: Optional[str] = None
color: Optional[str] = None


class UserUpdate(models.BaseUserUpdate, JupyterUser):
class UserCreate(models.BaseUserCreate, JupyterUser):
pass


class UserUpdate(models.BaseUserUpdate):
role: Optional[Role] = Role.READ
name: Optional[str] = None
color: Optional[str] = None
avatar_url: Optional[str] = None
workspace: Optional[str] = "{}"
settings: Optional[str] = "{}"

class UserDB(User, models.BaseUserDB):
pass
32 changes: 25 additions & 7 deletions plugins/auth/fps_auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
cookie_authentication,
github_authentication,
)
from .models import User, UserDB
from .models import User, UserDB, Role

logger = get_configured_logger("auth")

Expand All @@ -42,13 +42,18 @@ async def startup():
else:
global_user = UserDB(
id=uuid4(),
anonymous=True,
username="jovyan",
email=auth_config.global_email,
username=auth_config.global_email,
role=Role.ADMIN,
anonymous=False,
connected=False,
name="jovyan",
color=None,
avatar_url=None,
hashed_password=auth_config.token,
is_superuser=True,
is_active=True,
is_verified=True,
is_verified=True
)
await user_db.create(global_user)

Expand All @@ -66,10 +71,23 @@ async def shutdown():
await database.disconnect()


@router.get("/auth/users")
@router.get("/auth/collaborators")
async def get_users(user: User = Depends(current_user)):
users = await session.query(UserTable).all()
return [user for user in users if user.is_active]
# TODO: create a db request that returns non critical info
users = session.query(UserTable).filter(UserTable.id != user.id).all()
resp = []
for user in users:
resp.append({
"id": user.id,
"username": user.username,
"anonymous": user.anonymous,
"name": user.name,
"color": user.color,
"role": user.role,
"email": user.email,
"avatar_url": user.avatar_url
})
return resp


# Cookie based auth login and logout
Expand Down
Loading