Skip to content

Commit

Permalink
Additional Fields Reinforced
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnGrubba committed Jul 22, 2024
1 parent 7412087 commit 500567b
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 13 deletions.
4 changes: 3 additions & 1 deletion config/configtemplate.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"issuer_name": "EZAuth",
"issuer_image_url": "",
"qr_endpoint": true
}
},
"allow_add_fields_on_signup": [],
"allow_add_fields_patch_user": []
},
"security": {
"allow_origins": [
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ Make sure that all parameters are set correctly before starting the service.
| `account_features.2fa.issuer_name` | **Datatype:** String <br> **Default:** `"EZAuth"` <br> How the two factor code will be titled in the users 2FA App. (Mostly the App Name) |
| `account_features.2fa.issuer_image_url` | **Datatype:** String <br> **Default:** `""` <br> URL for an optional Image which will be displayed in the 2FA App. |
| `account_features.2fa.qr_endpoint` | **Datatype:** Boolean <br> **Default:** `true` <br> Enable or disable QR Code Generation Endpoint for 2FA Login. This can be useful if you don't want to use any libraries on the client Side. |
| `account_features.allow_add_fields_on_signup` | **Datatype:** List <br> **Default:** `[]` <br> Allow those additional fields on signup. Leave empty if not sure. |
| `account_features.allow_add_fields_patch_user` | **Datatype:** List <br> **Default:** `[]` <br> Allow those additional fields to be set when modifying user. Leave empty if not sure. The entries here extend already set `account_features.allow_add_fields_on_signup` fields. |

!!! Note "Additional Fields"
The `allow_add_fields_on_signup` makes it possible to add custom fields to the signup process. If you don't set the fields that are allowed here on signup, you can't update them later, except you also have them in `allow_add_fields_patch_user`.


### Security Configuration

Expand Down
22 changes: 20 additions & 2 deletions src/api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest
import datetime
import uuid
import bcrypt
from tools import users_collection
from tools import users_collection, sessions_collection


@pytest.fixture
Expand All @@ -15,7 +16,24 @@ def fixtureuser() -> dict:
"email": "[email protected]",
"username": "FixtureUser",
"createdAt": datetime.datetime.now(),
"test": True,
}
users_collection.insert_one(user_data)
result = users_collection.insert_one(user_data)
user_data["password"] = "Kennwort1!"
user_data["_id"] = str(result.inserted_id)
return user_data


@pytest.fixture
def fixturesessiontoken_user(fixtureuser) -> tuple[str, dict]:
# Generate a new session token
session_token = str(uuid.uuid4())
# Persist the session
sessions_collection.insert_one(
{
"session_token": session_token,
"user_id": fixtureuser["_id"],
"createdAt": datetime.datetime.now(),
}
)
return session_token, fixtureuser
89 changes: 89 additions & 0 deletions src/api/profile_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from fastapi.testclient import TestClient
from crud.user import get_dangerous_user

from .main import app

client = TestClient(app)


# Unauthorized Route Test
def test_unauthorized_profile(fixtureuser):
response = client.get("/profile")
assert response.status_code == 401


# Successfull tests
def test_get_profile(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
response = client.get("/profile")
resp_json = response.json()
assert response.status_code == 200
assert resp_json.get("email") == fixturesessiontoken_user[1]["email"]
assert resp_json.get("username") == fixturesessiontoken_user[1]["username"]
assert resp_json.get("createdAt") is not None
assert resp_json.get("password") is None
assert resp_json.get("_id") is None


def test_update_username_valid(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
response = client.patch("/profile", json={"username": "newusername"})
resp_json = response.json()
assert response.status_code == 200
assert resp_json.get("email") == fixturesessiontoken_user[1]["email"]
assert resp_json.get("username") == "newusername"
assert resp_json.get("createdAt") is not None


def test_update_email(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# E-Mail should not be updateable through this endpoint
response = client.patch("/profile", json={"email": "[email protected]"})
resp_json = response.json()
assert response.status_code == 200
assert resp_json.get("email") == fixturesessiontoken_user[1]["email"]
assert resp_json.get("username") == fixturesessiontoken_user[1]["username"]
assert resp_json.get("createdAt") is not None


def test_update_password(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# Password should not be updateable through this endpoint
response = client.patch("/profile", json={"password": "Kennwort2!"})
assert response.status_code == 200
assert (
get_dangerous_user(fixturesessiontoken_user[1]["_id"])["password"]
!= "Kennwort2!"
)


def test_update_createdAt(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# Password should not be updateable through this endpoint
response = client.patch("/profile", json={"createdAt": "1921"})
assert response.status_code == 200
assert get_dangerous_user(fixturesessiontoken_user[1]["_id"])["createdAt"] != "1921"


def test_update_additional_data_valid(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# Data that is not existent, but updateable should be possible
response = client.patch("/profile", json={"test2": "1921"})
assert response.status_code == 200
assert get_dangerous_user(fixturesessiontoken_user[1]["_id"]).get("test2") == "1921"


def test_update_nonexistent_data(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# Additional data should not be updateable through this endpoint
response = client.patch("/profile", json={"sas": "1921"})
assert response.status_code == 200
assert get_dangerous_user(fixturesessiontoken_user[1]["_id"]).get("sas") is None


def test_update_additional_existent_data(fixturesessiontoken_user):
client.cookies.set("session", fixturesessiontoken_user[0])
# Data that is additional but already exists should also be updateable
response = client.patch("/profile", json={"test": "Ya"})
assert response.status_code == 200
assert get_dangerous_user(fixturesessiontoken_user[1]["_id"]).get("test") == "Ya"
7 changes: 7 additions & 0 deletions src/api/signup_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi.testclient import TestClient
from tools import users_collection

from .main import app

Expand Down Expand Up @@ -37,6 +38,12 @@ def test_create_account_additional_data():
assert response.status_code == 200
assert resp_js.get("session_token") is not None
assert int(resp_js.get("expires")) is not None
# Check which aditional data was saved
usr = users_collection.find_one({"email": "[email protected]"})
# Check which data should be saved (testing_conf.json)
assert usr.get("test") is True
assert usr.get("test2") is None
assert usr.get("test3") is None


# MISSING DATA TESTS
Expand Down
37 changes: 32 additions & 5 deletions src/crud/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
send_email,
InternalConfig,
insecure_cols,
AccountFeaturesConfig,
default_signup_fields,
)
from fastapi import HTTPException, BackgroundTasks, Request
from api.model import UserSignupRequest
Expand Down Expand Up @@ -84,10 +86,22 @@ def update_public_user(user_id: str, data: dict) -> None:
user_id (str): User ID
data (dict): Data to Update
"""
# Retrieve the existing user data
existing_user = users_collection.find_one({"_id": bson.ObjectId(user_id)})
# Allow update of all columns except InternalConfig.internal_columns
data = {
k: v for k, v in data.items() if k not in InternalConfig.not_updateable_columns
}

if AccountFeaturesConfig.allow_add_fields_patch_user:
data = {
k: v
for k, v in data.items()
if k in AccountFeaturesConfig.allow_add_fields_patch_user
or k in existing_user
}
else:
data = {k: v for k, v in data.items() if k in existing_user}
return users_collection.find_one_and_update(
{"_id": bson.ObjectId(user_id)},
{"$set": data},
Expand Down Expand Up @@ -203,13 +217,26 @@ def create_user(
Returns:
str: Session Token
"""
data = {
**additional_data,
**(
signup_model.model_dump()
if AccountFeaturesConfig.allow_add_fields_on_signup:
# Dump all fields
dmp = (
signup_model.model_dump(
include=default_signup_fields
| AccountFeaturesConfig.allow_add_fields_on_signup,
)
if isinstance(signup_model, UserSignupRequest)
else {}
),
)
else:
# Only Dump Required Fields
dmp = (
signup_model.model_dump(include=default_signup_fields)
if isinstance(signup_model, UserSignupRequest)
else {}
)
data = {
**additional_data,
**dmp,
"createdAt": datetime.datetime.now(),
}
# Save the Account into the database
Expand Down
2 changes: 2 additions & 0 deletions src/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AccountFeaturesConfig,
insecure_cols,
SecurityConfig,
default_signup_fields,
)
from .mail import send_email, broadcast_emails
from .confirmation_codes import all_ids, regenerate_ids
Expand All @@ -27,4 +28,5 @@
"broadcast_emails",
"all_ids",
"regenerate_ids",
"default_signup_fields",
]
15 changes: 11 additions & 4 deletions src/tools/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
config = json.load(open("/src/app/config/config.json", "rb"))

# Columns that should never leave EZAuth (maybe get more in the future)
default_signup_fields = {"username", "email", "password"}
insecure_cols = {"password": 0, "2fa_secret": 0, "google_uid": 0, "github_uid": 0}
not_updateable_cols_internal = ["email", "username", "createdAt"]
not_updateable_cols_internal = ["email", "createdAt"]
# Columns that can leave EZAuth but should only be used internally can be defined in config


Expand All @@ -23,7 +24,7 @@ class SignupConfig:
conf_code_expiry: int = config["signup"]["conf_code_expiry"]
conf_code_complexity: int = config["signup"]["conf_code_complexity"]
enable_welcome_email: bool = config["signup"]["enable_welcome_email"]
oauth_providers: list = config["signup"]["oauth"]["providers_enabled"]
oauth_providers: list[str] = config["signup"]["oauth"]["providers_enabled"]
oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/")


Expand Down Expand Up @@ -65,11 +66,17 @@ class AccountFeaturesConfig:
issuer_name_2fa: str = config["account_features"]["2fa"]["issuer_name"]
issuer_image_url_2fa: str = config["account_features"]["2fa"]["issuer_image_url"]
qr_code_endpoint_2fa: bool = config["account_features"]["2fa"]["qr_endpoint"]
allow_add_fields_on_signup: set[str] = set(
config["account_features"]["allow_add_fields_on_signup"]
)
allow_add_fields_patch_user: set[str] = set(
config["account_features"]["allow_add_fields_patch_user"]
)


class SecurityConfig:
access_control_origins: list = config["security"]["allow_origins"]
allow_headers: list = config["security"]["allow_headers"]
access_control_origins: set[str] = set(config["security"]["allow_origins"])
allow_headers: set[str] = set(config["security"]["allow_headers"])
max_login_attempts: int = config["security"]["max_login_attempts"]
login_timeout: int = config["security"]["login_timeout"]
expire_unfinished_timeout: int = config["security"]["expire_unfinished_timeout"]
8 changes: 7 additions & 1 deletion src/tools/testing_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@
"issuer_name": "EZAuth",
"issuer_image_url": "",
"qr_endpoint": true
}
},
"allow_add_fields_on_signup": [
"test"
],
"allow_add_fields_patch_user": [
"test2"
]
},
"security": {
"allow_origins": [
Expand Down

0 comments on commit 500567b

Please sign in to comment.