stalwartlabs/stalwart · error · ScimError

Forbidden

Error message

Forbidden

What it means

SCIM protocol constructor `Error::forbidden(detail)` builds a SCIM Error with HTTP status 403 and a human-readable detail. It means authentication succeeded but the authenticated principal is not allowed to perform the operation. Returned by server code via this constructor; the detail string explains what was denied.

Source

Thrown at crates/scim-proto/src/message/error.rs:167

    pub fn invalid_count(detail: impl Into<Cow<'static, str>>) -> Self {
        Self::bad_request(ScimType::InvalidCount, detail)
    }

    pub fn uniqueness(detail: impl Into<Cow<'static, str>>) -> Self {
        Error {
            status: 409,
            scim_type: Some(ScimType::Uniqueness),
            detail: Some(detail.into()),
        }
    }

    pub fn unauthorized() -> Self {
        Error::new(401)
    }

    pub fn forbidden(detail: impl Into<Cow<'static, str>>) -> Self {
        Error::new(403).with_detail(detail)
    }

    pub fn not_found() -> Self {
        Error::new(404)
    }

    pub fn conflict(detail: impl Into<Cow<'static, str>>) -> Self {
        Error::new(409).with_detail(detail)
    }

    pub fn precondition_failed() -> Self {
        Error::new(412)
    }

    pub fn max_operations_exceeded(max_operations: usize) -> Self {
        Error::new(413).with_detail(format!(
            "The number of operations in the bulk request exceeds the maxOperations ({max_operations})."
        ))

View on GitHub (pinned to e962003857)

Solutions

  1. Read the `detail` field in the SCIM error body — it names the denied operation or resource.
  2. Request the missing scope/role via the OAuth2 flow and retry with a new token.
  3. Confirm the target resource belongs to the caller's tenant/organization.
  4. If you are the server author, ensure 403 details avoid leaking information about resources the caller should not know exist.

Example fix

// before
let token = get_token(&client_id, scopes=["scim:read"]);
client.put("/scim/v2/Users/2819", body).bearer_auth(token) // 403
// after
let token = get_token(&client_id, scopes=["scim:read", "scim:write"]);
client.put("/scim/v2/Users/2819", body).bearer_auth(token)
Defensive patterns

Strategy: fallback

Validate before calling

// check required scope client-side before a write request
fn can_write(scopes: &[String]) -> bool {
    scopes.iter().any(|s| s == "scim:write" || s == "scim:admin")
}

Try / catch

let resp = client.put(&url).bearer_auth(&token).json(&body).send().await?;
if resp.status() == StatusCode::FORBIDDEN {
    let scim_err: ScimError = resp.json().await?;
    anyhow::bail!("SCIM forbidden: {:?}", scim_err.detail); // surface detail, don't retry blindly
}

Prevention

When it happens

Trigger: Server code calls `Error::forbidden(detail)` (i.e. `Error::new(403).with_detail(detail)`) when a request targets a resource or operation outside the caller's granted scopes/permissions — e.g. reading another tenant's Users, writing with a read-only token.

Common situations: Access token lacking required scope (e.g. scim:write) for a PUT/POST/PATCH; non-admin user hitting admin-only SCIM endpoints; cross-tenant access attempts in multi-tenant deployments; RBAC policy changes revoking previously granted roles.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06). Data as JSON: /api/errors/8881a7b733d70fd4. Report an issue: GitHub.