ibmcloud_iam/
pdp.rs

1// Copyright 2022 Mathew Odden <mathewrodden@gmail.com>
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
15use std::collections::HashMap;
16
17use serde::{Deserialize, Serialize};
18use serde_json::value::Value;
19
20use crate::error::Error;
21use crate::token::{Token, TokenManager};
22
23pub struct PDPClient {
24    endpoint: String,
25    token_manager: TokenManager,
26}
27
28pub type Resource = HashMap<String, String>;
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
31struct AuthorizeRequestBody(Vec<AuthorizeRequest>);
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34struct AuthorizeRequest {
35    subject: Subject,
36    action: String,
37    resource: ResourceAttrs,
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct Subject {
43    access_token_body: String,
44}
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
47struct ResourceAttrs {
48    attributes: HashMap<String, String>,
49}
50
51impl From<Resource> for ResourceAttrs {
52    fn from(r: Resource) -> ResourceAttrs {
53        ResourceAttrs { attributes: r }
54    }
55}
56
57#[derive(Debug, Clone, Deserialize, Serialize)]
58struct AuthorizeResponseBody {
59    responses: Vec<AuthorizeResponse>,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
63struct AuthorizeResponse {
64    #[serde(rename = "authorizationDecision")]
65    pub decision: AuthorizationDecision,
66
67    status: String,
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
71pub struct AuthorizationDecision {
72    permitted: bool,
73    reason: Option<String>,
74    obligation: Option<Obligation>,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct Obligation {
80    actions: Vec<String>,
81    max_cache_age_seconds: u64,
82    subject: SubjectAttrs,
83}
84
85#[derive(Debug, Clone, Deserialize, Serialize)]
86struct SubjectAttrs {
87    attributes: HashMap<String, Value>,
88}
89
90impl PDPClient {
91    pub fn new(api_key: &str, endpoint: &str) -> Self {
92        Self {
93            endpoint: endpoint.to_string(),
94            token_manager: TokenManager::new(api_key, endpoint),
95        }
96    }
97
98    pub fn authorize(
99        &self,
100        subject: Subject,
101        action: &str,
102        resource: Resource,
103    ) -> Result<AuthorizationDecision, Error> {
104        let authreq = AuthorizeRequest {
105            subject: subject,
106            action: action.to_string(),
107            resource: resource.into(),
108        };
109
110        let req_body = serde_json::to_string(&AuthorizeRequestBody(vec![authreq])).unwrap();
111
112        let c = reqwest::blocking::Client::new();
113
114        let path = format!("{}/v2/authz", self.endpoint);
115
116        let token = self.token_manager.token()?.access_token;
117
118        let resp = c
119            .post(path)
120            .header("Accept", "application/json")
121            .header("Content-Type", "application/json")
122            .header("Authorization", format!("Bearer {}", token))
123            .body(req_body)
124            .send()
125            .expect("PDP Authorize request failed");
126
127        let status = resp.status();
128        let text = resp.text().expect("Getting body text failed");
129
130        if !status.is_success() {
131            return Err(
132                format!("Authz request failed: status='{}', body='{}'", status, text).into(),
133            );
134        }
135
136        let mut resp_body = match serde_json::from_str::<AuthorizeResponseBody>(&text) {
137            Ok(v) => v,
138            Err(_) => {
139                return Err(format!(
140                    "Unexpected response from PDP: status='{}', body='{}'",
141                    status, text
142                )
143                .into());
144            }
145        };
146
147        Ok(resp_body.responses.remove(0).decision)
148    }
149}
150
151pub fn subject_from_token(token: &Token) -> Subject {
152    let parts: Vec<&str> = token.access_token.split(".").collect();
153    Subject {
154        access_token_body: parts[1].to_string(),
155    }
156}