Permissions¶
The imbi.common.auth.permissions module authenticates a request and
checks its permissions. imbi-api, imbi-assistant, imbi-scheduler
and imbi-workflows use it. The module reads the graph, so the service
must have the graph pool in its lifespan.
Authenticate a request¶
get_current_user is a FastAPI dependency. It reads the
Authorization: Bearer <token> header and returns an AuthContext. It
accepts three kinds of token:
| Token | Format | auth_method |
|---|---|---|
| API key | ik_<key_id>_<secret> |
api_key |
| Agent API token | Detected by core.is_agent_api_token |
agent_api |
| JWT access token | Any other token | jwt |
A missing or bad token gives 401.
For an API key, the permissions are the permissions of the key owner,
limited to the key's scopes. An empty scope list means no limit. The
module caches a successful API-key result for 60 seconds, for at most
1024 keys. A cache hit skips the secret check and the permission queries,
but it still reads the key from the graph. Thus a revoked, rotated,
deleted, or expired key fails at once. A change to the owner's roles
applies within 60 seconds. Call clear_api_key_cache() to remove the
cached results at once.
A client-credentials JWT (auth_method client_credentials) follows the
same rule: when the token has a scope claim, the permissions are
limited to those scopes.
A service refuses agent API tokens until it calls
accept_agent_api_tokens(). Only imbi-api calls it. An agent API
token works in one organization only (AuthContext.organization_id).
get_current_user_cookie_fallback also reads the token from the
imbi_access_token cookie when the header is missing. Use it only for
endpoints that a browser loads as a subresource, such as <img src>.
Require a permission¶
require_permission(name) returns a dependency that authenticates the
request and then checks for the permission. An admin user passes every
check. A service account never passes a check that it does not have the
permission for. A missing permission gives 403.
import typing
import fastapi
from imbi.common import graph
from imbi.common.auth import permissions
router = fastapi.APIRouter()
@router.get('/blueprints')
async def list_blueprints(
db: graph.Pool,
auth: typing.Annotated[
permissions.AuthContext,
fastapi.Depends(permissions.require_permission('blueprint:read')),
],
) -> list[dict]:
...
Set allow_cookie=True to accept the cookie, as
get_current_user_cookie_fallback does.
The dependency sets the imbi_permission attribute on the function that
it returns. The API OpenAPI generator reads this attribute and adds
x-imbi-permission to each operation.
Permission sources¶
load_principal_permissions collects the permissions of a user or a
service account from its organization memberships. It follows the
INHERITS_FROM and GRANTS relationships of the roles. With
org_slug, it uses only the role in that organization.
validate_scopes raises 400 when a scope is not a seeded permission
name. Call it before you store the scopes of a new API key.
For the list of permissions and the roles that have them, see Roles and permissions.
Auth settings¶
The module reads the Auth settings through a provider. The default is
settings.get_auth_settings. A service that extends settings.Auth
must call set_auth_settings_provider() with its own getter. If it does
not, the process has two settings objects. When IMBI_AUTH_JWT_SECRET
is not set, each object makes its own JWT secret, and tokens made with
one secret fail verification with the other.
API Reference¶
get_current_user
async
¶
get_current_user(
db: Pool,
credentials: HTTPAuthorizationCredentials
| None = fastapi.Depends(oauth2_scheme),
) -> AuthContext
FastAPI dependency to get the current authenticated user.
Supports both JWT and API key authentication. API keys are detected by the 'ik_' prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Pool
|
Graph database connection (injected by FastAPI). |
required |
credentials
|
HTTPAuthorizationCredentials | None
|
HTTP Bearer credentials from Authorization header. |
Depends(oauth2_scheme)
|
Returns:
| Type | Description |
|---|---|
AuthContext
|
AuthContext with user and permissions |
Raises:
| Type | Description |
|---|---|
HTTPException
|
If authentication fails |
Source code in libraries/common/src/imbi/common/auth/permissions.py
get_current_user_cookie_fallback
async
¶
get_current_user_cookie_fallback(
db: Pool,
request: Request,
credentials: HTTPAuthorizationCredentials
| None = fastapi.Depends(oauth2_scheme),
) -> AuthContext
Like get_current_user but also accepts the access cookie.
Browser subresource requests (e.g. <img src>) cannot set an
Authorization header, so for the upload-serving GET endpoints the
access token is also read from the :data:ACCESS_COOKIE_NAME cookie
when the header is absent. The Bearer header still takes precedence.
Raises:
| Type | Description |
|---|---|
HTTPException
|
If neither a header nor cookie token is present. |
Source code in libraries/common/src/imbi/common/auth/permissions.py
require_permission ¶
require_permission(
permission: str, *, allow_cookie: bool = False
) -> typing.Callable[
[AuthContext], collections.abc.Awaitable[AuthContext]
]
Create a FastAPI dependency that enforces a specific permission.
The returned dependency validates the current request's AuthContext: admin users bypass the check; otherwise the dependency ensures the required permission is present and returns the AuthContext when allowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
permission
|
str
|
Permission name to require (e.g., "blueprint:read"). |
required |
allow_cookie
|
bool
|
When True, the access token may also be
supplied via the :data: |
False
|
Returns:
| Type | Description |
|---|---|
Callable[[AuthContext], Awaitable[AuthContext]]
|
Callable[[AuthContext], Awaitable[AuthContext]]: A dependency callable that returns the current AuthContext when the user has the required permission. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
Raised with status code 403 if the current user lacks the required permission. |
Source code in libraries/common/src/imbi/common/auth/permissions.py
AuthContext ¶
IdentityInfo ¶
Bases: BaseModel
A single third-party identity connection for an authenticated user.
authenticate_jwt
async
¶
Validate a JWT, load the corresponding user and their permissions, and return an AuthContext.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Graph
|
Graph database connection. |
required |
token
|
str
|
JWT access token string. |
required |
auth_settings
|
Auth
|
Configuration used to decode and validate the token. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
AuthContext |
AuthContext
|
Authentication context containing the resolved
user, the token's |
Raises:
| Type | Description |
|---|---|
HTTPException
|
On token expiry, invalid token, invalid token type, revoked token, missing subject, user not found, or inactive user account. |
Source code in libraries/common/src/imbi/common/auth/permissions.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 | |
authenticate_api_key
async
¶
Validate an API key, load the corresponding user and their permissions, and return an AuthContext (Phase 5).
API keys have the format: ik_
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Graph
|
Graph database connection. |
required |
key
|
str
|
Full API key string. |
required |
auth_settings
|
Auth
|
Configuration for validation. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
AuthContext |
AuthContext
|
Authentication context with user, key_id as session_id, auth_method set to 'api_key', and filtered permissions based on key scopes. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
On invalid format, revoked key, expired key, invalid secret, or inactive user. |
Source code in libraries/common/src/imbi/common/auth/permissions.py
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 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 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 | |
authenticate_agent_api
async
¶
Authenticate an agent API token as its service account, with the roles of that service account, in the token's organization only.
Raises:
| Type | Description |
|---|---|
HTTPException
|
401 when the token is not valid, has expired (its lease), or its service account is inactive. |
Source code in libraries/common/src/imbi/common/auth/permissions.py
accept_agent_api_tokens ¶
Accept agent API tokens (ADR 0022, harness plan P7) in this service. imbi-api calls it, and refuses them on its auth, token, service account, role, secret, and harness routes.
Source code in libraries/common/src/imbi/common/auth/permissions.py
clear_api_key_cache ¶
Drop every cached API-key AuthContext.
Tests use this to keep cases independent.
load_principal_permissions
async
¶
load_principal_permissions(
db: Graph,
label: PrincipalLabel,
match_prop: PrincipalMatchProp,
value: str,
org_slug: str | None = None,
) -> set[str]
Get permission names granted to a principal (user or SA).
Collects permissions from the principal's organization memberships,
following role inheritance via INHERITS_FROM and GRANTS
relationships. With org_slug, only the role in that
organization counts; without it, the roles in all of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Graph
|
Graph database connection. |
required |
label
|
PrincipalLabel
|
Node label of the principal ( |
required |
match_prop
|
PrincipalMatchProp
|
Property name on the principal used to match
( |
required |
value
|
str
|
Value of |
required |
org_slug
|
str | None
|
The organization to resolve the permissions in. |
None
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Set of permission names (for example, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in libraries/common/src/imbi/common/auth/permissions.py
load_all_permission_names
async
¶
Return the set of seeded Permission.name values.
Used to validate API-key / SA-key / client-credential scope lists at creation time — a scope that doesn't correspond to a seeded permission would silently grant nothing (the auth path intersects scopes with the principal's actual permissions), but persisting bogus scopes pollutes the audit trail and hides typos.
Source code in libraries/common/src/imbi/common/auth/permissions.py
validate_scopes
async
¶
Raise HTTPException(400) if any scope isn't a seeded Permission.
An empty / all-scopes list ([]) is treated as "no restriction"
by callers and passes validation trivially.
Source code in libraries/common/src/imbi/common/auth/permissions.py
set_auth_settings_provider ¶
Register how this module resolves Auth settings.
A member with an Auth subclass must call this so token
verification reads the same singleton the member's own code does.
Two singletons would each auto-generate a JWT secret when
IMBI_AUTH_JWT_SECRET is unset, and tokens minted against one
would fail verification against the other.
Scoped to the authentication path, and deliberately not a fix for the
split in general: :mod:imbi.common.access_log and
:mod:imbi.common.auth.encryption still call
settings.get_auth_settings directly, so they read imbi-common's
singleton even in a process that registered a provider here. Closing
that properly means one singleton for the whole process -- a
registerable concrete class on settings.get_auth_settings, or an
injected dependency -- which is a larger change than the auth
extraction warranted.
This is a process-wide mutable hook, so it is order-dependent by construction: the last caller wins, and in a single-process test run an import is enough to set it.