Skip to content
Draft
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
49 changes: 45 additions & 4 deletions crates/crates_io_database/src/models/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ impl Crate {
Ok(users.chain(teams).collect())
}

pub async fn owner_remove(
/// Remove owner given a crates.io username.
pub async fn owner_remove_with_username(
&self,
mut conn: &AsyncPgConnection,
login: &str,
Expand All @@ -225,7 +226,7 @@ impl Crate {
CASE WHEN crate_owners.owner_kind = 1 THEN
teams.login
ELSE
users.gh_login
users.username
END AS login
FROM crate_owners
LEFT JOIN teams
Expand All @@ -243,7 +244,47 @@ impl Crate {
WHERE crate_owners.crate_id = crate_owners_with_login.crate_id
AND crate_owners.owner_id = crate_owners_with_login.owner_id
AND crate_owners.owner_kind = crate_owners_with_login.owner_kind
AND lower(crate_owners_with_login.login) = lower($2);"#,
AND canon_username(crate_owners_with_login.login) = canon_username($2);"#,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just noticed that the change to this query, because crate_owners_with_login is selecting both teams.login and users.username as crate_owners_with_login.login, makes it so that we're comparing team names using the canon_username function (which also normalizes hyphen to underscore), whereas before we were only comparing team names using lower.

I don't think we want to change anything about how we handle team names right now. That's under GitHub's control (and we don't have plans to change the way teams work yet).

Looking at the production database, I don't see any team names that differ only by hyphen/underscore today, but I do see some team names with hyphens and some with underscores, so GitHub does allow it. While I don't think it would be a good idea for anyone to do this, I just confirmed that it's possible to create two teams in GitHub, one named crates-io and one named crates_io. I think the change to this query would mean you couldn't manage them independently. If they both existed in crates.io, then you ran cargo owner --add github:rust-lang:crates-io, both the crates-io and the crates_io team would get added, if my reasoning is correct.

Given that we now have this query in owner_remove_with_username and a slightly different query in owner_remove_with_gh_login, maybe we should split owner_remove_with_username into owner_remove_with_username and owner_remove_with_team_name?

@moskirathe moskirathe Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, thanks for catching this! i did a partial extraction from this pr and did the split in #14596. I'll rebase when #14596 is merged.

);

let num_updated_rows = query
.bind::<Integer, _>(self.id)
.bind::<Text, _>(login)
.execute(&mut conn)
.await?;

if num_updated_rows == 0 {
return Err(OwnerRemoveError::not_found(login));
}

Ok(())
}

/// Remove owner given a github username.
pub async fn owner_remove_with_gh_login(
&self,
mut conn: &AsyncPgConnection,
login: &str,
) -> Result<(), OwnerRemoveError> {
let query = diesel::sql_query(
r#"WITH crate_owners_with_gh_login AS (
SELECT
crate_owners.*,
login
FROM crate_owners
JOIN oauth_github
ON crate_owners.owner_id = oauth_github.user_id
AND crate_owners.owner_kind = 0
WHERE crate_owners.crate_id = $1
AND crate_owners.deleted = false
)
UPDATE crate_owners
SET deleted = true
FROM crate_owners_with_gh_login
WHERE crate_owners.crate_id = crate_owners_with_gh_login.crate_id
AND crate_owners.owner_id = crate_owners_with_gh_login.owner_id
AND crate_owners.owner_kind = crate_owners_with_gh_login.owner_kind
AND canon_username(crate_owners_with_gh_login.login) = canon_username($2);"#,
);

let num_updated_rows = query
Expand All @@ -265,7 +306,7 @@ impl Crate {
pub enum NewOwnerInvite {
/// The invitee was a [`User`], and they must accept the invite through the
/// UI or via the provided invite token.
User(User, SecretString),
User(User, SecretString, String),

/// The invitee was a [`Team`], and they were immediately added as an owner.
Team(Team),
Expand Down
11 changes: 9 additions & 2 deletions crates/crates_io_database/src/models/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,20 @@ impl Owner {
}
}

pub fn login(&self) -> &str {
pub fn username(&self) -> &str {
match self {
Owner::User(user) => &user.gh_login,
Owner::User(user) => &user.username,
Owner::Team(team) => &team.login,
}
}

pub fn gh_login(&self) -> Option<&str> {
match self {
Owner::User(user) => user.gh_username.as_deref(),
Owner::Team(team) => Some(&team.login),
}
}

pub fn id(&self) -> i32 {
match self {
Owner::User(user) => user.id,
Expand Down
68 changes: 68 additions & 0 deletions crates/crates_io_database/src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ pub struct User {
pub name: Option<String>,
pub gh_id: i32,
pub gh_login: String,
// This is the same as gh_login, but reads from oauth_github instead.
// Can rename to `gh_login` or something more appropriate when gh_login is removed from this struct.
#[diesel(select_expression = oauth_github::login.nullable())]
pub gh_username: Option<String>,
Comment thread
Turbo87 marked this conversation as resolved.
#[diesel(select_expression = oauth_github::avatar.nullable())]
pub gh_avatar: Option<String>,
#[diesel(select_expression = oauth_github::encrypted_token.nullable())]
Expand Down Expand Up @@ -216,6 +220,20 @@ pub struct OauthGithub {
pub user_id: i32,
}

impl OauthGithub {
pub async fn find_by_login(
mut conn: &AsyncPgConnection,
login: &str,
) -> QueryResult<OauthGithub> {
oauth_github::table
.filter(canon_username(oauth_github::login).eq(canon_username(login)))
.filter(oauth_github::account_id.ne(-1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The .filter(oauth_github::account_id.ne(-1)) isn't necessary-- users that have gh_id set to -1 in the users table just don't have any records in the oauth_github table. The -1 values were always sort of a hack and I'm looking forward to getting rid of it with this transition, not continue propagating it :)

.order(oauth_github::account_id.desc())
.first(&mut conn)
.await
}
}

/// Represents a new crates.io user to GitHub user OAuth link to be inserted into the
/// `oauth_github` table.
#[derive(Insertable, Debug, Builder)]
Expand Down Expand Up @@ -245,3 +263,53 @@ impl NewOauthGithub<'_> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crates_io_test_db::TestDatabase;

async fn insert_user(
conn: &AsyncPgConnection,
username: &str,
gh_login: &str,
gh_id: i32,
) -> QueryResult<i32> {
let user_id = NewUser::builder()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you able to use UserBuilder here and...

.gh_id(gh_id)
.gh_login(gh_login)
.username(username)
.build()
.insert(conn)
.await?;

NewOauthGithub::builder()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

... OauthGithubBuilder::for_user here?

These test builders might not have existed when you started this PR, but they should make other changes smaller, like the User.gh_login change you mentioned we should do in another comment :)

.account_id(gh_id as i64)
.encrypted_token(&[])
.login(gh_login)
.user_id(user_id)
.build()
.insert(conn)
.await?;

Ok(user_id)
}

#[tokio::test]
async fn test_find_by_login_returns_highest_account_id_account() {
let test_db = TestDatabase::new();
let conn = test_db.async_connect().await;

insert_user(&conn, "alice", "alice", 100).await.unwrap();
let user_id = insert_user(&conn, "alice", "Alice", 200).await.unwrap();

// case-insensitive checks
for login in ["alice", "Alice", "ALICE"] {
let user = OauthGithub::find_by_login(&conn, login).await.unwrap();

assert_eq!(user.account_id, 200);
assert_eq!(user.user_id, user_id);
assert_eq!(user.login, "Alice");
}
}
}
3 changes: 2 additions & 1 deletion crates/crates_io_test_utils/src/builders/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ impl<'a> UserBuilder<'a> {
pub fn build(self) -> User {
User {
id: 1,
gh_login: self.gh_login.into(),
name: self.display_name.map(ToString::to_string),
gh_login: self.gh_login.into(),
gh_username: Some(self.gh_login.into()),
gh_id: 123,
gh_avatar: None,
gh_encrypted_token: None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX CONCURRENTLY IF EXISTS index_oauth_github_login;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run_in_transaction = false
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS index_oauth_github_login ON oauth_github (lower(login));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Based on a resolved comment thread on this PR that I'm having trouble linking to and the current state of the query in owner_remove_with_gh_login that has AND canon_username(crate_owners_with_gh_login.login) = canon_username($2), I expected this index to be ON oauth_github (canon_username(login)) rather than lower? Maybe the change got lost in a rebase?

@Turbo87 Turbo87 Sep 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sorry, I wasn't done with refactorings in this branch yet 🙈

I think we should actually do the opposite and keep GitHub account comparisons as only lower(), since hyphen and underscore are not equivalent on the GitHub side, even if we plan on treating them that way for the crates.io usernames.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

oh whoops! sorry-- i'll hold off with the rest of my review til you say you're done :)

sounds good with the github normalization!

14 changes: 12 additions & 2 deletions packages/crates-io-api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3293,9 +3293,14 @@ export interface operations {
*
* For users, use just the username (e.g., `"octocat"`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tiny nit that I think will make this documentation clearer: The note this PR adds about disambiguation of usernames should be added up here with users rather than after the note about teams, and the "just" in "use just the username" should be removed because that's not necessarily true now.

* For GitHub teams, use the format `github:org:team` (e.g., `"github:rust-lang:owners"`).
*
* To disambiguate between crates.io and GitHub usernames, use
* the `crates.io:username` or `github:username` prefix.
* @example [
* "octocat",
* "github:rust-lang:owners"
* "github:rust-lang:owners",
* "crates.io:some_user",
* "github:other_user"
* ]
*/
owners: string[];
Expand Down Expand Up @@ -3358,9 +3363,14 @@ export interface operations {
*
* For users, use just the username (e.g., `"octocat"`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here with moving the disambiguation note up here and removing "just"

* For GitHub teams, use the format `github:org:team` (e.g., `"github:rust-lang:owners"`).
*
* To disambiguate between crates.io and GitHub usernames, use
* the `crates.io:username` or `github:username` prefix.
* @example [
* "octocat",
* "github:rust-lang:owners"
* "github:rust-lang:owners",
* "crates.io:some_user",
* "github:other_user"
* ]
*/
owners: string[];
Expand Down
Loading