snap_control/crpc_api/
api_service.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Connect RPC API endpoint definitions and endpoint handlers.
15
16use std::sync::Arc;
17
18use axum::{Extension, Router, extract::State, http::StatusCode};
19use scion_sdk_axum_connect_rpc::{error::CrpcError, extractor::ConnectRpc};
20use snap_tokens::snap_token::SnapTokenClaims;
21
22use crate::{
23    crpc_api::api_service::model::SessionManager,
24    protobuf::anapaya::snap::v1::api_service::{
25        GetSnapDataPlaneSessionGrantRequest, GetSnapDataPlaneSessionGrantResponse,
26        RenewSnapDataPlaneSessionGrantRequest, RenewSnapDataPlaneSessionGrantResponse,
27    },
28};
29
30/// SNAP control plane API models.
31pub mod model {
32    use std::net::SocketAddr;
33
34    use axum::http::StatusCode;
35    use snap_tokens::snap_token::SnapTokenClaims;
36
37    /// Session manager trait.
38    pub trait SessionManager: Send + Sync {
39        /// Create a SNAP data plane session for the given SNAP token.
40        fn create_session(
41            &self,
42            snap_token: SnapTokenClaims,
43        ) -> Result<Vec<SessionGrant>, (StatusCode, anyhow::Error)>;
44        /// Renew a SNAP data plane session for the given address and SNAP token.
45        fn renew_session(
46            &self,
47            address: SocketAddr,
48            snap_token: SnapTokenClaims,
49        ) -> Result<SessionGrant, (StatusCode, anyhow::Error)>;
50    }
51
52    /// Session grant.
53    pub struct SessionGrant {
54        /// The SNAP data plane address for which the session is valid.
55        pub address: SocketAddr,
56        /// The issued SNAP data plane session token.
57        pub token: String,
58    }
59}
60
61pub(crate) mod convert {
62    use std::net::AddrParseError;
63
64    use thiserror::Error;
65
66    use crate::{
67        crpc_api::api_service::model::SessionGrant, protobuf::anapaya::snap::v1::api_service as rpc,
68    };
69
70    // Model to Protobuf
71    impl From<SessionGrant> for rpc::SnapDataPlaneSessionGrant {
72        fn from(value: SessionGrant) -> Self {
73            rpc::SnapDataPlaneSessionGrant {
74                address: value.address.to_string(),
75                token: value.token,
76            }
77        }
78    }
79
80    /// Session grant error.
81    #[derive(Debug, Error, PartialEq, Eq)]
82    pub enum SessionGrantError {
83        /// Missing grant field.
84        // This error only exists as there are no required fields in protobuf.
85        #[error("session grant field is required")]
86        MissingGrant,
87        /// Invalid address.
88        #[error("invalid address: {0}")]
89        InvalidAddress(#[from] AddrParseError),
90    }
91
92    // Protobuf to Model
93    impl TryFrom<rpc::SnapDataPlaneSessionGrant> for SessionGrant {
94        type Error = SessionGrantError;
95        fn try_from(value: rpc::SnapDataPlaneSessionGrant) -> Result<Self, Self::Error> {
96            Ok(SessionGrant {
97                address: value.address.parse()?,
98                token: value.token,
99            })
100        }
101    }
102
103    impl TryFrom<rpc::GetSnapDataPlaneSessionGrantResponse> for Vec<SessionGrant> {
104        type Error = SessionGrantError;
105        fn try_from(value: rpc::GetSnapDataPlaneSessionGrantResponse) -> Result<Self, Self::Error> {
106            value
107                .grants
108                .into_iter()
109                .map(SessionGrant::try_from)
110                .collect()
111        }
112    }
113
114    impl TryFrom<rpc::RenewSnapDataPlaneSessionGrantResponse> for SessionGrant {
115        type Error = SessionGrantError;
116        fn try_from(
117            value: rpc::RenewSnapDataPlaneSessionGrantResponse,
118        ) -> Result<Self, Self::Error> {
119            match value.grant {
120                Some(grant) => SessionGrant::try_from(grant),
121                None => Err(SessionGrantError::MissingGrant),
122            }
123        }
124    }
125}
126
127pub(crate) const SERVICE_PATH: &str = "/anapaya.snap.v1.SnapControl";
128pub(crate) const GET_SNAP_DATA_PLANE_SESSION_GRANT: &str = "/GetSnapDataPlaneSessionGrant";
129pub(crate) const RENEW_SNAP_DATA_PLANE_SESSION_GRANT: &str = "/RenewSnapDataPlaneSessionGrant";
130
131/// Nests the SNAP control API routes into the provided `base_router`.
132pub fn nest_snap_control_api(
133    router: axum::Router,
134    session_service: Arc<dyn SessionManager>,
135) -> axum::Router {
136    router.nest(
137        SERVICE_PATH,
138        Router::new()
139            .route(
140                GET_SNAP_DATA_PLANE_SESSION_GRANT,
141                axum::routing::post(add_snap_data_plane_session_handler),
142            )
143            .route(
144                RENEW_SNAP_DATA_PLANE_SESSION_GRANT,
145                axum::routing::post(renew_snap_data_plane_session_handler),
146            )
147            .with_state(session_service),
148    )
149}
150
151#[axum_macros::debug_handler]
152async fn add_snap_data_plane_session_handler(
153    State(session_manager): State<Arc<dyn SessionManager>>,
154    snap_token: Extension<SnapTokenClaims>,
155    ConnectRpc(_request): ConnectRpc<GetSnapDataPlaneSessionGrantRequest>,
156) -> Result<ConnectRpc<GetSnapDataPlaneSessionGrantResponse>, CrpcError> {
157    let grants = session_manager.create_session(snap_token.0.clone())?;
158    Ok(ConnectRpc(GetSnapDataPlaneSessionGrantResponse {
159        grants: grants.into_iter().map(Into::into).collect(),
160    }))
161}
162
163async fn renew_snap_data_plane_session_handler(
164    State(session_manager): State<Arc<dyn SessionManager>>,
165    snap_token: Extension<SnapTokenClaims>,
166    ConnectRpc(request): ConnectRpc<RenewSnapDataPlaneSessionGrantRequest>,
167) -> Result<ConnectRpc<RenewSnapDataPlaneSessionGrantResponse>, CrpcError> {
168    let address = request.address.parse().map_err(|e| {
169        CrpcError::new(
170            StatusCode::BAD_REQUEST.into(),
171            format!("invalid data plane address: {e}"),
172        )
173    })?;
174
175    let grant = session_manager.renew_session(address, snap_token.0.clone())?;
176    Ok(ConnectRpc(RenewSnapDataPlaneSessionGrantResponse {
177        grant: Some(grant.into()),
178    }))
179}