Conversation
With log={"enable": True}, Permit() logged its whole config at DEBUG,
token included, and the SDK did not apply log.level, so the API key
reached the log whatever level was set (PER-16680). The line now names
the API and PDP URLs only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK's own log lines no longer hold the key, but some records carry
text from elsewhere: a PDP error body, an aiohttp error, a context the
caller passed to a check. The SDK's records now go through
permit.utils.sdk_logger, which replaces the API key of every client
created in the process with [REDACTED] before handing the record to
loguru, whether or not that client enabled logging (PER-16680).
Records are still attributed to the SDK module that logged them, so
logger.disable("permit") and the application's sinks see them as
before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LoggerConfig's level and label had no effect (PER-16680): loguru's
default sink printed every SDK record from DEBUG up, unlabelled. The
SDK still adds no sink and leaves the application's sinks and levels
alone. Instead, with log.enable True:
- level drops the SDK's records below it before they reach any sink.
Names are loguru's, in any case, plus warn and fatal. An unknown
name raises ValueError when the client is created.
- label is put in square brackets before every SDK message.
- logger.enable("permit") is called, so a client that enables logging
after one that disabled it is no longer silenced.
log_as_json stays unapplied: loguru serializes per sink, and a JSON
sink of the SDK's own would print every record a second time through
loguru's default sink or the application's.
The settings are process-wide, as loguru's logger is. With log.enable
False nothing changes: the SDK calls logger.disable("permit") and
reads no other setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LoggerConfig's descriptions said each option set something on "the Permit SDK Logger", which did not exist. They and a new Logging section in the README now say where the SDK's records go, what enable, level and label do, that json is not applied and how to get JSON from a loguru sink, that the settings are process-wide, and that no record holds the API key (PER-16680). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offline tests for PER-16680, with loguru sinks added the way an application adds them: a stderr sink in loguru's default format and a JSON sink, both at DEBUG. For the async and the blocking client, at level debug, info and unset, with json on and off, they go through every kind of record the SDK logs and find neither the API key nor a [REDACTED] marker. They also check that a key a PDP echoes back is redacted, that level drops the SDK's records and no others, that label prefixes every SDK message, that json leaves the format to the sinks, that a disabled client logs nothing, that the client created last decides, and that creating 40 clients adds no sink. Each test starts from a fresh SdkLogger and puts loguru's permit switch back the way it found it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loguru's level names are case-sensitive, and log.level was looked up
only in upper case, so a level such as logger.level("audit", no=35)
raised ValueError although the error message offers levels added with
logger.level(). Try the name as given first, then the upper-case name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unknown log level now raises ValueError while the client is created. An application that logs that failure with loguru's logger.exception (diagnose=True by default) printed the config's repr from the SDK's frames, and with it the API key. repr=False keeps the token out of repr() and str() of the config; the field itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keys were kept in a frozenset and replaced in its iteration order, which depends on the hash seed. When a key that is a prefix of another was replaced first, the rest of the longer key stayed in the record. Keep the keys in a tuple sorted longest first, and move the replacement into SdkLogger.scrub(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key read from a file or an environment variable may end with a space. aiohttp sends it, and a PDP that echoes the key back returns it trimmed, so the exact-match replacement missed it. Register the trimmed key as well, and ignore a key that is only whitespace, which would otherwise replace every space in the SDK's messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK replaced a key the PDP echoed back in its own error record, then raised PermitConnectionError with the same body unchanged, so an application that logged or reported the exception wrote the key. Scrub the body where it is read, which covers check, bulk_check and authorized_users, and say in the docs what is and is not replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every client created with log.enable True called logger.enable("permit").
In loguru that also drops every disable the application set for a
module of the package, and overrides an application-wide
logger.disable(""). Now the SDK calls logger.enable("permit") only to
undo a logger.disable("permit") that an earlier client made, which keeps
the fix for an enabled client created after a disabled one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait_for_sync() created a new client from the config, which ran configure_logger again on every call. That turned SDK logging back on after a client created later with logging disabled, and reset the level and label. It now yields a copy of the client whose PDP and API clients are rebuilt from the new config, through the _connect() hook the blocking client overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documented logger.add(sys.stderr, serialize=True) keeps loguru's default text sink, so every record was printed twice, once as text and once as JSON. Say that the application replaces the default sink with logger.remove() first, in the README, the log_as_json description and the configure_logger docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API key redaction and the log level and label hold only while every SDK log call goes through permit.utils.sdk_logger. A ruff banned-api rule (TID251) now flags loguru.logger anywhere in permit/ except the wrapper itself and permit/logger.py, which reads loguru's levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/per-16676/release-ci-hardening: Expect the cloud PDP's tenant-association role in user permissions Test real decisions on the cloud PDP instead of a 501 Test offline that a PDP's 501 makes the SDK raise
An unknown log.level was ignored before this branch, so raising ValueError at client creation would break an app on upgrade. The SDK now logs a warning that names the value and uses INFO. The traceback test forces a failure inside configure_logger instead, and still checks that the API key never shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency Security AuditScanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major) ✅ No known vulnerabilities found. Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL. |
* origin/per-16676/release-ci-hardening: Invite with a role of the invited resource in the invites e2e test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * origin/per-16676/release-ci-hardening: Poll for the PDP's role assignment list in the RBAC e2e tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
added this pull request to stack #145
October 2, 2026 18:24
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear issues
level,labelandjsonlog settings.Why
With
log={"enable": True}, creating a client (asyncPermitorpermit.sync.Permit) loggedjson.dumps(config.dict())at DEBUG. That dump includestoken, and loguru's default stderr sink prints DEBUG, so the API key reached the log whateverlog.levelsaid.configure_logger()read onlylog.enable;level,labelandjsonhad no effect.What changed
Permit SDK initialized: api_url=..., pdp=...at DEBUG.permit/utils/sdk_logger.py. It drops records belowlog.level, replaces with[REDACTED]the API key of every client created in the process (longest key first, and the key without surrounding whitespace too), and puts[label]before the message. It logs withlogger.opt(depth=2), so loguru still credits each record to the SDK module that logged it. The SDK adds no loguru sink and never callslogger.remove().read_error_body()redacts the PDP error body it returns. The body goes into both the SDK's error record and thePermitConnectionErrorraised bycheck,bulk_checkandauthorized_users.PermitConfig.tokenis declared withrepr=False, sorepr()/str()of a config, and tracebacks that print frame values (loguru'sdiagnose=True), do not show it.log.enableFalse callslogger.disable("permit"), as before. True callslogger.enable("permit")only to undo that call from an earlier client.wait_for_sync()yields a copy of the client whose PDP and API clients are rebuilt from the new config, through a_connect()hook the blocking client overrides. It no longer creates a new client, so it does not re-apply log settings.banned-apirule (TID251) flagsloguru.loggeranywhere inpermit/except the wrapper andpermit/logger.py.LoggerConfigdocstring, its field descriptions and theconfigure_loggerdocstring describe the options as they now behave.Behaviour changes
This is a bug and security fix inside 3.x, not an API break. It changes what users who enabled logs see.
log.enableTrue, creating a client no longer logs its config. 3.0.0 and earlier wrote the API key to the log at any configured level wheneverlog.enablewas True. Users who ran that way should rotate those keys and purge those logs.[REDACTED]in every message the SDK logs.PermitConnectionErrorfor a non-200 PDP response fromcheck,bulk_checkorauthorized_usersquotes the response body with the key replaced by[REDACTED].repr()andstr()ofPermitConfigno longer showtoken. The field,.dict()and.json()are unchanged.log.levelnow applies whenlog.enableis True: SDK records below it are dropped before any sink, and the application's own records and sinks are not affected. With the default"info", the SDK's DEBUG records no longer appear: HTTP request/response lines,check/authorized_users/get_user_permissionsbodies, API context changes and the init line. Set"debug"to keep them. Accepted names are loguru's built-in levels in any case, the aliaseswarnandfatal, and levels the application added withlogger.level(), matched by their exact name (or in any case if the application named the level in upper case). For an unknown name the SDK logs a warning that names it and uses INFO; before, any string was accepted and ignored.log.labelnow applies whenlog.enableis True: SDK messages start with[Permit]by default, or[<label>]. An empty label adds nothing.log.enableTrue after one created with logging disabled now logs; before, it stayed silent. The SDK callslogger.enable("permit")only in that case, so alogger.disable()the application made forpermitor one of its modules still applies. When the SDK does undo its own disable, loguru also drops anypermit.*disable made after that earlier client.log.enableTrue sets the level and label.wait_for_sync()withproxy_facts_via_pdpyields a copy of the client (same class, own PDP and API clients) instead of a new client. It no longer re-applies log settings or logs the init line.log.log_as_jsonstill has no effect. It is now documented as not applied, with the recipe:logger.remove(), thenlogger.add(sys.stderr, serialize=True).log.enableFalse (the default) callslogger.disable("permit"), prints nothing, and does not readlevel,labelorjson. An application that callslogger.enable("permit")itself still gets the SDK's records, now without the key.Known limitations, not changed here:
api_urlorpdpURL are not redacted. The SDK's request lines logged them at DEBUG before this change too.check()message.How it was tested
tests/test_fix_logging.py: 53 offline tests. pytest_httpserver serves the PDP and REST API. Each test adds and removes its own sinks: a stderr sink in loguru's default format, read through capsys, and a JSON sink, both at DEBUG. An autouse fixture restores the SDK logger and loguru'spermitswitches. The tests cover:diagnose=True;wait_for_sync()keeping log settings and sendingX-Wait-Timeout/X-Timeout-Policy;pytest -m "not e2e": 361 passed, 3 skipped on both the pydantic 2 and pydantic 1 lanes, warnings as errors, none raised (base: 305 passed, 3 skipped).tests/test_typing_surface.py: 3 passed on both lanes; the sync stub is unchanged.depth=0): 4 fail;logger.disabledropped: 11 fail;logger.remove(): 39 fail, 47 errors;repr=Falseremoved, insertion-order and shortest-first redaction, untrimmed key only, scrub removed from error bodies, always/neverlogger.enable, the oldwait_for_syncconstruction, a missing_connect, and a direct loguru import (flagged by ruff TID251).uv lock --check) passes. mypy is clean on both lanes. actionlint is clean. zizmor reports no findings.Owner actions before merge
log_as_jsonstays unapplied and documented for now; what it should do is decided separately.🤖 Generated with Claude Code