permkit 0.2.0

Generic permission registry, permission derive macro, and route guard macro
Documentation
use futures::executor::block_on;
use itertools::Itertools as _;
use permkit::{
    EffectivePermissions,
    HasPermission,
    Permission,
    PermissionCheckError,
    PermissionEffect,
    PermissionGrant,
    all_permissions,
    permissions,
};

#[derive(Permission)]
#[permission(roles = ["owner", "operator"])]
enum CompanyPermission {
    #[permission(name = "Companies.List")]
    List,
    #[permission(name = "Companies.Delete", roles = ["owner"])]
    Delete,
}

#[derive(Permission)]
enum CompanyActionPermission {
    #[permission(name = "update:companies")]
    UpdateCompanies,
}

struct Context {
    permissions: EffectivePermissions,
}

impl HasPermission<Context> for CompanyPermission {
    type Error = PermissionCheckError;

    async fn has_permission(&self, context: &Context) -> Result<bool, Self::Error> {
        Ok(context.permissions.allows(self.as_ref()))
    }
}

#[permissions(CompanyPermission::List, context = context)]
async fn guarded(context: Context) -> Result<(), PermissionCheckError> {
    let _ = context;
    Ok(())
}

#[test]
fn inventory_collects_derived_permissions() {
    let _ = CompanyPermission::Delete;
    let _ = CompanyActionPermission::UpdateCompanies;

    let entries = all_permissions();
    let mut company_permissions: Vec<_> = entries
        .iter()
        .filter(|entry| entry.enum_name == "CompanyPermission")
        .map(|entry| (entry.name.as_ref(), entry.roles))
        .collect();
    company_permissions.sort_unstable_by(|left, right| left.0.cmp(right.0));

    assert_eq!(
        company_permissions,
        vec![
            ("Companies.*", &[][..]),
            ("Companies.Delete", &["owner"][..]),
            ("Companies.List", &["owner", "operator"][..]),
        ]
    );

    let action_permissions: Vec<_> = entries
        .iter()
        .filter(|entry| entry.enum_name == "CompanyActionPermission")
        .map(|entry| entry.name.as_ref())
        .sorted()
        .collect();

    assert_eq!(action_permissions, vec!["update:*", "update:companies"]);
}

#[test]
fn auth_macro_allows_request_when_permission_passes() {
    let context = Context {
        permissions: EffectivePermissions::from_grants([PermissionGrant {
            scope: "role".to_owned(),
            pattern: "Companies.List".to_owned(),
            effect: PermissionEffect::Allow,
        }]),
    };

    assert_eq!(block_on(guarded(context)), Ok(()));
}

#[test]
fn auth_macro_denies_request_when_permission_fails() {
    let context = Context {
        permissions: EffectivePermissions::empty(),
    };

    assert_eq!(
        block_on(guarded(context)),
        Err(PermissionCheckError::Forbidden)
    );
}