Skip to content

Commit

Permalink
Add repository secrets api (#399)
Browse files Browse the repository at this point in the history
* added repo secrets api

* Apply suggestions from code review

Co-authored-by: XAMPPRocky <[email protected]>

* Also use method chaining in other places

* Reformat project

---------

Co-authored-by: XAMPPRocky <[email protected]>
  • Loading branch information
alesharik and XAMPPRocky authored Jul 8, 2023
1 parent b0beb85 commit 4b8bd44
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 0 deletions.
37 changes: 37 additions & 0 deletions examples/create_repo_secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use crypto_box::{self, aead::OsRng, PublicKey};
use octocrab::{models::repos::secrets::CreateRepositorySecret, Octocrab};
use std::convert::TryInto;

#[tokio::main]
async fn main() -> octocrab::Result<()> {
let token = std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env variable is required");

let octocrab = Octocrab::builder().personal_token(token).build()?;
let repo = octocrab.repos("owner", "repo");
let secrets = repo.secrets();

let public_key = secrets.get_public_key().await?;

let crypto_pk = {
let pk_bytes = B64.decode(public_key.key).unwrap();
let pk_array: [u8; crypto_box::KEY_SIZE] = pk_bytes.try_into().unwrap();
PublicKey::from(pk_array)
};

let encrypted_value = crypto_box::seal(&mut OsRng, &crypto_pk, b"Very secret value").unwrap();

let result = secrets
.create_or_update_secret(
"TEST_SECRET_RS",
&CreateRepositorySecret {
encrypted_value: &B64.encode(encrypted_value),
key_id: &public_key.key_id,
},
)
.await?;

println!("{:?}", result);

Ok(())
}
7 changes: 7 additions & 0 deletions src/api/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod generate;
mod merges;
mod pulls;
pub mod releases;
mod secrets;
mod stargazers;
mod status;
mod tags;
Expand All @@ -27,6 +28,7 @@ pub use generate::GenerateRepositoryBuilder;
pub use merges::MergeBranchBuilder;
pub use pulls::ListPullsBuilder;
pub use releases::ReleasesHandler;
pub use secrets::RepoSecretsHandler;
pub use stargazers::ListStarGazersBuilder;
pub use status::{CreateStatusBuilder, ListStatusesBuilder};
pub use tags::ListTagsBuilder;
Expand Down Expand Up @@ -588,4 +590,9 @@ impl<'octo> RepoHandler<'octo> {
) -> MergeBranchBuilder<'octo, '_> {
MergeBranchBuilder::new(self, head, base)
}

/// Handle secrets on the repository
pub fn secrets(&self) -> RepoSecretsHandler<'_> {
RepoSecretsHandler::new(self)
}
}
169 changes: 169 additions & 0 deletions src/api/repos/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use http::StatusCode;
use snafu::GenerateImplicitData;

use super::RepoHandler;
use crate::models::repos::secrets::{CreateRepositorySecret, CreateRepositorySecretResponse};

/// A client to GitHub's repository secrets API.
///
/// Created with [`Octocrab::repos`].
pub struct RepoSecretsHandler<'octo> {
repo: &'octo RepoHandler<'octo>,
}

impl<'octo> RepoSecretsHandler<'octo> {
pub(crate) fn new(repo: &'octo RepoHandler<'octo>) -> Self {
Self { repo }
}

/// Lists all secrets available in a repository without revealing their encrypted values.
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let all_secrets = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_secrets()
/// .await?;
/// # Ok(())
/// # }
pub async fn get_secrets(
&self,
) -> crate::Result<crate::models::repos::secrets::RepositorySecrets> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets",
owner = self.repo.owner,
repo = self.repo.repo
);
self.repo.crab.get(route, None::<&()>).await
}

/// Gets your public key, which you need to encrypt secrets.
/// You need to encrypt a secret before you can create or update secrets.
/// Anyone with read access to the repository can use this endpoint.
/// If the repository is private you must use an access token with the `repo` scope.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let public_key = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_public_key()
/// .await?;
/// # Ok(())
/// # }
pub async fn get_public_key(&self) -> crate::Result<crate::models::PublicKey> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/public-key",
owner = self.repo.owner,
repo = self.repo.repo
);
self.repo.crab.get(route, None::<&()>).await
}

/// Gets a single repository secret without revealing its encrypted value.
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let secret_info = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_secret("TOKEN")
/// .await?;
/// # Ok(())
/// # }
pub async fn get_secret(
&self,
secret_name: impl AsRef<str>,
) -> crate::Result<crate::models::repos::secrets::RepositorySecret> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);
self.repo.crab.get(route, None::<&()>).await
}

/// Creates or updates a repository secret with an encrypted value.
/// Encrypt your secret using [`crypto_box`](https://crates.io/crates/crypto_box).
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// use octocrab::models::repos::secrets::{CreateRepositorySecret, CreateRepositorySecretResponse};
///
/// let result = octocrab.repos("owner", "repo")
/// .secrets()
/// .create_or_update_secret("GH_TOKEN", &CreateRepositorySecret{
/// key_id: "123456",
/// encrypted_value: "some-b64-encrypted-string",
/// })
/// .await?;
///
/// match result {
/// CreateRepositorySecretResponse::Created => println!("Created secret!"),
/// CreateRepositorySecretResponse::Updated => println!("Updated secret!"),
/// }
/// # Ok(())
/// # }
pub async fn create_or_update_secret(
&self,
secret_name: impl AsRef<str>,
secret: &CreateRepositorySecret<'_>,
) -> crate::Result<CreateRepositorySecretResponse> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);

let resp = {
let resp = self.repo.crab._put(route, Some(secret)).await?;
crate::map_github_error(resp).await?
};

match resp.status() {
StatusCode::CREATED => Ok(CreateRepositorySecretResponse::Created),
StatusCode::NO_CONTENT => Ok(CreateRepositorySecretResponse::Updated),
status_code => Err(crate::Error::Other {
source: format!(
"Unexpected status code from request: {}",
status_code.as_str()
)
.into(),
backtrace: snafu::Backtrace::generate(),
}),
}
}

/// Deletes a secret in an organization using the secret name.
/// You must authenticate using an access token with the `admin:org` scope to use this endpoint.
/// GitHub Apps must have the `secrets` organization permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let repo = octocrab.repos("owner", "repo")
/// .secrets()
/// .delete_secret("GH_TOKEN")
/// .await?;
///
/// # Ok(())
/// # }
pub async fn delete_secret(&self, secret_name: impl AsRef<str>) -> crate::Result<()> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);

let resp = self.repo.crab._delete(route, None::<&()>).await?;
crate::map_github_error(resp).await?;
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/models/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use hyper::{body, Response};
use snafu::ResultExt;
use url::Url;

pub mod secrets;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
Expand Down
31 changes: 31 additions & 0 deletions src/models/repos/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use super::super::*;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepositorySecret {
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RepositorySecrets {
pub total_count: i32,
pub secrets: Vec<RepositorySecret>,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CreateRepositorySecret<'a> {
/// Value for your secret,
/// encrypted with LibSodium using the public key retrieved from the Get an organization public key endpoint.
pub encrypted_value: &'a str,
/// ID of the key you used to encrypt the secret.
pub key_id: &'a str,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CreateRepositorySecretResponse {
Created,
Updated,
}
Loading

0 comments on commit 4b8bd44

Please sign in to comment.