1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
pub mod credentials;
pub mod error;
mod misc;
pub mod token;
pub mod token_source;

use crate::credentials::CredentialsFile;
use crate::misc::EMPTY;
use crate::token_source::authorized_user_token_source::UserAccountTokenSource;
use crate::token_source::compute_token_source::ComputeTokenSource;
use crate::token_source::reuse_token_source::ReuseTokenSource;
use crate::token_source::service_account_token_source::OAuth2ServiceAccountTokenSource;
use crate::token_source::service_account_token_source::ServiceAccountTokenSource;
use crate::token_source::TokenSource;

use google_cloud_metadata::on_gce;

const SERVICE_ACCOUNT_KEY: &str = "service_account";
const USER_CREDENTIALS_KEY: &str = "authorized_user";

pub struct Config<'a> {
    pub audience: Option<&'a str>,
    pub scopes: Option<&'a [&'a str]>,
}

impl Config<'_> {
    pub fn scopes_to_string(&self, sep: &str) -> String {
        match self.scopes {
            Some(s) => s.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(sep),
            None => EMPTY.to_string(),
        }
    }
}

#[derive(Clone)]
pub struct ProjectInfo {
    pub project_id: Option<String>,
}

#[derive(Clone)]
pub enum Project {
    FromFile(Box<CredentialsFile>),
    FromMetadataServer(ProjectInfo),
}

// Possible sensitive info in debug messages
impl std::fmt::Debug for Project {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Project::FromFile(_) => write!(f, "Project::FromFile"),
            Project::FromMetadataServer(_) => write!(f, "Project::FromMetadataServer"),
        }
    }
}

impl Project {
    pub fn project_id(&self) -> Option<&String> {
        match self {
            Self::FromFile(file) => file.project_id.as_ref(),
            Self::FromMetadataServer(info) => info.project_id.as_ref(),
        }
    }
}

/// project() returns the project credentials or project info from metadata server.
pub async fn project() -> Result<Project, error::Error> {
    let credentials = credentials::CredentialsFile::new().await;
    match credentials {
        Ok(credentials) => Ok(Project::FromFile(Box::new(credentials))),
        Err(e) => {
            if on_gce().await {
                let project_id = google_cloud_metadata::project_id().await;
                Ok(Project::FromMetadataServer(ProjectInfo {
                    project_id: if project_id.is_empty() { None } else { Some(project_id) },
                }))
            } else {
                Err(e)
            }
        }
    }
}

/// create_token_source_from_project creates the token source.
pub async fn create_token_source_from_project(
    project: &Project,
    config: Config<'_>,
) -> Result<Box<dyn TokenSource>, error::Error> {
    match project {
        Project::FromFile(file) => {
            let ts = credentials_from_json_with_params(file, &config)?;
            let token = ts.token().await?;
            Ok(Box::new(ReuseTokenSource::new(ts, token)))
        }
        Project::FromMetadataServer(_) => {
            let ts = ComputeTokenSource::new(&config.scopes_to_string(","))?;
            let token = ts.token().await?;
            Ok(Box::new(ReuseTokenSource::new(Box::new(ts), token)))
        }
    }
}

/// create_token_source creates the token source
pub async fn create_token_source(config: Config<'_>) -> Result<Box<dyn TokenSource>, error::Error> {
    let project = project().await?;
    create_token_source_from_project(&project, config).await
}

fn credentials_from_json_with_params(
    credentials: &CredentialsFile,
    config: &Config,
) -> Result<Box<dyn TokenSource>, error::Error> {
    match credentials.tp.as_str() {
        SERVICE_ACCOUNT_KEY => {
            match config.audience {
                None => {
                    if config.scopes.is_none() {
                        return Err(error::Error::ScopeOrAudienceRequired);
                    }

                    // use Standard OAuth 2.0 Flow
                    let source =
                        OAuth2ServiceAccountTokenSource::new(credentials, config.scopes_to_string(" ").as_str())?;
                    Ok(Box::new(source))
                }
                Some(audience) => {
                    // use self-signed JWT.
                    let source = ServiceAccountTokenSource::new(credentials, audience)?;
                    Ok(Box::new(source))
                }
            }
        }
        USER_CREDENTIALS_KEY => Ok(Box::new(UserAccountTokenSource::new(credentials)?)),
        //TODO support GDC https://console.developers.google.com,
        //TODO support external account
        _ => Err(error::Error::UnsupportedAccountType(credentials.tp.to_string())),
    }
}