Skip to main content

openstack_keystone_core/
policy.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14//! # Policy enforcement
15//!
16//! Policy enforcement in Keystone is delegated to the Open Policy Agent. It can
17//! be invoked either with the HTTP request or as a WASM module.
18
19use async_trait::async_trait;
20#[cfg(any(test, feature = "mock"))]
21use mockall::mock;
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use thiserror::Error;
26
27use crate::token::Token;
28
29/// Policy related error.
30#[derive(Debug, Error)]
31pub enum PolicyError {
32    /// Module compilation error.
33    #[error("module compilation task crashed")]
34    Compilation(#[from] eyre::Report),
35
36    /// Dummy policy enforcer cannot be used.
37    #[error("dummy (empty) policy enforcer")]
38    Dummy,
39
40    /// Forbidden error.
41    #[error("{}", .0.violations.as_ref().map(
42        |v| v.iter().cloned().map(|x| x.msg)
43        .reduce(|acc, s| format!("{acc}, {s}"))
44        .unwrap_or_default()
45    ).unwrap_or("The request you made requires authentication.".into()))]
46    Forbidden(PolicyEvaluationResult),
47
48    #[error(transparent)]
49    IO(#[from] std::io::Error),
50
51    #[error(transparent)]
52    Join(#[from] tokio::task::JoinError),
53
54    /// Json serializaion error.
55    #[error(transparent)]
56    Json(#[from] serde_json::Error),
57
58    /// HTTP client error.
59    #[error(transparent)]
60    Reqwest(#[from] reqwest::Error),
61
62    /// Url parsing error.
63    #[error(transparent)]
64    UrlParse(#[from] url::ParseError),
65}
66
67#[async_trait]
68pub trait PolicyEnforcer: Send + Sync {
69    async fn enforce(
70        &self,
71        policy_name: &'static str,
72        credentials: &Token,
73        target: Value,
74        update: Option<Value>,
75    ) -> Result<PolicyEvaluationResult, PolicyError>;
76}
77
78#[cfg(any(test, feature = "mock"))]
79mock! {
80    pub Policy {}
81
82    #[async_trait]
83    impl PolicyEnforcer for Policy {
84        async fn enforce(
85            &self,
86            policy_name: &'static str,
87            credentials: &Token,
88            target: Value,
89            current: Option<Value>
90        ) -> Result<PolicyEvaluationResult, PolicyError>;
91    }
92}
93
94#[derive(Debug, Error)]
95#[error("failed to evaluate policy")]
96pub enum EvaluationError {
97    Serialization(#[from] serde_json::Error),
98    Evaluation(#[from] eyre::Report),
99}
100
101/// OpenPolicyAgent `Credentials` object.
102#[derive(Serialize, Debug)]
103pub struct Credentials {
104    pub user_id: String,
105    pub roles: Vec<String>,
106    #[serde(default)]
107    pub project_id: Option<String>,
108    #[serde(default)]
109    pub domain_id: Option<String>,
110    #[serde(default)]
111    pub system: Option<String>,
112}
113
114impl From<&Token> for Credentials {
115    fn from(token: &Token) -> Self {
116        Self {
117            user_id: token.user_id().clone(),
118            roles: token
119                .effective_roles()
120                .map(|x| {
121                    x.iter()
122                        .filter_map(|role| role.name.clone())
123                        .collect::<Vec<_>>()
124                })
125                .unwrap_or_default(),
126            project_id: token.project().map(|val| val.id.clone()),
127            domain_id: token.domain().map(|val| val.id.clone()),
128            system: None,
129        }
130    }
131}
132
133/// A single violation of a policy.
134#[derive(Clone, Deserialize, Debug, JsonSchema, Serialize)]
135pub struct Violation {
136    pub msg: String,
137    pub field: Option<String>,
138}
139
140/// The OpenPolicyAgent response.
141#[derive(Deserialize, Debug)]
142pub struct OpaResponse {
143    pub result: PolicyEvaluationResult,
144}
145
146/// The result of a policy evaluation.
147#[derive(Clone, Deserialize, Debug, Serialize)]
148pub struct PolicyEvaluationResult {
149    /// Whether the user is allowed to perform the request or not.
150    pub allow: bool,
151    /// Whether the user is allowed to see resources of other domains.
152    #[serde(default)]
153    pub can_see_other_domain_resources: Option<bool>,
154    /// List of violations.
155    #[serde(rename = "violation")]
156    pub violations: Option<Vec<Violation>>,
157}
158
159impl std::fmt::Display for PolicyEvaluationResult {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        let mut first = true;
162        if let Some(violations) = &self.violations {
163            for violation in violations {
164                if first {
165                    first = false;
166                } else {
167                    write!(f, ", ")?;
168                }
169                write!(f, "{}", violation.msg)?;
170            }
171        }
172        Ok(())
173    }
174}
175
176impl PolicyEvaluationResult {
177    #[must_use]
178    pub fn allow(&self) -> bool {
179        self.allow
180    }
181
182    /// Returns true if the policy evaluation was successful.
183    #[must_use]
184    pub fn valid(&self) -> bool {
185        self.violations
186            .as_deref()
187            .map(|x| x.is_empty())
188            .unwrap_or(false)
189    }
190
191    #[cfg(any(test, feature = "mock"))]
192    pub fn allowed() -> Self {
193        Self {
194            allow: true,
195            can_see_other_domain_resources: None,
196            violations: None,
197        }
198    }
199
200    #[cfg(any(test, feature = "mock"))]
201    pub fn allowed_admin() -> Self {
202        Self {
203            allow: true,
204            can_see_other_domain_resources: Some(true),
205            violations: None,
206        }
207    }
208
209    #[cfg(any(test, feature = "mock"))]
210    pub fn forbidden() -> Self {
211        Self {
212            allow: false,
213            can_see_other_domain_resources: Some(false),
214            violations: None,
215        }
216    }
217}