Malan is a basic authentication service that you can drop into you microservice ecosystem, or even use as a base for a new Phoenix project.
If using Malan as an authentication service, there are 3 main endpoints you'll use:
- Create a user:
POST /api/users
- Login (get an auth token) for a user:
POST /api/sessions
- Check auth status:
GET /api/users/whoami
There are a couple of different ways to structure your application. One way to structure your app around Malan is to outsource your user and session model to Malan. Malan allows you to set an arbitrary JSON blob (called custom_attrs
) on each user, so you can pack a decent amount of info in there. The user's API token can be stored in session storage and you can easily use just the token to retrieve the relevant user from Malan. If the token is expired, revoked, or otherwise invalid then no user will be returned so you can trigger a new login page.
Another common option is to maintain a minimal User table in your app that contains the user's malan ID. If you have a number of things you want to store then this may be a better approach than jamming everything into custom_attrs
.
If you have a clone of this repo, you can start Malan easily using Docker Compose:
docker-compose up
If you are adding Malan to your current application, you can make use of the example docker-compose file. You will need to add Malan, and a Postgres for Malan to use as its data store.
version: "3.9"
services:
postgres:
image: 'docker.io/postgres:12.6-alpine'
volumes:
- 'pgdata:/var/lib/postgresql/data' # Use a docker volume for the database files
environment:
POSTGRES_USER: 'postgres'
POSTGRES_PASSWORD: 'postgres'
malan:
image: 'docker.io/freedomben/malan-dev:latest'
ports:
- "4000:4000"
environment:
DB_INIT: 'Yes'
DB_USERNAME: 'postgres'
DB_PASSWORD: 'postgres'
DB_HOSTNAME: 'postgres'
BIND_ADDR: '0.0.0.0'
depends_on:
- 'postgres'
volumes:
pgdata:
You'll need to:
Setup instructions vary by platform.
Fedora, CentOS, or RHEL:
dnf install -y elixir
Ubuntu:
apt install -y elixir
Mac OS:
brew install elixir
mix local.hex
If you need to install Phoenix separately, you can do so with hex:
mix archive.install hex phx_new 1.5.8
git clone https://github.com/freedomben/malan.git \
cd malan
mix deps.get
Note: You'll need Postgres to be running before completing this step. If you are not using docker-compose, you can make use of the script at script/start-postgres.sh
to quickly get a database running.
mix ecto.setup
mix phx.server
The Malan API is a pretty standard REST interface. For details, please visit API.md.
If your client will be in TypeScript, you can also consider using libmalan, a simple utility package that provides TypeScript methods.
Staging deploys automatically upon merge to main. Prod deploys after being tagged:
git tag "prod-$(date '+%Y-%m-%d-%H-%M-%S')"
git push --tags
You should run the web application as a non-privileged user that cannot run DDL commands, and the migrations as a privileged user who can.
CREATE ROLE malan WITH LOGIN PASSWORD '<somepassword>';
GRANT CONNECT ON DATABASE malan_prod TO user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO malan;
ALTER DEFAULT PRIVILEGES FOR ROLE
malan
IN SCHEMA
public
GRANT
SELECT, INSERT, UPDATE, DELETE
ON TABLES TO
malan;
- Official website: https://www.phoenixframework.org/
- Guides: https://hexdocs.pm/phoenix/overview.html
- Docs: https://hexdocs.pm/phoenix
- Forum: https://elixirforum.com/c/phoenix-forum
- Source: https://github.com/phoenixframework/phoenix
The CI/CD system utilizes Github Actions to run automated builds, tests, and deployments to all environments.
The Production environment is where the production instances of the application are running. Deployments to Production are fully automated but are not automatic. Deploys to Production are triggered using git tags.
The staging environment. Staging is typically a small change ahead of production to allow for testing in a "prod-like" environment
All commits, merges, and tags added to the main
branch will automatically trigger a deployment to staging.
.github/workflows/build-test-deploy.yaml
: This yaml file contains the Github-specific configuration. It tells Github Actions how to run the build, push the image, run the tests, and deploy the change.- `scripts/build-release.sh: This script contains the instructions that build the release into an image.
- `scripts/push-release.sh: This script contains the instructions that push the application image to the registry.
- `scripts/deploy-release.sh: This script contains the instructions that deploy the change to Kubernetes. It contains the bulk of the CD logic.
Many actions are logged in the audit log. Whether the action result is success or failure, it is logged. The data that is sent as part of the request is recorded for later analysis.
Here is a (non comprehensive) list of actions logged:
- Creating a user. Includes the original creation data except password
- Updating a user. Includes the changed data
- Locking a user
- Unlocking a user
- Deleting a user
- Requesting a password reset token
- Using a password reset token
- Changing a user password
- Creating a session (aka "logging in")
- Deleting a session (aka "logging out")
- Extending a session
While there aren't (currently) any REST API endpoints for logs they can be accessed through the database, either using iex
or using Postgres (examples shown using psql
).
NOTE: In order to optimize the logs table for writes, the indexes are minimal. This means there is a long and beefy table scan for querying. Keep this in mind if you have a large production table!
- Get a shell in a running container. If using Kubernetes, you can use
kubectl exec
. Substitute the pod name for a current pod in your environment. You can list them withkubectl -n malan-staging get pods
$ kubectl -n malan-staging exec -it <valid-pod-name> -- bash
- Start a
psql
shell. There is a convenient alias in the bashrc already that you can use to connect to the database for that pod:
$ psql-malan
- Run your queries. There are some examples in the next section:
Get entire log history for a user with ID ffa9c147-900b-4813-b738-9b924237fdc7
(Note this could be huge! Use caution in production)
SELECT *
FROM logs
WHERE user_id = 'ffa9c147-900b-4813-b738-9b924237fdc7'
ORDER BY logs.inserted_at DESC;
Get 10 most recent logs for a user with ID ffa9c147-900b-4813-b738-9b924237fdc7
SELECT *
FROM logs
WHERE user_id = 'ffa9c147-900b-4813-b738-9b924237fdc7'
ORDER BY logs.inserted_at DESC
LIMIT 10;
Get 10 most recent logs for a user with email address hello@example.com
SELECT logs.*
FROM logs
JOIN users ON logs.user_id = users.id
WHERE users.email = 'hello@example.com'
ORDER BY logs.inserted_at DESC
LIMIT 10;
Get the 10 most recent logs for user with ID ef886248-32b9-48c1-bd4d-303c1cda1f94
that were "Unauthorized login attempt":
SELECT *
FROM logs
WHERE user_id = 'ef886248-32b9-48c1-bd4d-303c1cda1f94'
AND what LIKE '%Unauthorized%'
ORDER BY inserted_at DESC
LIMIT 10;
Get the 10 most recent logs for user email hello@example.com
that were "Unauthorized login attempt":
SELECT logs.*
FROM logs
JOIN users ON logs.user_id = users.id
WHERE users.email = 'hello@example.com'
AND logs.what LIKE '%Unauthorized%'
ORDER BY logs.inserted_at DESC
LIMIT 10;
It's an extremely nerdy name based on a character from the Stormlight Archive series by Brandon Sanderson.