Skip to content

Create "Trusted Publishing" database tables #11062

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
26 changes: 13 additions & 13 deletions crates/crates_io_database/src/schema.patch
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
--- original
+++ patched
@@ -21,9 +21,7 @@
@@ -14,9 +14,7 @@
/// The `pg_catalog.tsvector` SQL type
///
/// (Automatically generated by Diesel.)
Expand All @@ -9,9 +9,9 @@
- pub struct Tsvector;
+ pub use diesel_full_text_search::Tsvector;
}

diesel::table! {
@@ -74,9 +72,9 @@
@@ -67,9 +65,9 @@
/// (Automatically generated by Diesel.)
revoked -> Bool,
/// NULL or an array of crate scope patterns (see RFC #2947)
Expand All @@ -23,7 +23,7 @@
/// The `expired_at` column of the `api_tokens` table.
///
/// Its SQL type is `Nullable<Timestamptz>`.
@@ -180,12 +178,6 @@
@@ -175,12 +173,6 @@
///
/// (Automatically generated by Diesel.)
created_at -> Timestamptz,
Expand All @@ -35,8 +35,8 @@
- path -> Ltree,
}
}

@@ -476,7 +468,7 @@
@@ -483,7 +475,7 @@
/// Its SQL type is `Array<Nullable<Text>>`.
///
/// (Automatically generated by Diesel.)
Expand All @@ -45,9 +45,9 @@
/// The `target` column of the `dependencies` table.
///
/// Its SQL type is `Nullable<Varchar>`.
@@ -703,6 +695,24 @@
@@ -710,6 +702,24 @@
}

diesel::table! {
+ /// Representation of the `recent_crate_downloads` view.
+ ///
Expand All @@ -70,7 +70,7 @@
/// Representation of the `reserved_crate_names` table.
///
/// (Automatically generated by Diesel.)
@@ -1018,7 +1028,8 @@
@@ -1094,7 +1104,8 @@
diesel::joinable!(crate_downloads -> crates (crate_id));
diesel::joinable!(crate_owner_invitations -> crates (crate_id));
diesel::joinable!(crate_owners -> crates (crate_id));
Expand All @@ -80,19 +80,19 @@
diesel::joinable!(crates_categories -> categories (category_id));
diesel::joinable!(crates_categories -> crates (crate_id));
diesel::joinable!(crates_keywords -> crates (crate_id));
@@ -1031,6 +1042,7 @@
@@ -1110,6 +1121,7 @@
diesel::joinable!(publish_limit_buckets -> users (user_id));
diesel::joinable!(publish_rate_overrides -> users (user_id));
diesel::joinable!(readme_renderings -> versions (version_id));
+diesel::joinable!(recent_crate_downloads -> crates (crate_id));
diesel::joinable!(trustpub_configs_github -> crates (crate_id));
diesel::joinable!(version_downloads -> versions (version_id));
diesel::joinable!(version_owner_actions -> api_tokens (api_token_id));
diesel::joinable!(version_owner_actions -> users (user_id));
@@ -1058,6 +1070,7 @@
@@ -1140,6 +1152,7 @@
publish_limit_buckets,
publish_rate_overrides,
readme_renderings,
+ recent_crate_downloads,
reserved_crate_names,
teams,
users,
trustpub_configs_github,
56 changes: 56 additions & 0 deletions crates/crates_io_database/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,58 @@ diesel::table! {
}
}

diesel::table! {
/// Trusted Publisher configuration for GitHub Actions
trustpub_configs_github (id) {
/// Unique identifier of the `trustpub_configs_github` row
id -> Int4,
/// Date and time when the configuration was created
created_at -> Timestamptz,
/// Unique identifier of the crate that this configuration is for
crate_id -> Int4,
/// GitHub name of the user or organization that owns the repository
repository_owner -> Varchar,
/// GitHub ID of the user or organization that owns the repository
repository_owner_id -> Int4,
/// Name of the repository that this configuration is for
repository_name -> Varchar,
/// Name of the workflow file inside the repository that will be used to publish the crate
workflow_filename -> Varchar,
/// GitHub Actions environment that will be used to publish the crate (if `NULL` the environment is unrestricted)
environment -> Nullable<Varchar>,
}
}

diesel::table! {
/// Temporary access tokens for Trusted Publishing
trustpub_tokens (id) {
/// Unique identifier of the `trustpub_tokens` row
id -> Int8,
/// Date and time when the token was created
created_at -> Timestamptz,
/// Date and time when the token will expire
expires_at -> Timestamptz,
/// SHA256 hash of the token that can be used to publish the crate
hashed_token -> Bytea,
/// Unique identifiers of the crates that can be published using this token
crate_ids -> Array<Nullable<Int4>>,
}
}

diesel::table! {
/// Used JWT IDs to prevent token reuse in the Trusted Publishing flow
trustpub_used_jtis (id) {
/// Unique identifier of the `trustpub_used_jtis` row
id -> Int8,
/// JWT ID from the OIDC token
jti -> Varchar,
/// Date and time when the JWT was used
used_at -> Timestamptz,
/// Date and time when the JWT would expire
expires_at -> Timestamptz,
}
}

diesel::table! {
/// Representation of the `users` table.
///
Expand Down Expand Up @@ -1070,6 +1122,7 @@ diesel::joinable!(publish_limit_buckets -> users (user_id));
diesel::joinable!(publish_rate_overrides -> users (user_id));
diesel::joinable!(readme_renderings -> versions (version_id));
diesel::joinable!(recent_crate_downloads -> crates (crate_id));
diesel::joinable!(trustpub_configs_github -> crates (crate_id));
diesel::joinable!(version_downloads -> versions (version_id));
diesel::joinable!(version_owner_actions -> api_tokens (api_token_id));
diesel::joinable!(version_owner_actions -> users (user_id));
Expand Down Expand Up @@ -1102,6 +1155,9 @@ diesel::allow_tables_to_appear_in_same_query!(
recent_crate_downloads,
reserved_crate_names,
teams,
trustpub_configs_github,
trustpub_tokens,
trustpub_used_jtis,
users,
version_downloads,
version_owner_actions,
Expand Down
25 changes: 25 additions & 0 deletions crates/crates_io_database_dump/src/dump-db.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,31 @@ name = "public"
avatar = "public"
org_id = "public"

[trustpub_configs_github]
dependencies = ["crates"]
[trustpub_configs_github.columns]
id = "private"
created_at = "private"
crate_id = "private"
repository_owner = "private"
repository_owner_id = "private"
repository_name = "private"
workflow_filename = "private"
environment = "private"

[trustpub_tokens.columns]
id = "private"
created_at = "private"
expires_at = "private"
hashed_token = "private"
crate_ids = "private"

[trustpub_used_jtis.columns]
id = "private"
jti = "private"
used_at = "private"
expires_at = "private"

[users]
filter = """
id in (
Expand Down
3 changes: 3 additions & 0 deletions migrations/2025-04-25-090000_trusted-publishing/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
drop table trustpub_configs_github;
drop table trustpub_tokens;
drop table trustpub_used_jtis;
61 changes: 61 additions & 0 deletions migrations/2025-04-25-090000_trusted-publishing/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
create table trustpub_configs_github
(
id serial primary key,
created_at timestamptz not null default now(),
crate_id int not null references crates on delete cascade,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine on a database level, but we will have to ensure that there's a reasonable path for multi-crate workspaces to be configured without necessarily having to do each one with many clicks in the UI.

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 what I can tell, PyPI requires you to set it up for each individual project too. let's start simple, this is what was agreed upon in the RFC.

Copy link
Member Author

Choose a reason for hiding this comment

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

we could open up the configuration creation for API tokens in the future and not just cookie auth, then those that want to automate it can do so without us having to make the web UI more complex for the simple use cases.

repository_owner varchar not null,
repository_owner_id int not null,
repository_name varchar not null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also be tracking repository ID?

Copy link
Member Author

Choose a reason for hiding this comment

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

we use the owner ID to protect against resurrection attacks, but I'm not sure if the same would apply for the repository ID. if the user recreates the repository with the same name, do we really want to break the publishing workflow because of it?

aside from that, we might not be able to get the repository ID for private repositories, unless we use the auth token of a repository owner. that might work right now with GitHub being our only auth provider, but would cause some issues if that were to change eventually.

workflow_filename varchar not null,
environment varchar
);

comment on table trustpub_configs_github is 'Trusted Publisher configuration for GitHub Actions';
comment on column trustpub_configs_github.id is 'Unique identifier of the `trustpub_configs_github` row';
comment on column trustpub_configs_github.created_at is 'Date and time when the configuration was created';
comment on column trustpub_configs_github.crate_id is 'Unique identifier of the crate that this configuration is for';
comment on column trustpub_configs_github.repository_owner is 'GitHub name of the user or organization that owns the repository';
comment on column trustpub_configs_github.repository_owner_id is 'GitHub ID of the user or organization that owns the repository';
comment on column trustpub_configs_github.repository_name is 'Name of the repository that this configuration is for';
comment on column trustpub_configs_github.workflow_filename is 'Name of the workflow file inside the repository that will be used to publish the crate';
comment on column trustpub_configs_github.environment is 'GitHub Actions environment that will be used to publish the crate (if `NULL` the environment is unrestricted)';

-------------------------------------------------------------------------------

create table trustpub_tokens
(
id bigserial primary key,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
hashed_token bytea not null,
crate_ids int[] not null
);

comment on table trustpub_tokens is 'Temporary access tokens for Trusted Publishing';
comment on column trustpub_tokens.id is 'Unique identifier of the `trustpub_tokens` row';
comment on column trustpub_tokens.created_at is 'Date and time when the token was created';
comment on column trustpub_tokens.expires_at is 'Date and time when the token will expire';
comment on column trustpub_tokens.hashed_token is 'SHA256 hash of the token that can be used to publish the crate';
comment on column trustpub_tokens.crate_ids is 'Unique identifiers of the crates that can be published using this token';

create unique index trustpub_tokens_hashed_token_uindex
on trustpub_tokens (hashed_token);

-------------------------------------------------------------------------------

create table trustpub_used_jtis
(
id bigserial primary key,
jti varchar not null,
used_at timestamptz not null default now(),
expires_at timestamptz not null
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to track this?

Copy link
Member Author

Choose a reason for hiding this comment

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

otherwise we wouldn't be able to clean up this table and it would grow forever. I was planning on having a background job that regularly deletes expired tokens and jtis.

);

comment on table trustpub_used_jtis is 'Used JWT IDs to prevent token reuse in the Trusted Publishing flow';
comment on column trustpub_used_jtis.id is 'Unique identifier of the `trustpub_used_jtis` row';
comment on column trustpub_used_jtis.jti is 'JWT ID from the OIDC token';
comment on column trustpub_used_jtis.used_at is 'Date and time when the JWT was used';
comment on column trustpub_used_jtis.expires_at is 'Date and time when the JWT would expire';

create unique index trustpub_used_jtis_jti_uindex
on trustpub_used_jtis (jti);