Skip to main content

alibi_plugins/helpers/
api_key_authorization.rs

1use alibi_core::{AuthContext, AuthError, AuthResult};
2
3/// Fetch an API key by ID and verify that it belongs to the given user.
4///
5/// Returns `AuthError::not_found` if the key does not exist or belongs to
6/// another user.
7///
8/// # Errors
9///
10/// Returns an error if the key is missing, belongs to another owner, or its lookup fails.
11pub async fn get_owned_api_key(
12    ctx: &AuthContext<impl alibi_core::AuthSchema>,
13    config: &crate::api_key::ApiKeyConfig,
14    key_id: &str,
15    user_id: &str,
16    action: &str,
17) -> AuthResult<alibi_core::ApiKey> {
18    use crate::api_key::{ApiKeyReferences, config_id_matches};
19
20    let api_key = config
21        .read_key(ctx, key_id, false)
22        .await?
23        .ok_or_else(|| AuthError::not_found("API Key not found"))?;
24
25    // A key only exists as far as the configuration that addressed it.
26    if !config_id_matches(&api_key.config_id, &config.config_id) {
27        return Err(AuthError::not_found("API Key not found"));
28    }
29
30    match config.references {
31        ApiKeyReferences::User => {
32            if api_key.reference_id != user_id {
33                return Err(AuthError::not_found("API Key not found"));
34            }
35        }
36        ApiKeyReferences::Organization => {
37            require_org_api_key_permission(ctx, user_id, &api_key.reference_id, action).await?;
38        }
39    }
40
41    Ok(api_key)
42}
43
44/// Authorize a user against an organization-owned API key, mirroring
45/// upstream's `checkOrgApiKeyPermission`.
46///
47/// # Errors
48///
49/// Returns an error if organization permissions cannot be loaded or do not authorize the operation.
50pub async fn require_org_api_key_permission(
51    ctx: &AuthContext<impl alibi_core::AuthSchema>,
52    user_id: &str,
53    organization_id: &str,
54    action: &str,
55) -> AuthResult<()> {
56    use crate::api_key::{ApiKeyErrorCode, api_key_error};
57    use crate::organization::{
58        DynamicAccessControlConfig, METADATA_CREATOR_ROLE, METADATA_ENABLED, METADATA_ROLES,
59        OrganizationConfig,
60    };
61
62    // Organization-owned keys are meaningless without the organization plugin,
63    // which is what supplies the access control below.
64    if !ctx
65        .get_metadata(METADATA_ENABLED)
66        .and_then(serde_json::Value::as_bool)
67        .unwrap_or(false)
68    {
69        return Err(api_key_error(ApiKeyErrorCode::OrganizationPluginRequired));
70    }
71
72    let Some(member) = ctx.database.get_member(organization_id, user_id).await? else {
73        return Err(api_key_error(ApiKeyErrorCode::UserNotMemberOfOrganization));
74    };
75
76    // Upstream passes `allowCreatorAllPermissions`, so the creator role clears
77    // every action without consulting the statements. Roles are composite
78    // (comma-separated), so holding it alongside others still counts.
79    let creator_role = ctx
80        .get_metadata(METADATA_CREATOR_ROLE)
81        .and_then(serde_json::Value::as_str)
82        .map_or_else(|| "owner".to_owned(), str::to_owned);
83    let config = OrganizationConfig {
84        roles: ctx
85            .get_metadata(METADATA_ROLES)
86            .and_then(|value| serde_json::from_value(value.clone()).ok())
87            .flatten(),
88        access_control: ctx
89            .get_metadata("organization.access_control")
90            .and_then(|value| serde_json::from_value(value.clone()).ok())
91            .flatten(),
92        dynamic_access_control: DynamicAccessControlConfig {
93            enabled: ctx
94                .get_metadata("organization.dynamic_roles.enabled")
95                .and_then(serde_json::Value::as_bool)
96                .unwrap_or(false),
97            ..Default::default()
98        },
99        ..Default::default()
100    };
101    // Source resolves dynamic roles before the creator permission shortcut.
102    // Invalid legacy rows must deny owners too, without exposing loader errors.
103    if member.role.split(',').any(|role| role == creator_role) {
104        return crate::organization::handlers::extension_common::organization_roles(
105            &config,
106            ctx,
107            organization_id,
108        )
109        .await
110        .map(|_| ())
111        .map_err(|_error| api_key_error(ApiKeyErrorCode::InsufficientApiKeyPermissions));
112    }
113
114    // The pinned API-key plugin turns failed dynamic role resolution into a
115    // denied permission rather than allowing or exposing its internal error.
116    let allowed = crate::organization::handlers::extension_common::has_action(
117        &member.role,
118        "apiKey",
119        action,
120        &config,
121        ctx,
122        organization_id,
123    )
124    .await
125    .unwrap_or(false);
126
127    if allowed {
128        Ok(())
129    } else {
130        Err(api_key_error(
131            ApiKeyErrorCode::InsufficientApiKeyPermissions,
132        ))
133    }
134}