14次发布

0.5.1 2023年10月12日
0.5.0 2022年8月31日
0.4.4 2022年7月6日
0.4.0 2022年2月10日
0.1.1 2022年1月7日

#217 in 身份验证

28次每月下载

MIT 许可证

45KB
784

Dacquiri

将授权漏洞转换为编译器错误的框架。

什么是Dacquiri?

Dacquiri是一个框架,它使用类型系统在编译时验证所有代码路径是否满足您的访问控制策略。它通过让开发者能够对任何函数添加访问控制策略的注释来实现这一点。这些策略被转换为复杂的特质边界,强制调用者在提前满足这些策略的情况下进行检查。

它是如何工作的?

Dacquiri由两个主要组件组成:属性策略

属性

属性相当于您在if语句和其他控制流逻辑中找到的条件。

以下是一个在Actix web应用程序中的示例Web端点。

// An example endpoint built in actix.
// Assume that Session is an extractor
#[get("/documents/{doc_id}")]
async fn access_document(req: HttpRequest, session: Session, doc_id: Path<String>) -> impl Responder {
    let document_service = req.get_document_service();
    let doc_id = doc_id.into_inner();
    let document_meta = document_service.fetch_doc_metadata(doc_id).await?;

    // Only allow caller to read document if they own it
    if document_meta.owner == session.user_id {
        let document = document_service.fetch_doc_contents(doc_id).await?;

        Ok(document)
    } else {
        Err(AppError::Unauthorized)
    }
}

document.owner == session.user_id条件是您在Dacquiri中编写的属性的一个示例。

让我们使用Dacquiri构建这个属性,看看我们如何使用它来保护这个应用程序。

定义一个属性

use dacquiri::prelude::*;

// define the attribute
#[attribute(Owner)]
fn check_caller_owns_document(session: &Session, document_meta: &DocumentMeta) -> AttributeResult<AppError> {
    // check user owns document
    (session.user_id == document_meta.owner)
        .then_some(())
        .ok_or(AppError::Unauthorized)
}

现在我们可以使用Owner属性来讨论我们是否拥有特定的文档。现在让我们使用它来描述我们如何安全地检索文档!

策略

策略允许我们在一组方法上定义访问控制策略。它们由实体守卫组成。

实体是我们想要在方法中测试属性或访问的任何对象。

守卫是一组属性,必须满足才能访问此方法。

让我们构建一个简单的策略,只允许调用者如果他们拥有文档则检索文档内容。我们将它在策略特质定义上的异步方法中实现。

use dacquiri::prelude::*;

#[policy(
    entities = (
        user: Session,
        document_metadata: DocumentMeta
    ),
    guard = (
        user is Owner for document_metadata
    )
)]
pub trait DocumentOwnerPolicy {
    async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result<Document, AppError> {
        // grab the DocumentMeta from our policy definition
        let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();

        // fetch the document contents with the provided document_service
        let document = document_service.fetch_doc_contents(meta.doc_id).await?;

        // return the document!
        Ok(document)
    }
}

策略是用以下形式的约束集合定义的

<subject entity> is <attribute> [for <resource entity>]

同时,请注意,我们使用self.get_entity来访问在策略定义中定义的document_meta对象。

这为什么重要?

我们想确保我们用来检索数据的DocumentMeta对象是我们用来验证访问控制策略的完全相同的对象。

这使我们能够避免以下类型的漏洞

let document_meta_one = document_service.fetch_doc_metadata(doc_id_one).await?;
let document_meta_two = document_service.fetch_doc_metadata(doc_id_two).await?;

// checking ownership of the first document...
if document_meta_one.owner == session.user_id {
    // Ahh! A vulnerability!
    // We're fetching the wrong document!
    // This uses `document_meta_two` instead of the tested `document_meta_one`
    let document = document_service.fetch_doc_contents(document_meta_two.doc_id).await?;

    Ok(document)
} else {
    Err(AppError::Unauthorized)
}

只要实体定义在策略的entities部分,我们就可以使用get_entity来获取它。

使用策略

既然我们已经用策略保护了文档获取方法,那么我们应该如何调用它呢?

首先,我们需要将我们想要一起证明的实体合并为一个EntityProof。这管理着我们添加的实体,并使得在策略中获取实体变得容易。

当我们向EntityProof添加实体时,我们必须给它们命名。仅仅依赖于实体的类型是不够的,因为我们需要同时谈论两个或更多的实体。这就是为什么它们各自有独特名称很重要。

// coalesce our entities together
let entities = session
    .into_entity::<"user">()
    .add_entity::<_, "document_metadata">(document_meta)?;

接下来,我们检查实体之间的属性是否为真。我们可以通过调用之前定义的按名称的属性函数来实现这一点。例如,我们将Owner属性函数定义为check_caller_owns_document(...),并且可以在这里调用它。

// prove `Owner` for "user" and "document_metadata"
let proof = entities.check_caller_owns_document::<"user", "document_metadata">()?;

现在我们已经添加了证明"user"拥有由"document_metadata"描述的文档的检查,我们可以调用我们的受保护方法了!

让我们一起看看这个。

#[get("/documents/{doc_id}")]
async fn access_document(req: HttpRequest, session: Session, doc_id: Path<String>) -> impl Responder {
    let document_service = req.get_document_service();
    let doc_id = doc_id.into_inner();
    let document_meta = document_service.fetch_doc_metadata(doc_id).await?;

    // coalesce our entities
    let entities = session
        .into_entity::<"user">()
        .add_entity::<_, "document_metadata">(document_meta)?;

    // prove our properties
    let proof = entities.check_caller_owns_document::<"user", "document_metadata">()?;

    // call the protected function!
    proof.fetch_document_contents(&document_service).await
}

当然,如果您将这些方法链接起来可以使事情更简单,那么您可以这样做。

高级属性

属性并不特别复杂(部分作为功能),但它们确实有一些额外的功能,这些功能可能并不明显。

主题、资源和上下文

属性函数支持最多三个参数。

第一个参数是主体实体。这个实体必须始终存在,并且是对实体类型的不可变引用。

#[attribute(Enabled)]
// 'User' is the subject entity type
fn check_user_enabled(user: &User) -> AttributeResult<AppError> {
    // check user is enabled
    (user.enabled)
        .then_some(())
        .ok_or(AppError::Unauthorized)
}

第二个,可选的参数是资源实体。在策略约束表达式中,主体资源之间实际上并没有有意义的区别,除非它们出现在不同的位置。与主体实体类似,资源实体也必须是对其实体类型的不可变引用。

属性函数的最后一个可能的参数是上下文。这是任何对象(或对象的集合),它可以帮助您验证属性。一个规范性的上下文对象示例是数据库连接。没有这个连接,您可能无法查询数据库并验证某个属性是否为真。

上下文对象可以在不关联资源的情况下提供,并且可能是也可能不是引用。如果您想定义一个仅包含主体实体和上下文对象的属性函数,将资源实体类型设置为&(),它将被忽略。

#[attribute(Adult)]
fn check_user_is_adult(user: &User, _: &(), db: &DbConnection) -> AttributeResult<AppError> {
    const AGE_ADULT: u32 = 18;
    // use db to query user's current age
    // we'd *probably* expect this to be a property on `User`, but this is for the sake of the example
    let age = db.query_user_age(user.user_id)?;

    if age >= AGE_ADULT {
        Ok(())
    } else {
        Err(AppError::Unauthorized)
    }
}

异步属性

属性可以是async!除了编写要async的函数之外,您不需要做任何特别的事情。这对于具有数据库连接或grpc服务的上下文对象特别有用。

#[attribute(Member)]
async fn check_user_is_member_of_team(user: &User, team: &Team, service: &TeamService) -> AttributeResult<AppError> {
    // attempt to fetch the membership record of this user
    let membership: Option<Membership> = service.get_membership(user.user_id, team.team_id).await?;

    if membership.is_some() {
        Ok(())
    } else {
        Err(AppError::UserNotAMember)
    }
}

当您在其他代码中测试这个属性时,它将是一个异步方法,您必须像预期的那样在它上面调用await

属性名称重用

属性支持定义多个属性函数,允许不同类型的实体证明特定属性。属性仍然局限于特定的主体和资源实体类型,从而防止属性混淆。

允许多个属性函数的主要好处是,不同的实体可以使用相同的属性名来描述关系。例如,从可读性的角度来看,定义以下约束是不理想的。

#[policy(
    entities = (
        user: User,
        team: Team,
    ),
    guard = (
        user is UserEnabled,
        team is TeamEnabled,
    )
)]
pub trait Something {}

通过定义多个属性函数,我们可以重用属性名 Enabled,但为每个实体类型提供了强大、类型检查的属性证明。要定义多个属性函数,我们注释模块声明并在其中放置所有属性函数。然后,我们使用以下注释来注释属性函数:#[attribute]

#[attribute(Enabled)]
mod enabled {
    use crate::{User, Team, AppError};
    
    #[attribute]
    fn check_user_is_enabled(user: &User) -> AttributeResult<AppError> {
        (user.enabled)
            .then_some(())
            .ok_or(AppError::UserNotEnabled)
    }

    #[attribute]
    fn check_team_is_enabled(team: &Team) -> AttributeResult<AppError> {
        (team.enabled)
            .then_some(())
            .ok_or(AppError::TeamNotEnabled)
    }
}

#[policy(
    entities = (
        user: User,
        team: Team,
    ),
    // this reads much better!
    guard = (
        user is Enabled,
        team is Enabled,
    )
)]
pub trait Something {}

高级策略

依赖策略

除了使用属性之外,守卫可以通过以下语法依赖于其他策略

<policy_name>(<entities>)

例如,如果我们创建了一个依赖于我们之前的 DocumentOwnerPolicy 的新策略,我们可能会以以下方式定义守卫

#[policy(
    entities = (
        user: Session,
        document_metadata: DocumentMeta
    ),
    guard = (
        user is OtherAttribute,
        DocumentOwnerPolicy(user, document_metadata)
    )
)]
pub trait OtherPolicy {
    // prints document contents to stdout
    async fn do_stuff(&self, document_service: &DocumentService) -> Result<(), AppError> {
        // we can call `fetch_document_contents` because we're guaranteed to satisfy `DocumentOwnerPolicy`!
        let document = self.fetch_document_contents(document_service).await?;

        println!("Document contents: {}", document);
    }
}

OtherPolicy 中的任何方法都可以调用由 DocumentOwnerPolicy 定义的函数。即使我们没有在 guard 语句中明确依赖于 DocumentOwnerPolicy,这也是正确的。只要所有策略约束都已知满足,我们的方法就可以调用由其他策略保护的函数。

多个守卫

有时有人应该在多个上下文中能够调用给定的方法。在我们之前的例子中,调用者必须在检索内容之前证明用户拥有特定的文档。但如果我们有一个索引文档以供搜索的后台服务怎么办?该服务如何在没有用户会话的情况下获取文档内容呢?

策略支持这种情况的多个守卫条件。每个守卫条件被视为一个 OR 语句的一个分支,这意味着只要其中一个分支满足条件,调用者就可以调用受策略保护的函数。

让我们重新构思我们的 DocumentOwnerPolicy,以便允许后台服务访问文档内容。

#[policy(
    entities = (
        user: Session,
        service: ServiceSession,
        document_metadata: DocumentMeta
    ),
    guard = (
        user is Owner for document_metadata
    ),
    guard = (
        service is Valid
    )
)]
pub trait DocumentOwnerPolicy {
    async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result<Document, AppError> {
        // grab the DocumentMeta from our policy definition
        let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();

        // fetch the document contents with the provided document_service
        let document = document_service.fetch_doc_contents(meta.doc_id).await?;

        // return the document!
        Ok(document)
    }
}

不幸的是,这里有两个主要的限制。

第一个限制是,如果策略使用多个守卫,则不允许使用依赖策略。如果您的重要的多守卫策略能够调用其他策略,您仍然可以要求所有必需的属性,但不能依赖于该策略本身。

第二个限制是,Dacquiri 将要求所有描述的实体都存在,才能满足策略。这意味着尽管我们只需要 ServiceSessionValid 来调用 fetch_document_contents,我们仍然需要提供一个用户的 Session

为了避免这个问题,Dacquiri 支持 可选 实体!

可选实体

可选实体允许我们放宽策略满足条件时描述的实体必须存在的需求。要标记一个实体为可选,请在类型末尾添加一个 ?

在我们的前一个例子中,我们可以将 ServiceSessionSession 类型标记为可选!

#[policy(
    // 'user' and 'service' are now optional types!
    entities = (
        user: Session?,
        service: ServiceSession?,
        document_metadata: DocumentMeta
    ),
    guard = (
        user is Owner for document_metadata
    ),
    guard = (
        service is Valid
    )
)]
pub trait DocumentOwnerPolicy {
    async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result<Document, AppError> {
        // grab the DocumentMeta from our policy definition
        let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();

        // fetch the document contents with the provided document_service
        let document = document_service.fetch_doc_contents(meta.doc_id).await?;

        // return the document!
        Ok(document)
    }
}

关于可选实体的一个重要注意事项是,任何标记为可选的实体将无法使用 self.get_entity 获取,因为这个方法使用编译时检查来验证实体是否存在。

要访问可选实体,可以使用 self.try_get_entity

依赖项

~0.4–1MB
~21K SLoC