Procházet zdrojové kódy

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

Lucas Fernandes Nogueira před 1 rokem
rodič
revize
fd4bf94d4c

+ 4 - 3
core/tauri-build/src/acl.rs

@@ -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(", ")
           );
         }

+ 30 - 4
core/tauri-utils/src/acl/capability.rs

@@ -7,11 +7,37 @@
 use crate::{acl::Identifier, platform::Target};
 use serde::{Deserialize, Serialize};
 
+use super::Scopes;
+
+/// 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,
+  },
+}
 
-/// A set of direct capabilities grouped together under a new name.
-pub struct CapabilitySet {
-  inner: Vec<Capability>,
+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>,

+ 29 - 5
core/tauri-utils/src/acl/resolved.rs

@@ -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 {

+ 40 - 0
core/tests/acl/fixtures/capabilities/scope-extended/cap.json

@@ -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"
+        }
+      ]
+    }
+  ]
+}

+ 1 - 0
core/tests/acl/fixtures/capabilities/scope-extended/required-plugins.json

@@ -0,0 +1 @@
+["fs"]

+ 212 - 0
core/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap

@@ -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",
+                        ),
+                    },
+                ),
+            ],
+        },
+    },
+}

+ 1 - 1
core/tests/acl/src/lib.rs

@@ -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())