better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![allow(
    clippy::expect_used,
    reason = "Fixture setup and response assertions require successful fallible construction."
)]
use super::*;
use better_auth_core::{
    AuthInitContext, CreateMember, CreateOrganization, CreateOrganizationRole, FieldMap,
    FieldValue, SchemaValue, UpdateOrganizationRole,
    organization_fields::OrganizationFields,
    store::{EphemeralStore, schema::EntityRole},
    user_fields::{
        FieldTransforms, UserConfig, UserFieldConfig, UserFieldTransform, UserFieldType,
    },
};

async fn check_organization_reference<S: better_auth_core::AuthSchema>(ctx: AuthContext<S>) {
    let plugin = ApiKeyPlugin::builder()
        .references(ApiKeyReferences::Organization)
        .build();
    let mut model_fields = better_auth_core::plugin_runtime::ModelFields::default();
    let mut init = AuthInitContext::new(ctx.config.clone(), ctx.database.clone())
        .expect("fixture context initializes");
    better_auth_core::AuthPlugin::<S>::schema(&plugin, &mut model_fields).unwrap();
    plugin.on_init(&mut init).await.unwrap();
    model_fields
        .register_model_fields(
            EntityRole::ApiKey,
            UserConfig {
                additional_fields: Some(
                    [(
                        "referenceId".into(),
                        UserFieldConfig {
                            field_type: UserFieldType::Number,
                            transform: Some(FieldTransforms {
                                input: None,
                                output: Some(UserFieldTransform::new(|_| Ok(41.0.into()))),
                            }),
                            ..Default::default()
                        },
                    )]
                    .into(),
                ),
            },
        )
        .unwrap();
    let organization_reference = UserConfig {
        additional_fields: Some(
            [(
                "organizationId".into(),
                UserFieldConfig {
                    field_type: UserFieldType::Number,
                    ..Default::default()
                },
            )]
            .into(),
        ),
    };
    model_fields.register_organization_schema(
        &OrganizationFields {
            member: organization_reference.clone(),
            organization_role: organization_reference,
            ..Default::default()
        },
        false,
        true,
    );
    let database = ctx
        .database
        .with_runtime(ctx.config.clone(), Vec::new(), model_fields)
        .unwrap();
    use crate::plugins::organization::{
        METADATA_AC, METADATA_DYNAMIC_ACCESS_CONTROL, METADATA_ENABLED,
    };
    let ctx = AuthContext::with_metadata(
        ctx.config.clone(),
        database,
        HashMap::from([
            (METADATA_ENABLED.into(), serde_json::json!(true)),
            (
                METADATA_DYNAMIC_ACCESS_CONTROL.into(),
                serde_json::json!(true),
            ),
            (METADATA_AC.into(), serde_json::json!({"apiKey":["read"]})),
        ]),
    )
    .expect("fixture context initializes");
    let (user, session) = create_user_with_session(&ctx, "dynamic-owner@example.com").await;
    let mut organization = CreateOrganization::new("Machines", "machines");
    organization.id = Some("41".into());
    let _ = ctx
        .database
        .create_organization(organization)
        .await
        .unwrap();
    let member = ctx
        .database
        .create_member(CreateMember {
            additional_fields: Default::default(),
            organization_id: SchemaValue::from_field(41.0.into()),
            user_id: user.id.clone(),
            role: "machine-reader".into(),
        })
        .await
        .unwrap();
    let role = ctx
        .database
        .create_organization_role(CreateOrganizationRole {
            additional_fields: [("organizationId".into(), 41.0.into())].into(),
            organization_id: "41".into(),
            role: "machine-reader".into(),
            permission: FieldValue::from_json(serde_json::json!({"apiKey":["read"]})).unwrap(),
        })
        .await
        .unwrap();
    let created = super::super::handlers::create_key_for_user(
        &super::super::request::CapturedInput::new(
            &CreateKeyRequest::default(),
            &FieldValue::Undefined,
        )
        .unwrap(),
        &"41".into(),
        &ApiKeyPlugin::builder().build(),
        &ctx,
        ApiKeyEndpoint::new(&ctx, None, Some("/api-key/create"), &FieldMap::new().into()),
    )
    .await
    .unwrap();
    assert_eq!(created.api_key.reference_id.field_value(), 41.0.into());
    let request = create_auth_request(
        HttpMethod::Get,
        "/api-key/get",
        Some(session.token.typed().unwrap()),
        None,
        Some(HashMap::from([(
            "id".into(),
            created.api_key.id.typed().unwrap().clone(),
        )])),
    );
    let response = plugin.handle_get(&request, &ctx).await.unwrap();
    assert_eq!(response.status, 200);
    assert_eq!(json_body(&response)["referenceId"], 41);

    for (permission, member_role, allowed) in [
        (serde_json::json!({"apiKey":[]}), "machine-reader", false),
        (serde_json::json!("invalid permissions"), "owner", false),
        (serde_json::json!({"apiKey":[]}), "owner", true),
        (serde_json::json!({"apiKey":[]}), " owner", false),
    ] {
        ctx.database
            .update_member_role(member.id.typed().unwrap(), member_role)
            .await
            .unwrap();
        ctx.database
            .update_organization_role(
                role.id.typed().unwrap(),
                UpdateOrganizationRole {
                    permission: Some(FieldValue::from_json(permission).unwrap()),
                    ..Default::default()
                },
            )
            .await
            .unwrap();
        match plugin.handle_get(&request, &ctx).await {
            Ok(response) => {
                assert!(allowed, "{member_role}");
                assert_eq!(response.status, 200);
            }
            Err(error) => {
                assert!(!allowed, "{member_role}: {error}");
                crate::plugins::test_helpers::assert_coded_api_error(
                    &error,
                    "FORBIDDEN",
                    403,
                    "INSUFFICIENT_API_KEY_PERMISSIONS",
                    "You do not have permission to perform this action on organization API keys.",
                );
            }
        }
    }
}

#[tokio::test]
async fn memory_organization_key_uses_projected_reference_and_dynamic_roles() {
    let config = Arc::new(crate::plugins::test_helpers::create_test_config());
    let store = Arc::new(EphemeralStore::new(config.clone()));
    check_organization_reference(
        AuthContext::new(config, store).expect("fixture context initializes"),
    )
    .await;
}

#[tokio::test]
async fn sqlite_organization_key_uses_projected_reference_and_dynamic_roles() {
    check_organization_reference(crate::plugins::test_helpers::create_test_context().await).await;
}