1use crate::{ActionClass, ExecutionMode};
9use adk_auth::check_scopes;
10use thiserror::Error;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ComputerUseAuthContext {
15 pub principal_id: String,
17 pub tenant_id: Option<String>,
19 pub session_id: String,
21 pub execution_group_id: String,
23 pub requested_mode: ExecutionMode,
25 pub action_class: ActionClass,
27 pub target_app: Option<String>,
29 pub target_window: Option<String>,
31 pub policy_digest: String,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct ScopeAuthorizer {
38 scopes: Vec<String>,
39 principal_id: Option<String>,
40 tenant_id: Option<String>,
41}
42
43#[derive(Debug, Error, PartialEq, Eq)]
45pub enum AuthorizationError {
46 #[error("missing required computer-use scope: {0}")]
48 MissingScope(&'static str),
49 #[error("principal does not match the verified ADK identity")]
51 PrincipalMismatch,
52 #[error("tenant does not match the verified ADK identity")]
54 TenantMismatch,
55}
56
57impl ScopeAuthorizer {
58 pub fn new(scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
60 Self {
61 scopes: scopes.into_iter().map(Into::into).collect(),
62 principal_id: None,
63 tenant_id: None,
64 }
65 }
66
67 pub fn from_verified_identity(
69 principal_id: impl Into<String>,
70 tenant_id: Option<String>,
71 scopes: impl IntoIterator<Item = impl Into<String>>,
72 ) -> Self {
73 Self {
74 scopes: scopes.into_iter().map(Into::into).collect(),
75 principal_id: Some(principal_id.into()),
76 tenant_id,
77 }
78 }
79
80 pub fn verified_tenant_id(&self) -> Option<&str> {
83 self.tenant_id.as_deref()
84 }
85
86 pub fn authorize(&self, context: &ComputerUseAuthContext) -> Result<(), AuthorizationError> {
95 if self.principal_id.as_deref().is_some_and(|id| id != context.principal_id) {
96 return Err(AuthorizationError::PrincipalMismatch);
97 }
98 if self.tenant_id.is_some() && self.tenant_id != context.tenant_id {
99 return Err(AuthorizationError::TenantMismatch);
100 }
101 let required = match context.requested_mode {
102 ExecutionMode::Shadow => "computer:plan",
103 ExecutionMode::Background => "computer:execute:background",
104 ExecutionMode::Foreground => "computer:execute:foreground",
105 };
106 check_scopes(&[required], &self.scopes)
107 .map_err(|_| AuthorizationError::MissingScope(required))
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn foreground_scope_does_not_follow_from_background_scope() {
117 let authorizer = ScopeAuthorizer::new(["computer:execute:background"]);
118 let context = ComputerUseAuthContext {
119 principal_id: "p".into(),
120 tenant_id: None,
121 session_id: "s".into(),
122 execution_group_id: "g".into(),
123 requested_mode: ExecutionMode::Foreground,
124 action_class: ActionClass::Navigate,
125 target_app: None,
126 target_window: None,
127 policy_digest: "d".into(),
128 };
129 assert_eq!(
130 authorizer.authorize(&context),
131 Err(AuthorizationError::MissingScope("computer:execute:foreground"))
132 );
133 }
134
135 #[test]
136 fn verified_identity_must_match_runtime_principal_and_tenant() {
137 let authorizer = ScopeAuthorizer::from_verified_identity(
138 "verified",
139 Some("tenant-a".into()),
140 ["computer:execute:background"],
141 );
142 let mut context = ComputerUseAuthContext {
143 principal_id: "attacker".into(),
144 tenant_id: Some("tenant-a".into()),
145 session_id: "s".into(),
146 execution_group_id: "g".into(),
147 requested_mode: ExecutionMode::Background,
148 action_class: ActionClass::Navigate,
149 target_app: None,
150 target_window: None,
151 policy_digest: "d".into(),
152 };
153 assert_eq!(authorizer.authorize(&context), Err(AuthorizationError::PrincipalMismatch));
154 context.principal_id = "verified".into();
155 context.tenant_id = Some("tenant-b".into());
156 assert_eq!(authorizer.authorize(&context), Err(AuthorizationError::TenantMismatch));
157 }
158}