158 lines
7.5 KiB
Markdown
158 lines
7.5 KiB
Markdown
# Auth & Authorization Service
|
||
|
||
Backend application built with **FastAPI + SQLAlchemy**, implementing a custom
|
||
authentication and authorization system without using the framework's built-in
|
||
auth mechanisms.
|
||
|
||
## Stack
|
||
|
||
- **FastAPI** — web framework, routing, dependency injection
|
||
- **SQLAlchemy (ORM)** — database access, `sessionmaker`, declarative models
|
||
- **python-jose (jwt)** — creating and validating JWT tokens
|
||
- **bcrypt / passlib** — password hashing
|
||
- **Pydantic v2** — request/response validation schemas
|
||
- **Alembic** — database migrations
|
||
|
||
|
||
## Authentication
|
||
|
||
Authentication is built on **JWT tokens** (Bearer), without server-side sessions.
|
||
|
||
1. The client sends `POST /protected/token` with `email` (as `username`) and
|
||
`password` in `OAuth2PasswordRequestForm` format (form-data).
|
||
2. The server looks up the user by email and verifies the password via bcrypt
|
||
(`hash_check(plain, hashed)`).
|
||
3. If the email/password pair is correct — a JWT is issued with payload
|
||
`{"sub": <user.id>}`, signed with a secret key (`ENV.SECRET_STRING`,
|
||
algorithm `ENV.ALGORITHM`).
|
||
4. The client passes the token in the `Authorization: Bearer <token>` header
|
||
with every subsequent request.
|
||
5. On protected endpoints, the `get_current_user` dependency:
|
||
- decodes the token (`jwt.decode`),
|
||
- extracts `sub` (user id),
|
||
- loads the user from the DB along with their permissions,
|
||
- on any error (invalid/expired token, user not found from the token) —
|
||
**401 Unauthorized**.
|
||
|
||
> Logout in a pure JWT scheme requires either a short token TTL + a refresh
|
||
> token, or a blacklist of revoked tokens (a `revoked_tokens` table).
|
||
> Alternatively, the token can simply be cleared on the frontend at logout, as
|
||
> the simplest option.
|
||
|
||
## Authorization Model
|
||
|
||
Instead of the classic `roles / business_elements / access_roles_rules` scheme
|
||
(with separate `read/create/update/delete` × `_all`), a simpler **flat
|
||
permission model** was chosen, which is easier to demonstrate but covers the
|
||
same principles: a user is granted a set of string permissions, and each
|
||
endpoint declares which permissions are required to call it.
|
||
|
||
### Database Tables
|
||
|
||
**users**
|
||
|
||
| field | type | description |
|
||
|-------------------|-------------|-----------------------------------------------------------------------------|
|
||
| id | int, PK | user identifier |
|
||
| name | str(64) | first name |
|
||
| last_name | str(64) | last name |
|
||
| middle_name | str(64) | middle name |
|
||
| email | str(255) | email, unique |
|
||
| status | bool | whether the account is active (used instead of `is_active` for soft delete) |
|
||
| hashed_password | str(255) | bcrypt password hash |
|
||
|
||
**permissions**
|
||
|
||
| field | type | description |
|
||
|-------------|-----------|--------------------------------------------------------------------------|
|
||
| id | int, PK | permission identifier |
|
||
| permission | str(64) | permission code: `admin`, `can_view`, `can_create`, `can_edit`, `can_delete` |
|
||
|
||
**user_permission** (association table, many-to-many)
|
||
|
||
| field | type | description |
|
||
|------------------|------|---------------------------|
|
||
| user_id | FK | → users.id |
|
||
| permission_id | FK | → permissions.id |
|
||
|
||
A single user can hold several permissions at once (e.g. `can_view` +
|
||
`can_edit`); the `admin` permission grants full access and allows
|
||
granting/revoking any permissions, including `admin`, to/from other users.
|
||
|
||
### Access Check Rules
|
||
|
||
Every business-logic method (`CrudActions`) explicitly declares the set of
|
||
permitted permissions and checks it before performing the action:
|
||
|
||
| Action | Required permission |
|
||
|------------------------------|----------------------------------|
|
||
| View user (by email/id) | `can_view` or `admin` |
|
||
| Create user | `can_create` or `admin` |
|
||
| Update user | `can_edit` or `admin` |
|
||
| Delete user | `can_delete` or `admin` |
|
||
|
||
Additionally, a **privilege escalation protection** rule applies: a user
|
||
without the `admin` permission cannot grant the `admin` permission to
|
||
themselves or any other user when creating/editing an account.
|
||
|
||
### Access Error Handling
|
||
|
||
- **401 Unauthorized** — if the request cannot be matched to a valid logged-in
|
||
user (no token, expired/invalid token, user from the token does not exist).
|
||
- **403 Forbidden** — the user is identified, but lacks the required
|
||
permission for the requested action/resource (including privilege
|
||
escalation attempts, attempts to claim someone else's email).
|
||
- **404 Not Found** — the requested resource (user, permission) does not
|
||
exist.
|
||
|
||
## API
|
||
|
||
| Method | Path | Required permission | Description |
|
||
|--------|--------------------------------------|--------------------------|----------------------------------------|
|
||
| POST | `/protected/token` | — | Login, JWT issuance |
|
||
| POST | `/protected/logout` | — | Login, JWT issuance |
|
||
| GET | `/protected/get_user_by_email` | `can_view` / `admin` | Get user by email |
|
||
| GET | `/protected/get_user_by_id` | `can_view` / `admin` | Get user by id |
|
||
| POST | `/protected/create_user` | `can_create` / `admin` | Create user |
|
||
| PATCH | `/protected/update_user` | `can_edit` / `admin` | Update own profile, or (for admin/can_edit) someone else's via `target_id` |
|
||
| DELETE | `/protected/delete_user` | `can_delete` / `admin` | Delete (deactivate) user |
|
||
|
||
## Test Data
|
||
|
||
For demonstrating the system, the DB must contain at least:
|
||
Passwords d@d.d, d1@d.d, d2@d.d 12345678
|
||
- 1 user with the `admin` permission (full access)
|
||
- 1 user with the `can_view` permission (view only)
|
||
- 1 user with no permissions (for testing 403)
|
||
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
src/
|
||
db/ # ActionsDB — DB access (SQLAlchemy sessions, queries)
|
||
model/
|
||
database_model/ # SQLAlchemy models (User, Permissions)
|
||
user/ # Pydantic schemas (UserOut, UserCreate, UserUpdate, PermissionIn/Out)
|
||
env_read/ # Reading env via pydantic settings
|
||
service/
|
||
crud_actions/ # CrudActions — business logic + access permission checks
|
||
JWT/ # JWT (token encoding/decoding), Hash (bcrypt)
|
||
auth/ # JWT token creation and verification
|
||
errors/ # centralized HTTP errors (401/403/404)
|
||
web/ # FastAPI routers (endpoints)
|
||
migrations/ # DB migrations
|
||
tests/ # tests (unit, integration)
|
||
```
|
||
|
||
## Running
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install poetry
|
||
poetry install
|
||
make run
|
||
```
|
||
Rename env and fill with correct data
|