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.

72 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 // The workspace's id (`wsp_…`), or for an older setup its name.
22 let valid = !workspace.is_empty() && workspace.len() <= 64 && workspace.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
23 valid.then_some((workspace, path))
24}
25
26fn reply(status: u16, body: Option<serde_json::Value>) -> Result<Response> {
27 let mut response = match body {
28 Some(body) => Response::from_json(&body)?,
29 None => Response::empty()?,
30 }
31 .with_status(status);
32 if status != 204 {
33 response.headers_mut().set("content-type", "application/scim+json")?;
34 }
35 response.headers_mut().set("cache-control", "no-store")?;
36 Ok(response)
37}
38
39pub async fn handle(mut request: Request, services: &Services, method: &str, rest: &str) -> Result<Response> {
40 let Some((workspace, path)) = split(rest) else {
41 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." })));
42 };
43 let query = request.url()?.query().map(str::to_owned);
44 let body = if matches!(method, "POST" | "PUT" | "PATCH") { Some(request.text().await?) } else { None };
45 let base = format!("{}/scim/v2/{workspace}", services.addresses.api);
46 let args = ScimRequestArgs {
47 workspace,
48 token: crate::toolkit::bearer(&request),
49 method: method.to_owned(),
50 path,
51 query,
52 body,
53 base,
54 };
55 let answer: ScimAnswer = g1t_kit::call(&services.identity, "scim_request", &args).await?;
56 reply(answer.status, answer.body)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn the_workspace_comes_first() {
65 assert_eq!(split("acme/Users/scu_1"), Some(("acme".into(), "/Users/scu_1".into())));
66 assert_eq!(split("Acme/Users"), Some(("acme".into(), "/Users".into())));
67 assert_eq!(split("acme"), Some(("acme".into(), "/".into())));
68 assert_eq!(split("/Users"), None);
69 assert_eq!(split("ac me/Users"), None);
70 assert_eq!(split("wsp_01m54abc/Users"), Some(("wsp_01m54abc".into(), "/Users".into())));
71 }
72}