Skip to main content

zixcel_github/
config.rs

1use serde::{Deserialize, Serialize};
2
3use crate::boundary::{github_name, identifier, reference};
4use crate::secret_boundary::reject_secrets;
5use crate::{CONFIG_SCHEMA, ConnectorError};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct ConnectorConfig {
10    pub schema: String,
11    pub config_id: String,
12    pub connection_ref: String,
13    pub organization: String,
14    pub repositories: Vec<String>,
15    #[serde(default)]
16    pub allowed_actions: Vec<String>,
17}
18
19impl ConnectorConfig {
20    pub fn validate(&self) -> Result<(), ConnectorError> {
21        if self.schema != CONFIG_SCHEMA {
22            return Err(ConnectorError::new(
23                "schema",
24                "expected zixcel://github/connector-config/v1",
25            ));
26        }
27        identifier("config_id", &self.config_id)?;
28        reference("connection_ref", &self.connection_ref)?;
29        github_name("organization", &self.organization)?;
30        if self.repositories.is_empty() || self.repositories.len() > 256 {
31            return Err(ConnectorError::new(
32                "repositories",
33                "must contain 1..=256 repositories",
34            ));
35        }
36        for repository in &self.repositories {
37            github_name("repositories", repository)?;
38        }
39        const WRITES: &[&str] = &[
40            "create-private-repository",
41            "push-repository-snapshot",
42            "delete-repository",
43            "create-issue",
44            "create-pull-request",
45            "dispatch-workflow",
46        ];
47        if self
48            .allowed_actions
49            .iter()
50            .any(|action| !WRITES.contains(&action.as_str()))
51            || self.allowed_actions.len() > WRITES.len()
52        {
53            return Err(ConnectorError::new(
54                "allowed_actions",
55                "unknown or excessive write capabilities",
56            ));
57        }
58        Ok(())
59    }
60
61    /// Local capability ceiling; does not replace a signed caller grant or provider permissions.
62    pub fn authorize_request(
63        &self,
64        request: &crate::GitHubApiRequest,
65    ) -> Result<(), ConnectorError> {
66        self.validate()?;
67        crate::validate_api_request(request)?;
68        if self.connection_ref != request.connection_ref
69            || !self.organization.eq_ignore_ascii_case(&request.owner)
70            || !self
71                .repositories
72                .iter()
73                .any(|name| name.eq_ignore_ascii_case(&request.repository))
74        {
75            return Err(ConnectorError::new(
76                "target",
77                "request is outside the configured connection and repositories",
78            ));
79        }
80        if !request.action.is_read_only()
81            && !self
82                .allowed_actions
83                .iter()
84                .any(|action| action == request.action.kind())
85        {
86            return Err(ConnectorError::new(
87                "action",
88                "write action is not enabled by local configuration",
89            ));
90        }
91        Ok(())
92    }
93
94    pub(crate) fn normalized(&self) -> Self {
95        let mut value = self.clone();
96        value.allowed_actions.sort();
97        value.allowed_actions.dedup();
98        value.repositories.sort_by_key(|item| item.to_lowercase());
99        value
100            .repositories
101            .dedup_by(|left, right| left.eq_ignore_ascii_case(right));
102        value
103    }
104}
105
106pub fn parse_config(source: &str) -> Result<ConnectorConfig, ConnectorError> {
107    reject_secrets(source)?;
108    let config: ConnectorConfig = toml::from_str(source).map_err(|_| {
109        ConnectorError::new(
110            "config",
111            "invalid TOML or fields do not match the GitHub v1 schema",
112        )
113    })?;
114    config.validate()?;
115    Ok(config)
116}