Skip to content

Commit 82056b1

Browse files
committed
User sessions.
1 parent bd5a6bd commit 82056b1

File tree

10 files changed

+73
-47
lines changed

10 files changed

+73
-47
lines changed

auth_backend/auth_plugins/auth_method.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414

1515
from auth_backend.base import Base, ResponseModel
1616
from auth_backend.exceptions import AlreadyExists
17-
from auth_backend.models.db import AuthMethod, User, UserSession, Scope, UserSessionScope
17+
from auth_backend.models.db import AuthMethod, Scope, User, UserSession, UserSessionScope
1818
from auth_backend.schemas.types.scopes import Scope as TypeScope
1919
from auth_backend.settings import get_settings
2020
from auth_backend.utils.security import UnionAuth
2121
from auth_backend.utils.user_session_control import create_session
2222

23+
2324
logger = logging.getLogger(__name__)
2425
settings = get_settings()
2526

auth_backend/auth_plugins/email.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from auth_backend.base import Base, ResponseModel
1010
from auth_backend.exceptions import AlreadyExists, AuthFailed, IncorrectUserAuthType, SessionExpired
1111
from auth_backend.models.db import AuthMethod, User, UserSession
12+
from auth_backend.schemas.types.scopes import Scope
1213
from auth_backend.settings import get_settings
1314
from auth_backend.utils.security import UnionAuth
1415
from auth_backend.utils.smtp import (
@@ -17,8 +18,9 @@
1718
send_confirmation_email,
1819
send_reset_email,
1920
)
20-
from .auth_method import AuthMethodMeta, Session, random_string, MethodMeta
21-
from auth_backend.schemas.types.scopes import Scope
21+
22+
from .auth_method import AuthMethodMeta, MethodMeta, Session, random_string
23+
2224

2325
settings = get_settings()
2426

auth_backend/models/db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
from typing import Iterator
55

66
import sqlalchemy.orm
7-
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, func
7+
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
88
from sqlalchemy.ext.hybrid import hybrid_property
9-
from sqlalchemy.orm import Mapped, mapped_column, relationship, backref, Session
9+
from sqlalchemy.orm import Mapped, Session, backref, mapped_column, relationship
1010

1111
from auth_backend.exceptions import ObjectNotFound
1212
from auth_backend.settings import get_settings
1313

14+
1415
settings = get_settings()
1516

1617

auth_backend/routes/scopes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from fastapi import APIRouter, HTTPException, Depends
1+
from fastapi import APIRouter, Depends, HTTPException
22
from fastapi_sqlalchemy import db
33
from pydantic import parse_obj_as
44
from sqlalchemy import func
55

66
from auth_backend.base import ResponseModel
7-
from auth_backend.models.db import UserSession, Scope
8-
from auth_backend.schemas.models import ScopeGet, ScopePost, ScopePatch
7+
from auth_backend.models.db import Scope, UserSession
8+
from auth_backend.schemas.models import ScopeGet, ScopePatch, ScopePost
99
from auth_backend.utils.security import UnionAuth
1010

1111

auth_backend/routes/user_session.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import logging
2+
import string
23
from datetime import datetime
34
from typing import Literal
4-
import string
5-
from fastapi import APIRouter, Query, Depends
5+
6+
from fastapi import APIRouter, Depends, Query
67
from fastapi_sqlalchemy import db
7-
from starlette.responses import JSONResponse
88
from sqlalchemy import not_
9+
from starlette.responses import JSONResponse
10+
911
from auth_backend.base import ResponseModel
10-
from auth_backend.exceptions import SessionExpired, ObjectNotFound
11-
from auth_backend.schemas.models import Session
12-
from auth_backend.models.db import AuthMethod, UserSession, User
12+
from auth_backend.exceptions import ObjectNotFound, SessionExpired
13+
from auth_backend.models.db import AuthMethod, User, UserSession
1314
from auth_backend.schemas.models import (
15+
Session,
16+
SessionPost,
17+
SessionScopes,
1418
UserAuthMethods,
19+
UserGet,
1520
UserGroups,
1621
UserIndirectGroups,
1722
UserInfo,
18-
UserGet,
1923
UserScopes,
20-
SessionScopes,
21-
SessionPost,
2224
)
23-
from auth_backend.utils.security import UnionAuth
2425
from auth_backend.utils import user_session_control
26+
from auth_backend.utils.security import UnionAuth
2527

2628

2729
user_session = APIRouter(prefix="", tags=["User session"])
@@ -83,7 +85,9 @@ async def me(
8385
async def create_session(
8486
new_session: SessionPost, session: UserSession = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True))
8587
):
86-
return await user_session_control.create_session(session.user, new_session.scopes, db_session=db.session)
88+
return await user_session_control.create_session(
89+
session.user, new_session.scopes, new_session.expires, db_session=db.session
90+
)
8791

8892

8993
@user_session.delete("/session/{token}")
@@ -108,13 +112,14 @@ async def delete_sessions(
108112
delete_current: bool = Query(default=False),
109113
current_session: UserSession = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True)),
110114
):
111-
other_sessions = current_session.user.active_sessions
112-
for session in other_sessions:
113-
if session.token == current_session.token and not delete_current:
114-
continue
115-
if session.expired:
116-
raise SessionExpired(session.token)
117-
session.expires = datetime.utcnow()
115+
query = (
116+
db.session.query(UserSession)
117+
.filter(UserSession.user_id == current_session.user_id)
118+
.filter(not_(UserSession.expired))
119+
)
120+
if not delete_current:
121+
query = query.filter(UserSession.token != current_session.token)
122+
query.update({"expires": datetime.utcnow()})
118123
db.session.commit()
119124

120125

auth_backend/schemas/models.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

3-
from pydantic import Field
3+
from datetime import datetime
4+
5+
from pydantic import Field, constr, validator
46

57
from auth_backend.base import Base
68
from auth_backend.schemas.types.scopes import Scope
7-
from pydantic import constr
8-
from datetime import datetime
99

1010

1111
class PinchedScope(Base):
@@ -136,6 +136,12 @@ class SessionPost(Base):
136136
scopes: list[Scope] = []
137137
expires: datetime | None = None
138138

139+
@validator("expires")
140+
def expires_validator(self, exp):
141+
if exp < datetime.utcnow():
142+
raise ValueError("Session is expired")
143+
return exp
144+
139145

140146
Group.update_forward_refs()
141147
GroupGet.update_forward_refs()

auth_backend/utils/user_session_control.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import random
2+
import string
13
from datetime import datetime, timedelta
24

3-
from auth_backend.models.db import User, UserSession, Scope, UserSessionScope
4-
from auth_backend.schemas.models import Session
5-
from auth_backend.schemas.types.scopes import Scope as TypeScope
6-
from sqlalchemy.orm import Session as DbSession
7-
from fastapi_sqlalchemy import db
85
from fastapi import HTTPException
6+
from fastapi_sqlalchemy import db
7+
from sqlalchemy.orm import Session as DbSession
8+
99
from auth_backend.base import ResponseModel
10-
from auth_backend.settings import Settings
11-
import random
12-
import string
13-
from auth_backend.settings import get_settings
10+
from auth_backend.models.db import Scope, User, UserSession, UserSessionScope
11+
from auth_backend.schemas.models import Session
12+
from auth_backend.schemas.types.scopes import Scope as TypeScope
13+
from auth_backend.settings import Settings, get_settings
14+
1415

1516
settings = get_settings()
1617

migrations/env.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from logging.config import fileConfig
22

3-
from sqlalchemy import engine_from_config
4-
from sqlalchemy import pool
5-
63
from alembic import context
4+
from sqlalchemy import engine_from_config, pool
5+
76
from auth_backend.models.base import Base
87
from auth_backend.settings import get_settings
98

9+
1010
# this is the Alembic Config object, which provides
1111
# access to the values within the .ini file in use.
1212
config = context.config

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from sqlalchemy import create_engine
77
from sqlalchemy.orm import sessionmaker
88
from starlette import status
9+
910
from auth_backend.auth_plugins.auth_method import random_string
10-
from auth_backend.models.db import Group, UserGroup, User, GroupScope
11+
from auth_backend.models.db import AuthMethod, Group, GroupScope, Scope, User, UserGroup, UserSession, UserSessionScope
1112
from auth_backend.routes.base import app
1213
from auth_backend.settings import get_settings
13-
from auth_backend.models.db import AuthMethod, UserSession, Scope, UserSessionScope
1414

1515

1616
@pytest.fixture

tests/test_routes/test_user_sessions.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import logging
2+
from datetime import datetime, timedelta
23
from time import sleep
34

4-
from starlette.testclient import TestClient
55
from sqlalchemy.orm import Session
6-
from datetime import datetime
76
from starlette import status
8-
from auth_backend.models.db import UserSession, UserSessionScope, GroupScope, Group, UserGroup
9-
from auth_backend.models.db import Scope
7+
from starlette.testclient import TestClient
8+
9+
from auth_backend.models.db import Group, GroupScope, Scope, UserGroup, UserSession, UserSessionScope
10+
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -52,6 +53,11 @@ def test_create_session(client_auth: TestClient, dbsession: Session, user_scopes
5253
new_session_: UserSession = dbsession.query(UserSession).get(new_session2.json()["id"])
5354
assert new_session_.id != current_session_id_get.json()["id"]
5455
assert new_session_.scopes != current_session_scopes_get.json()["session_scopes"]
56+
time = datetime.utcnow() + timedelta(days=999999)
57+
new_session3 = client_auth.post("/session", headers=header, json={"expires": str(time)})
58+
assert new_session3.status_code == status.HTTP_200_OK
59+
new_session3_: UserSession = dbsession.query(UserSession).get(new_session3.json()['id'])
60+
assert new_session3_.expires == time
5561
dbsession.query(UserSessionScope).filter(
5662
UserSessionScope.scope_id == scope1.id, UserSessionScope.scope_id == scope2.id
5763
).delete()
@@ -60,6 +66,7 @@ def test_create_session(client_auth: TestClient, dbsession: Session, user_scopes
6066
UserSession.user_id == user_id,
6167
UserSession.id == new_session1.json()["id"],
6268
UserSession.id == new_session2.json()["id"],
69+
UserSession.id == new_session3.json()["id"],
6370
).delete()
6471
dbsession.query(UserGroup).filter(UserGroup.group_id == group_id).delete()
6572
dbsession.delete(scope1)
@@ -159,3 +166,6 @@ def test_get_sessions(client_auth: TestClient, dbsession: Session, user_scopes):
159166
assert current_session.token in list(all_sessions.json()[i]['token'] for i in range(len(all_sessions.json())))
160167
assert new_session1.token in list(all_sessions.json()[i]['token'] for i in range(len(all_sessions.json())))
161168
assert new_session2.token in list(all_sessions.json()[i]['token'] for i in range(len(all_sessions.json())))
169+
client_auth.delete(f'/session/{new_session1.token}', headers=header)
170+
all_sessions = client_auth.get("/session", headers=header)
171+
assert new_session1.token not in list(all_sessions.json()[i]['token'] for i in range(len(all_sessions.json())))

0 commit comments

Comments
 (0)