refactor(acl): allow extending scope on the capability file (#8674)

This commit is contained in:
Lucas Fernandes Nogueira 2024-01-24 23:36:07 -03:00 committed by GitHub
parent 734d78f736
commit fd4bf94d4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 318 additions and 14 deletions

View File

@ -132,8 +132,9 @@ pub fn validate_capabilities(
continue;
}
for permission in &capability.permissions {
if let Some((plugin_name, permission_name)) = permission.get().split_once(':') {
for permission_entry in &capability.permissions {
let permission_id = permission_entry.identifier();
if let Some((plugin_name, permission_name)) = permission_id.get().split_once(':') {
let permission_exists = plugin_manifests
.get(plugin_name)
.map(|manifest| {
@ -162,7 +163,7 @@ pub fn validate_capabilities(
anyhow::bail!(
"Permission {} not found, expected one of {}",
permission.get(),
permission_id.get(),
available_permissions.join(", ")
);
}

View File

@ -7,11 +7,37 @@
use crate::{acl::Identifier, platform::Target};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
use super::Scopes;
/// A set of direct capabilities grouped together under a new name.
pub struct CapabilitySet {
inner: Vec<Capability>,
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
/// or an object that references a permission and extends its scope.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PermissionEntry {
/// Reference a permission or permission set by identifier.
PermissionRef(Identifier),
/// Reference a permission or permission set by identifier and extends its scope.
ExtendedPermission {
/// Identifier of the permission or permission set.
identifier: Identifier,
/// Scope to append to the existing permission scope.
#[serde(default, flatten)]
scope: Scopes,
},
}
impl PermissionEntry {
/// The identifier of the permission referenced in this entry.
pub fn identifier(&self) -> &Identifier {
match self {
Self::PermissionRef(identifier) => identifier,
Self::ExtendedPermission {
identifier,
scope: _,
} => identifier,
}
}
}
/// a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.
@ -36,7 +62,7 @@ pub struct Capability {
/// List of windows that uses this capability. Can be a glob pattern.
pub windows: Vec<String>,
/// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
pub permissions: Vec<Identifier>,
pub permissions: Vec<PermissionEntry>,
/// Target platforms this capability applies. By default all platforms applies.
#[serde(default = "default_platforms")]
pub platforms: Vec<Target>,

View File

@ -14,7 +14,7 @@ use glob::Pattern;
use crate::platform::Target;
use super::{
capability::{Capability, CapabilityContext},
capability::{Capability, CapabilityContext, PermissionEntry},
plugin::Manifest,
Error, ExecutionContext, Permission, PermissionSet, Scopes, Value,
};
@ -83,24 +83,48 @@ impl Resolved {
continue;
}
for permission_id in &capability.permissions {
for permission_entry in &capability.permissions {
let permission_id = permission_entry.identifier();
let permission_name = permission_id.get_base();
if let Some(plugin_name) = permission_id.get_prefix() {
let permissions = get_permissions(plugin_name, permission_name, &acl)?;
for permission in permissions {
let scope = match permission_entry {
PermissionEntry::PermissionRef(_) => permission.scope.clone(),
PermissionEntry::ExtendedPermission {
identifier: _,
scope,
} => {
let mut merged = permission.scope.clone();
if let Some(allow) = scope.allow.clone() {
merged
.allow
.get_or_insert_with(Default::default)
.extend(allow);
}
if let Some(deny) = scope.deny.clone() {
merged
.deny
.get_or_insert_with(Default::default)
.extend(deny);
}
merged
}
};
if permission.commands.allow.is_empty() && permission.commands.deny.is_empty() {
// global scope
global_scope
.entry(plugin_name.to_string())
.or_default()
.push(permission.scope.clone());
.push(scope.clone());
} else {
let has_scope = permission.scope.allow.is_some() || permission.scope.deny.is_some();
let has_scope = scope.allow.is_some() || scope.deny.is_some();
if has_scope {
current_scope_id += 1;
command_scopes.insert(current_scope_id, permission.scope.clone());
command_scopes.insert(current_scope_id, scope.clone());
}
let scope_id = if has_scope {

View File

@ -0,0 +1,40 @@
{
"identifier": "run-app",
"description": "app capability",
"windows": [
"main"
],
"permissions": [
{
"identifier": "fs:read",
"allow": [
{
"path": "$HOME/.config/**"
}
]
},
"fs:deny-home",
{
"identifier": "fs:allow-read-resources",
"deny": [
{
"path": "$RESOURCE/**/*.key"
}
]
},
"fs:allow-move-temp",
{
"identifier": "fs:allow-app",
"allow": [
{
"path": "$APP/**"
}
],
"deny": [
{
"path": "$APP/*.db"
}
]
}
]
}

View File

@ -0,0 +1,212 @@
---
source: core/tests/acl/src/lib.rs
assertion_line: 59
expression: resolved
---
Resolved {
allowed_commands: {
CommandKey {
name: "plugin:fs|move",
context: Local,
}: ResolvedCommand {
windows: [
Pattern {
original: "main",
tokens: [
Char(
'm',
),
Char(
'a',
),
Char(
'i',
),
Char(
'n',
),
],
is_recursive: false,
},
],
scope: Some(
792017965103506125,
),
},
CommandKey {
name: "plugin:fs|read_dir",
context: Local,
}: ResolvedCommand {
windows: [
Pattern {
original: "main",
tokens: [
Char(
'm',
),
Char(
'a',
),
Char(
'i',
),
Char(
'n',
),
],
is_recursive: false,
},
],
scope: Some(
5856262838373339618,
),
},
CommandKey {
name: "plugin:fs|read_file",
context: Local,
}: ResolvedCommand {
windows: [
Pattern {
original: "main",
tokens: [
Char(
'm',
),
Char(
'a',
),
Char(
'i',
),
Char(
'n',
),
],
is_recursive: false,
},
],
scope: Some(
10252531491715478446,
),
},
},
denied_commands: {},
command_scope: {
792017965103506125: ResolvedScope {
allow: [
Map(
{
"path": String(
"$TEMP/*",
),
},
),
],
deny: [],
},
5856262838373339618: ResolvedScope {
allow: [
Map(
{
"path": String(
"$HOME/.config/**",
),
},
),
Map(
{
"path": String(
"$RESOURCE/**",
),
},
),
Map(
{
"path": String(
"$RESOURCE",
),
},
),
],
deny: [
Map(
{
"path": String(
"$RESOURCE/**/*.key",
),
},
),
],
},
10252531491715478446: ResolvedScope {
allow: [
Map(
{
"path": String(
"$HOME/.config/**",
),
},
),
Map(
{
"path": String(
"$RESOURCE/**",
),
},
),
Map(
{
"path": String(
"$RESOURCE",
),
},
),
],
deny: [
Map(
{
"path": String(
"$RESOURCE/**/*.key",
),
},
),
],
},
},
global_scope: {
"fs": ResolvedScope {
allow: [
Map(
{
"path": String(
"$APP",
),
},
),
Map(
{
"path": String(
"$APP/**",
),
},
),
],
deny: [
Map(
{
"path": String(
"$HOME",
),
},
),
Map(
{
"path": String(
"$APP/*.db",
),
},
),
],
},
},
}

View File

@ -50,7 +50,7 @@ mod tests {
.expect("required-plugins.json is not a valid JSON");
let manifests = load_plugins(&fixture_plugins);
let capabilities = parse_capabilities(&format!("{}/*.toml", fixture_entry.path().display()))
let capabilities = parse_capabilities(&format!("{}/cap*", fixture_entry.path().display()))
.expect("failed to parse capabilities");
let resolved = Resolved::resolve(manifests, capabilities, Target::current())