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
6 changes: 5 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,8 @@ build.sh
docker-compose.yml
Dockerfile


# Frontend build artifacts and local env files
frontend/node_modules/
frontend/dist/
frontend/.env
frontend/.env.*
21 changes: 21 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

if ! command -v uv >/dev/null 2>&1; then
echo "uv is required for hooks. Install uv and retry."
exit 1
fi

mapfile -t STAGED_FILES < <(git diff --cached --name-only --diff-filter=ACMR | grep -E '(\.py$|^pyproject\.toml$)' || true)

if [[ ${#STAGED_FILES[@]} -eq 0 ]]; then
exit 0
fi

echo "pre-commit: auto-fixing lint/style with Ruff for staged files..."
uv run ruff check --fix "${STAGED_FILES[@]}"
uv run ruff format "${STAGED_FILES[@]}"
git add "${STAGED_FILES[@]}"
26 changes: 26 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

if ! command -v uv >/dev/null 2>&1; then
echo "uv is required for hooks. Install uv and retry."
exit 1
fi

if ! git diff --quiet || ! git diff --cached --quiet; then
echo "pre-push: working tree must be clean before push."
echo "Commit or stash your changes, then push again."
exit 1
fi

echo "pre-push: running full quality gate (lint/type/tests)..."
./scripts/quality.sh fix

if ! git diff --quiet || ! git diff --cached --quiet; then
echo "pre-push: Ruff applied fixes."
echo "Review and commit these changes before pushing:"
git --no-pager diff --name-only
exit 1
fi
19 changes: 13 additions & 6 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ to the public under the [project's open source license](/LICENSE).
## Submitting a pull request

1. [Fork](https://github.com/bearlike/simple-secrets-manager/fork) and clone the repository
2. Configure and install the dependencies: `pip install -r requirements.txt`
3. Create a new branch: `git checkout -b my-branch-name`
4. Make your changes
5. Format code and check code formatting: `flake8`
6. Push to your fork and [submit a pull request](https://github.com/bearlike/simple-secrets-manager/compare)
7. Pat your self on the back and wait for your pull request to be reviewed and merged.
2. Configure and install the dependencies: `uv sync`
3. Install project hooks: `./scripts/install-git-hooks.sh`
4. Create a new branch: `git checkout -b my-branch-name`
5. Make your changes
6. Run quality checks:
- Backend: `./scripts/quality.sh check` (or `./scripts/quality.sh fix`)
- Frontend: `cd frontend && npm run lint && npm run build`
7. Push to your fork and [submit a pull request](https://github.com/bearlike/simple-secrets-manager/compare)
8. Pat your self on the back and wait for your pull request to be reviewed and merged.

Installed hooks behavior:
- `pre-commit` auto-fixes Ruff issues on staged Python files.
- `pre-push` runs `./scripts/quality.sh fix` and blocks the push if auto-fixes are needed.

Here are a few things you can do that will increase the likelihood of your pull request being accepted:

Expand Down
72 changes: 38 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
name: Build and deploy multiarch image
name: Build and publish unified Docker image

on:
push:
branches:
- main
- feat/v1.3.0
tags:
- "v*"
paths-ignore:
- "docs/**"
release:
types: [published]
paths:
- ".github/workflows/ci.yml"
- "Dockerfile"
- "docker/**"
- "frontend/**"
- "Api/**"
- "Access/**"
- "Engines/**"
- "pyproject.toml"
- "uv.lock"
- "server.py"
- "connection.py"
workflow_dispatch:
inputs:
tag:
description: "Tag for the Docker image"
required: true
default: "latest"
description: "Optional extra Docker tag to publish (for example: staging)"
required: false

jobs:
docker:
name: Build and push image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -36,38 +49,29 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract version information
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION_FULL="${{ github.event.inputs.tag }}"
# Remove 'v' prefix if present
VERSION_FULL=${VERSION_FULL#v}
elif [ "${{ github.event_name }}" = "release" ]; then
VERSION_FULL="${{ github.event.release.tag_name }}"
# Remove 'v' prefix if present
VERSION_FULL=${VERSION_FULL#v}
else
# Extract from git tag
VERSION_FULL=${GITHUB_REF#refs/tags/v}
fi

# Split version into parts
IFS='.' read -r VERSION_MAJOR VERSION_MINOR VERSION_PATCH <<< "$VERSION_FULL"
VERSION_SHORT="$VERSION_MAJOR.$VERSION_MINOR"

echo "VERSION_FULL=$VERSION_FULL" >> $GITHUB_OUTPUT
echo "VERSION_SHORT=$VERSION_SHORT" >> $GITHUB_OUTPUT
echo "VERSION_MAJOR=$VERSION_MAJOR" >> $GITHUB_OUTPUT

echo "Versions: Full=$VERSION_FULL, Short=$VERSION_SHORT, Major=$VERSION_MAJOR"
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/bearlike/simple-secrets-manager
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,format=short
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/bearlike/simple-secrets-manager:latest,ghcr.io/bearlike/simple-secrets-manager:${{ steps.version.outputs.VERSION_FULL }},ghcr.io/bearlike/simple-secrets-manager:${{ steps.version.outputs.VERSION_SHORT }},ghcr.io/bearlike/simple-secrets-manager:${{ steps.version.outputs.VERSION_MAJOR }}
build-args: |
VITE_API_BASE_URL=/api
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
36 changes: 36 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Quality checks

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:

jobs:
quality:
name: Ruff, MyPy, and tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install uv
run: pip install uv

- name: Install dependencies
run: uv sync

- name: Run quality command
run: ./scripts/quality.sh fix

- name: Fail if auto-fixes were needed
run: |
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Ruff auto-fixed files in CI. Run './scripts/quality.sh fix' locally and commit the changes."
git --no-pager diff --name-only
exit 1
fi
19 changes: 13 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,9 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# uv
# Commit uv.lock for reproducible dependency resolution across environments.
#uv.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
Expand Down Expand Up @@ -147,3 +144,13 @@ cython_debug/
# IDE Configurations
.idea/
.vscode/

# Frontend workspace artifacts
node_modules/
frontend/node_modules/
frontend/dist/
frontend/.env.local

# Frontend source files under src/lib must be versioned.
!frontend/src/lib/
!frontend/src/lib/**
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# AGENTS

## Repository scope

This is a monorepo with:

- Backend API at repository root.
- Frontend Admin Console at `frontend/`.

## Backend working rules

- Run backend quality checks with `./scripts/quality.sh check`.
- Keep API response contracts stable unless an explicit versioned change is requested.
- Do not remove legacy `/api/secrets/kv` endpoints.

## Frontend working rules

- Run frontend checks with:
- `cd frontend && npm run lint`
- `cd frontend && npm run build`
- Frontend talks to backend using `VITE_API_BASE_URL` (defaults to `/api`).

## Docker workflows

- Full stack: `docker compose up -d --build`
- Frontend: `http://localhost:8080`
- Backend API via proxy: `http://localhost:8080/api`
- Backend API direct: `http://localhost:5000/api`
94 changes: 78 additions & 16 deletions Access/is_auth.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,92 @@
#!/usr/bin/env python3
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from functools import wraps
from time import perf_counter

from flask import g, request
from flask_httpauth import HTTPBasicAuth

from Api.api import conn, api
from Access.policy import authorize

# Auth Init
userpass = HTTPBasicAuth()
token = HTTPTokenAuth(scheme="Bearer")
userpass: HTTPBasicAuth = HTTPBasicAuth()

# TODO: error_handler

def _extract_token():
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
return auth_header.replace("Bearer ", "", 1).strip()
return request.headers.get("X-API-KEY", type=str, default=None)

@token.verify_token
def abort_if_authorization_fail(token_to_check):
"""Check if an API token is valid
Args:
token_to_check (str): API Token
"""
check, username = conn.tokens.is_authorized(token_to_check)
if check:
return username
api.abort(401, "Not Authorized to access the requested resource")
return None

def _audit_request(event):
event.update(
{
"method": request.method,
"path": request.path,
"ip": request.remote_addr,
"user_agent": request.user_agent.string,
"status_code": event.get("status_code", 200),
"latency_ms": int((perf_counter() - g.get("request_started", perf_counter())) * 1000),
}
)
conn.audit.write_event(event)


def require_token():
token_to_check = _extract_token()
if not token_to_check:
api.abort(401, "Missing API token")
actor, err = conn.tokens.authenticate(token_to_check)
if err:
_audit_request(
{
"actor_type": "token",
"actor_id": None,
"token_id": None,
"action": "auth.fail",
"status_code": 401,
"reason": err,
}
)
api.abort(401, "Not Authorized to access the requested resource")
g.actor = actor
return actor


def require_scope(action, project_id=None, config_id=None):
actor = getattr(g, "actor", None) or require_token()
if not authorize(actor, action, project_id=project_id, config_id=config_id):
api.abort(403, f"Missing scope: {action}")
return actor


def audit_event(action, **kwargs):
actor = getattr(g, "actor", None) or {}
_audit_request(
{
"actor_type": "token" if actor.get("type") == "token" else "user",
"actor_id": actor.get("subject_user") or actor.get("subject_service_name") or actor.get("id"),
"token_id": actor.get("token_id"),
"action": action,
**kwargs,
}
)


@userpass.verify_password
def verify_userpass(username, password):
if conn.userpass.is_authorized(username, password):
g.actor = {"type": "userpass", "subject_user": username, "id": username}
return username
api.abort(401, "Not Authorized to access the requested resource")
return None


def with_token(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
g.request_started = perf_counter()
require_token()
return fn(*args, **kwargs)

return wrapper
Loading
Loading