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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
max-line-length = 100
ignore = E203, E266, E501, W503
ignore = E203, E266, E501, W503, E731
exclude = .yaml,.json
[pycodestyle]
max_line_length = 100
50 changes: 50 additions & 0 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Build and Push Docker Image

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,5 @@ cython_debug/

.envrc
.idea

mm-doorman.json
3 changes: 2 additions & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[settings]
known_third_party = flask,ldap3,requests
profile = black
known_third_party = apprise,fastapi,httpx,ldap3,rich,sentry_sdk,websocket
36 changes: 26 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
default_language_version:
python: python3.7

exclude: ^(migrations|venv)/

repos:
- repo: https://github.com/asottile/seed-isort-config
rev: v1.9.4
rev: v2.2.0
hooks:
- id: seed-isort-config

- repo: git://github.com/pre-commit/mirrors-isort
rev: v4.3.21
- repo: https://github.com/pycqa/isort
rev: 9.0.0a3
hooks:
- id: isort
name: isort (python)

- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
name: flake8 (python)

- repo: git://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-builtin-literals
- id: trailing-whitespace
- id: end-of-file-fixer
- id: flake8
- id: check-ast

- repo: https://github.com/ambv/black
rev: 19.10b0
rev: 26.3.1
hooks:
- id: black

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.16.0
hooks:
- id: mypy
additional_dependencies:
- fastapi
- httpx
- apprise
- ldap3
- websocket-client
- rich
- sentry-sdk
23 changes: 17 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
FROM tiangolo/meinheld-gunicorn-flask:python3.9
COPY ./doorman /app/doorman
COPY requirements.txt /app
RUN pip install --upgrade pip
RUN pip install -r /app/requirements.txt
FROM python:3.14-alpine3.23

ENV MODULE_NAME=doorman.app
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

COPY ./doorman ./doorman
COPY ./websocket_client.py .
COPY ./main.py .

EXPOSE 5000

# Keep --workers at 1 (uvicorn's default). Each worker spawns its own
# MemberMatters websocket client; multiple workers would produce duplicate
# door_access events.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# doorman
A solution for connecting the Fanvil i31s to FreeIPA
A solution for connecting the Fanvil i31s (or compatible) line of door phones to LDAP for access control.
Also supports the [MemberMatters](https://github.com/membermatters/MemberMatters) door controller protocol, with optional
fallback to LDAP.

# Quickstart

Required:
* python 3.7+
* python 3.9+

Suggested:
* [direnv](https://direnv.net/docs/installation.html)
Expand Down Expand Up @@ -37,6 +39,26 @@ cp envrc.example .env

# 3. Edit your config to suit

# 4. Run with:
docker run -d -p 5000:80 --name=doorman --env-file=.env doorman:latest
# 4. Run the webserver with:
docker run -d -p 5000:80 --name=doorman --env-file=.env -v ./mm-doorman.json:/tmp/mm-doorman.json doorman:latest

# 5. For use with Member Matters, run the websocket client:
docker run -d --name=doorman_ws --env-file=.env -v ./mm-doorman.json:/tmp/mm-doorman.json doorman:latest python3 websocket_client.py
```

# Configure for use with MemberMatters

## Add API key for access control device

1. In the MemberMatters Django Admin (`/admin` URL), navigate to **Access** > **API Key for Access Controlled Devices**, and click **Add**.
2. Give your connector an unique name less than 50 characters. Set that same name for `DOORMAN_MM_DEVICE_NAME` variable for doorman.
3. Optionally set an expiration date for the key. Click **Save**.
4. The API key will be flashed at the top of the screen in an orange box. Use that value for `DOORMAN_MM_API_KEY`.

![MM API key add screen](docs/mm-api-key.png)

## Enable the connector device

1. Run `websocket_client.py`. Doorman will try to register with the server, but the server will disconnect the connector until it is authorized.
2. In the Django Admin, click on **Access** > **Doors**, then click `New Device (doorman)` (the value in perens will match `DOORMAN_MM_DEVICE_NAME` from earlier).
3. Check `Is this device authorised to access the system?`, and set any of the option checkboxes according to your setup, and click **Save**.
Binary file added docs/mm-api-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added doorman/__init__.py
Empty file.
Loading
Loading