Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PATCH /crates/:crate/:version route #9423

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Rustin170506
Copy link
Member

@Rustin170506 Rustin170506 commented Sep 10, 2024

ref #9193

This pull request introduces a new patch API for yanking and unyanking crates with a message.

The logic is very similar to the current yank and unyank API, but it supports setting a yank message.

Copy link

codecov bot commented Sep 10, 2024

Codecov Report

Attention: Patch coverage is 97.94872% with 4 lines in your changes missing coverage. Please review.

Project coverage is 89.14%. Comparing base (3678583) to head (3d5b4f8).
Report is 35 commits behind head on main.

Files with missing lines Patch % Lines
src/controllers/version/metadata.rs 96.07% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9423      +/-   ##
==========================================
+ Coverage   89.06%   89.14%   +0.08%     
==========================================
  Files         285      286       +1     
  Lines       28941    29195     +254     
==========================================
+ Hits        25777    26027     +250     
- Misses       3164     3168       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Rustin170506 Rustin170506 force-pushed the rustin-patch-update-patch branch 4 times, most recently from 4f51afb to 629c370 Compare September 11, 2024 13:56
Rustin170506

This comment was marked as outdated.

@Rustin170506 Rustin170506 marked this pull request as ready for review September 11, 2024 14:15
@Rustin170506 Rustin170506 requested a review from a team September 11, 2024 14:15
Copy link
Member Author

@Rustin170506 Rustin170506 left a comment

Choose a reason for hiding this comment

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

🔢 Self-check (PR reviewed by myself and ready for feedback.)

@Rustin170506 Rustin170506 force-pushed the rustin-patch-update-patch branch 3 times, most recently from fb5c1f2 to 30b5ddb Compare September 11, 2024 14:28
@Turbo87 Turbo87 added C-enhancement ✨ Category: Adding new behavior or a change to the way an existing feature works A-backend ⚙️ labels Sep 11, 2024
Copy link
Contributor

@eth3lbert eth3lbert left a comment

Choose a reason for hiding this comment

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

I've left some suggestions before a full review.

src/controllers/version/metadata.rs Outdated Show resolved Hide resolved
Comment on lines +187 to +180
let auth = AuthCheck::default()
.with_endpoint_scope(EndpointScope::Yank)
.for_crate(&krate.name)
.check(req, conn)?;

state
.rate_limiter
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, the current implementation will not trigger a rate limit with invalid data. However, I believe we should check for a rate limit first, as data validation can (and should?) also be performed on the frontend before making a request. This can also prevent potential malicious requests.

Copy link
Member Author

Choose a reason for hiding this comment

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

/// Changes `yanked` flag on a crate version record
async fn modify_yank(
    crate_name: String,
    version: String,
    state: AppState,
    req: Parts,
    yanked: bool,
) -> AppResult<Response> {
    // FIXME: Should reject bad requests before authentication, but can't due to
    // lifetime issues with `req`.

    if semver::Version::parse(&version).is_err() {
        return Err(version_not_found(&crate_name, &version));
    }

    let conn = state.db_write().await?;
    spawn_blocking(move || {
        let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();

        let auth = AuthCheck::default()
            .with_endpoint_scope(EndpointScope::Yank)
            .for_crate(&crate_name)
            .check(&req, conn)?;

        state
            .rate_limiter
            .check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;

From the old Yank implementation, it seems we check the parse error(validation) first. Also as you can see from the comment, it seems we want to reject the bad requests before authentication.

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding the version part, I think both options are acceptable, as an invalid version string will result in a 404 page.
However, for invalid payload, as I previously mentioned, I recommend consuming the rate limit token.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just noticed that this function is used in two places, I'm wondering if it would be better to have something similar to the following:

enum YankAction {
    UpdateWithReason {
        yanked: Option<bool>,
        yank_message: Option<String>,
    },
    Toggle {
        yanked: bool,
    },
}

impl YankAction {
    pub fn from_update(data: &VersionUpdate) -> Self {
        ...
    }

    fn validate_with_version(&self, version: &Version) -> AppResult<()> {
        ...
    }

    fn apply(
        &self,
        state: &AppState,
        req: &Parts,
        conn: &mut impl Conn,
        version: &mut Version,
        krate: &Crate,
    ) -> AppResult<()> {
      ...
    }
}

I'm not sure if this would overcomplicate things, but IMHO, with this approach, we could distinguish the operations more easily, making it easier to follow and debug. Additionally, it would provide the potential opportunity to optimize each case individually while still sharing most of the functionality.

(Note: this is just a brief example. The naming, methods, and their arguments can be further modified as needed.)

Copy link
Member Author

Choose a reason for hiding this comment

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

However, for invalid payload, as I previously mentioned, I recommend consuming the rate limit token.

From our current API design, it seems we prefer do it before the auth check.

let metadata: PublishMetadata = serde_json::from_slice(&json_bytes)

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure if this would overcomplicate things, but IMHO, with this approach, we could distinguish the operations more easily, making it easier to follow and debug.

I just simplified the API design to make all updates more obvious. Directly pass the 'yanked' and 'yank_message' to the function, instead of implicitly depending on the updated behavior of the 'version' parameter. What do you think?

src/controllers/version/metadata.rs Outdated Show resolved Hide resolved
@Turbo87
Copy link
Member

Turbo87 commented Sep 12, 2024

I've rebased this on top of #9437 to get that first part out of the way and make the PR a bit easier to review :)

src/tests/krate/yanking.rs Outdated Show resolved Hide resolved
src/tests/krate/yanking.rs Outdated Show resolved Hide resolved
Comment on lines +34 to +40
#[derive(Deserialize)]
pub struct VersionUpdate {
yanked: Option<bool>,
yank_message: Option<String>,
}
Copy link
Member

Choose a reason for hiding this comment

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

we should probably make the request and response JSON consistent by including the version wrapper. something like this:

Suggested change
#[derive(Deserialize)]
pub struct VersionUpdate {
yanked: Option<bool>,
yank_message: Option<String>,
}
#[derive(Deserialize)]
pub struct VersionUpdate {
yanked: Option<bool>,
yank_message: Option<String>,
}
#[derive(Deserialize)]
pub struct VersionUpdateRequest {
version: VersionUpdate,
}

Copy link
Member Author

Choose a reason for hiding this comment

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

From the design of GitHub's patch API, it seems they don't have this kind of wrapper. I'm not sure what the purpose of this kind of wrapper is.
See: https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#update-a-pull-request

Copy link
Member

Choose a reason for hiding this comment

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

GitHub is using a different API response design in general. If you look at https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request you will see that there is also no wrapper object for the GET method.

If you compare that to https://crates.io/api/v1/crates/ripgrep/14.1.0, you can see that our GET responses are using a wrapper object. For this particular URL/endpoint this might not be relevant, but if you look at https://crates.io/api/v1/crates/ripgrep, you can see that it not only include the crate field with the primary object in the response, but also secondary included objects like categories, keywords and versions.

While GitHub may not use the wrapper object for responses, I think it's more important to stay somewhat consistent with our own API and use it here.

@Rustin170506 Rustin170506 marked this pull request as draft September 13, 2024 02:25
@Rustin170506 Rustin170506 force-pushed the rustin-patch-update-patch branch 2 times, most recently from f00807f to 3e33515 Compare September 13, 2024 15:17
@Rustin170506 Rustin170506 changed the title Add PATCH /crates/:crate/:version route WIP: Add PATCH /crates/:crate/:version route Sep 13, 2024
@Rustin170506 Rustin170506 force-pushed the rustin-patch-update-patch branch 3 times, most recently from a9e1768 to 5f8fbb9 Compare September 18, 2024 14:30
Copy link
Member Author

@Rustin170506 Rustin170506 left a comment

Choose a reason for hiding this comment

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

🔢 Self-check (PR reviewed by myself and ready for feedback.)

@Rustin170506 Rustin170506 changed the title WIP: Add PATCH /crates/:crate/:version route Add PATCH /crates/:crate/:version route Sep 18, 2024
@Rustin170506 Rustin170506 marked this pull request as ready for review September 18, 2024 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-backend ⚙️ C-enhancement ✨ Category: Adding new behavior or a change to the way an existing feature works
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants