Implement hierarchical recipient policies (#5)
This commit is contained in:
@@ -327,6 +327,10 @@ impl RepositorySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recipient_policy_at(&self, directory: &DirectoryPath) -> Option<&RecipientPolicy> {
|
||||
self.recipients.get(directory)
|
||||
}
|
||||
|
||||
pub fn git_repository(&self, entry: &EntryPath) -> Option<&GitRepository> {
|
||||
let mut directory = entry.parent();
|
||||
loop {
|
||||
@@ -409,6 +413,95 @@ impl Repository {
|
||||
Ok(EncryptedEntry(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_policy_file(
|
||||
&self,
|
||||
directory: &DirectoryPath,
|
||||
signature: bool,
|
||||
) -> Result<Option<Vec<u8>>, RepositoryError> {
|
||||
let opened = match self.open_directory(&directory.0) {
|
||||
Ok(opened) => opened,
|
||||
Err(RepositoryError::NotFound { .. }) => return Ok(None),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let name = if signature {
|
||||
RECIPIENT_SIGNATURE_FILE
|
||||
} else {
|
||||
RECIPIENT_FILE
|
||||
};
|
||||
let path = directory.0.join(name);
|
||||
let Some(metadata) = child_metadata(&opened, name, &path)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
require_regular_file(metadata, &path)?;
|
||||
let mut file = opened
|
||||
.open(name)
|
||||
.map_err(|error| io_error("open recipient policy", &path, error))?;
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)
|
||||
.map_err(|error| io_error("read recipient policy", &path, error))?;
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn replace_policy_file(
|
||||
&self,
|
||||
directory: &DirectoryPath,
|
||||
signature: bool,
|
||||
contents: Option<&[u8]>,
|
||||
) -> Result<(), RepositoryError> {
|
||||
let name = if signature {
|
||||
RECIPIENT_SIGNATURE_FILE
|
||||
} else {
|
||||
RECIPIENT_FILE
|
||||
};
|
||||
let path = directory.0.join(name);
|
||||
let (opened, created) = if contents.is_some() {
|
||||
self.create_directory_path(directory)?
|
||||
} else {
|
||||
match self.open_directory(&directory.0) {
|
||||
Ok(opened) => (opened, Vec::new()),
|
||||
Err(RepositoryError::NotFound { .. }) => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
};
|
||||
let result = (|| {
|
||||
if let Some(contents) = contents {
|
||||
if let Some(metadata) = child_metadata(&opened, name, &path)? {
|
||||
require_regular_file(metadata, &path)?;
|
||||
}
|
||||
let mut temporary = TempFile::new(&opened)
|
||||
.map_err(|error| io_error("create atomic recipient policy", &path, error))?;
|
||||
set_private_permissions(&temporary, &path)?;
|
||||
temporary
|
||||
.write_all(contents)
|
||||
.map_err(|error| io_error("write atomic recipient policy", &path, error))?;
|
||||
temporary
|
||||
.as_file()
|
||||
.sync_all()
|
||||
.map_err(|error| io_error("sync atomic recipient policy", &path, error))?;
|
||||
temporary
|
||||
.replace(name)
|
||||
.map_err(|error| io_error("replace recipient policy", &path, error))?;
|
||||
sync_directory(&opened, &directory.0)
|
||||
.map_err(|_| RepositoryError::DurabilityUncertain { path: path.clone() })
|
||||
} else {
|
||||
let Some(metadata) = child_metadata(&opened, name, &path)? else {
|
||||
return Ok(());
|
||||
};
|
||||
require_regular_file(metadata, &path)?;
|
||||
opened
|
||||
.remove_file(name)
|
||||
.map_err(|error| io_error("remove recipient policy", &path, error))?;
|
||||
sync_directory(&opened, &directory.0)
|
||||
.map_err(|_| RepositoryError::DurabilityUncertain { path: path.clone() })
|
||||
}
|
||||
})();
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.committed() => Err(error),
|
||||
Err(error) => self.rollback_created(created, error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Atomically replace an encrypted entry and durably commit its containing directory.
|
||||
pub fn write_entry(
|
||||
&self,
|
||||
@@ -604,6 +697,54 @@ impl Repository {
|
||||
unreachable!("EntryPath has at least one component")
|
||||
}
|
||||
|
||||
fn create_directory_path(
|
||||
&self,
|
||||
path: &DirectoryPath,
|
||||
) -> Result<(Dir, Vec<PathBuf>), RepositoryError> {
|
||||
let mut directory = self
|
||||
.root
|
||||
.try_clone()
|
||||
.map_err(|error| io_error("clone repository root", Path::new("."), error))?;
|
||||
let mut relative = PathBuf::new();
|
||||
let mut created = Vec::new();
|
||||
for component in path.0.components() {
|
||||
let Component::Normal(name) = component else {
|
||||
unreachable!("DirectoryPath is validated")
|
||||
};
|
||||
if let Err(error) = reject_entry_directory_collision(&directory, name, &relative) {
|
||||
return self.rollback_created(created, error);
|
||||
}
|
||||
relative.push(name);
|
||||
match child_metadata(&directory, name, &relative) {
|
||||
Ok(Some(metadata)) => {
|
||||
if let Err(error) = require_directory(metadata, &relative) {
|
||||
return self.rollback_created(created, error);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
if let Err(error) = create_private_directory(&directory, name, &relative) {
|
||||
return self.rollback_created(created, error);
|
||||
}
|
||||
created.push(relative.clone());
|
||||
if let Err(error) = sync_directory(&directory, &relative) {
|
||||
return self.rollback_created(created, error);
|
||||
}
|
||||
}
|
||||
Err(error) => return self.rollback_created(created, error),
|
||||
}
|
||||
match directory.open_dir(name) {
|
||||
Ok(opened) => directory = opened,
|
||||
Err(error) => {
|
||||
return self.rollback_created(
|
||||
created,
|
||||
io_error("open recipient policy directory", &relative, error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((directory, created))
|
||||
}
|
||||
|
||||
fn rollback_created<T>(
|
||||
&self,
|
||||
mut created: Vec<PathBuf>,
|
||||
|
||||
Reference in New Issue
Block a user