src/content/docs/learn/Security/writing-plugin-permissions.mdx
import { Steps } from '@astrojs/starlight/components'; import ShowSolution from '@components/ShowSolution.astro' import Cta from '@fragments/cta.mdx';
The goal of this exercise is to get a better understanding on how plugin permissions can be created when writing your own plugin.
At the end you will have the ability to create simple permissions for your plugins. You will have an example Tauri plugin where permissions are partially autogenerated and hand crafted.
<Steps>In our example we will facilitate the Tauri cli
to bootstrap a Tauri plugin source code structure.
Make sure you have installed all Prerequisites
and verify you have the Tauri CLI in the correct version
by running cargo tauri info.
The output should indicate the tauri-cli version is 2.x.
We will proceed in this step-by-step explanation with pnpm but you can choose another
package manager and replace it in the commands accordingly.
Once you have a recent version installed you can go ahead and create the plugin using the Tauri CLI.
<ShowSolution> ```sh mkdir -p tauri-learning cd tauri-learning cargo tauri plugin new test cd tauri-plugin-test pnpm install pnpm build cargo build ``` </ShowSolution>To showcase something practical and simple let us assume our command writes user input to a file in our temporary folder while adding some custom header to the file.
Let's name our command write_custom_file, implement it in src/commands.rs
and add it to our plugin builder to be exposed to the frontend.
Tauri's core utils will autogenerate allow and deny permissions for this
command, so we do not need to care about this.
The command implementation:
use tauri::{AppHandle, command, Runtime};
use crate::models::*;
use crate::Result;
use crate::TestExt;
#[command]
pub(crate) async fn ping<R: Runtime>(
app: AppHandle<R>,
payload: PingRequest,
) -> Result<PingResponse> {
app.test1().ping(payload)
}
#[command]
pub(crate) async fn write_custom_file<R: Runtime>(
user_input: String,
app: AppHandle<R>,
) -> Result<String> {
std::fs::write(app.path().temp_dir().unwrap(), user_input)?;
Ok("success".to_string())
}
Auto-Generate inbuilt permissions for your new command:
const COMMANDS: &[&str] = &["ping", "write_custom_file"];
These inbuilt permissions will be automatically generated by the Tauri build
system and will be visible in the permissions/autogenerated/commands folder.
By default an enable-<command> and deny-<command> permission will
be created.
The previous step was to write the actual command implementation. Next we want to expose it to the frontend so it can be consumed.
<ShowSolution>Configure the Tauri builder to generate the invoke handler to pass frontend IPC requests to the newly implemented command:
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("test")
.invoke_handler(tauri::generate_handler![
commands::ping,
commands::write_custom_file,
])
.setup(|app, api| {
#[cfg(mobile)]
let test = mobile::init(app, api)?;
#[cfg(desktop)]
let test = desktop::init(app, api)?;
app.manage(test);
// manage state so it is accessible by the commands
app.manage(MyState::default());
Ok(())
})
.build()
}
Expose the new command in the frontend module.
This step is essential for the example application to successfully import the frontend module. This is for convenience and has no security impact, as the command handler is already generated and the command can be manually invoked from the frontend.
import { invoke } from '@tauri-apps/api/core'
export async function ping(value: string): Promise<string | null> {
return await invoke<{value?: string}>('plugin:test|ping', {
payload: {
value,
},
}).then((r) => (r.value ? r.value : null));
}
export async function writeCustomFile(user_input: string): Promise<string> {
return await invoke('plugin:test|write_custom_file',{userInput: user_input});
}
:::tip
The invoke parameter needs to be CamelCase. In this example it is userInput instead of user_input.
:::
Make sure your package is built:
pnpm build
As our plugin should expose the write_custom_file command by default
we should add this to our default.toml permission.
"$schema" = "schemas/schema.json"
[default]
description = "Default permissions for the plugin"
permissions = ["allow-ping", "allow-write-custom-file"]
The created plugin directory structure contains an examples/tauri-app folder,
which has a ready to use Tauri application to test out the plugin.
Since we added a new command we need to slightly modify the frontend to invoke our new command instead.
<ShowSolution> ```svelte title="src/App.svelte" del={11-13,42-45} ins={14-16,45-49} <script> import Greet from './lib/Greet.svelte' import { ping, writeCustomFile } from 'tauri-plugin-test-api'let response = ''
function updateResponse(returnValue) {
response += [${new Date().toLocaleTimeString()}] + (typeof returnValue === 'string' ? returnValue : JSON.stringify(returnValue)) + '
'
}
function _ping() { ping("Pong!").then(updateResponse).catch(updateResponse) } function _writeCustomFile() { writeCustomFile("HELLO FROM TAURI PLUGIN").then(updateResponse).catch(updateResponse) } </script>
<main class="container"> <h1>Welcome to Tauri!</h1> <div class="row"> <a href="https://vitejs.dev" target="_blank"> </a>
<a href="https://tauri.app" target="_blank">
</a>
<a href="https://svelte.dev" target="_blank">
</a>
Running this and pressing the "Write" button you should be greeted with this:
success
And you should find a `test.txt` file in your temporary folder containing a message
from our new implemented plugin command.
🥳
</ShowSolution>