Skip to content
Merged
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
44 changes: 44 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: security checks
on: pull_request
jobs:
security-checks:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Cache Python dependencies
id: cache-deps
uses: actions/cache@v3
with:
path: |
~/.cache/pypoetry/virtualenvs
.venv
key: ${{ runner.os }}-security-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-security-
- name: Install dependencies
run: |
pip install poetry
poetry sync
- name: Poetry audit
id: poetry-audit
run: |-
pip install poetry-audit-plugin
poetry audit > audit.txt
cat audit.txt
continue-on-error: true
- name: Comment with audit result
uses: marocchino/sticky-pull-request-comment@v2
if: github.event_name == 'pull_request'
with:
recreate: true
path: audit.txt
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type-check:

test:
poetry run populate-tiers --test
poetry run pytest --cov=virtual_labs --cov-report=xml --cov-report=html
poetry run pytest

init-db:
poetry run alembic upgrade head
Expand Down
219 changes: 219 additions & 0 deletions alembic/versions/551e7395c078_add_promotion_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""add promotion tables

Revision ID: 551e7395c078
Revises: 2a8e7c720792
Create Date: 2025-10-15 10:33:57.253106

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "551e7395c078"
down_revision: Union[str, None] = "2a8e7c720792"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"promotion_code",
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.Column("code", sa.String(length=50), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("credits_amount", sa.Float(), nullable=False),
sa.Column("validity_period_days", sa.Integer(), nullable=False),
sa.Column("max_uses_per_user_per_period", sa.Integer(), nullable=False),
sa.Column("max_total_uses", sa.Integer(), nullable=True),
sa.Column("current_total_uses", sa.Integer(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False),
sa.Column("valid_until", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_by", sa.UUID(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.CheckConstraint("credits_amount > 0", name="check_positive_credits"),
sa.CheckConstraint(
"max_total_uses IS NULL OR max_total_uses > 0",
name="check_positive_max_uses",
),
sa.CheckConstraint(
"max_uses_per_user_per_period > 0", name="check_positive_user_period_uses"
),
sa.CheckConstraint("valid_until > valid_from", name="check_valid_date_range"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_promotion_code_active"), "promotion_code", ["active"], unique=False
)
op.create_index(
op.f("ix_promotion_code_code"), "promotion_code", ["code"], unique=False
)
op.create_index(
"ix_promotion_code_validity",
"promotion_code",
["active", "valid_from", "valid_until"],
unique=False,
)
op.create_table(
"promotion_code_redemption_attempt",
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.Column("code_attempted", sa.String(length=50), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("virtual_lab_id", sa.UUID(), nullable=True),
sa.Column("success", sa.Boolean(), nullable=False),
sa.Column("failure_reason", sa.String(length=100), nullable=True),
sa.Column("attempted_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_promotion_attempt_code_time",
"promotion_code_redemption_attempt",
["code_attempted", "attempted_at"],
unique=False,
)
op.create_index(
"ix_promotion_attempt_user_time",
"promotion_code_redemption_attempt",
["user_id", "attempted_at"],
unique=False,
)
op.create_index(
op.f("ix_promotion_code_redemption_attempt_code_attempted"),
"promotion_code_redemption_attempt",
["code_attempted"],
unique=False,
)
op.create_index(
op.f("ix_promotion_code_redemption_attempt_user_id"),
"promotion_code_redemption_attempt",
["user_id"],
unique=False,
)
op.create_table(
"promotion_code_usage",
sa.Column(
"id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False
),
sa.Column("promotion_code_id", sa.UUID(), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("virtual_lab_id", sa.UUID(), nullable=False),
sa.Column("credits_granted", sa.Integer(), nullable=False),
sa.Column("redeemed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("accounting_transaction_id", sa.String(length=255), nullable=True),
sa.Column(
"status",
sa.Enum("PENDING", "COMPLETED", "FAILED", name="promotioncodeusagestatus"),
nullable=False,
),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.CheckConstraint(
"credits_granted > 0", name="check_positive_credits_granted"
),
sa.ForeignKeyConstraint(
["promotion_code_id"],
["promotion_code.id"],
),
sa.ForeignKeyConstraint(
["virtual_lab_id"],
["virtual_lab.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_promotion_code_usage_promotion_code_id"),
"promotion_code_usage",
["promotion_code_id"],
unique=False,
)
op.create_index(
op.f("ix_promotion_code_usage_status"),
"promotion_code_usage",
["status"],
unique=False,
)
op.create_index(
op.f("ix_promotion_code_usage_user_id"),
"promotion_code_usage",
["user_id"],
unique=False,
)
op.create_index(
op.f("ix_promotion_code_usage_virtual_lab_id"),
"promotion_code_usage",
["virtual_lab_id"],
unique=False,
)
op.create_index(
"ix_promotion_usage_code_status",
"promotion_code_usage",
["promotion_code_id", "status"],
unique=False,
)
op.create_index(
"ix_promotion_usage_lab_date",
"promotion_code_usage",
["virtual_lab_id", "redeemed_at"],
unique=False,
)
op.create_index(
"ix_promotion_usage_user_code_date",
"promotion_code_usage",
["user_id", "promotion_code_id", "redeemed_at"],
unique=False,
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
"ix_promotion_usage_user_code_date", table_name="promotion_code_usage"
)
op.drop_index("ix_promotion_usage_lab_date", table_name="promotion_code_usage")
op.drop_index("ix_promotion_usage_code_status", table_name="promotion_code_usage")
op.drop_index(
op.f("ix_promotion_code_usage_virtual_lab_id"),
table_name="promotion_code_usage",
)
op.drop_index(
op.f("ix_promotion_code_usage_user_id"), table_name="promotion_code_usage"
)
op.drop_index(
op.f("ix_promotion_code_usage_status"), table_name="promotion_code_usage"
)
op.drop_index(
op.f("ix_promotion_code_usage_promotion_code_id"),
table_name="promotion_code_usage",
)
op.drop_table("promotion_code_usage")
op.drop_index(
op.f("ix_promotion_code_redemption_attempt_user_id"),
table_name="promotion_code_redemption_attempt",
)
op.drop_index(
op.f("ix_promotion_code_redemption_attempt_code_attempted"),
table_name="promotion_code_redemption_attempt",
)
op.drop_index(
"ix_promotion_attempt_user_time", table_name="promotion_code_redemption_attempt"
)
op.drop_index(
"ix_promotion_attempt_code_time", table_name="promotion_code_redemption_attempt"
)
op.drop_table("promotion_code_redemption_attempt")
op.drop_index("ix_promotion_code_validity", table_name="promotion_code")
op.drop_index(op.f("ix_promotion_code_code"), table_name="promotion_code")
op.drop_index(op.f("ix_promotion_code_active"), table_name="promotion_code")
op.drop_table("promotion_code")
# ### end Alembic commands ###
13 changes: 8 additions & 5 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ networks:

services:
keycloak-db:
image: postgres:latest
image: postgres:17.6
container_name: keycloak-db
environment:
POSTGRES_USER: keycloak
Expand All @@ -30,7 +30,7 @@ services:
- ls

keycloak:
image: quay.io/keycloak/keycloak:24.0
image: quay.io/keycloak/keycloak:26.3
container_name: keycloak
environment:
KEYCLOAK_ADMIN: admin
Expand All @@ -47,8 +47,11 @@ services:
- start-dev
- --http-port=9090
- --hostname=keycloak
- --hostname-port=9090
- --hostname-strict-backchannel=true
- --hostname-admin=localhost
- --hostname-strict=false
- --http-enabled=true
- --hostname=http://keycloak:9090
- --hostname-admin=http://localhost:9090
- --import-realm
ports:
- "9090:9090"
Expand All @@ -58,7 +61,7 @@ services:
- ./env-prep/realm-export.json:/opt/keycloak/data/import/realm-import.json

virtual-lab-db:
image: postgres:latest
image: postgres:17.6
container_name: vlm-db
environment:
POSTGRES_USER: vlm
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ networks:

services:
keycloak-db:
image: postgres:latest
image: postgres:17.6
container_name: keycloak-db
environment:
POSTGRES_USER: keycloak
Expand Down Expand Up @@ -61,7 +61,7 @@ services:
- ./env-prep/realm-export.json:/opt/keycloak/data/import/realm-import.json

virtual-lab-db:
image: postgres:latest
image: postgres:17.6
container_name: vlm-db
environment:
POSTGRES_USER: vlm
Expand Down
Loading
Loading