mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 19:29:28 -07:00
Fix ASPE logic, add aspe delete, apse create -> aspe upload, couple more tests
This commit is contained in:
parent
61baa8f52f
commit
29c940c623
11 changed files with 340 additions and 112 deletions
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
default-members = ["crates/aspm"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
|
@ -18,7 +18,7 @@ pub struct AspeVersion {
|
|||
|
||||
/// An ASPE-compatible server
|
||||
pub struct AspeServer {
|
||||
host: Host,
|
||||
pub host: Host,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ impl From<reqwest::Error> for AspeFetchFailure {
|
|||
} else {
|
||||
match value.status().unwrap() {
|
||||
StatusCode::NOT_FOUND => Self::NotFound,
|
||||
StatusCode::BAD_REQUEST => Self::NotFound, // Non-spec compliant: as of now, keyoxide.org will return a 400 when it should return a 404
|
||||
StatusCode::PAYLOAD_TOO_LARGE => Self::TooLarge,
|
||||
StatusCode::TOO_MANY_REQUESTS => Self::RateLimited,
|
||||
_ => Self::Unknown(value),
|
||||
|
@ -117,7 +118,7 @@ impl AspeServer {
|
|||
pub async fn post_request(&self, jws: impl Into<String>) -> Result<(), AspeRequestFailure> {
|
||||
self.client
|
||||
.post(format!(
|
||||
"https://{host}/.well-known/aspe/post/",
|
||||
"https://{host}/.well-known/aspe/post",
|
||||
host = self.host
|
||||
))
|
||||
.header(
|
||||
|
@ -126,7 +127,8 @@ impl AspeServer {
|
|||
)
|
||||
.body(jws.into())
|
||||
.send()
|
||||
.await?;
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -148,6 +150,7 @@ impl AspeServer {
|
|||
)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text_with_charset("utf-8")
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use anyhow::Context;
|
||||
use josekit::{
|
||||
jwk::Jwk,
|
||||
|
@ -37,6 +39,8 @@ pub enum JwtSerializationError {
|
|||
PayloadSerializationError,
|
||||
#[error("jwt was unable to be serialized for an unknown reason")]
|
||||
SerializationError,
|
||||
#[error("unable to get system time")]
|
||||
SystemTimeError,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -69,8 +73,9 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
|
|||
.as_object()
|
||||
.context("serialized struct was not a Map")
|
||||
.or(Err(JwtSerializationError::PayloadSerializationError))?;
|
||||
let payload = JwtPayload::from_map(map.clone())
|
||||
let mut payload = JwtPayload::from_map(map.clone())
|
||||
.or(Err(JwtSerializationError::PayloadSerializationError))?;
|
||||
payload.set_issued_at(&SystemTime::now());
|
||||
|
||||
// Sign it into a JWT
|
||||
jwt::encode_with_signer(
|
||||
|
|
106
crates/aspm/src/commands/aspe/delete.rs
Normal file
106
crates/aspm/src/commands/aspe/delete.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit as _};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _};
|
||||
use asp::{
|
||||
aspe::{
|
||||
requests::{AspeRequest, AspeRequestType, AspeRequestVariant},
|
||||
AspeRequestFailure, AspeServer,
|
||||
},
|
||||
keys::AspKey,
|
||||
url::Host,
|
||||
utils::jwt::JwtSerialize,
|
||||
};
|
||||
use clap::Parser;
|
||||
use data_encoding::BASE64_NOPAD;
|
||||
use dialoguer::{theme::ColorfulTheme, Password};
|
||||
use josekit::jwk::Jwk;
|
||||
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
||||
|
||||
use crate::{
|
||||
commands::AspmSubcommand,
|
||||
entities::{keys, prelude::*},
|
||||
};
|
||||
|
||||
/// Upload an existing profile to an ASPE server.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AspeDeleteCommand {
|
||||
/// The fingerprint or alias of the key to delete
|
||||
key: String,
|
||||
/// The ASPE server to upload this profile to
|
||||
#[clap(value_parser = Host::parse)]
|
||||
server: Host,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AspmSubcommand for AspeDeleteCommand {
|
||||
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
|
||||
let server = AspeServer::new(self.server)
|
||||
.await
|
||||
.context("Unable to parse provided server into ASPE server, please verify that it is a valid ASPE server")?;
|
||||
|
||||
let Some(key) = Keys::find()
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(keys::Column::Fingerprint.eq(&self.key))
|
||||
.add(keys::Column::Alias.eq(&self.key)),
|
||||
)
|
||||
.one(&state.db)
|
||||
.await
|
||||
.context("Unable to query for keys with fingerprint or alias")?
|
||||
else {
|
||||
bail!("No key found with that fingerprint or alias");
|
||||
};
|
||||
|
||||
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
|
||||
Password::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Please enter a password to decrypt the key with")
|
||||
.interact()
|
||||
.context("Unable to prompt on stderr")
|
||||
})?;
|
||||
|
||||
let argon_salt =
|
||||
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes()))
|
||||
.context("Unable to decode argon2 salt")?;
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(key_password.as_bytes(), &argon_salt)
|
||||
.or(Err(anyhow!("Unable to derive encryption key")))?;
|
||||
let aes_key = hash.hash.context("Unable to derive encryption key")?;
|
||||
let aes_key = &aes_key.as_bytes()[0..32];
|
||||
|
||||
let Ok(decrypted) = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(aes_key))
|
||||
.decrypt((&aes_key[0..12]).into(), key.cipher_text.as_slice())
|
||||
else {
|
||||
bail!("Unable to decrypt key from database (wrong password?)")
|
||||
};
|
||||
|
||||
let aspe_request = AspeRequest {
|
||||
version: 0,
|
||||
r#type: AspeRequestType::Request,
|
||||
request: AspeRequestVariant::Delete {
|
||||
aspe_uri: format!("aspe:{}:{}", server.host, key.fingerprint),
|
||||
},
|
||||
};
|
||||
|
||||
let encoded_request = aspe_request
|
||||
.encode_and_sign(&AspKey::from_jwk(Jwk::from_bytes(&decrypted)?)?)
|
||||
.context("Unable to encode the delete request as a JWT and sign it")?;
|
||||
|
||||
match server.post_request(encoded_request).await {
|
||||
Ok(_) => {
|
||||
println!("Successfully deleted profile from ASPE server!");
|
||||
Ok(())
|
||||
}
|
||||
Err(AspeRequestFailure::BadRequest) => {
|
||||
bail!("The ASPE server rejected the request due to invalid data")
|
||||
}
|
||||
Err(AspeRequestFailure::TooLarge) => {
|
||||
bail!("The ASPE server rejected the request as being too large")
|
||||
}
|
||||
Err(AspeRequestFailure::RateLimited) => {
|
||||
bail!("The ASPE server rejected the request due to a ratelimit")
|
||||
}
|
||||
Err(AspeRequestFailure::Unknown(e)) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod create;
|
||||
mod upload;
|
||||
mod delete;
|
||||
|
||||
/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles.
|
||||
/// A subcommand to allow management of profiles on ASPE servers.
|
||||
#[derive(Parser)]
|
||||
pub struct AspeSubcommand {
|
||||
#[command(subcommand)]
|
||||
|
@ -11,5 +12,6 @@ pub struct AspeSubcommand {
|
|||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AspeSubcommands {
|
||||
Create(create::AspeCreateCommand),
|
||||
Upload(upload::AspeUploadCommand),
|
||||
Delete(delete::AspeDeleteCommand),
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _};
|
|||
use asp::{
|
||||
aspe::{
|
||||
requests::{AspeRequest, AspeRequestType, AspeRequestVariant},
|
||||
AspeRequestFailure, AspeServer,
|
||||
AspeFetchFailure, AspeRequestFailure, AspeServer,
|
||||
},
|
||||
hex_color::HexColor,
|
||||
keys::AspKey,
|
||||
|
@ -24,9 +24,9 @@ use crate::{
|
|||
entities::{prelude::*, profiles},
|
||||
};
|
||||
|
||||
/// Upload an existing profile to an ASPE server
|
||||
/// Upload an existing profile to an ASPE server.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AspeCreateCommand {
|
||||
pub struct AspeUploadCommand {
|
||||
/// The fingerprint or alias of the profile to upload
|
||||
profile: String,
|
||||
/// The ASPE server to upload this profile to
|
||||
|
@ -35,10 +35,13 @@ pub struct AspeCreateCommand {
|
|||
/// The key to sign this profile with
|
||||
#[clap(short, long)]
|
||||
key: Option<String>,
|
||||
/// If passed, this command will only create new keys on the server, rather than updating if the key already exists
|
||||
#[clap(long)]
|
||||
no_update: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AspmSubcommand for AspeCreateCommand {
|
||||
impl AspmSubcommand for AspeUploadCommand {
|
||||
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
|
||||
let server = AspeServer::new(self.server)
|
||||
.await
|
||||
|
@ -63,7 +66,8 @@ impl AspmSubcommand for AspeCreateCommand {
|
|||
.await
|
||||
.context("Unable to query for related claims")?;
|
||||
|
||||
let Some(key) = Keys::find_by_id(self.key.unwrap_or(profile.key))
|
||||
let fingerprint = self.key.unwrap_or(profile.key);
|
||||
let Some(key) = Keys::find_by_id(&fingerprint)
|
||||
.one(&state.db)
|
||||
.await
|
||||
.context("Unable to query database for key")?
|
||||
|
@ -118,17 +122,37 @@ impl AspmSubcommand for AspeCreateCommand {
|
|||
.encode_and_sign(&AspKey::from_jwk(Jwk::from_bytes(&decrypted)?)?)
|
||||
.context("Unable to encode the profile as a JWT and sign it")?;
|
||||
|
||||
let create_request = AspeRequest {
|
||||
// Check if the key is already used on the server, to send an update request instead of a create request
|
||||
let should_update_profile = match server.fetch_profile(&fingerprint).await {
|
||||
Ok(_) if self.no_update => bail!("A profile with the selected key was already present on the selected server, but the --no-update flag was passed"),
|
||||
Ok(_) => true,
|
||||
Err(AspeFetchFailure::NotFound) => false,
|
||||
Err(AspeFetchFailure::TooLarge) => bail!("ASPE server rejected the request to fetch existing profile as it was too large"),
|
||||
Err(AspeFetchFailure::RateLimited) => bail!("ASPE server returned a ratelimit error while fetching existing profile, try again later"),
|
||||
Err(AspeFetchFailure::Unknown(e)) => bail!(e),
|
||||
};
|
||||
|
||||
let aspe_request = match should_update_profile {
|
||||
false => AspeRequest {
|
||||
version: 0,
|
||||
r#type: AspeRequestType::Request,
|
||||
request: AspeRequestVariant::Create {
|
||||
profile_jws: encoded_profile,
|
||||
},
|
||||
},
|
||||
true => AspeRequest {
|
||||
version: 0,
|
||||
r#type: AspeRequestType::Request,
|
||||
request: AspeRequestVariant::Update {
|
||||
profile_jws: encoded_profile,
|
||||
aspe_uri: format!("aspe:{}:{}", server.host, fingerprint),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let encoded_request = create_request
|
||||
let encoded_request = aspe_request
|
||||
.encode_and_sign(&AspKey::from_jwk(Jwk::from_bytes(&decrypted)?)?)
|
||||
.context("Unable to encode the profile as a JWT and sign it")?;
|
||||
.context("Unable to encode the profile request as a JWT and sign it")?;
|
||||
|
||||
match server.post_request(encoded_request).await {
|
||||
Ok(_) => {
|
|
@ -22,6 +22,8 @@ use crate::{
|
|||
pub enum KeyImportFormat {
|
||||
// An unencrypted raw JSON Web Key
|
||||
Jwk,
|
||||
// An unencrypted raw PKCS#8 key
|
||||
Pkcs8,
|
||||
// Imports a PGP key from a local GPG store
|
||||
#[cfg(feature = "gpg-compat")]
|
||||
Gpg,
|
||||
|
@ -71,6 +73,9 @@ impl AspmSubcommand for KeysImportCommand {
|
|||
Jwk::from_bytes(self.key.as_bytes()).context("Unable to parse provided JWK")?,
|
||||
)
|
||||
.context("Unable to convert parsed JWK to an AspKey")?,
|
||||
KeyImportFormat::Pkcs8 => {
|
||||
AspKey::from_pkcs8(&self.key).context("Unable to convert PKCS#8 to an AspKey")?
|
||||
}
|
||||
#[cfg(feature = "gpg-compat")]
|
||||
KeyImportFormat::Gpg => {
|
||||
use gpgme::{Context as GpgContext, PassphraseRequest};
|
||||
|
|
|
@ -7,7 +7,7 @@ pub mod export;
|
|||
pub mod import;
|
||||
pub mod list;
|
||||
|
||||
/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles.
|
||||
/// A subcommand to allow the management of profiles.
|
||||
#[derive(Parser)]
|
||||
pub struct ProfilesSubcommand {
|
||||
#[command(subcommand)]
|
||||
|
|
|
@ -174,7 +174,8 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
|
|||
ProfilesSubcommands::Import(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
},
|
||||
AspmSubcommands::Aspe(subcommand) => match subcommand.subcommand {
|
||||
AspeSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
AspeSubcommands::Upload(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
AspeSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
mod consts;
|
||||
|
||||
use anyhow::Context;
|
||||
use assert_cmd::prelude::*;
|
||||
use consts::*;
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static KEY_ALIAS: &str = "TESTKEY";
|
||||
static KEY_PASSWORD: &str = "TESTKEYPASSWORD";
|
||||
|
@ -19,7 +22,9 @@ fn assert_key_generated(datadir: &str) -> Result<(), anyhow::Error> {
|
|||
.arg(KEY_ALIAS)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::starts_with("Successfully generated a new key!"));
|
||||
.stdout(predicate::str::starts_with(
|
||||
"Successfully generated a new key!",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -27,7 +32,10 @@ fn assert_key_generated(datadir: &str) -> Result<(), anyhow::Error> {
|
|||
#[test]
|
||||
fn help_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
Command::cargo_bin("aspm")?
|
||||
.env("ASPM_DATA_DIR", datadir)
|
||||
|
@ -62,7 +70,10 @@ fn help_works() -> Result<(), anyhow::Error> {
|
|||
#[test]
|
||||
fn keys_generate_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
assert_key_generated(datadir)
|
||||
}
|
||||
|
@ -70,7 +81,10 @@ fn keys_generate_works() -> Result<(), anyhow::Error> {
|
|||
#[test]
|
||||
fn keys_list_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
assert_key_generated(datadir)?;
|
||||
|
||||
|
@ -89,7 +103,10 @@ fn keys_list_works() -> Result<(), anyhow::Error> {
|
|||
#[test]
|
||||
fn keys_export_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
assert_key_generated(datadir)?;
|
||||
|
||||
|
@ -109,10 +126,66 @@ fn keys_export_works() -> Result<(), anyhow::Error> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_import_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
for (export_format, key) in [
|
||||
("pkcs8", TEST_KEY_PKCS8),
|
||||
("asp-tool", TEST_KEY_ASPTOOL),
|
||||
("jwk", TEST_KEY_JWK),
|
||||
] {
|
||||
Command::cargo_bin("aspm")?
|
||||
.env("ASPM_DATA_DIR", datadir)
|
||||
.arg("keys")
|
||||
.arg("import")
|
||||
.env("KEY_PASSWORD", TEST_KEY_PASSWORD)
|
||||
.arg("--key-alias")
|
||||
.arg(KEY_ALIAS)
|
||||
.arg(export_format)
|
||||
.arg(key)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("Successfully imported key!")
|
||||
.and(predicate::str::contains(TEST_FINGERPRINT)),
|
||||
);
|
||||
|
||||
Command::cargo_bin("aspm")?
|
||||
.env("ASPM_DATA_DIR", datadir)
|
||||
.arg("keys")
|
||||
.arg("list")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains(TEST_FINGERPRINT));
|
||||
|
||||
Command::cargo_bin("aspm")?
|
||||
.env("ASPM_DATA_DIR", datadir)
|
||||
.arg("keys")
|
||||
.arg("delete")
|
||||
.arg("--no-confirm")
|
||||
.arg(TEST_FINGERPRINT)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains(format!(
|
||||
"Successfully deleted key with fingerprint {TEST_FINGERPRINT}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_delete_works() -> Result<(), anyhow::Error> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?;
|
||||
let datadir = tempdir
|
||||
.path()
|
||||
.to_str()
|
||||
.context("Tempdir path was not valid utf8")?;
|
||||
|
||||
assert_key_generated(datadir)?;
|
||||
|
||||
|
@ -124,7 +197,9 @@ fn keys_delete_works() -> Result<(), anyhow::Error> {
|
|||
.arg(KEY_ALIAS)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Successfully deleted key with fingerprint "));
|
||||
.stdout(predicate::str::contains(
|
||||
"Successfully deleted key with fingerprint ",
|
||||
));
|
||||
|
||||
Command::cargo_bin("aspm")?
|
||||
.env("ASPM_DATA_DIR", datadir)
|
||||
|
|
6
crates/aspm/tests/consts.rs
Normal file
6
crates/aspm/tests/consts.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub const TEST_FINGERPRINT: &str = "XOWIFY4ZKMWYIXXVVAEHC437HE";
|
||||
pub const TEST_KEY_PASSWORD: &str = "test";
|
||||
pub const TEST_KEY_PKCS8: &str = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3B9nMzvh+bGKn3zUsF5M+0VJJXAOShG2HJDC/uez0RehRANCAARzaHaGFWH/DXwbzDAMDmU7eCyb79wUcytBut7oKh2keujlkBSDBOd6gCRuqwZDPoNMYDAeFCKAoll10K6Oeg+D";
|
||||
pub const TEST_KEY_JWK: &str = r#"{"kty":"EC","crv":"P-256","d":"3B9nMzvh-bGKn3zUsF5M-0VJJXAOShG2HJDC_uez0Rc","x":"c2h2hhVh_w18G8wwDA5lO3gsm-_cFHMrQbre6CodpHo","y":"6OWQFIME53qAJG6rBkM-g0xgMB4UIoCiWXXQro56D4M"}"#;
|
||||
pub const TEST_KEY_ASPTOOL: &str = "eyJhbGciOiJzY3J5cHQiLCJwcm0iOnsiTiI6MTMxMDcyLCJyIjo4LCJwIjoxfSwic2x0IjoiUUJSQXZ5Q1JtNGVkMnBVb2R6RktpUT09Iiwia2V5IjoiYVdCWWdMTGR4T2tBb1JWYWI4T1EzcG15UXpDR1V3VXVBVDJ2WDhTZUwrTTJxY2tVOFNVR3FKTWJpVkh4cnZWQjNZNXVTbGZQc3F0RGc3M0NIZmROZ2U2Q0NQN1hVZTVuek1MUUVERVZOU3IwTDAxeG92TDc5WE4rZVV1V3ZwU1BjaEU5Ukt2SG5YQ25sWXZSTjZIdlVKVURuVGV1V3pKM0RScVY3RmxST2RjSHplMW5wYlhjd0pDWiJ9";
|
||||
pub const TEST_PROFILE: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlhPV0lGWTRaS01XWUlYWFZWQUVIQzQzN0hFIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiYzJoMmhoVmhfdzE4Rzh3d0RBNWxPM2dzbS1fY0ZITXJRYnJlNkNvZHBIbyIsInkiOiI2T1dRRklNRTUzcUFKRzZyQmtNLWcweGdNQjRVSW9DaVdYWFFybzU2RDRNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicHJvZmlsZSIsImh0dHA6Ly9hcmlhZG5lLmlkL25hbWUiOiJUeSB0ZXN0aW5nIiwiaHR0cDovL2FyaWFkbmUuaWQvY2xhaW1zIjpbImh0dHBzOi8vZ29vZ2xlLmNvbSJdLCJodHRwOi8vYXJpYWRuZS5pZC9kZXNjcmlwdGlvbiI6IlRoaXMgaXMgcHVyZWx5IGZvciB0ZXN0aW5nIEFTUE0iLCJodHRwOi8vYXJpYWRuZS5pZC9lbWFpbCI6ImV4YW1wbGVAZXhhbXBsZS5jb20iLCJodHRwOi8vYXJpYWRuZS5pZC9jb2xvciI6IiM3OTI5MUEifQ.EZv1ZKqU6xAtzaOv_EhE_6uUZDd6Mnk7jNQcsc8CUcw8mKNnQQ-yEC-MZpAiL9KL-ljZTKEW7gNTB7GnI6uVJQ";
|
Loading…
Reference in a new issue