credentials
Store extension repository credentials so no secret reaches the config in the clear.
One extension has one credential, and this module is the only thing that reads or
writes it. It lives in the extension's own config section (MyTool.extension)
under a single key:
.. code-block:: ini
[MyTool.extension]
private_repo = true
credential = "<base64 of a DPAPI-sealed blob>"
Examples:
from pyrevit.coreutils import credentials
credentials.set_credential("MyTool.extension", "oauth2", token)
cred = credentials.get_credential("MyTool.extension")
git.git_clone(url, dest, username=cred.username, password=cred.secret)
Why a single key, and why the username inside it: the alternative - a plaintext
token/password/username triple per extension - is what a migration has
to clear up, and every way of doing that can remove a secret while leaving the
username that gave it meaning, which breaks authentication permanently and
silently. One blob means clearing a credential is always one key removal, and
there is no half-migrated state to reason about.
The crypto lives in C#, not here. This module owns the config contract and
the migration; the sealing itself is
pyRevitLabs.Configurations.Security.ExtensionCredentialProtector, in the
assembly that already backs the user config service. The only thing the two
sides share is the stored format: three base64 fields joined with '.', a
separator the base64 alphabet cannot contain, UTF-8 encoded and sealed with DPAPI
against the current Windows user under a fixed entropy. That is what lets the
out-of-Revit CLI store a credential the in-Revit extension manager can read, and
the reverse. Changing the format on one side alone orphans every stored
credential, so the two must change together.
Scope is the current Windows user. The blob survives pyRevit upgrades and
config backups, and is unreadable after an OS reinstall without a profile
backup, on another machine, and under another account. That is the trade for a
secret that is not sitting in a file every local user can read: a lost profile
means re-entering the token. :func:get_credential therefore reports that case
as :class:PyRevitCredentialUnavailable rather than returning None, so a
caller can tell "re-enter your token" from "this extension needs no token" and
fail with a message about the credential instead of letting libgit2 fall back to
an anonymous fetch.
Note
Nothing here is reachable from a non-Windows runtime; :func:is_available
reports whether sealing works, and every entry point degrades to a logged
no-op rather than raising at import time, so importing this module from
:mod:pyrevit.versionmgr.updater can never take the updater down.
Attributes
mlogger = get_logger(__name__)
module-attribute
CONFIG_KEY = 'credential'
module-attribute
LEGACY_CREDENTIAL_KEYS = ('token', 'password', 'username')
module-attribute
EXTENSION_SECTION_POSTFIXES = ('.extension', '.lib')
module-attribute
DEFAULT_TOKEN_USERNAME = 'oauth2'
module-attribute
CREDENTIAL_KINDS = ('token', 'password')
module-attribute
Classes
PyRevitCredentialError
Bases: Exception
A credential could not be read or stored.
PyRevitCredentialUnavailable
Bases: PyRevitCredentialError
A stored credential exists but cannot be decrypted.
Important: never treat this as "no credential is configured". A fetch that
answers with None here falls back to an anonymous request and surfaces as
a libgit2 error about a missing authentication callback, which points at the
wrong problem. The user has to re-enter their token; no local action recovers
it.
PyRevitCredentialStoreReadOnly
Bases: PyRevitCredentialError
A credential write was aimed at a read-only (admin-locked) config.
Raised instead of silently dropping the value, because
user_config.save_changes() is a no-op on a read-only config: reporting
success there would leave the user believing a token is stored when it is not.
ExtensionCredential(username, secret, kind='token')
Bases: object
A username and secret for a private repository, recovered from storage.
Attributes:
| Name | Type | Description |
|---|---|---|
username |
str
|
account name to send with the secret |
secret |
str
|
personal access token or password |
kind |
str
|
|
Build a credential.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
account name |
required |
secret
|
str
|
token or password |
required |
kind
|
str
|
|
'token'
|
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
Attributes
username = username
instance-attribute
secret = secret
instance-attribute
kind = kind
instance-attribute
Functions:
is_available()
Whether this runtime can seal and unseal a credential.
Returns:
| Type | Description |
|---|---|
bool
|
False when the config is read-only, or when the DPAPI assembly
could not be loaded. Callers that only need to read a credential
should still try :func: |
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
has_credential(section_name)
Whether a sealed credential is stored for this extension.
True for a stored value that will not decrypt - that is still a configured
credential, and the difference matters to the user, who has a token to
re-enter. Use :func:get_credential to find out whether it is usable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
section_name
|
str
|
extension config section name |
required |
Returns:
| Type | Description |
|---|---|
bool
|
whether the credential key is present |
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
get_credential(section_name)
Recover the stored credential for an extension.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
section_name
|
str
|
extension config section name, e.g.
|
required |
Returns:
| Type | Description |
|---|---|
ExtensionCredential
|
the credential, or None when none is stored |
Raises:
| Type | Description |
|---|---|
PyRevitCredentialUnavailable
|
one is stored but cannot be decrypted. This is deliberately not reported as None - see the module docstring. |
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
set_credential(section_name, username, secret, kind='token')
Seal a credential and store it in the extension's config section.
The new value is read back from the config file and matched against what was written before anything else is touched, so a failure part-way through leaves the working credential in place instead of deleting it. That is the one ordering that cannot lose a token: the alternative - clearing the old keys first - destroys the only usable copy whenever sealing then fails.
Note
A flush that does not land is reported as a failure even when the section
already held a credential, because the file would then still hold the old
one. The in-memory value is rolled back to match the file, so what
get_credential reports is what the next session will see.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
section_name
|
str
|
extension config section name |
required |
username
|
str
|
account name. Pass |
required |
secret
|
str
|
token or password |
required |
kind
|
str
|
|
'token'
|
Raises:
| Type | Description |
|---|---|
PyRevitCredentialStoreReadOnly
|
the config is admin-locked, so the value would have been dropped silently |
PyRevitCredentialError
|
the credential is empty, or DPAPI is unavailable |
PyRevitCredentialUnavailable
|
the value could not be verified after being written |
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 | |
delete_credential(section_name)
Remove the stored credential for an extension.
Also clears private_repo, which would otherwise be left claiming a
credential that no longer exists.
The removal is read back off disk before it is reported, for the same reason
:func:set_credential verifies its write: save_changes swallows a failed
flush, so the in-memory store would report a cleared credential that a
restart brings straight back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
section_name
|
str
|
extension config section name |
required |
Returns:
| Type | Description |
|---|---|
bool
|
whether a credential was there to remove, and is now gone from |
|
the config file |
Raises:
| Type | Description |
|---|---|
PyRevitCredentialStoreReadOnly
|
the config is admin-locked |
PyRevitCredentialUnavailable
|
the credential was removed in memory but is still in the config file, so it will return on the next start |
Source code in pyrevitlib/pyrevit/coreutils/credentials.py
migrate_legacy_credentials()
Seal any plaintext credential still sitting in an extension config section.
Older pyRevit wrote token / password / username straight into the
config file, and the CLI still could until --persist-credentials learned
to seal. This runs once per session from
:func:pyrevit.versionmgr.upgrade.upgrade_existing_pyrevit.
Every *.extension / *.lib section is considered, not just the
extensions currently installed and authorized: a credential for an extension
that is temporarily uninstalled, or sits in a path that fails authorization,
would otherwise stay in the clear indefinitely.
A section is only cleared after the sealed value is stored and verified by decrypting it back. A section that fails to seal keeps its plaintext and is reported, because losing a working token is worse than leaving it where it was.
Returns:
| Type | Description |
|---|---|
int
|
how many sections were migrated |