Decoupled User Management in Python

작성자

카테고리:

← 피드로
DEV Community · Alain Airom (Ayrom) · 2026-07-29 개발(SW)

Hands-On tests with ‘UserHarbor’ and IBM Bob: A Modular Approach to Python User Authentication and Permissions

Introduction

When evaluating open-source libraries for core application infrastructure - such as authentication, session management, and fine-grained Role-Based Access Control (RBAC) - getting hands-on with a complete reference application is invaluable. This is especially true for libraries aiming to be framework-agnostic, promising flexibility but requiring more explicit wiring. Recently, I wanted to explore UserHarbor (github.com/userharbor/userharbor), a lightweight Python user-management library designed without direct coupling to any web framework or database toolkit.
Rather than manually bootstrapping a new project, setting up the SQLite database, and writing boilerplate code to explore every edge of the library, I used IBM Bob, to scaffold and implement an end-to-end reference demonstration integrated with FastAPI, SQLAlchemy for persistence, and SMTP (or a local console fallback) for transactional emails.
The goal was to rapidly test UserHarbor’s entire feature lifecycle - registration, email verification, session tokens, optional authentication, RBAC guards, password resets, and account deletion, which I personally find really useful. These capacities could be implemented in many applications and ease the phase of user registration, email validation etc…
This post details the architecture built, highlights the key implementation logic, and illustrates how easily a decoupled core can be integrated into a modern web stack.

UserHabor (from official GitHub repostory)

Image from official project's repository

Image from official project’s repository

Project status: UserHarbor is currently in an early stage of development. The API may change frequently. The library is not ready for production use yet.

UserHarbor is a framework-agnostic Python library for user account management.
Its goal is to provide a simple, stable, and framework-independent interface for common user-related operations:

  • user registration
  • email verification
  • login
  • session management
  • logout from one or all sessions
  • password change
  • password reset
  • account deletion
  • role and permission checks

UserHarbor is not a web framework. It does not provide routers, views, or HTTP endpoints. Instead, it exposes a simple domain-level API that can be integrated with FastAPI, Flask, Django, Litestar, CLI applications, or any other environment.

Implementation: System Architecture & Component Design

The application architecture is based on the idea to demonstrate decoupling core business logic(s) from framework adapters, database persistence, and external service gateways, in a universal way.

Component Overview

  • UserHarbor Core (userharbor): Manages business logic including user registration, session management, role/permission logic, and password flows.
  • Official Adapters:
  • userharbor-fastapi: Handles routing integration and FastAPI Depends() dependency guards.
  • userharbor-sqlalchemy: Provides persistence via SQLAlchemyUserStore mapped to SQLite.
  • userharbor-smtp: Handles email dispatching with a local console fallback when SMTP credentials are not present.
  • FastAPI Web Shell (app/): Provides the REST API endpoints and static file serving for a lightweight Single Page Application (SPA).

Implementation Key Highlights

Modular Bootstrapping and Pre-seeding Roles (main.py)

In main.py, IBM Bob established the foundation of the modular design. The application factory bootstraps the SQLite engine, configures UserHarbor with the SQLite store and email factory, pre-seeds initial RBAC permissions, and attaches the authentication routers.

# app/main.py
"""
UserHarbor Demo – FastAPI application entry point.

Stack
─────
* userharbor         – core user-management domain logic
* userharbor-sqlalchemy – SQLite-backed UserStore
* userharbor-fastapi – FastAPI router adapter with bearer-token auth
* ConsoleEmailSender / SMTPEmailSender – from app/email.py

Environment variables (copy .env.example → .env):
  SECRET_KEY     – signing key for session tokens
  DATABASE_URL   – SQLAlchemy URL          (default: sqlite:///./users.db)
  SMTP_*         – optional SMTP credentials; falls back to console logging
"""
from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from userharbor import UserHarbor
from userharbor_fastapi import UserHarborFastAPI
from userharbor_sqlalchemy import SQLAlchemyUserStore

from app.email import get_email_sender
from app.routes import build_demo_router

FRONTEND_DIR = Path(__file__).parent.parent / "frontend"

load_dotenv()

# ── Database ───────────────────────────────────────────────────────────────
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./users.db")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine)

# ── UserHarbor setup ───────────────────────────────────────────────────────
store = SQLAlchemyUserStore(SessionLocal)
store.metadata.create_all(engine)  # create tables if they don't exist

harbor = UserHarbor(
    secret_key=os.getenv("SECRET_KEY", "demo-secret-key-change-in-production"),
    store=store,
    email_sender=get_email_sender(),
)

# Pre-seed roles and permissions so the demo endpoints work out of the box.
try:
    harbor.roles.create("admin")
    harbor.roles.create("editor")
    harbor.permissions.create("articles.write")
    harbor.permissions.create("users.manage")
    harbor.roles.grant_permission("admin", "articles.write")
    harbor.roles.grant_permission("admin", "users.manage")
    harbor.roles.grant_permission("editor", "articles.write")
except Exception:
    pass  # roles/permissions already exist from a previous run

# ── FastAPI adapter ────────────────────────────────────────────────────────
auth = UserHarborFastAPI(harbor)

# ── Application ────────────────────────────────────────────────────────────
app = FastAPI(
    title="UserHarbor Demo",
    description=(
        "End-to-end demonstration of UserHarbor with FastAPI + SQLAlchemy + SMTP.\n\n"
        "Workflow:\n"
        "1. `POST /auth/register` – create an account\n"
        "2. `POST /auth/verify-email` – verify your email with the token printed to console\n"
        "3. `POST /auth/login` – obtain a bearer token\n"
        "4. Use the **Authorize** button (🔒) to paste the token\n"
        "5. Try the protected `/demo/*` endpoints\n"
    ),
    version="1.0.0",
)

# Built-in auth routes: /auth/register, /auth/login, /auth/me, etc.
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])

# Demo application routes that showcase role/permission guards.
app.include_router(
    build_demo_router(auth, harbor),
    prefix="/demo",
    tags=["Demo – Protected Routes"],
)

# Serve the SPA frontend
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")

@app.get("/", tags=["Root"], include_in_schema=False)
def serve_ui():
    """Serve the SPA shell."""
    return FileResponse(str(FRONTEND_DIR / "index.html"))

if __name__ == "__main__":
    import uvicorn

    uvicorn.run("app.main:app", host="0.0.0.0", port=8080, reload=True)

Enter fullscreen mode Exit fullscreen mode

Dev-Friendly Console Email Fallback (email.py)

To remove dependencies during exploration, an email factory tis configured, that switches to logging transactional email tokens directly to stdout during development, rather than requiring real SMTP credentials.

"""
Email sender implementations for the demo.

* ConsoleEmailSender – prints tokens to stdout; ideal for development when no
  SMTP server is available.
* get_email_sender()  – factory that returns an SMTPEmailSender when SMTP
  credentials are present, otherwise falls back to the console sender.
"""
from __future__ import annotations

import os

class ConsoleEmailSender:
    """Prints email content to the console instead of sending real mail."""

    def send_verification(
        self,
        username: str,
        email: str,
        verification_token: str,
    ) -> None:
        print(
            f"\n[EMAIL] Verification token for {username} ({email}):\n"
            f"  Token: {verification_token}\n"
        )

    def send_password_reset(
        self,
        username: str,
        email: str,
        reset_token: str,
    ) -> None:
        print(
            f"\n[EMAIL] Password-reset token for {username} ({email}):\n"
            f"  Token: {reset_token}\n"
        )

    def send_email_verified(self, username: str, email: str) -> None:
        print(f"\n[EMAIL] Email verified – {username} ({email})\n")

    def send_password_changed(self, username: str, email: str) -> None:
        print(f"\n[EMAIL] Password changed – {username} ({email})\n")

    def send_account_deleted(self, username: str, email: str) -> None:
        print(f"\n[EMAIL] Account deleted – {username} ({email})\n")

def get_email_sender():
    """Return an SMTPEmailSender when credentials are available, else console."""
    host = os.getenv("SMTP_HOST", "")
    username = os.getenv("SMTP_USERNAME", "")
    password = os.getenv("SMTP_PASSWORD", "")
    from_email = os.getenv("SMTP_FROM_EMAIL", "")

    if host and username and password and from_email:
        from userharbor_smtp import SMTPEmailSender  # type: ignore

        return SMTPEmailSender(
            host=host,
            port=int(os.getenv("SMTP_PORT", "587")),
            username=username,
            password=password,
            from_email=from_email,
            from_name=os.getenv("SMTP_FROM_NAME", "UserHarbor Demo"),
        )

    print("[INFO] No SMTP credentials found – using ConsoleEmailSender.")
    return ConsoleEmailSender()

Enter fullscreen mode Exit fullscreen mode

Fine-Grained Authorization Guards (routes.py)

The application router exercises the complete RBAC and permission guards provided by userharbor-fastapi. The pre-built dependencies (Depends()) can enforce authentication, specific role requirements, or granular permissions.

"""
Demo application routes that showcase every UserHarbor feature:

Route                     Auth required    Extra guard
─────────────────────────────────────────────────────
GET  /demo/public          none            –
GET  /demo/profile         session token   –
GET  /demo/optional        optional        –
POST /demo/assign-admin    session token   – (grant admin role to a user)
GET  /demo/articles/write  session token   permission: articles.write
GET  /demo/admin/users     session token   role: admin
"""
from __future__ import annotations

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from userharbor import UserHarbor
from userharbor_fastapi import UserHarborFastAPI

# ── Request / response models ──────────────────────────────────────────────

class AssignRoleRequest(BaseModel):
    username: str
    role: str

# ── Router factory ─────────────────────────────────────────────────────────

def build_demo_router(auth: UserHarborFastAPI, harbor: UserHarbor) -> APIRouter:
    router = APIRouter()

    # ── Public route – no authentication ──────────────────────────────────
    @router.get("/public")
    def public():
        """Anyone can call this endpoint – no token required."""
        return {
            "message": "This is a public endpoint. No authentication needed.",
            "hint": "Call POST /auth/register to create an account, then POST /auth/login.",
        }

    # ── Optional auth – behaves differently for guests vs. logged-in users ─
    @router.get("/optional")
    def optional(user=Depends(auth.optional_user)):
        """Shows personalised content when a valid token is present."""
        if user is None:
            return {"authenticated": False, "message": "Hello, anonymous visitor!"}
        return {
            "authenticated": True,
            "message": f"Hello, {user.username}! You are verified: {user.verified}",
        }

    # ── Protected – requires a valid session token ─────────────────────────
    @router.get("/profile")
    def profile(user=Depends(auth.current_user)):
        """Requires a valid session token (Authorization: Bearer <token>)."""
        return {
            "username": user.username,
            "email": user.email,
            "verified": user.verified,
        }

    # ── Assign a role to any user (admin can do this) ──────────────────────
    @router.post("/assign-role")
    def assign_role(
        body: AssignRoleRequest,
        user=Depends(auth.current_user),
    ):
        """
        Grant a role to a user. Requires a valid session.

        Useful for promoting someone to 'admin' or 'editor' so you can
        test the role-restricted endpoints below.
        """
        harbor.grant_role(body.username, body.role)
        return {
            "message": f"Role '{body.role}' granted to '{body.username}'.",
            "granted_by": user.username,
        }

    # ── Permission-restricted route ────────────────────────────────────────
    @router.get("/articles/write")
    def write_articles(user=Depends(auth.require_permission("articles.write"))):
        """
        Requires the **articles.write** permission.

        Roles that have this permission: `admin`, `editor`.
        Grant one with POST /demo/assign-role.
        """
        return {
            "message": f"Welcome, {user.username}! You can write articles.",
            "permission": "articles.write",
        }

    # ── Role-restricted route ──────────────────────────────────────────────
    @router.get("/admin/users")
    def admin_users(user=Depends(auth.require_role("admin"))):
        """
        Requires the **admin** role.

        Grant with: POST /demo/assign-role  { "username": "...", "role": "admin" }
        """
        return {
            "message": f"Welcome, admin {user.username}!",
            "note": "In a real app you would return a paginated user list here.",
        }

    return router

Enter fullscreen mode Exit fullscreen mode

Overall Features demonstrated

I find this library really promissing, and so far in this demo, the following features are provided;

| Feature                 | UserHarbor API                                               |
| ----------------------- | ------------------------------------------------------------ |
| User registration       | `harbor.register()`                                          |
| Email verification      | `harbor.verify_email()`                                      |
| Login / bearer token    | `harbor.login()`                                             |
| Session validation      | `harbor.verify_session()`                                    |
| Current user            | `harbor.get_current_user()`                                  |
| Optional authentication | `auth.optional_user` dependency                              |
| Role management         | `harbor.roles.create()` / `harbor.grant_role()`              |
| Permission management   | `harbor.permissions.create()` / `harbor.roles.grant_permission()` |
| Role guard              | `auth.require_role("admin")` dependency                      |
| Permission guard        | `auth.require_permission("articles.write")` dependency       |
| Password change         | `harbor.change_password()`                                   |
| Password reset flow     | `harbor.send_password_reset()` / `harbor.reset_password()`   |
| Logout                  | `harbor.logout()`                                            |
| Logout all sessions     | `harbor.logout_all()`                                        |
| Account deletion        | `harbor.delete_account()`                                    |

---

Enter fullscreen mode Exit fullscreen mode

Conclusion and Key Learnings

Testing UserHarbor proved to be a powerful exercise in building a modular application foundation. By keeping the authentication domain decoupled from the delivery framework and persistence logic, it achieves a clean separation of concerns without introducing significant architectural overhead.

Partnering with IBM Bob allowed for rapid exploration and integration, making it straightforward to validate that framework-agnostic approaches to core infrastructure are highly viable for modern web applications.

Thanks for reading 👨‍🏭

Links

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다