Skip to content

Feature/expose app credential#948

Open
konac-hamza wants to merge 8 commits into
openstack-experimental:mainfrom
konac-hamza:feature/expose-app-credential
Open

Feature/expose app credential#948
konac-hamza wants to merge 8 commits into
openstack-experimental:mainfrom
konac-hamza:feature/expose-app-credential

Conversation

@konac-hamza

Copy link
Copy Markdown
Collaborator

This PR implements the Application Credential API (part of #782).

Changes

  • Added API type definitions for application_credential and access_rule, along with their converters in the api-types crate.
  • Followed the Python Keystone implementation to preserve compatibility and prevent known security issues in the endpoints.
  • Implemented the delete operation in both the core and SQL layers.
  • Implemented policy enforcement based on the default Keystone policies.

// SPDX-License-Identifier: Apache-2.0

pub mod access_rule;
pub mod application_credential;

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, look good, but there are issues that need to be addressed

state
.provider
.get_identity_provider()
.get_user(&ExecutionContext::from_auth(&state, &user_auth), &user_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should only create ExecutionContext once. You recreate it few times in every handler

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will define it at begin of the handler and reuse it in the handler if I need to use it.

Comment thread crates/api-types/src/v3/application_credential/access_rule.rs
pub roles: Vec<RoleRef>,

/// Only present in create response. Never returned again.
pub secret: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this SecretString

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is still open


pub unrestricted: bool,

#[cfg_attr(feature = "validate", validate(length(min = 1, max = 64)))]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python keystone does not expose user_id for the application credential, since they are always already bound to the URL. Drop it in all structs

default allow := false

allow if {
input.credentials.user_id == input.target.user_id

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be input.target.application_credential.user_id


allow if { input.credentials.is_admin }

allow if { input.credentials.user_id == input.target.user_id } No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's going to be input.target.existing.user_id

input.credentials.system == "all"
}

allow if { input.credentials.user_id == input.target.user_id } No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target.application_credential.user_id or target.user_id, but depends on how you set pass it from handler. Anyway - please look at the current policies which document the structure of the data - add it to all policies

input.credentials.system == "all"
}

allow if { input.credentials.user_id == input.target.user_id } No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here user_id is wrapped under the input.existing.application_credential.user_id

pub async fn get_project_scoped_client() -> Result<Arc<AsyncOpenStack>> {
let mut tc = AsyncOpenStack::new(&CloudConfig::from_env()?).await?;

tc.authorize(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be necessary - the 'tc' already should be project scoped as directed by the env vars.

@konac-hamza

Copy link
Copy Markdown
Collaborator Author

thanks, look good, but there are issues that need to be addressed

Thanks for your review. I'm going to address all issues.

@konac-hamza
konac-hamza force-pushed the feature/expose-app-credential branch 2 times, most recently from e386a90 to c392df6 Compare July 15, 2026 23:17

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very very close, just few nits

Comment thread crates/api-types/src/error_conv.rs Outdated
Self::BadRequest("application credential has expired".into())
}
ApplicationCredentialProviderError::AccessRuleInUse(x) => {
Self::BadRequest(format!("access rule {x} is still in use"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InUse is a typical 409 - conflict and not bad request. User 'Self::Conflict' instead

#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,

/// Optional client-supplied identifier for the application credential. If

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing ID in create through API must not be possible - inconsistency to python Keystone


assert_eq!(cred.name, "test-cred");
assert!(!cred.secret.is_empty());
// assert_eq!(cred.user_id, user_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why commented out?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is mistake, i've forgotten to uncomment after debugging

&tc,
&user_id,
ApplicationCredentialCreateBuilder::default()
.name(format!("cred-1"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use .name("cred-1") - Builder should already have Into

#[derive(OpenApi)]
#[openapi(
tags(
(name="domains", description=r#"Domains are a collection of projects and users that define administrative boundaries for managing Identity entities. Domains can represent an individual, company, or operator-owned space. They expose administrative activities directly to system users. Users can be granted the administrator role for a domain. A domain administrator can create projects, users, and groups in a domain and assign roles to users and groups in a domain.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related change - please undo

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for review, I have no idea when I changed this file.I will be more careful after that 👍

Comment thread crates/api-types/src/error_conv.rs Fixed
@konac-hamza
konac-hamza force-pushed the feature/expose-app-credential branch 4 times, most recently from 20fe189 to ea6182a Compare July 16, 2026 19:14
@konac-hamza

Copy link
Copy Markdown
Collaborator Author

@gtema I didnt understand reason of failure in the CI tests.The /health endpoint doesn't response respect of the logs. I have checked last merged commits but none of them has issue like this, Should I change anything in the PR?

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good progress, there are still some issues. I am currently working on adding more jobs/gates to prevent some of the issues you have here in policies

pub roles: Vec<RoleRef>,

/// Only present in create response. Never returned again.
pub secret: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is still open

Comment thread crates/api-types/src/error_conv.rs Fixed
.enforce(
"identity/user/application_credential/delete",
&user_auth,
json!({"application_credential": serde_json::to_value(&current)?}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong order: policy_name, auth, target, current. You pass current under target

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • - **Show**: Pass serde_json::to_value(stored_object)?as target,Noneas existing

  • - **Delete**: Pass serde_json::to_value(stored_object)?as target,None as existing

    Based on ADR doc the current object must pass as target. Should I still change it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice spot - this is a mistake in the ADR. Everywhere in the code we follow this convention (logical that "existing" parameter always points at the object stored on the server side). So yes, please change it and I fix the ADR

.enforce(
"identity/user/application_credential/show",
&user_auth,
json!({"application_credential": serde_json::to_value(&current)?}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here wrong order of target/current

input.credentials.system == "all"
}

allow if { input.credentials.user_id == input.target.application_credential.user_id }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the check in this form tells that while input.target.application_credential.user_id = none would not be allowed, we should throw dedicated violation message (smth like listing application credentials require user_id ...)


allow if { input.credentials.is_admin }

allow if { input.credentials.user_id == input.target.application_credential.user_id }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here it matches the handler, but not the general policy: delete/update/show take input.current and not input.target

@konac-hamza
konac-hamza force-pushed the feature/expose-app-credential branch from ea6182a to ce559ae Compare July 18, 2026 21:59
@konac-hamza
konac-hamza requested a review from gtema July 19, 2026 07:21
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
@gtema
gtema force-pushed the feature/expose-app-credential branch from ce559ae to 9cfe76f Compare July 20, 2026 07:13

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few more remaining issues, very close now

)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "validate", derive(validator::Validate))]
pub struct ApplicationCredentialCreate {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you still do not have a "secret" specified by the user during AC creation.

}
}

impl From<api_types_application_credential::ApplicationCredentialCreate>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when user passes the expected secret we would need to set it here as well

.enforce(
"identity/user/application_credential/delete",
&user_auth,
json!({"application_credential": null}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, simply serde_json::Value::Null

};

let mut target = serde_json::to_value(&payload.application_credential)?;
target["user_id"] = json!(user_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once you add "secret" to the payload you need to explicitly remove it from the json here so that it is not passed to the policy

.enforce(
"identity/user/application_credential/show",
&user_auth,
json!({"application_credential": null}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simply use serde_json::Value::Null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants