Skip to content

Commit

Permalink
Prepare the CLI for license renewals (#2192)
Browse files Browse the repository at this point in the history
* Revamped CredentialStore

* More docs

* Changelog entry

* Handling both fingerprint and subscription id in the CredentialStore

* Updated changelog

* Fixed CredentialStore logic

* Doc extended

* Another doc
  • Loading branch information
Razz4780 authored Jan 24, 2024
1 parent 803fc38 commit 30414a2
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 99 deletions.
1 change: 1 addition & 0 deletions changelog.d/2190.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added license subscription id to operator status CRD. Adjusted `CredentialStore` to preserve signing key pair for the same operator license subscription id.
161 changes: 111 additions & 50 deletions mirrord/auth/src/credential_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
certificate::Certificate,
credentials::Credentials,
error::{AuthenticationError, CertificateStoreError, Result},
key_pair::KeyPair,
};

/// "~/.mirrord"
Expand All @@ -33,11 +34,15 @@ static CREDENTIALS_PATH: LazyLock<PathBuf> = LazyLock::new(|| CREDENTIALS_DIR.jo
/// Container that is responsible for creating/loading `Credentials`
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct CredentialStore {
/// User-specified CN to be used in all new certificates in this store. (Defaults to hostname)
common_name: Option<String>,
/// Credentials for operator
/// Can be linked to several diffrent operator licenses via diffrent keys
/// Can be linked to several different operator licenses via different keys.
#[serde(default)]
credentials: HashMap<String, Credentials>,
/// Associates previously seen operator subscription ids with the [`KeyPair`]s used to generate
/// a certification request. Enables using the same [`KeyPair`] when the operator license
/// changes, but it belongs to the same subscription.
#[serde(default)]
signing_keys: HashMap<String, KeyPair>,
}

/// Information about user gathered from the local system to be shared with the operator
Expand Down Expand Up @@ -87,45 +92,108 @@ impl CredentialStore {
.map_err(AuthenticationError::from)
}

/// Get or create and ready up a certificate at `active_credential` slot
/// Get hostname to be used as common name in a certification request.
fn certificate_common_name() -> String {
whoami::hostname()
}

/// Get or create and ready up a certificate for specific operator installation.
/// Assign the key pair used to sign the certificate with the given `operator_subscription_id`.
///
/// If an expired certificate for the given `operator_fingerprint` is found, new certificate
/// request will be signed by the same key pair. If a key pair assigned to the given
/// `operator_subscription_id` is found, new certificate request will be signed by the same key
/// pair.
///
/// # Note
///
/// Whenever we create/retrieve user's [`Credentials`], we associate the found key pair with
/// operator's subscription id. Then, the operator's license is renewed - its fingerprint
/// changes and we don't have any matching [`Credentials`]. But the subscription id does not
/// change, so we look up the mapping inside [`CredentialStore`] to find the key pair we used
/// previously for the same subscription id.
///
/// Also, subscription id is accepted as an [`Option`] to make the CLI backwards compatible.
#[tracing::instrument(level = "trace", skip(self, client))]
pub async fn get_or_init<R>(
&mut self,
client: &Client,
credential_name: String,
operator_fingerprint: String,
operator_subscription_id: Option<String>,
) -> Result<&mut Credentials>
where
R: Resource + Clone + Debug,
R: for<'de> Deserialize<'de>,
R::DynamicType: Default,
{
let credentials = match self.credentials.entry(credential_name) {
Entry::Vacant(entry) => entry.insert(Credentials::init()?),
Entry::Occupied(entry) => entry.into_mut(),
};
let credentials = match self.credentials.entry(operator_fingerprint) {
Entry::Vacant(entry) => {
let key_pair = operator_subscription_id
.as_ref()
.and_then(|id| self.signing_keys.get(id))
.cloned();

if !credentials.is_ready() {
let common_name = self.common_name.clone().unwrap_or_else(whoami::hostname);

credentials
.get_client_certificate::<R>(client.clone(), &common_name)
let credentials = Credentials::init::<R>(
client.clone(),
&Self::certificate_common_name(),
key_pair,
)
.await?;
entry.insert(credentials)
}
Entry::Occupied(entry) => {
let credentials = entry.into_mut();

if !credentials.is_valid() {
credentials
.refresh::<R>(client.clone(), &Self::certificate_common_name())
.await?;
}

credentials
}
};

if let Some(sub_id) = operator_subscription_id {
self.signing_keys
.insert(sub_id, credentials.key_pair().clone());
}

Ok(credentials)
}
}

/// A `CredentialStore` but saved loaded from file and saved with exclusive lock on the file
pub struct CredentialStoreSync;
/// Exposes methods to safely access [`CredentialStore`] stored in a file.
pub struct CredentialStoreSync {
store_file: fs::File,
}

impl CredentialStoreSync {
/// Try and get/create a client certificate used for `access_store_file` once a file lock is
/// created
async fn access_store_credential<R, C, V>(
pub async fn open() -> Result<Self> {
if !CREDENTIALS_DIR.exists() {
fs::create_dir_all(&*CREDENTIALS_DIR)
.await
.map_err(CertificateStoreError::from)?;
}

let store_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&*CREDENTIALS_PATH)
.await
.map_err(CertificateStoreError::from)?;

Ok(Self { store_file })
}

/// Try and get/create a specific client certificate.
/// The exclusive file lock is already acquired.
async fn access_credential<R, C, V>(
&mut self,
client: &Client,
credential_name: String,
store_file: &mut fs::File,
operator_fingerprint: String,
operator_subscription_id: Option<String>,
callback: C,
) -> Result<V>
where
Expand All @@ -134,61 +202,54 @@ impl CredentialStoreSync {
R::DynamicType: Default,
C: FnOnce(&mut Credentials) -> V,
{
let mut store = CredentialStore::load(store_file)
let mut store = CredentialStore::load(&mut self.store_file)
.await
.inspect_err(|err| info!("CredentialStore Load Error {err:?}"))
.unwrap_or_default();

let value = callback(store.get_or_init::<R>(client, credential_name).await?);
let value = callback(
store
.get_or_init::<R>(client, operator_fingerprint, operator_subscription_id)
.await?,
);

// Make sure the store_file's cursor is at the start of the file before sending it to save
store_file
self.store_file
.seek(SeekFrom::Start(0))
.await
.map_err(CertificateStoreError::from)?;

store.save(store_file).await?;
store.save(&mut self.store_file).await?;

Ok(value)
}

/// Get or create speific client-certificate with an exclusive lock on `CREDENTIALS_PATH`.
/// Get or create specific client certificate with an exclusive lock on the file.
pub async fn get_client_certificate<R>(
&mut self,
client: &Client,
credential_name: String,
operator_fingerprint: String,
operator_subscription_id: Option<String>,
) -> Result<Certificate>
where
R: Resource + Clone + Debug,
R: for<'de> Deserialize<'de>,
R::DynamicType: Default,
{
if !CREDENTIALS_DIR.exists() {
fs::create_dir_all(&*CREDENTIALS_DIR)
.await
.map_err(CertificateStoreError::from)?;
}

let mut store_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&*CREDENTIALS_PATH)
.await
.map_err(CertificateStoreError::from)?;

store_file
self.store_file
.lock_exclusive()
.map_err(CertificateStoreError::Lockfile)?;

let result = Self::access_store_credential::<R, _, Certificate>(
client,
credential_name,
&mut store_file,
|credentials| credentials.as_ref().clone(),
)
.await;
let result = self
.access_credential::<R, _, Certificate>(
client,
operator_fingerprint,
operator_subscription_id,
|credentials| credentials.as_ref().clone(),
)
.await;

store_file
self.store_file
.unlock()
.map_err(CertificateStoreError::Lockfile)?;

Expand Down
Loading

0 comments on commit 30414a2

Please sign in to comment.