Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

70 linesCodeBlameRaw
1//! SCIM 2.0 for a workspace's identity provider, at
2//! `/scim/v2/{workspace}/…`: the provider assigns and unassigns people,
3//! with the workspace's SCIM token as its bearer token, which is not a g1t
4//! access token. Identity checks the token and answers everything
5//! (services/identity/src/sso/provisioning.rs); this passes the request on
6//! and writes the answer as `application/scim+json`.
7
8use g1t_contracts::sso::{ScimAnswer, ScimRequestArgs};
9use serde_json::json;
10use worker::{Request, Response, Result};
11
12use crate::operations::Services;
13
14/// The workspace and the rest of a path after `/scim/v2/`.
15pub fn split(rest: &str) -> Option<(String, String)> {
16 let (workspace, path) = match rest.split_once('/') {
17 Some((workspace, path)) => (workspace, format!("/{path}")),
18 None => (rest, "/".to_owned()),
19 };
20 let workspace = workspace.trim().to_lowercase();
21 let valid = !workspace.is_empty() && workspace.len() <= 39 && workspace.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
22 valid.then_some((workspace, path))
23}
24
25fn reply(status: u16, body: Option<serde_json::Value>) -> Result<Response> {
26 let mut response = match body {
27 Some(body) => Response::from_json(&body)?,
28 None => Response::empty()?,
29 }
30 .with_status(status);
31 if status != 204 {
32 response.headers_mut().set("content-type", "application/scim+json")?;
33 }
34 response.headers_mut().set("cache-control", "no-store")?;
35 Ok(response)
36}
37
38pub async fn handle(mut request: Request, services: &Services, method: &str, rest: &str) -> Result<Response> {
39 let Some((workspace, path)) = split(rest) else {
40 return reply(404, Some(json!({ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "404", "detail": "Name the workspace: /scim/v2/{workspace}/Users." })));
41 };
42 let query = request.url()?.query().map(str::to_owned);
43 let body = if matches!(method, "POST" | "PUT" | "PATCH") { Some(request.text().await?) } else { None };
44 let base = format!("{}/scim/v2/{workspace}", services.addresses.api);
45 let args = ScimRequestArgs {
46 workspace,
47 token: crate::toolkit::bearer(&request),
48 method: method.to_owned(),
49 path,
50 query,
51 body,
52 base,
53 };
54 let answer: ScimAnswer = g1t_kit::call(&services.identity, "scim_request", &args).await?;
55 reply(answer.status, answer.body)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn the_workspace_comes_first() {
64 assert_eq!(split("acme/Users/scu_1"), Some(("acme".into(), "/Users/scu_1".into())));
65 assert_eq!(split("Acme/Users"), Some(("acme".into(), "/Users".into())));
66 assert_eq!(split("acme"), Some(("acme".into(), "/".into())));
67 assert_eq!(split("/Users"), None);
68 assert_eq!(split("ac me/Users"), None);
69 }
70}