Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scripted-client): supporting passing templating variables and a top level filter #107

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions docs/running-the-scripted-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ provided in this repo can be run in a scripted mode. Doing so requires writing
a prompt sequence file specifying the prompts that expected to be seen and how
they should be actioned.

## Running the scripted client from the command line
# Running the scripted client from the command line

The scripted client can be invoked using the `scripted` subcommand as shown below:
```bash
$ apparmor-prompting scripted --script my-script.json
$ apparmor-prompting.scripted \
--script my-script.json \
--var "MY_VAR:foo" \
--var "MY_OTHER_VAR:bar"
```

The JSON script file provided as an argument must parse as a valid prompt
Expand All @@ -18,19 +22,24 @@ if the sequence completes successfully and exiting non-0 if there are any
errors.


## Writing a prompt sequence
# Writing a prompt sequence

A prompt sequence is a simple JSON file with the following structure:
```json
{
"version": 1,
"prompt-filter": {
"snap": "snap-name",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/.*"
}
},
Comment on lines +31 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, it might be nice in the future to have an array of top level filters, and any prompt which matches any of the filters in the array is considered. But we don't need this yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup 🙂

As and when you come across a test case where you need this let me know and we can take a look at implementing it with that in mind 👍

"prompts": [
{
"prompt-filter": {
"snap": "snap-name",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/example.txt",
"path": ".*/example.txt",
"permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand All @@ -40,7 +49,7 @@ A prompt sequence is a simple JSON file with the following structure:
"lifespan": "timespan",
"duration": "1h",
"constraints": {
"path-pattern": "/home/foo/example.txt",
"path-pattern": "$BASE_PATH/example.txt",
"permissions": [ "read", "write" ]
}
}
Expand All @@ -50,9 +59,36 @@ A prompt sequence is a simple JSON file with the following structure:
}
```

## Variables

Bash style `$VAR_NAME` and `${VAR_NAME}` variables are supported within prompt sequence JSON
files, being replaced using the `--var "VAR_NAME:value"` command line flag as shown in the
example above. This is handled as a simple string replacement before attempting to parse the
file, so long as the resulting string parses as valid JSON prompt sequence you are free to
make use of variable substitution as you see fit.

In the event that a scripted client run fails, it will output the fully resolved script to
standard out as part of its exit behaviour so you are able to inspect what the final run
looked like.


## Fields

### Version
The only supported version at this time is `1`. This field is required.

### Prompt Filter
The top-level `prompt-filter` field is used to provide a filter that will be used
to determine which prompts the client will attempt to match against as part of the
provided `prompts` sequence. It is not required that you specify a top level filter,
but if you do then all prompts not matching it will be ignored by the scripted client.

> This is particularly useful for factoring out common elements such the name of the
> snap being matched against and based paths for home prompts etc.

See the `Prompt filter fields` section below for specific details on each of the
supported fields for a filter.

### Prompts
A sequence of prompt cases: a prompt filter allowing for structural matching
against the next prompt in the sequence along with a template for build the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-1.txt",
"path": "$BASE_PATH/test-1.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
{
"version": 1,
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/*"
}
},
"prompts": [
{
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-1.txt",
"path": ".*/test-1.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand All @@ -24,7 +31,7 @@
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-2.txt",
"path": ".*/test-2.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
{
"version": 1,
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/*"
}
},
"prompts": [
{
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-1.txt",
"path": ".*/test-1.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand All @@ -21,10 +26,8 @@
},
{
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-2.txt",
"path": ".*/test-2.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand All @@ -33,16 +36,15 @@
"action": "allow",
"lifespan": "forever",
"constraints": {
"path": "${PROMPT_PATH}/../**",
"permissions": [ "read", "write" ]
}
}
},
{
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-3.txt",
"path": ".*/test-3.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/test-1.txt",
"path": "$BASE_PATH/test-1.txt",
"requested-permissions": [ "write" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand All @@ -24,7 +24,7 @@
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "/home/[a-zA-Z0-9]+/test/.+/incorrect.txt",
"path": "$BASE_PATH/incorrect.txt",
"requested-permissions": [ "read" ],
"available-permissions": [ "read", "write", "execute" ]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": 1,
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/*"
}
},
"prompts": [
{
"prompt-filter": {
"snap": "testSnap",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/bar",
"requested-permissions": [ "read" ],
"available-permissions": [ "read", "write", "execute" ]
}
},
"reply": {
"action": "allow",
"lifespan": "single",
"constraints": {
"path-pattern": "${BASE_PATH}/bar",
"permissions": [ "read", "write" ]
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": 1,
"prompt-filter": {
"snap": "aa-prompting-test",
"interface": "home",
"constraints": {
"path": "$BASE_PATH/.*"
}
},
"prompts": [
{
"prompt-filter": {
"constraints": {
"path": ".*/test.txt",
"requested-permissions": [ "read" ]
}
},
"reply": {
"action": "allow",
"lifespan": "single",
"constraints": {
"path-pattern": "${BASE_PATH}/test.txt",
"permissions": [ "read" ]
}
}
}
]
}
31 changes: 31 additions & 0 deletions prompting-client/resources/scripted-tests/happy-path-read/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env sh
# A simple allow once test of a read prompt.
# Running this test script requires that the accompanying prompt-sequence.json
# file be present in the output directory.

PREFIX="$1"
TEST_DIR="/home/ubuntu/test/$PREFIX"

prompting-client.scripted \
--script="$TEST_DIR/prompt-sequence.json" \
--var "BASE_PATH:$TEST_DIR" | tee "$TEST_DIR/outfile" &

# Ensure that the test client is already listening
sleep 0.2
TEST_OUTPUT="$(aa-prompting-test.read "$PREFIX")"

# Ensure that the test client has time to write its output
# For tests with a grace period this will need to be taken into account as well
sleep 0.2
CLIENT_OUTPUT="$(cat "$TEST_DIR/outfile")"

if [ "$CLIENT_OUTPUT" != "success" ]; then
echo "test failed"
echo "output='$CLIENT_OUTPUT'"
exit 1
fi

if [ "$TEST_OUTPUT" != "testing testing 1 2 3" ]; then
echo "test script failed"
exit 1
fi
64 changes: 60 additions & 4 deletions prompting-client/src/bin/scripted.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
//! A simple command line prompting client
use clap::Parser;
use prompting_client::{
cli_actions::run_scripted_client_loop, snapd_client::SnapdSocketClient, Result,
cli_actions::ScriptedClient, snapd_client::SnapdSocketClient, Error, Result,
};
use std::io::stderr;
use std::{io::stderr, process::exit};
use tracing::subscriber::set_global_default;
use tracing_subscriber::FmtSubscriber;

/// Run a scripted client expecting a given sequence of prompts
/// Run a scripted client expecting a given sequence of prompts.
///
/// If the provided prompt sequence completes cleanly the client will print "success" on standard
/// out. If there are any errors then the first error will be printed as "error: $errorMessage" on
/// standard out and the client will exit with a non-zero exit code.
#[derive(Debug, Parser)]
#[clap(about, long_about = None)]
struct Args {
Expand All @@ -18,6 +22,9 @@ struct Args {
#[clap(short, long, value_name = "FILE")]
script: String,

#[clap(long, action = clap::ArgAction::Append)]
var: Vec<String>,

/// The number of seconds to wait following completion of the script to check for any
/// unexpected additional prompts.
#[clap(short, long, value_name = "SECONDS")]
Expand All @@ -29,6 +36,7 @@ async fn main() -> Result<()> {
let Args {
verbose,
script,
var,
grace_period,
} = Args::parse();

Expand All @@ -44,5 +52,53 @@ async fn main() -> Result<()> {
let mut c = SnapdSocketClient::default();
c.exit_if_prompting_not_enabled().await?;

run_scripted_client_loop(&mut c, script, grace_period).await
let vars = parse_vars(&var)?;

eprintln!("creating client");
let mut scripted_client = ScriptedClient::try_new(script, &vars, c.clone())?;
match scripted_client.run(&mut c, grace_period).await {
Ok(_) => println!("success"),
Err(e) => {
println!("{e}\n\nscript: {}", scripted_client.raw_seq());
exit(1);
}
}

Ok(())
}

fn parse_vars(raw: &[String]) -> Result<Vec<(&str, &str)>> {
let mut vars: Vec<(&str, &str)> = Vec::with_capacity(raw.len());

for s in raw.iter() {
match s.split_once(':') {
Some((k, v)) => vars.push((k.trim(), v.trim())),
None => return Err(Error::InvalidScriptVariable { raw: s.clone() }),
}
}

Ok(vars)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_vars_works() {
let raw_vars = vec![
"foo:bar".into(),
"a: b".to_string(),
"x :y".to_string(),
"1 : 2".to_string(),
];

match parse_vars(&raw_vars) {
Err(e) => panic!("ERROR: {e}"),
Ok(vars) => assert_eq!(
vars,
vec![("foo", "bar"), ("a", "b"), ("x", "y"), ("1", "2")]
),
}
}
}
2 changes: 1 addition & 1 deletion prompting-client/src/cli_actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ mod scripted;

pub use echo_loop::run_echo_loop;
pub use log_level::set_logging_filter;
pub use scripted::run_scripted_client_loop;
pub use scripted::ScriptedClient;
Loading
Loading