From 47c7c8ed815c213b99e6311a9704779eb221ab36 Mon Sep 17 00:00:00 2001 From: "Michael J. Stealey" Date: Wed, 21 Oct 2026 19:43:05 -0400 Subject: [PATCH] security: remediate urllib3 CVE-2026-97688 and idna packaging, release 0.2.3 Bump urllib3>=2.8.0 (resolved to 2.8.0 in the lock file) to fix the moderate-severity vulnerability affecting 2.6.2 through 2.7.x. Declare idna>=3.15 as a direct runtime dependency. The 0.2.2 fix for CVE-2026-45409 only pinned idna in uv.lock, so PyPI installs could still resolve a vulnerable version. Also correct CLAUDE.md to describe the mixin-class architecture. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++ CLAUDE.md | 95 ++++++++++++++-------------------------- comanage_api/__init__.py | 2 +- pyproject.toml | 3 +- tests/test_api.py | 2 +- uv.lock | 14 +++--- 6 files changed, 54 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5ee7c..4b76ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to `fabric-comanage-api` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.3] - 2026-10-21 + +### Security + +- Bumped `urllib3` minimum to `>=2.8.0` to remediate + [CVE-2026-97688](https://github.com/advisories) (affects 2.6.2 through 2.7.x). +- Declared `idna>=3.15` as a direct runtime dependency. The 0.2.2 fix for + CVE-2026-45409 only pinned `idna` in `uv.lock`, so installs from PyPI could + still resolve a vulnerable version. + ## [0.2.2] - 2026-05-28 ### Security diff --git a/CLAUDE.md b/CLAUDE.md index b7288e9..ac45efc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,71 +1,40 @@ -# CLAUDE.md +# fabric-comanage-api -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## What this is +Python 3 client wrapper for the [COmanage REST API v1](https://spaces.at.internet2.edu/display/COmanage/REST+API+v1), published to PyPI as `fabric-comanage-api`. Part of the FABRIC Testbed. MIT. -## Project Overview +## Stack +Python >=3.10. Runtime deps: `requests`, `urllib3`, `idna` (pinned directly for CVE floors). Dev: `pytest`, `requests-mock`, `python-dotenv`, `ruff`. setuptools backend (PEP 621); version sourced dynamically from `comanage_api.__VERSION__`. -**fabric-comanage-api** — a Python 3 client wrapper for the [COmanage REST API v1](https://spaces.at.internet2.edu/display/COmanage/REST+API+v1), published to PyPI as `fabric-comanage-api` (current version 0.2.0). Part of the [FABRIC Testbed](https://github.com/fabric-testbed) project. MIT licensed. - -## Build & Install - -```bash -# Development setup with uv -uv venv --python 3.12 -uv pip install -e ".[dev]" - -# Install from PyPI -pip install fabric-comanage-api -``` - -Build is configured via `pyproject.toml` (PEP 621 metadata, setuptools backend). Version is sourced from `comanage_api.__VERSION__`. Requires Python >= 3.9. - -## Dependencies - -Runtime: `requests>=2.25.0`. Dev: `pytest`, `requests-mock`, `python-dotenv` (installed via `pip install -e ".[dev]"`). - -## Testing - -Unit tests use `pytest` + `requests-mock`. Run with: +## Conventions +- `ComanageApi` (`comanage_api/__init__.py`) is the single public class. It holds connection state + a `requests.Session` (HTTP Basic Auth) and centralizes all I/O in helpers `_get`/`_post`/`_put`/`_delete`/`_get_by_entity`. +- Each API domain lives in its own private module as a mixin class (e.g. `COUsMixin`, `SshKeysMixin`); `ComanageApi` inherits all nine and they call the shared HTTP helpers via `self`. Methods named `_` (e.g. `cous_add`, `cous_view_all`). + - `_coorgidentitylinks.py` — CoOrgIdentityLink (requires COmanage v4.0.0+) + - `_copeople.py` — CoPerson + - `_copersonroles.py` — CoPersonRole + - `_cous.py` — COU (Collaborative Organizational Unit) + - `_emailaddresses.py` — EmailAddress + - `_identifiers.py` — Identifier + - `_names.py` — Name + - `_orgidentities.py` — OrgIdentity + - `_sshkeys.py` — SshKey (requires COmanage v4.0.0+, experimental) +- Methods validate params against instance-level option sets (`STATUS_OPTIONS`, `AFFILIATION_OPTIONS`, `SSH_KEY_OPTIONS`, `ENTITY_OPTIONS`, `PERSON_OPTIONS`, `EMAILADDRESS_OPTIONS`) before any HTTP call; bad input raises `ValueError`. +- Unimplemented endpoints raise `NotImplementedError`. +- Commit messages reference GitHub issues with `[#N]` prefix. +## Commands ```bash -uv run pytest -v +uv venv --python 3.12 && uv pip install -e ".[dev]" # dev setup +uv run pytest -v # tests (requests-mock, no live calls) +uv run ruff check . && uv run ruff format . # lint/format (line-length 120, examples/ excluded) ``` +Examples in `examples/` hit a LIVE COmanage instance: copy `template.env` -> `.env`, fill creds, then `uv run python examples/cous_example.py`. -138 tests across 10 files in `tests/` cover all endpoint modules: successful responses, parameter validation (`ValueError`), HTTP error propagation, and edge cases (deduplication, 204 empty, `parent_id=0`). +## Gotchas +- All requests have a configurable timeout (default 30s) and auto-retry w/ exponential backoff on 429/500/502/503/504. +- `_coorgidentitylinks.py` requires COmanage v4.0.0+; `_sshkeys.py` requires v4.0.0+ and is experimental. +- Logging via stdlib `logging` under the `comanage_api` logger — silent by default. +- Config env vars: `COMANAGE_API_USER`/`_PASS`, `COMANAGE_API_CO_NAME`/`_CO_ID`, `COMANAGE_API_URL`, optional `COMANAGE_API_SSH_KEY_AUTHENTICATOR_ID`. -The `examples/` directory contains per-endpoint scripts that exercise the API against a live COmanage instance. To run them, copy `template.env` to `.env`, fill in credentials, then run individual example scripts (e.g., `uv run python examples/cous_example.py`). - -## Architecture - -`ComanageApi` (in `comanage_api/__init__.py`) is the single public class. It holds connection state (`_CO_API_URL`, `_CO_API_USER`, `_CO_API_PASS`, `_CO_API_ORG_ID`, `_CO_API_ORG_NAME`) and a `requests.Session` for HTTP Basic Auth. - -**HTTP helpers** on `ComanageApi` (`_get`, `_post`, `_put`, `_delete`, `_get_by_entity`) centralize all request/response handling. Each API domain module delegates to these helpers. - -Each API domain lives in its own private module (`_copeople.py`, `_cous.py`, `_sshkeys.py`, etc.). These modules define standalone functions that accept a `ComanageApi` instance as `self`. The `__init__.py` imports these functions and wraps them as methods on `ComanageApi`, creating a facade. - -**API endpoint modules:** -- `_coorgidentitylinks.py` — CoOrgIdentityLink (requires COmanage v4.0.0+) -- `_copeople.py` — CoPerson -- `_copersonroles.py` — CoPersonRole -- `_cous.py` — COU (Collaborative Organizational Unit) -- `_emailaddresses.py` — EmailAddress -- `_identifiers.py` — Identifier -- `_names.py` — Name -- `_orgidentities.py` — OrgIdentity -- `_sshkeys.py` — SshKey (requires COmanage v4.0.0+, experimental) - -**Pattern for each module:** functions named `_` (e.g., `cous_add`, `cous_view_all`, `cous_delete`) that build paths/params/bodies and delegate to `self._get()`, `self._post()`, `self._put()`, or `self._delete()`. Unimplemented endpoints raise `NotImplementedError`. - -## Configuration - -Environment variables (see `template.env`): -- `COMANAGE_API_USER` / `COMANAGE_API_PASS` — API credentials -- `COMANAGE_API_CO_NAME` / `COMANAGE_API_CO_ID` — target CO -- `COMANAGE_API_URL` — registry base URL -- `COMANAGE_API_SSH_KEY_AUTHENTICATOR_ID` — optional SSH key authenticator plugin ID - -## Conventions - -- Instance-level constants for valid option sets: `STATUS_OPTIONS`, `AFFILIATION_OPTIONS`, `SSH_KEY_OPTIONS`, `ENTITY_OPTIONS`, `PERSON_OPTIONS`, `EMAILADDRESS_OPTIONS`. -- All API methods validate parameters against these option sets before making HTTP calls. -- Commit messages reference GitHub issues with `[#N]` prefix. +## Don't +- Don't run `examples/` against production without verifying `.env` points at a test CO — they perform real add/delete. diff --git a/comanage_api/__init__.py b/comanage_api/__init__.py index 4f02909..109e2f3 100644 --- a/comanage_api/__init__.py +++ b/comanage_api/__init__.py @@ -16,7 +16,7 @@ from ._sshkeys import SshKeysMixin # fabric-comanage-api version -__VERSION__ = "0.2.2" +__VERSION__ = "0.2.3" # Library logging: NullHandler prevents "last resort" output for callers # who don't configure logging. Callers who want logs should add their own diff --git a/pyproject.toml b/pyproject.toml index 7dcf4ad..6ee34eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,8 @@ classifiers = [ ] dependencies = [ "requests>=2.32.4", - "urllib3>=2.7.0", + "urllib3>=2.8.0", + "idna>=3.15", ] [project.optional-dependencies] diff --git a/tests/test_api.py b/tests/test_api.py index 0b6db8e..cc95868 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -52,7 +52,7 @@ def test_retry_adapter_mounted(self, api, mock_adapter): assert isinstance(adapter, HTTPAdapter) def test_version(self): - assert __VERSION__ == '0.2.2' + assert __VERSION__ == '0.2.3' class TestOptionSets: diff --git a/uv.lock b/uv.lock index 93cea56..cec7cd4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" [[package]] @@ -130,7 +130,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -141,6 +141,7 @@ wheels = [ name = "fabric-comanage-api" source = { editable = "." } dependencies = [ + { name = "idna" }, { name = "requests" }, { name = "urllib3" }, ] @@ -155,12 +156,13 @@ dev = [ [package.metadata] requires-dist = [ + { name = "idna", specifier = ">=3.15" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "python-dotenv", marker = "extra == 'dev'" }, { name = "requests", specifier = ">=2.32.4" }, { name = "requests-mock", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'" }, - { name = "urllib3", specifier = ">=2.7.0" }, + { name = "urllib3", specifier = ">=2.8.0" }, ] provides-extras = ["dev"] @@ -353,9 +355,9 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ]