Protect an operation

Authentication answers who is calling. Authorization answers what that caller may do. A successful login does not entitle a user to every note in a database.

Put the access requirement on the operation

The notes application's inputs implement iet_core::operation::Operation with Access::Public. A service calls operation::check before doing work. That call checks the access requirement and then validates the input.

For an operation that requires a signed-in user, the descriptor becomes:

rust
// Fragment: CreateNote is your input type and implements Validate + Sync.
impl iet_core::operation::Operation for CreateNote {
    type Input = Self;
    const ACCESS: iet_core::operation::Access =
        iet_core::operation::Access::Authenticated;
}

The service still calls check::<CreateNote, _>(authorizer, context, &input).await?. With an empty context, that now fails with Unauthenticated; it does not create a login mechanism. Your adapter must authenticate credentials using a trusted provider and build the context from the resulting principal.

Never deserialize a client-supplied Principal or subject directly into trusted context. CallContext is metadata, not cryptographic proof of identity. See authentication for producing that trusted value.

Require a permission when identity is not enough

Use Access::Permission("notes.create") for a named permission. check first requires a nonempty principal, then invokes your iet_core::Authorizer, then validates the input. The permission string is application policy, not a built-in role name.

iet_authz::DenyPermissions is useful while no permission system is selected: permission-protected operations fail closed. It does not deny public operations, because those do not ask the authorizer for a decision.

If you need roles, iet-auth-rbac::Rbac<S> delegates to a RoleStore. If you need permissions on specific resources, iet-auth-acl::Acl<S> delegates to an AccessStore through ResourceAuthorizer. Select those only when your policy needs them; an ownership check does not require inventing a role hierarchy.

Enforce ownership separately

Access::Authenticated says nothing about the ID being accessed. Access::Permission("notes.read") can grant an action without identifying a particular row. For owner-scoped data, derive the owner from trusted context and include it in the repository's read or mutation predicate.

Do not fetch or update a row solely by its client-supplied ID, and do not trust an owner query parameter as proof of ownership. The persistence guide shows typed owner predicates. The framework does not automatically inject tenant or owner filters.

Test the denial path

Keep direct service tests for:

  • No principal on an authenticated operation.
  • A principal without the required permission.
  • A different user's resource, even when the caller has the general permission.
  • Authorization backend failure, which must remain an error rather than an allow.

iet_authz::All requires both authorizers to succeed. Any tries the second only when the first returns Forbidden; an unavailable backend is not interpreted as a denial that can be bypassed by another check.

Source: crates/core/iet-core/src/operation.rs and crates/core/iet-authz/src/lib.rs.

IET Framework documentation — built with the IET Framework.