Skip to main content

made_client/
authorization.rs

1use made_proto::v1::{
2    ApproveAuthorizationOperationRequest, AuthorizationDecisionRecord, AuthorizationPolicyRecord,
3    IssueAuthorizationGrantRequest, IssueAuthorizationGrantResponse,
4    ListAuthorizationDecisionsRequest, ListAuthorizationDecisionsResponse,
5    RevokeAuthorizationGrantRequest, RevokeAuthorizationGrantResponse,
6};
7
8use crate::{MadeClient, MadeClientError};
9
10impl MadeClient {
11    pub async fn approve_authorization_operation(
12        &self,
13        request: ApproveAuthorizationOperationRequest,
14    ) -> Result<AuthorizationDecisionRecord, MadeClientError> {
15        let response = self
16            .rpc()
17            .approve_authorization_operation(Self::request(
18                &self.context(),
19                "/underpass.made.v1.MadeService/ApproveAuthorizationOperation",
20                request,
21            ))
22            .await
23            .map_err(MadeClientError::from_status)?
24            .into_inner();
25        response.decision.ok_or_else(|| {
26            MadeClientError::ProtocolViolation(
27                "approve authorization operation response has no decision".to_owned(),
28            )
29        })
30    }
31
32    pub async fn authorization_policy(&self) -> Result<AuthorizationPolicyRecord, MadeClientError> {
33        let response = self
34            .rpc()
35            .get_authorization_policy(Self::request(
36                &self.context(),
37                "/underpass.made.v1.MadeService/GetAuthorizationPolicy",
38                made_proto::v1::GetAuthorizationPolicyRequest {},
39            ))
40            .await
41            .map_err(MadeClientError::from_status)?
42            .into_inner();
43        response.policy.ok_or_else(|| {
44            MadeClientError::ProtocolViolation(
45                "get authorization policy response has no policy".to_owned(),
46            )
47        })
48    }
49
50    pub async fn issue_authorization_grant(
51        &self,
52        request: IssueAuthorizationGrantRequest,
53    ) -> Result<IssueAuthorizationGrantResponse, MadeClientError> {
54        self.rpc()
55            .issue_authorization_grant(Self::request(
56                &self.context(),
57                "/underpass.made.v1.MadeService/IssueAuthorizationGrant",
58                request,
59            ))
60            .await
61            .map(tonic::Response::into_inner)
62            .map_err(MadeClientError::from_status)
63    }
64
65    pub async fn revoke_authorization_grant(
66        &self,
67        grant_id: impl Into<String>,
68        reason: impl Into<String>,
69    ) -> Result<RevokeAuthorizationGrantResponse, MadeClientError> {
70        self.rpc()
71            .revoke_authorization_grant(Self::request(
72                &self.context(),
73                "/underpass.made.v1.MadeService/RevokeAuthorizationGrant",
74                RevokeAuthorizationGrantRequest {
75                    grant_id: grant_id.into(),
76                    reason: reason.into(),
77                },
78            ))
79            .await
80            .map(tonic::Response::into_inner)
81            .map_err(MadeClientError::from_status)
82    }
83
84    pub async fn authorization_decisions(
85        &self,
86        after_decision_id: Option<String>,
87        limit: u32,
88    ) -> Result<ListAuthorizationDecisionsResponse, MadeClientError> {
89        self.rpc()
90            .list_authorization_decisions(Self::request(
91                &self.context(),
92                "/underpass.made.v1.MadeService/ListAuthorizationDecisions",
93                ListAuthorizationDecisionsRequest {
94                    after_decision_id,
95                    limit,
96                },
97            ))
98            .await
99            .map(tonic::Response::into_inner)
100            .map_err(MadeClientError::from_status)
101    }
102}
103
104/// Compute the exact target digest used by the direct gRPC authorization gate.
105///
106/// Callers construct the final execution request first, approve this digest,
107/// and then send those same protobuf fields with the returned decision ID.
108#[must_use]
109pub fn authorization_target_digest<T: prost::Message>(request: &T) -> String {
110    use sha2::{Digest, Sha256};
111
112    format!("{:x}", Sha256::digest(request.encode_to_vec()))
113}
114
115#[cfg(test)]
116mod tests {
117    use std::collections::BTreeMap;
118
119    use made_proto::v1::BindCeremonyParticipantsRequest;
120
121    use super::authorization_target_digest;
122
123    #[test]
124    fn target_digest_is_stable_for_canonical_protobuf_maps() {
125        let left = BindCeremonyParticipantsRequest {
126            ceremony_id: "ceremony-1".to_owned(),
127            seating: BTreeMap::from([
128                ("reviewer".to_owned(), "review".to_owned()),
129                ("author".to_owned(), "writing".to_owned()),
130            ]),
131            ..Default::default()
132        };
133        let right = BindCeremonyParticipantsRequest {
134            ceremony_id: "ceremony-1".to_owned(),
135            seating: BTreeMap::from([
136                ("author".to_owned(), "writing".to_owned()),
137                ("reviewer".to_owned(), "review".to_owned()),
138            ]),
139            ..Default::default()
140        };
141
142        assert_eq!(
143            authorization_target_digest(&left),
144            authorization_target_digest(&right)
145        );
146    }
147}