Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions policy/federation/idp/identity_provider_list.rego
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import data.identity

default allow := false

default can_see_other_domain_resources := false

can_see_other_domain_resources := true if {
"admin" in input.credentials.roles
}

allow if {
identity.own_idp
"reader" in input.credentials.roles
Expand Down
21 changes: 17 additions & 4 deletions src/api/v4/federation/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod tests {

use crate::config::Config;
use crate::federation::MockFederationProvider;
use crate::identity::types::UserResponse;
use crate::keystone::{Service, ServiceState};
use crate::policy::{MockPolicy, MockPolicyFactory, PolicyError, PolicyEvaluationResult};
use crate::provider::Provider;
Expand All @@ -59,6 +60,7 @@ mod tests {
pub(crate) fn get_mocked_state(
federation_mock: MockFederationProvider,
policy_allowed: bool,
policy_allowed_see_other_domains: Option<bool>,
) -> ServiceState {
let mut token_mock = MockTokenProvider::default();
token_mock.expect_validate_token().returning(|_, _, _| {
Expand All @@ -72,6 +74,11 @@ mod tests {
.returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedPayload {
user_id: "bar".into(),
user: Some(UserResponse {
id: "bar".into(),
domain_id: "udid".into(),
..Default::default()
}),
..Default::default()
}))
});
Expand All @@ -84,11 +91,17 @@ mod tests {

let mut policy_factory_mock = MockPolicyFactory::default();
if policy_allowed {
policy_factory_mock.expect_instantiate().returning(|| {
policy_factory_mock.expect_instantiate().returning(move || {
let mut policy_mock = MockPolicy::default();
policy_mock
.expect_enforce()
.returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed()));
if policy_allowed_see_other_domains.is_some_and(|x| x) {
policy_mock
.expect_enforce()
.returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed_admin()));
} else {
policy_mock
.expect_enforce()
.returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed()));
}
Ok(policy_mock)
});
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/api/v4/federation/identity_provider/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ mod tests {
})
});

let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down
2 changes: 1 addition & 1 deletion src/api/v4/federation/identity_provider/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ mod tests {
.withf(|_: &DatabaseConnection, id: &'_ str| id == "bar")
.returning(|_, _| Ok(()));

let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down
131 changes: 124 additions & 7 deletions src/api/v4/federation/identity_provider/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ use axum::{
};
use mockall_double::double;
use serde_json::to_value;
use std::collections::HashSet;

use crate::api::auth::Auth;
use crate::api::error::KeystoneApiError;
use crate::api::v4::federation::types::*;
use crate::federation::FederationApi;
use crate::federation::{
FederationApi, types::IdentityProviderListParameters as ProviderIdentityProviderListParameters,
};
use crate::keystone::ServiceState;
#[double]
use crate::policy::Policy;
Expand Down Expand Up @@ -59,7 +62,7 @@ pub(super) async fn list(
Query(query): Query<IdentityProviderListParameters>,
State(state): State<ServiceState>,
) -> Result<impl IntoResponse, KeystoneApiError> {
policy
let res = policy
.enforce(
"identity/identity_provider_list",
&user_auth,
Expand All @@ -68,10 +71,27 @@ pub(super) async fn list(
)
.await?;

let mut provider_list_params = ProviderIdentityProviderListParameters {
name: query.name,
..Default::default()
};
if query.domain_id.as_ref().is_none() {
if !res.can_see_other_domain_resources.is_some_and(|x| x) {
let domain_ids: HashSet<Option<String>> = HashSet::from([
None,
// TODO: perhaps we should first look at the domain_scope and than user domain.
user_auth.user().as_ref().map(|val| val.domain_id.clone()),
]);
provider_list_params.domain_ids = Some(domain_ids);
}
} else {
provider_list_params.domain_ids = Some(HashSet::from([query.domain_id]));
}

let identity_providers: Vec<IdentityProvider> = state
.provider
.get_federation_provider()
.list_identity_providers(&state.db, &query.try_into()?)
.list_identity_providers(&state.db, &provider_list_params)
.await
.map_err(KeystoneApiError::federation)?
.into_iter()
Expand All @@ -88,6 +108,7 @@ mod tests {
};
use http_body_util::BodyExt; // for `collect`
use sea_orm::DatabaseConnection;
use std::collections::HashSet;

use tower::ServiceExt; // for `call`, `oneshot`, and `ready`
use tower_http::trace::TraceLayer;
Expand Down Expand Up @@ -117,7 +138,7 @@ mod tests {
..Default::default()
}])
});
let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down Expand Up @@ -168,7 +189,7 @@ mod tests {
|_: &DatabaseConnection, qp: &provider_types::IdentityProviderListParameters| {
provider_types::IdentityProviderListParameters {
name: Some("name".into()),
domain_id: Some("did".into()),
domain_ids: Some(HashSet::from([Some("did".into())])),
} == *qp
},
)
Expand All @@ -181,7 +202,7 @@ mod tests {
}])
});

let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down Expand Up @@ -223,7 +244,7 @@ mod tests {
..Default::default()
}])
});
let state = get_mocked_state(federation_mock, false);
let state = get_mocked_state(federation_mock, false, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand All @@ -243,4 +264,100 @@ mod tests {

assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

#[tokio::test]
#[traced_test]
async fn test_list_shared_and_own() {
let mut federation_mock = MockFederationProvider::default();
federation_mock
.expect_list_identity_providers()
.withf(
|_: &DatabaseConnection, qp: &provider_types::IdentityProviderListParameters| {
provider_types::IdentityProviderListParameters {
name: Some("name".into()),
domain_ids: Some(HashSet::from([None, Some("udid".into())])),
} == *qp
},
)
.returning(|_, _| {
Ok(vec![provider_types::IdentityProvider {
id: "id".into(),
name: "name".into(),
domain_id: Some("did".into()),
..Default::default()
}])
});

let state = get_mocked_state(federation_mock, true, Some(false));

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
.with_state(state);

let response = api
.as_service()
.oneshot(
Request::builder()
.uri("/?name=name")
.header("x-auth-token", "foo")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);

let body = response.into_body().collect().await.unwrap().to_bytes();
let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap();
}

#[tokio::test]
#[traced_test]
async fn test_list_all() {
// Test listing ALL idps when the user does not specify the domain_id and is allowed to see
// IDP of other domains (admin)
let mut federation_mock = MockFederationProvider::default();
federation_mock
.expect_list_identity_providers()
.withf(
|_: &DatabaseConnection, qp: &provider_types::IdentityProviderListParameters| {
provider_types::IdentityProviderListParameters {
name: Some("name".into()),
domain_ids: None,
} == *qp
},
)
.returning(|_, _| {
Ok(vec![provider_types::IdentityProvider {
id: "id".into(),
name: "name".into(),
domain_id: Some("did".into()),
..Default::default()
}])
});

let state = get_mocked_state(federation_mock, true, Some(true));

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
.with_state(state);

let response = api
.as_service()
.oneshot(
Request::builder()
.uri("/?name=name")
.header("x-auth-token", "foo")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);

let body = response.into_body().collect().await.unwrap().to_bytes();
let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap();
}
}
4 changes: 2 additions & 2 deletions src/api/v4/federation/identity_provider/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ mod tests {
}))
});

let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down Expand Up @@ -193,7 +193,7 @@ mod tests {
}))
});

let state = get_mocked_state(federation_mock, false);
let state = get_mocked_state(federation_mock, false, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down
2 changes: 1 addition & 1 deletion src/api/v4/federation/identity_provider/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ mod tests {
})
});

let state = get_mocked_state(federation_mock, true);
let state = get_mocked_state(federation_mock, true, None);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
Expand Down
2 changes: 1 addition & 1 deletion src/api/v4/federation/types/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ impl TryFrom<IdentityProviderListParameters> for types::IdentityProviderListPara
fn try_from(value: IdentityProviderListParameters) -> Result<Self, Self::Error> {
Ok(Self {
name: value.name,
domain_id: value.domain_id,
domain_ids: None, //value.domain_id,
})
}
}
Loading
Loading