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
51 changes: 38 additions & 13 deletions src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ pub enum KeystoneApiError {
// source: OidcError,
// },
#[error(transparent)]
IdentityError {
#[from]
source: IdentityProviderError,
},
IdentityError { source: IdentityProviderError },

#[error(transparent)]
Policy {
Expand All @@ -120,10 +117,7 @@ pub enum KeystoneApiError {
},

#[error(transparent)]
TokenError {
#[from]
source: TokenProviderError,
},
TokenError { source: TokenProviderError },

#[error(transparent)]
WebAuthN {
Expand Down Expand Up @@ -159,9 +153,17 @@ pub enum KeystoneApiError {
#[error(transparent)]
JsonExtractorRejection(#[from] JsonRejection),

#[error("The account is disabled for user: {0}")]
#[error("the account is disabled for user: {0}")]
UserDisabled(String),

/// Selected authentication is forbidden.
#[error("selected authentication is forbidden")]
SelectedAuthenticationForbidden,

/// Selected authentication is forbidden.
#[error("changing current authentication scope is forbidden")]
AuthenticationRescopeForbidden,

/// Others.
#[error(transparent)]
Other(#[from] eyre::Report),
Expand All @@ -180,6 +182,8 @@ impl IntoResponse for KeystoneApiError {
// KeystoneApiError::AuthenticationInfo { .. } => StatusCode::UNAUTHORIZED,
KeystoneApiError::Forbidden => StatusCode::FORBIDDEN,
KeystoneApiError::Policy { .. } => StatusCode::FORBIDDEN,
KeystoneApiError::SelectedAuthenticationForbidden
| KeystoneApiError::AuthenticationRescopeForbidden => StatusCode::BAD_REQUEST,
KeystoneApiError::InternalError(_)
| KeystoneApiError::IdentityError { .. }
| KeystoneApiError::ResourceError { .. }
Expand Down Expand Up @@ -209,7 +213,7 @@ impl KeystoneApiError {
resource: "role".into(),
identifier: x,
},
_ => Self::AssignmentError { source },
_ => source.into(),
}
}
pub fn federation(source: FederationProviderError) -> Self {
Expand All @@ -223,7 +227,7 @@ impl KeystoneApiError {
identifier: x,
},
FederationProviderError::Conflict(x) => Self::Conflict(x),
_ => Self::Federation { source },
_ => source.into(),
}
}
pub fn identity(source: IdentityProviderError) -> Self {
Expand All @@ -236,7 +240,7 @@ impl KeystoneApiError {
resource: "group".into(),
identifier: x,
},
_ => Self::IdentityError { source },
_ => source.into(),
}
}
pub fn resource(source: ResourceProviderError) -> Self {
Expand All @@ -245,7 +249,7 @@ impl KeystoneApiError {
resource: "domain".into(),
identifier: x,
},
_ => Self::ResourceError { source },
_ => source.into(),
}
}
}
Expand Down Expand Up @@ -330,7 +334,28 @@ impl From<AuthenticationError> for KeystoneApiError {
KeystoneApiError::InternalError(source.to_string())
}
AuthenticationError::UserDisabled(data) => KeystoneApiError::UserDisabled(data),
AuthenticationError::TokenRenewalForbidden => {
KeystoneApiError::SelectedAuthenticationForbidden
}
AuthenticationError::Unauthorized => KeystoneApiError::Unauthorized,
}
}
}

impl From<IdentityProviderError> for KeystoneApiError {
fn from(value: IdentityProviderError) -> Self {
match value {
IdentityProviderError::AuthenticationInfo { source } => source.into(),
_ => Self::IdentityError { source: value },
}
}
}

impl From<TokenProviderError> for KeystoneApiError {
fn from(value: TokenProviderError) -> Self {
match value {
TokenProviderError::AuthenticationInfo { source } => source.into(),
_ => Self::TokenError { source: value },
}
}
}
16 changes: 16 additions & 0 deletions src/api/v3/auth/token/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ impl Token {
);
}
}
ProviderToken::Restricted(token) => {
if project.is_none() {
project = Some(
state
.provider
.get_resource_provider()
.get_project(&state.db, &token.project_id)
.await
.map_err(KeystoneApiError::resource)?
.ok_or_else(|| KeystoneApiError::NotFound {
resource: "project".into(),
identifier: token.project_id.clone(),
})?,
);
}
}
}

if let Some(domain) = domain {
Expand Down
26 changes: 20 additions & 6 deletions src/api/v3/auth/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ async fn get_authz_info(
Ok(authz_info)
}

/// Authenticate user issuing a new token
/// Authenticate user issuing a new token.
#[utoipa::path(
post,
path = "/",
Expand All @@ -163,11 +163,25 @@ async fn post(
) -> Result<impl IntoResponse, KeystoneApiError> {
let authed_info = authenticate_request(&state, &req).await?;
let authz_info = get_authz_info(&state, &req).await?;
if let Some(restriction_id) = &authed_info.token_restriction_id {
let restriction = state
.provider
.get_token_provider()
.get_token_restriction(&state.db, restriction_id, true)
.await?
.ok_or(KeystoneApiError::InternalError(
"token restriction {restriction_id} not found".to_string(),
))?;
if !restriction.allow_rescope && req.auth.scope.is_some() {
return Err(KeystoneApiError::AuthenticationRescopeForbidden);
}
}

let mut token = state
.provider
.get_token_provider()
.issue_token(authed_info, authz_info)?;
let mut token =
state
.provider
.get_token_provider()
.issue_token(authed_info, authz_info, None)?;

token = state
.provider
Expand Down Expand Up @@ -839,7 +853,7 @@ mod tests {
.withf(|_: &DatabaseConnection, id: &'_ str| id == "pdid")
.returning(move |_, _| Ok(Some(project_domain.clone())));
let mut token_mock = MockTokenProvider::default();
token_mock.expect_issue_token().returning(|_, _| {
token_mock.expect_issue_token().returning(|_, _, _| {
Ok(ProviderToken::ProjectScope(ProjectScopePayload {
user_id: "bar".into(),
methods: Vec::from(["password".to_string()]),
Expand Down
9 changes: 5 additions & 4 deletions src/api/v4/auth/passkey/finish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ pub(super) async fn finish(
.map_err(AuthenticationError::from)?;
authed_info.validate()?;

let token = state
.provider
.get_token_provider()
.issue_token(authed_info, AuthzInfo::Unscoped)?;
let token =
state
.provider
.get_token_provider()
.issue_token(authed_info, AuthzInfo::Unscoped, None)?;

let api_token = TokenResponse {
token: ApiResponseToken::from_provider_token(&state, &token).await?,
Expand Down
16 changes: 16 additions & 0 deletions src/api/v4/auth/token/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ impl Token {
);
}
}
ProviderToken::Restricted(token) => {
if project.is_none() {
project = Some(
state
.provider
.get_resource_provider()
.get_project(&state.db, &token.project_id)
.await
.map_err(KeystoneApiError::resource)?
.ok_or_else(|| KeystoneApiError::NotFound {
resource: "project".into(),
identifier: token.project_id.clone(),
})?,
);
}
}
}

if let Some(domain) = domain {
Expand Down
2 changes: 1 addition & 1 deletion src/api/v4/auth/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ mod tests {
.withf(|_: &DatabaseConnection, id: &'_ str| id == "pdid")
.returning(move |_, _| Ok(Some(project_domain.clone())));
let mut token_mock = MockTokenProvider::default();
token_mock.expect_issue_token().returning(|_, _| {
token_mock.expect_issue_token().returning(|_, _, _| {
Ok(ProviderToken::ProjectScope(ProjectScopePayload {
user_id: "bar".into(),
methods: Vec::from(["password".to_string()]),
Expand Down
59 changes: 29 additions & 30 deletions src/api/v4/federation/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ use crate::federation::types::{
Scope as ProviderScope, identity_provider::IdentityProvider as ProviderIdentityProvider,
mapping::Mapping as ProviderMapping,
};
use crate::identity::IdentityApi;
use crate::keystone::ServiceState;

/// Convert ProviderScope to AuthZ information
Expand Down Expand Up @@ -140,41 +139,41 @@ pub(super) fn validate_bound_claims(
/// # Returns
/// The mapped user data
pub(super) async fn map_user_data(
state: &ServiceState,
_state: &ServiceState,
idp: &ProviderIdentityProvider,
mapping: &ProviderMapping,
claims_as_json: &Value,
) -> Result<MappedUserData, OidcError> {
let mut builder = MappedUserDataBuilder::default();
if let Some(token_user_id) = &mapping.token_user_id {
// TODO: How to check that the user belongs to the right domain)
if let Ok(Some(user)) = state
.provider
.get_identity_provider()
.get_user(&state.db, token_user_id)
.await
{
builder.unique_id(token_user_id.clone());
builder.user_name(user.name.clone());
} else {
return Err(OidcError::UserNotFound(token_user_id.clone()))?;
}
} else {
builder.unique_id(
claims_as_json
.get(&mapping.user_id_claim)
.and_then(|x| x.as_str())
.ok_or_else(|| OidcError::UserIdClaimRequired(mapping.user_id_claim.clone()))?
.to_string(),
);
//if let Some(token_user_id) = &mapping.token_user_id {
// // TODO: How to check that the user belongs to the right domain)
// if let Ok(Some(user)) = state
// .provider
// .get_identity_provider()
// .get_user(&state.db, token_user_id)
// .await
// {
// builder.unique_id(token_user_id.clone());
// builder.user_name(user.name.clone());
// } else {
// return Err(OidcError::UserNotFound(token_user_id.clone()))?;
// }
//} else {
builder.unique_id(
claims_as_json
.get(&mapping.user_id_claim)
.and_then(|x| x.as_str())
.ok_or_else(|| OidcError::UserIdClaimRequired(mapping.user_id_claim.clone()))?
.to_string(),
);

builder.user_name(
claims_as_json
.get(&mapping.user_name_claim)
.and_then(|x| x.as_str())
.ok_or_else(|| OidcError::UserNameClaimRequired(mapping.user_name_claim.clone()))?,
);
}
builder.user_name(
claims_as_json
.get(&mapping.user_name_claim)
.and_then(|x| x.as_str())
.ok_or_else(|| OidcError::UserNameClaimRequired(mapping.user_name_claim.clone()))?,
);
//}

builder.domain_id(
mapping
Expand Down
20 changes: 16 additions & 4 deletions src/api/v4/federation/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ pub async fn login(
})?
.to_owned();

tracing::debug!("Mapping is {:?}", mapping);
let token_restriction = if let Some(tr_id) = &mapping.token_restriction_id {
state
.provider
.get_token_provider()
.get_token_restriction(&state.db, tr_id, true)
.await?
} else {
None
};

//if !matches!(mapping.r#type, ProviderMappingType::Jwt) {
// // need to log helping message, since the error is wrapped
// // to prevent existence exposure.
Expand Down Expand Up @@ -294,10 +305,11 @@ pub async fn login(
)
.await?;

let mut token = state
.provider
.get_token_provider()
.issue_token(authed_info, authz_info)?;
let mut token = state.provider.get_token_provider().issue_token(
authed_info,
authz_info,
token_restriction.as_ref(),
)?;

// TODO: roles should be granted for the jwt login already

Expand Down
8 changes: 3 additions & 5 deletions src/api/v4/federation/mapping/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,8 @@ mod tests {
bound_subject: None,
bound_claims: None,
oidc_scopes: None,
token_user_id: None,
token_role_ids: None,
token_project_id: None
token_project_id: None,
token_restriction_id: None
}],
res.mappings
);
Expand Down Expand Up @@ -193,9 +192,8 @@ mod tests {
bound_subject: None,
bound_claims: None,
oidc_scopes: None,
token_user_id: None,
token_role_ids: None,
token_project_id: None,
token_restriction_id: None,
}])
});

Expand Down
3 changes: 1 addition & 2 deletions src/api/v4/federation/mapping/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,8 @@ mod tests {
bound_subject: None,
bound_claims: None,
oidc_scopes: None,
token_user_id: None,
token_role_ids: None,
token_project_id: None,
token_restriction_id: None,
},
res.mapping,
);
Expand Down
Loading
Loading