diff --git a/.env.example b/.env.example
index 2ef7a29..142d97b 100644
--- a/.env.example
+++ b/.env.example
@@ -27,7 +27,7 @@ DJANGO_DB_LOG_HANDLER=console
# Should the database be migrated before start (entrypoint.sh - docker setup). Will be migrated anyway if $SITE_ROOT=api. Comment out for False
DJANGO_MIGRATE=True
# Should the modules be searched for scheduled tasks. Comment out for false
-# SCHEDULER_AUTOSTART=True
+SCHEDULER_AUTOSTART=True
PROJECT_NAME=dev
NEW_OPENIMIS_HOST=dev-openimis.org
HTTP_PORT=80
@@ -41,5 +41,23 @@ GW_BRANCH=develop
BE_BRANCH=develop
FE_BRANCH=develop
+# Lockout mechanism
+LOGIN_LOCKOUT_FAILURE_LIMIT=5 # Allowed login failures before lockout
+LOGIN_LOCKOUT_COOLOFF_TIME=5 # Lockout duration in minutes
+PASSWORD_MIN_LENGTH=8
+PASSWORD_UPPERCASE=1 # Minimum number of uppercase letters
+PASSWORD_LOWERCASE=1 # Minimum number of lowercase letters
+PASSWORD_DIGITS=1 # Minimum number of digits
+PASSWORD_SYMBOLS=1 # Minimum number of symbols
+PASSWORD_SPACES=1 # Maximum number of spaces allowed
+CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:8000 # Define the trusted origins for CSRF protection, separated by commas
+
+# Rate limiting settings
+RATELIMIT_CACHE=default # The cache alias to use for rate limiting
+RATELIMIT_KEY=ip # Key to identify the client; 'ip' means it will use the client's IP address
+RATELIMIT_RATE=150/m # Rate limit (150 requests per minute)
+RATELIMIT_METHOD=ALL # HTTP methods to rate limit; 'ALL' means all methods
+RATELIMIT_GROUP=graphql # Group name for the rate limit
+RATELIMIT_SKIP_TIMEOUT=False # Whether to skip rate limiting during c
diff --git a/.github/workflows/ci_assembly.yml b/.github/workflows/ci_assembly.yml
index a91bbfa..1e60d52 100755
--- a/.github/workflows/ci_assembly.yml
+++ b/.github/workflows/ci_assembly.yml
@@ -63,13 +63,13 @@ jobs:
${{ runner.os }}-
- name: Install Python dependencies
+ working-directory: ./script
run: |
sudo apt-get update
sudo apt-get install jq
-
python -m pip install --upgrade pip
- pip install -r requirements.txt
- python modules-requirements.py openimis.json > modules-requirements.txt
+ pip install -r ../requirements.txt
+ python modules-requirements.py ../openimis.json > modules-requirements.txt
pip install --no-cache-dir -r modules-requirements.txt
export MODULES=$(jq -r '(.modules[].name)' openimis.json | xargs)
echo $modules
diff --git a/.github/workflows/ci_module.yml b/.github/workflows/ci_module.yml
index 498963b..b4fce97 100755
--- a/.github/workflows/ci_module.yml
+++ b/.github/workflows/ci_module.yml
@@ -81,15 +81,15 @@ jobs:
echo "MODULE_NAME=$MODULE_NAME" >> $GITHUB_ENV
# Add or replace MODULE_NAME module in openimis.json (local version)
- echo $(jq --arg name "$MODULE_NAME" 'if [.modules[].name == ($name)]| max then (.modules[] | select(.name == ($name)) | .pip)|="-e ../current-module" else .modules |= .+ [{name:($name), pip:"../current-module"}] end' openimis.json) > openimis.json
+ echo $(jq --arg name "$MODULE_NAME" 'if [.modules[].name == ($name)]| max then (.modules[] | select(.name == ($name)) | .pip)|="-e ../../current-module" else .modules |= .+ [{name:($name), pip:"../../current-module"}] end' openimis.json) > openimis.json
cat openimis.json
- name: Install Python dependencies
- working-directory: ./openimis
+ working-directory: ./openimis/script
run: |
python -m pip install --upgrade pip
- pip install -r requirements.txt
- python modules-requirements.py openimis.json > modules-requirements.txt
+ pip install -r ../requirements.txt
+ python modules-requirements.py ../openimis.json > modules-requirements.txt
cat modules-requirements.txt
pip install --no-cache-dir -r modules-requirements.txt
diff --git a/.gitignore b/.gitignore
index d2d6a77..da2798b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,9 @@ extracted_translations_fe
script/config.py
**/src/*
**/images/insurees
+openimis-dev.json
+# Except for the runConfigurations folder
+!.idea/runConfigurations
+# Ensure all files in runConfigurations are included
+!.idea/runConfigurations/*
diff --git a/.idea/runConfigurations/RunTestsWithSetup.xml b/.idea/runConfigurations/RunTestsWithSetup.xml
new file mode 100644
index 0000000..5b50783
--- /dev/null
+++ b/.idea/runConfigurations/RunTestsWithSetup.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run_Migrations_.xml b/.idea/runConfigurations/Run_Migrations_.xml
new file mode 100644
index 0000000..81ab375
--- /dev/null
+++ b/.idea/runConfigurations/Run_Migrations_.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Runserver_.xml b/.idea/runConfigurations/Runserver_.xml
new file mode 100644
index 0000000..d61befc
--- /dev/null
+++ b/.idea/runConfigurations/Runserver_.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/_CreateTestDB.xml b/.idea/runConfigurations/_CreateTestDB.xml
new file mode 100644
index 0000000..0b25bd0
--- /dev/null
+++ b/.idea/runConfigurations/_CreateTestDB.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/_TeardownTestDB.xml b/.idea/runConfigurations/_TeardownTestDB.xml
new file mode 100644
index 0000000..8d64f2f
--- /dev/null
+++ b/.idea/runConfigurations/_TeardownTestDB.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 2bd02f5..b26dc3f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -139,6 +139,19 @@
},
"justMyCode": true
},
+ {
+ "name": "make migration",
+ "type": "python",
+ "request": "launch",
+ "program": "${workspaceFolder}/openIMIS/manage.py",
+ "args": ["makemigrationsy"],
+ "django": true,
+ "cwd": "${workspaceRoot}/openIMIS",
+ "env": {
+ "DB_DEFAULT": "${input:dbEngine}"
+ },
+ "justMyCode": true
+ },
{
"name": "Start",
"type": "python",
diff --git a/Dockerfile b/Dockerfile
index 68c80e2..4f842cb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -31,8 +31,8 @@ WORKDIR /openimis-be
ARG OPENIMIS_CONF_JSON
ENV OPENIMIS_CONF_JSON=${OPENIMIS_CONF_JSON}
-RUN python modules-requirements.py openimis.json > modules-requirements.txt && pip install -r modules-requirements.txt
-
+WORKDIR /openimis-be/script
+RUN python modules-requirements.py ../openimis.json > modules-requirements.txt && pip install -r modules-requirements.txt
WORKDIR /openimis-be/openIMIS
# Compile messages (Exclude zh_Hans)
diff --git a/Dockerfile_win b/Dockerfile_win
deleted file mode 100644
index ec9ccc3..0000000
--- a/Dockerfile_win
+++ /dev/null
@@ -1,36 +0,0 @@
-ARG PYTHON_VERSION="3.7"
-ARG WINDOWS_VERSION="ltsc2016"
-FROM "python:${PYTHON_VERSION}-windowsservercore-${WINDOWS_VERSION}"
-ARG GETTEXT_VERSION="0.19.8.1"
-ARG OPENIMIS_VERSION="1.4.1"
-ENV GETTEXT_URL="https://github.com/vslavik/gettext-tools-windows/releases/download/v${GETTEXT_VERSION}/gettext-tools-windows-${GETTEXT_VERSION}.zip"
-LABEL vendor="openIMIS"\
- maintainer="Patrick Delcroix "\
- org.openimis.fe.is-beta= \
- org.openimis.fe.is-production="" \
- org.openimis.fe.version="${OPENIMIS_VERSION}"
-SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
-RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;\
- Invoke-WebRequest -Uri "https://chocolatey.org/install.ps1" -UseBasicParsing | iex ; \
- choco install curl -y --no-progress ; \
- choco install sqlserver-odbcdriver -y --no-progress;\
- Write-Output $Env:GETTEXT_URL;\
- Invoke-WebRequest -Uri $Env:GETTEXT_URL -OutFile gettext.zip ;\
- Expand-Archive -Path gettext.zip -DestinationPath C:\gettext;
-ENV PYTHONUNBUFFERED 1
-RUN pip install --upgrade pip;
-COPY . /openimis-be
-WORKDIR /openimis-be
-RUN pip install -r requirements.txt
-RUN python modules-requirements.py openimis.json > modules-requirements.txt
-RUN pip install -r modules-requirements.txt
-WORKDIR /openimis-be/openIMIS
-RUN $env:PATH = 'C:\gettext\bin;' + $env:PATH; \
- [Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine);
-RUN refreshenv;Set-Item -Path Env:NO_DATABASE -value 'True'; python manage.py compilemessages | Write-Output
-RUN refreshenv;Set-Item -Path Env:NO_DATABASE -value 'True';python manage.py collectstatic --clear --noinput | Write-Output
-ENTRYPOINT ["powershell ", "-Command","/openimis-be/script/entrypoint.ps1"]
-# CMD ["powershell ", "-Command","/openimis-be/script/entrypoint.ps1"]
-CMD ["start","database","1433","5"]
-ENV REMOTE_USER_AUTHENTICATION = False
-#HEALTHCHECK ["powershell ", "-Command","/openimis-be/script/healthcheck.ps1"]
diff --git a/README.md b/README.md
index e0a0272..3fc617d 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,13 @@
| CACHE_BACKEND | String | Specifies the [caching backend](https://docs.djangoproject.com/en/5.0/topics/cache/#setting-up-the-cache) to be used. Default is set to PyMemcached. |
| CACHE_URL | String | Defines the location of the cache backend. Default is `unix:/tmp/memcached.sock` for a Unix socket connection. |
| CACHE_OPTIONS | String | A JSON string representing a dictionary of additional options passed to the cache backend. Empty by default |
+| RATELIMIT_CACHE | String | The cache alias to use for rate limiting. Defaults to `default`. |
+| RATELIMIT_KEY | String | Key to identify the client for rate limiting; `ip` means it will use the client's IP address. Defaults to `ip`. |
+| RATELIMIT_RATE | String | Rate limit value (e.g., `150/m` for 150 requests per minute). Defaults to `150/m`. |
+| RATELIMIT_METHOD | String | HTTP methods to rate limit; `ALL` means all methods. Defaults to `ALL`. |
+| RATELIMIT_GROUP | String | Group name for the rate limit. Defaults to `graphql`. |
+| RATELIMIT_SKIP_TIMEOUT | Boolean | Whether to skip rate limiting during cache timeout. Defaults to `False`. |
+| CSRF_TRUSTED_ORIGINS | String | Define the trusted origins for CSRF protection, separated by commas. Defaults to `http://localhost:3000,http://localhost:8000`. |
## Developers setup
@@ -120,7 +127,7 @@ At this stage, you may (depends on the database you connect to) need to:
### To manage translations of your module
-- from your module root dir, execute '../openimis-be_py/gettext.sh'
+- from your module root dir, execute '../openimis-be_py/script/gettext.sh'
... this extract all your translations keys from your code into your module root dir/locale/en/LC_MESSAGES/django.po
- you may want to provide translation in generated django.po file... or manage them via lokalize (need to upload the keys,...)
@@ -186,9 +193,7 @@ When release candidate is accepted:
- from tarball: `https://github.com/openimis/openimis-be_py/archive/v1.1.0.tar.gz`
- (required only once)`python -m venv ./venv`: create the python venv
- `./venv/Script/activate[.sh/.ps1]`: Activate the venv
-- `python modules-list.py openimis.json > module-list.txt`: list the module to install
-- `python -m pip uninstall -r module-list.txt`: uninstall the previously installed module
-- `python modules-requirements.py openimis.json > modules-requirements.txt`: list the source of the module to install
+- `python script/modules-requirements.py openimis.json > modules-requirements.txt`: list the source of the module to install
- `python -m pip install -r modules-requirements.txt`: Install the modules
- `cp .env.example .env`: Copy the example environment setup and adjust the variables (refer to .env.example for more info)
- `python manage.py migrate`: execute the migrations
@@ -322,6 +327,73 @@ module skeleton in single command` section
to extract frontend translations of all modules present in `openimis.json`.
- those translations will be copied into 'extracted_translations_fe' folder in assembly backend module
+### JWT Security Configuration
+
+To enhance JWT token security, you can configure the system to use RSA keys for signing and verifying tokens.
+
+1. **Generate RSA Keys**:
+ ```bash
+ # Generate a private key
+ openssl genpkey -algorithm RSA -out jwt_private_key.pem -aes256
+
+ # Generate a public key
+ openssl rsa -pubout -in jwt_private_key.pem -out jwt_public_key.pem
+
+2. **Store RSA Keys**:
+ Place jwt_private_key.pem and jwt_public_key.pem in a secure directory within your project, e.g., keys/.
+
+3. **Django Configuration**:
+ Ensure that the settings.py file is configured to read these keys. If RSA keys are found, the system will use RS256. Otherwise, it will fallback to HS256 using DJANGO_SECRET_KEY.
+
+Note: If RSA keys are not provided, the system defaults to HS256. Using RS256 with RSA keys is recommended for enhanced security.
+
+
+## CSRF Setup Guide
+
+CSRF (Cross-Site Request Forgery) protection ensures that unauthorized commands are not performed on behalf of authenticated users without their consent. It achieves this by including a unique token in each form submission or AJAX request, which is then validated by the server.
+When using JWT (JSON Web Token) for authentication, CSRF protection is not executed because the server does not rely on cookies for authentication. Instead, the JWT is included in the request headers, making CSRF attacks less likely.
+
+### Development Environment
+
+In the development environment, CSRF protection is configured to allow requests from `localhost:3000` and `localhost:8000` by default in .env.example file.
+
+### Production Environment
+
+In the production environment, you need to specify the trusted origins in your `.env` file.
+
+1. **Trusted Origins Setup**:
+ - Define the trusted origins in your `.env` file to allow cross-origin requests from specific domains.
+ - Use a comma-separated list to specify multiple origins.
+ - Example of setting trusted origins in `.env`:
+ ```env
+ CSRF_TRUSTED_ORIGINS=https://example.com,https://api.example.com
+ ```
+
+
+## Security Headers
+
+This section describes the security headers used in the application, based on OWASP recommendations, to enhance the security of your Django application.
+
+### Security Headers in Production
+
+In the production environment, several security headers are set to protect the application from common vulnerabilities:
+
+- **Strict-Transport-Security**: `max-age=63072000; includeSubDomains` - Enforces secure (HTTP over SSL/TLS) connections to the server and ensures all subdomains also follow this rule.
+- **Content-Security-Policy**: `default-src 'self';` - Prevents a wide range of attacks, including Cross-Site Scripting (XSS), by restricting sources of content to the same origin.
+- **X-Frame-Options**: `DENY` - Protects against clickjacking attacks by preventing the page from being framed.
+- **X-Content-Type-Options**: `nosniff` - Prevents the browser from MIME-sniffing the content type, ensuring that the browser uses the declared content type.
+- **Referrer-Policy**: `no-referrer` - Controls how much referrer information is included with requests by not sending any referrer information with requests.
+- **Permissions-Policy**: `geolocation=(), microphone=()` - Controls access to browser features by disabling access to geolocation and microphone features.
+
+In production, additional security settings are applied to cookies used for CSRF and JWT:
+
+- **CSRF_COOKIE_SECURE**: Ensures the CSRF cookie is only sent over HTTPS.
+- **CSRF_COOKIE_HTTPONLY**: Prevents JavaScript from accessing the CSRF cookie.
+- **CSRF_COOKIE_SAMESITE**: Sets the `SameSite` attribute to 'Lax', which allows the cookie to be sent with top-level navigations and gets rid of the risk of CSRF attacks.
+- **JWT_COOKIE_SECURE**: Ensures the JWT cookie is only sent over HTTPS.
+- **JWT_COOKIE_SAMESITE**: Sets the `SameSite` attribute to 'Lax' for the JWT cookie.
+
+
## Custom exception handler for new modules REST-based modules
If the module you want to add to the openIMIS uses its own REST exception handler you have to register
diff --git a/dev-requirements.txt b/dev-requirements.txt
deleted file mode 100644
index cdd61a7..0000000
--- a/dev-requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file includes requirements that are not necessary for production systems
-# faker, for example is used to generate test data
--r requirements.txt
-faker==8.1.3
diff --git a/dev_build_launch.ps1 b/dev_build_launch.ps1
deleted file mode 100644
index 4d27dd7..0000000
--- a/dev_build_launch.ps1
+++ /dev/null
@@ -1,92 +0,0 @@
-# Sync file
-# Requires
-# - Python
-# - GIT
-
-$base = "c:\dev\fs"
-$branch = "develop"
-
-$reposBranch ="openimis-be-contribution_py"`
-,"openimis-be-contribution_plan_py"`
-,"openimis-be-core_py"`
-,"openimis-be-calculation_py"`
-,"openimis-be-policyholder_py"`
-,"openimis-be-contract_py"
-
-# create base if not existing
-if ( -Not (Test-Path -Path $base) ){
- mkdir $base
-}
-
-
-cd $base
-
-if ( -Not (Test-Path -Path openimis-be_py) ){
- Invoke-Expression "git clone https://github.com/openimis/openimis-be_py.git --quiet"
-}
-cd openimis-be_py
-# get the other file from git
-#git checkout $branch --quiet -f
-
-
- $reposBranch | ForEach-Object -Process {
- $array = $_.split('@')
- $repo = $array[0];
-
- $curBranch = if ($array.Count -eq 2) {$array[1]} else {$branch}
- Write-output "Pulling repository $repo, branch $curBranch"
- # FIXMEfetch the repository if not existing
- if ( -Not (Test-Path -Path $repo )){
- Invoke-Expression "git clone https://github.com/openimis/$repo.git --quiet"
- }
- # get the other file from git
- cd $repo
-
- git fetch
- git pull
- git checkout $curBranch -f
- cd $base
-}
-
-# build the front end
-
-cd openimis-be_py
-
-
-
-# assuming venv and openimis-be_py folders are on the same level
-if ( -Not (Test-Path -Path ..\venv) ){
- Invoke-Expression "python -m venv ..\venv"
-}
-Invoke-Expression ..\venv\Scripts\Activate.ps1
-$OPENIMIS_CONF='openimis.json '
-pip install -r requirements.txt
-python modules-requirements.py $OPENIMIS_CONF > modules-requirements.txt
-pip install -r modules-requirements.txt
-cd openIMIS
-$SITE_ROOT='iapi'
-$DB_NAME='IMISfs'
-$DB_USER='IMISuser'
-$DB_PASSWORD='IMISuser@1234'
-$DB_HOST='127.0.0.1'
-$DB_PORT='1433'
-$DJANGO_PORT='8000'
-
-
-[Environment]::SetEnvironmentVariable("SITE_ROOT", $SITE_ROOT)
-[Environment]::SetEnvironmentVariable("REMOTE_USER_AUTHENTICATION", "True")
-[Environment]::SetEnvironmentVariable("ROW_SECURITY", "False")
-[Environment]::SetEnvironmentVariable("DEBUG", "True")
-[Environment]::SetEnvironmentVariable("DB_NAME", $DB_NAME)
-[Environment]::SetEnvironmentVariable("DB_USER", $DB_USER)
-[Environment]::SetEnvironmentVariable("DB_PASSWORD", $DB_PASSWORD)
-[Environment]::SetEnvironmentVariable("DB_HOST", $DB_HOST)
-[Environment]::SetEnvironmentVariable("DB_PORT", $DB_PORT)
-[Environment]::SetEnvironmentVariable("DJANGO_PORT", $DJANGO_PORT)
-[Environment]::SetEnvironmentVariable("OPENIMIS_CONF", "../"$OPENIMIS_CONF)
-
-python manage.py migrate
-python manage.py runserver 0.0.0.0:$DJANGO_PORT
-
-
-
diff --git a/dev_launch.ps1 b/dev_launch.ps1
deleted file mode 100644
index 5031c5a..0000000
--- a/dev_launch.ps1
+++ /dev/null
@@ -1,22 +0,0 @@
-# assuming venv and openimis-be_py folders are on the same level
-..\venv\Scripts\Activate.ps1
-cd openIMIS
-$SITE_ROOT='iapi'
-$DB_NAME='IMISfs'
-$DB_USER='IMISuser'
-$DB_PASSWORD='IMISuser@1234'
-$DB_HOST='127.0.0.1'
-$DB_PORT='1433'
-$DJANGO_PORT='8000'
-[Environment]::SetEnvironmentVariable("SITE_ROOT", $SITE_ROOT)
-[Environment]::SetEnvironmentVariable("REMOTE_USER_AUTHENTICATION", "False")
-[Environment]::SetEnvironmentVariable("ROW_SECURITY", "False")
-[Environment]::SetEnvironmentVariable("DEBUG", "True")
-[Environment]::SetEnvironmentVariable("DB_NAME", $DB_NAME)
-[Environment]::SetEnvironmentVariable("DB_USER", $DB_USER)
-[Environment]::SetEnvironmentVariable("DB_PASSWORD", $DB_PASSWORD)
-[Environment]::SetEnvironmentVariable("DB_HOST", $DB_HOST)
-[Environment]::SetEnvironmentVariable("DB_PORT", $DB_PORT)
-[Environment]::SetEnvironmentVariable("DJANGO_PORT", $DJANGO_PORT)
-[Environment]::SetEnvironmentVariable("OPENIMIS_CONF", "../"$OPENIMIS_CONF)
-python manage.py runserver
\ No newline at end of file
diff --git a/dev_launch.sh b/dev_launch.sh
deleted file mode 100755
index 34f123b..0000000
--- a/dev_launch.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-source venv/bin/activate
-cd openIMIS
-SITE_ROOT=api REMOTE_USER_AUTHENTICATION=False ROW_SECURITY=False DEBUG=True python manage.py runserver
diff --git a/modules-links.py b/modules-links.py
deleted file mode 100644
index ca375aa..0000000
--- a/modules-links.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import os
-import json
-import sys
-
-def load_openimis_conf():
- conf_file_path = sys.argv[1]
- if not conf_file_path:
- sys.exit("Missing config file path argument")
- if not os.path.isfile(conf_file_path):
- sys.exit("Config file parameter refers to missing file %s" % conf_file_path)
-
- with open(conf_file_path) as conf_file:
- return json.load(conf_file)
-
-def extract_requirement(module):
- return "ln -s ../../openimis-be-%s_py %s" % (module["name"], module["name"])
-
-OPENIMIS_CONF = load_openimis_conf()
-MODULES = list(map(extract_requirement, OPENIMIS_CONF["modules"]))
-print("\n".join(MODULES))
\ No newline at end of file
diff --git a/modules-list.py b/modules-list.py
deleted file mode 100644
index 59c29bc..0000000
--- a/modules-list.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import os
-import json
-import sys
-
-def load_openimis_conf():
- conf_file_path = sys.argv[1]
- if not conf_file_path:
- sys.exit("Missing config file path argument")
- if not os.path.isfile(conf_file_path):
- sys.exit("Config file parameter refers to missing file %s" % conf_file_path)
-
- with open(conf_file_path) as conf_file:
- return json.load(conf_file)
-
-def extract_requirement(module):
- return "%s" % module["name"]
-
-OPENIMIS_CONF = load_openimis_conf()
-MODULES = list(map(extract_requirement, OPENIMIS_CONF["modules"]))
-print("\n".join(MODULES))
\ No newline at end of file
diff --git a/modules-unlinks.py b/modules-unlinks.py
deleted file mode 100644
index a736388..0000000
--- a/modules-unlinks.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import os
-import json
-import sys
-
-def load_openimis_conf():
- conf_file_path = sys.argv[1]
- if not conf_file_path:
- sys.exit("Missing config file path argument")
- if not os.path.isfile(conf_file_path):
- sys.exit("Config file parameter refers to missing file %s" % conf_file_path)
-
- with open(conf_file_path) as conf_file:
- return json.load(conf_file)
-
-def extract_requirement(module):
- return "rm %s" % module["name"]
-
-OPENIMIS_CONF = load_openimis_conf()
-MODULES = list(map(extract_requirement, OPENIMIS_CONF["modules"]))
-print("\n".join(MODULES))
\ No newline at end of file
diff --git a/openIMIS/apscheduler_runner/apps.py b/openIMIS/apscheduler_runner/apps.py
index 125974e..d3c493b 100644
--- a/openIMIS/apscheduler_runner/apps.py
+++ b/openIMIS/apscheduler_runner/apps.py
@@ -5,6 +5,8 @@
from django.conf import settings
from django.apps import AppConfig
from copy import deepcopy
+import importlib.util
+
logger = logging.getLogger(__name__)
@@ -27,14 +29,14 @@ def _setup_scheduler_background_task(self):
self.scheduler.start()
def __add_module_tasks_to_scheduler(self, app_):
- try:
- module = __import__(f"{app_}.scheduled_tasks")
- if hasattr(module.scheduled_tasks, "schedule_tasks"):
- module.scheduled_tasks.schedule_tasks(self.scheduler)
+ spec = importlib.util.find_spec(f"{app_}.scheduled_tasks")
+ if spec:
+ try:
+ app = __import__(f"{app_}.scheduled_tasks")
+ app.scheduled_tasks.schedule_tasks(self.scheduler)
logger.debug(f"{app_} tasks scheduled")
- else:
- logger.debug(f"{app_} has a scheduled_tasks package but no schedule_tasks callable")
- except ModuleNotFoundError as exc:
+ except Exception as exc:
+ logger.debug(f"{app_}: unknown exception occurred during registering scheduled tasks: {exc}")
+ else:
logger.debug(f"{app_} has no scheduled_tasks module, skipping")
- except Exception as exc:
- logger.debug(f"{app_}: unknown exception occurred during registering scheduled tasks: {exc}")
+
diff --git a/openIMIS/openIMIS/asgi.py b/openIMIS/openIMIS/asgi.py
index e84aac7..563c093 100644
--- a/openIMIS/openIMIS/asgi.py
+++ b/openIMIS/openIMIS/asgi.py
@@ -1,5 +1,4 @@
from channels.auth import AuthMiddlewareStack
-import dashboard_etl.routing
import json
import os
import logging
diff --git a/openIMIS/openIMIS/openimisapps.py b/openIMIS/openIMIS/openimisapps.py
index 202bb6f..0200373 100644
--- a/openIMIS/openIMIS/openimisapps.py
+++ b/openIMIS/openIMIS/openimisapps.py
@@ -21,8 +21,11 @@ def get_locale_folders():
basedirs = []
for mod in load_openimis_conf()["modules"]:
mod_name = mod["name"]
- with resources.path(mod_name, "__init__.py") as path:
- apps.append(path.parent.parent) # This might need to be more restrictive
+ try:
+ with resources.path(mod_name, "__init__.py") as path:
+ apps.append(path.parent.parent)
+ except ModuleNotFoundError:
+ raise Exception(f"Module \"{mod_name}\" not found.")
for topdir in ["."] + apps:
for dirpath, dirnames, filenames in os.walk(topdir, topdown=True):
diff --git a/openIMIS/openIMIS/settings.py b/openIMIS/openIMIS/settings.py
index 85432ef..f3df59f 100644
--- a/openIMIS/openIMIS/settings.py
+++ b/openIMIS/openIMIS/settings.py
@@ -8,6 +8,7 @@
from dotenv import load_dotenv
from .openimisapps import openimis_apps, get_locale_folders
from datetime import timedelta
+from cryptography.hazmat.primitives import serialization
load_dotenv()
@@ -169,7 +170,8 @@ def SITE_URL():
"django_apscheduler",
"channels", # Websocket support
"developer_tools",
- "drf_spectacular" # Swagger UI for FHIR API
+ "drf_spectacular", # Swagger UI for FHIR API
+ "axes",
]
INSTALLED_APPS += OPENIMIS_APPS
INSTALLED_APPS += ["apscheduler_runner", "signal_binding"] # Signal binding should be last installed module
@@ -180,6 +182,7 @@ def SITE_URL():
AUTHENTICATION_BACKENDS += ["django.contrib.auth.backends.RemoteUserBackend"]
AUTHENTICATION_BACKENDS += [
+ "axes.backends.AxesStandaloneBackend",
"rules.permissions.ObjectPermissionBackend",
"graphql_jwt.backends.JSONWebTokenBackend",
"django.contrib.auth.backends.ModelBackend",
@@ -216,13 +219,31 @@ def SITE_URL():
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
+ 'core.middleware.GraphQLRateLimitMiddleware',
+ "axes.middleware.AxesMiddleware",
+ "core.middleware.DefaultAxesAttributesMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
+ "core.middleware.SecurityHeadersMiddleware",
]
+MODE = os.environ.get("MODE")
+
+# Lockout mechanism configuration
+AXES_ENABLED = True if os.environ.get("MODE", "DEV") == "PROD" else False
+AXES_FAILURE_LIMIT = int(os.getenv("LOGIN_LOCKOUT_FAILURE_LIMIT", 5))
+AXES_COOLOFF_TIME = timedelta(minutes=int(os.getenv("LOGIN_LOCKOUT_COOLOFF_TIME", 5)))
+
+RATELIMIT_CACHE = os.getenv('RATELIMIT_CACHE', 'default')
+RATELIMIT_KEY = os.getenv('RATELIMIT_KEY', 'ip')
+RATELIMIT_RATE = os.getenv('RATELIMIT_RATE', '150/m')
+RATELIMIT_METHOD = os.getenv('RATELIMIT_METHOD', 'ALL')
+RATELIMIT_GROUP = os.getenv('RATELIMIT_GROUP', 'graphql')
+RATELIMIT_SKIP_TIMEOUT = os.getenv('RATELIMIT_SKIP_TIMEOUT', 'False')
+
if DEBUG:
# Attach profiler middleware
MIDDLEWARE.append(
@@ -271,7 +292,6 @@ def SITE_URL():
GRAPHQL_JWT = {
"JWT_VERIFY_EXPIRATION": True,
- "JWT_LONG_RUNNING_REFRESH_TOKEN": True,
"JWT_EXPIRATION_DELTA": timedelta(days=1),
"JWT_REFRESH_EXPIRATION_DELTA": timedelta(days=30),
"JWT_AUTH_HEADER_PREFIX": "Bearer",
@@ -288,6 +308,50 @@ def SITE_URL():
],
}
+# Load RSA keys
+private_key_path = os.path.join(BASE_DIR, 'keys', 'jwt_private_key.pem')
+public_key_path = os.path.join(BASE_DIR, 'keys', 'jwt_public_key.pem')
+
+if os.path.exists(private_key_path) and os.path.exists(public_key_path):
+ with open(private_key_path, 'rb') as f:
+ private_key = serialization.load_pem_private_key(
+ f.read(),
+ password=None,
+ )
+
+ with open(public_key_path, 'rb') as f:
+ public_key = serialization.load_pem_public_key(
+ f.read(),
+ )
+
+ # If RSA keys exist, update the algorithm and add keys to GRAPHQL_JWT settings
+ GRAPHQL_JWT.update({
+ "JWT_ALGORITHM": "RS256",
+ "JWT_PRIVATE_KEY": private_key,
+ "JWT_PUBLIC_KEY": public_key,
+ })
+
+if MODE == "PROD":
+ # Enhance security in production
+ GRAPHQL_JWT.update({
+ "JWT_COOKIE_SECURE": True,
+ "JWT_COOKIE_SAMESITE": "Lax",
+ })
+
+ CSRF_COOKIE_SECURE = True
+ CSRF_COOKIE_HTTPONLY = True
+ CSRF_COOKIE_SAMESITE = 'Lax'
+
+ SECURE_BROWSER_XSS_FILTER = True
+ SECURE_CONTENT_TYPE_NOSNIFF = True
+ SECURE_HSTS_SECONDS = 63072000
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = True
+ SECURE_HSTS_PRELOAD = True
+ SECURE_SSL_REDIRECT = True
+
+csrf_trusted_origins = os.environ.get('CSRF_TRUSTED_ORIGINS', default='')
+CSRF_TRUSTED_ORIGINS = csrf_trusted_origins.split(',') if csrf_trusted_origins else []
+
# no db
DATABASES = {}
DB_DEFAULT = os.environ.get("DB_DEFAULT", 'postgresql')
@@ -321,14 +385,14 @@ def SITE_URL():
"unicode_results": True,
}
PSQL_DATABASE_OPTIONS = {'options': '-c search_path=django,public'}
-
+
DEFAULT_ENGINE = os.environ.get("DB_ENGINE", "mssql" if DB_DEFAULT == 'mssql' else "django.db.backends.postgresql")
DEFAULT_NAME = os.environ.get("DB_NAME", "imis")
DEFAULT_USER = os.environ.get("DB_USER", "IMISuser")
DEFAULT_PASSWORD = os.environ.get("DB_PASSWORD")
DEFAULT_HOST = os.environ.get("DB_HOST", 'db')
DEFAULT_PORT = os.environ.get("DB_PORT", "1433" if DB_DEFAULT == 'mssql' else "5432")
-
+
if DB_DEFAULT == 'mssql':
@@ -455,20 +519,15 @@ def SITE_URL():
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
-AUTH_PASSWORD_VALIDATORS = [
- {
- "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
- },
-]
+if not DEBUG:
+ AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "core.utils.CustomPasswordValidator",
+ }
+ ]
# Internationalization
@@ -550,3 +609,9 @@ def SITE_URL():
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
+PASSWORD_MIN_LENGTH = int(os.getenv('PASSWORD_MIN_LENGTH', 8))
+PASSWORD_UPPERCASE = int(os.getenv('PASSWORD_UPPERCASE', 1))
+PASSWORD_LOWERCASE = int(os.getenv('PASSWORD_LOWERCASE', 1))
+PASSWORD_DIGITS = int(os.getenv('PASSWORD_DIGITS', 1))
+PASSWORD_SYMBOLS = int(os.getenv('PASSWORD_SYMBOLS', 1))
diff --git a/openIMIS/signal_binding/apps.py b/openIMIS/signal_binding/apps.py
index e0f3be6..55a9d6f 100644
--- a/openIMIS/signal_binding/apps.py
+++ b/openIMIS/signal_binding/apps.py
@@ -1,6 +1,7 @@
import logging
from django.apps import AppConfig
from django.conf import settings
+import importlib.util
logger = logging.getLogger(__name__)
@@ -17,13 +18,22 @@ def bind_service_signals(self):
def _bind_app_signals(self, app_):
try:
- signals_module = __import__(f"{app_}.signals")
- if hasattr(signals_module.signals, "bind_service_signals"):
- signals_module.signals.bind_service_signals()
- logger.debug(f"{app_} service signals connected")
+ spec = importlib.util.find_spec(f"{app_}.signals")
+ if spec:
+ app = __import__(f"{app_}.signals")
+ if (
+ hasattr(app, "signals") and
+ hasattr(app.signals, "bind_service_signals")
+ ):
+ app.signals.bind_service_signals()
+ logger.debug(f"{app_} service signals connected")
+ else:
+ logger.debug(
+ f"{app_} has signals but no bind_service_signals function"
+ )
else:
- logger.debug(f"{app_} has a signals module but no bind_service_signals function")
- except ModuleNotFoundError as exc:
- logger.debug(f"{app_} has no signals module, skipping")
+ logger.debug(
+ f"{app_} has no signals submodule"
+ )
except Exception as exc:
logger.debug(f"{app_}: unknown exception occurred during bind_service_signals: {exc}")
diff --git a/openimis.json b/openimis.json
index a305321..4fa9b6c 100644
--- a/openimis.json
+++ b/openimis.json
@@ -155,6 +155,10 @@
{
"name": "grievance",
"pip": "git+https://github.com/openimis/openimis-be-grievance_py.git@develop#egg=openimis-be-grievance"
+ },
+ {
+ "name": "claim_sampling",
+ "pip": "git+https://github.com/openimis/openimis-be-claim_sampling_py.git@develop#egg=openimis-be-claim_sampling"
}
]
}
diff --git a/requirements.txt b/requirements.txt
index 73a109a..5bc8b0b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -32,7 +32,7 @@ waitress
wheel
whitenoise
django-health-check
-requests~=2.31.0
+requests~=2.32.0
apscheduler==3.10.1
# As from v0.4, Django-apscheduler has a migration that is incompatible with SQL Server
# (autoincrement int => bigint) so we are using our own fork with a squashed migration
@@ -52,4 +52,8 @@ twisted>=23.10.0rc1 # not directly required, pinned by Snyk to avoid a vulnerabi
pillow>=10.2.0 # not directly required, pinned by Snyk to avoid a vulnerability
django-redis==5.4.0
-django-opensearch-dsl==0.5.1
\ No newline at end of file
+django-opensearch-dsl==0.5.1
+
+zxcvbn~=4.4.28
+password-validator==1.0
+django-axes==6.4.0
diff --git a/gettext.sh b/script/gettext.sh
old mode 100755
new mode 100644
similarity index 100%
rename from gettext.sh
rename to script/gettext.sh
diff --git a/lokalise-upload.py b/script/lokalise-upload.py
old mode 100755
new mode 100644
similarity index 100%
rename from lokalise-upload.py
rename to script/lokalise-upload.py
diff --git a/modules-requirements.py b/script/modules-requirements.py
similarity index 81%
rename from modules-requirements.py
rename to script/modules-requirements.py
index 4d4474b..56520c9 100644
--- a/modules-requirements.py
+++ b/script/modules-requirements.py
@@ -3,7 +3,8 @@
import sys
-sys.path.insert(0, './openIMIS/openIMIS')
+app_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', "openIMIS", "openIMIS")
+sys.path.insert(0, app_path)
from openimisconf import load_openimis_conf
conf_file_path = 'openimis.json'
diff --git a/modules-tests.py b/script/modules-tests.py
similarity index 76%
rename from modules-tests.py
rename to script/modules-tests.py
index 7e5f405..6e35fd7 100644
--- a/modules-tests.py
+++ b/script/modules-tests.py
@@ -3,16 +3,9 @@
import sys
import itertools
from distutils.sysconfig import get_python_lib
-
-def load_openimis_conf():
- conf_file_path = sys.argv[1]
- if not conf_file_path:
- sys.exit("Missing config file path argument")
- if not os.path.isfile(conf_file_path):
- sys.exit("Config file parameter refers to missing file %s" % conf_file_path)
-
- with open(conf_file_path) as conf_file:
- return json.load(conf_file)
+app_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', "openIMIS", "openIMIS")
+sys.path.insert(0, app_path)
+from openimisconf import load_openimis_conf
def extract_test(module):
cmds = [
diff --git a/script/setup-local-dev.py b/script/setup-local-dev.py
index ed05c0b..08acea1 100644
--- a/script/setup-local-dev.py
+++ b/script/setup-local-dev.py
@@ -2,50 +2,71 @@
from utils import parse_pip, walk_config_be
import os
import json
-import git # pip install GitPython
-from github import Github # pip install pyGithub
+import git # pip install GitPython
+from github import Github # pip install pyGithub
+import subprocess, sys
-ref = 'develop'
-ref_assembly = 'develop'
+ref = BRANCH#"develop"
+ref_assembly = BRANCH#"develop"
def main():
- g=Github(GITHUB_TOKEN)
- #assembly_fe='openimis/openimis-fe_js'
- assembly_be='openimis/openimis-be_py'
- #refresh openimis.json from git
-
+ g = Github(GITHUB_TOKEN)
+ # assembly_fe='openimis/openimis-fe_js'
+ assembly_be = "openimis/openimis-be_py"
+ # refresh openimis.json from git
+
be_config = []
repo = g.get_repo(assembly_be)
- be = json.loads(repo.get_contents("openimis.json", ref =ref_assembly ).decoded_content)
- be['modules'] = walk_config_be(g,be,clone_repo)
+ be = json.loads(
+ repo.get_contents("openimis.json", ref=ref_assembly).decoded_content
+ )
+ be["modules"] = walk_config_be(g, be, clone_repo)
# Writing to sample.json
- with open("../openimis.json", "w") as outfile:
- outfile.write(json.dumps(be, indent = 4, default=set_default) )
-
-def clone_repo(repo, module_name):
- src_path = os.path.abspath('../src/')
+ with open("../openimis-dev.json", "w") as outfile:
+ outfile.write(json.dumps(be, indent=4, default=set_default))
+ install_modules()
+
+def install_modules():
+ print("installing dependencies and modules")
+ root_path = os.path.abspath("../")
+ command = f'pip install {root_path}/requirements.txt & python modules-requirements.py openimis-dev.json > modules-requirements.txt & pip install -r modules-requirements.txt'
+
+ try:
+ result = subprocess.check_output(command, shell = True, executable = "/bin/bash", stderr = subprocess.STDOUT)
+
+ except subprocess.CalledProcessError as cpe:
+ result = cpe.output
+ return result
+
+
+def clone_repo(repo, module_name):
+ src_path = os.path.abspath("../src/")
path = os.path.join(src_path, module_name)
remote = f"https://{USER_NAME}:{GITHUB_TOKEN}@{repo.git_url[6:]}"
if os.path.exists(path):
-
+
repo_git = git.Repo(path)
try:
repo_git.git.checkout(ref)
repo_git.remotes.origin.pull()
print(f"{module_name} pulled and checked out")
except:
- print(f'error while checking out {module_name} to {ref}, please ensure the local changes are commited')
+ print(
+ f"error while checking out {module_name} to {ref}, please ensure the local changes are commited"
+ )
else:
print(f"cloning {module_name}")
repo_git = git.Repo.clone_from(remote, path)
repo_git.git.checkout(ref)
- return {"name":f"{module_name}", "pip":f"-e {path}"}
+ return {"name": f"{module_name}", "pip": f"-e {path}"}
+
def set_default(obj):
if isinstance(obj, set):
return list(obj)
raise TypeError
-if __name__ == '__main__':
- main()
\ No newline at end of file
+
+if __name__ == "__main__":
+ main()