Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Application Credential authentication #76

Merged
merged 3 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions src/identity/application_credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2023 Matt Williams <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Application Credential authentication.

use async_trait::async_trait;
use reqwest::{Client, RequestBuilder, Url};
use static_assertions::assert_impl_all;

use super::internal::Internal;
use super::protocol;
use super::IdOrName;
use crate::{AuthType, EndpointFilters, Error};

/// Application Credential authentication using Identity API V3.
///
/// For any Identity authentication you need to know `auth_url`, which is an authentication endpoint
/// of the Identity service. For the Application Credential authentication you also need:
/// 1. Application Credential ID
/// 2. Application Credential secret
///
/// Start with creating a `ApplicationCredential` object using [new](#method.new):
///
/// ```rust,no_run
/// use osauth::common::IdOrName;
/// let auth = osauth::identity::ApplicationCredential::new(
/// "https://cloud.local/identity",
/// "<a cred id>",
/// "<a cred secret>",
/// )
/// .expect("Invalid auth_url");
///
/// let session = osauth::Session::new(auth);
/// ```
///
/// The authentication token is cached while it's still valid or until
/// [refresh](../trait.AuthType.html#tymethod.refresh) is called.
/// Clones of an `ApplicationCredential` also start with an empty cache.
#[derive(Debug, Clone)]
pub struct ApplicationCredential {
inner: Internal,
}

assert_impl_all!(ApplicationCredential: Send, Sync);

impl ApplicationCredential {
/// Create an application credential authentication.
pub fn new<U, S1, S2>(auth_url: U, id: S1, secret: S2) -> Result<Self, Error>
where
U: AsRef<str>,
S1: Into<String>,
S2: Into<String>,
{
let app_cred = protocol::ApplicationCredential {
id: IdOrName::Id(id.into()),
secret: secret.into(),
user: None,
};
let body = protocol::AuthRoot {
auth: protocol::Auth {
identity: protocol::Identity::ApplicationCredential(app_cred),
scope: None,
},
};
Ok(Self {
inner: Internal::new(auth_url.as_ref(), body)?,
})
}

/// Create an application credential authentication from a credential name.
pub fn with_user_id<U, S1, S2, S3>(
auth_url: U,
name: S1,
secret: S2,
user_id: S3,
) -> Result<Self, Error>
where
U: AsRef<str>,
S1: Into<String>,
S2: Into<String>,
S3: Into<String>,
{
let app_cred = protocol::ApplicationCredential {
id: IdOrName::Name(name.into()),
secret: secret.into(),
user: Some(IdOrName::Id(user_id.into())),
};
let body = protocol::AuthRoot {
auth: protocol::Auth {
identity: protocol::Identity::ApplicationCredential(app_cred),
scope: None,
},
};
Ok(Self {
inner: Internal::new(auth_url.as_ref(), body)?,
})
}

/// Project name or ID (if project scoped).
#[inline]
pub fn project(&self) -> Option<&IdOrName> {
self.inner.project()
}
}

#[async_trait]
impl AuthType for ApplicationCredential {
/// Authenticate a request.
async fn authenticate(
&self,
client: &Client,
request: RequestBuilder,
) -> Result<RequestBuilder, Error> {
self.inner.authenticate(client, request).await
}

/// Get a URL for the requested service.
async fn get_endpoint(
&self,
client: &Client,
service_type: &str,
filters: &EndpointFilters,
) -> Result<Url, Error> {
self.inner.get_endpoint(client, service_type, filters).await
}

/// Refresh the cached token and service catalog.
async fn refresh(&self, client: &Client) -> Result<(), Error> {
self.inner.refresh(client, true).await
}
}

#[cfg(test)]
pub mod test {
#![allow(unused_results)]

use reqwest::Url;

use super::ApplicationCredential;

#[test]
fn test_identity_new() {
let id = ApplicationCredential::new("http://127.0.0.1:8080/", "abcdef", "shhhh").unwrap();
let e = Url::parse(id.inner.token_endpoint()).unwrap();
assert_eq!(e.scheme(), "http");
assert_eq!(e.host_str().unwrap(), "127.0.0.1");
assert_eq!(e.port().unwrap(), 8080u16);
assert_eq!(e.path(), "/v3/auth/tokens");
}
}
4 changes: 3 additions & 1 deletion src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@

//! Authentication using Identity API v3.
//!
//! Currently only supports [Password](struct.Password.html) authentication.
//! Currently supports [Password](struct.Password.html) and [ApplicationCredential] authentication.
//! Identity API v2 is not and will not be supported.
mod application_credential;
mod internal;
mod password;
pub(crate) mod protocol;
mod token;

use super::common::IdOrName;

pub use self::application_credential::ApplicationCredential;
pub use self::password::Password;
pub use self::token::Token;

Expand Down
79 changes: 79 additions & 0 deletions src/identity/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,25 @@ pub struct UserAndPassword {
pub domain: Option<IdOrName>,
}

/// Application credential.
#[derive(Clone, Debug, Serialize)]
pub struct ApplicationCredential {
#[serde(flatten)]
pub id: IdOrName,
pub secret: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user: Option<IdOrName>,
}

/// Authentication identity.
#[derive(Clone, Debug)]
pub enum Identity {
/// Authentication with a user and a password.
Password(UserAndPassword),
/// Authentication with a token.
Token(String),
/// Authentication with an application credential.
ApplicationCredential(ApplicationCredential),
}

/// A reference to a project in a domain.
Expand Down Expand Up @@ -152,6 +164,10 @@ impl Serialize for Identity {
inner.serialize_field("methods", &["token"])?;
inner.serialize_field("token", &TokenAuth { id: token })?;
}
Identity::ApplicationCredential(ref cred) => {
inner.serialize_field("methods", &["application_credential"])?;
inner.serialize_field("application_credential", &cred)?;
}
}
inner.end()
}
Expand Down Expand Up @@ -254,6 +270,39 @@ mod test {
}
}"#;

const APPLICATION_CREDENTIAL_ID: &str = r#"
{
"auth": {
"identity": {
"methods": [
"application_credential"
],
"application_credential": {
"id": "abcdef",
"secret": "shhhh"
}
}
}
}"#;

const APPLICATION_CREDENTIAL_NAME: &str = r#"
{
"auth": {
"identity": {
"methods": [
"application_credential"
],
"application_credential": {
"name": "abcdef",
"secret": "shhhh",
"user": {
"id": "a6b3c6e7a6d"
}
}
}
}
}"#;

#[test]
fn test_password_name_unscoped() {
let value = AuthRoot {
Expand Down Expand Up @@ -309,4 +358,34 @@ mod test {
};
test::compare(TOKEN_SCOPED_WITH_NAME, value);
}

#[test]
fn test_application_credential_id() {
let value = AuthRoot {
auth: Auth {
identity: Identity::ApplicationCredential(ApplicationCredential {
id: IdOrName::Id("abcdef".to_string()),
secret: "shhhh".to_string(),
user: None,
}),
scope: None,
},
};
test::compare(APPLICATION_CREDENTIAL_ID, value);
}

#[test]
fn test_application_credential_name() {
let value = AuthRoot {
auth: Auth {
identity: Identity::ApplicationCredential(ApplicationCredential {
id: IdOrName::Name("abcdef".to_string()),
secret: "shhhh".to_string(),
user: Some(IdOrName::Id("a6b3c6e7a6d".into())),
}),
scope: None,
},
};
test::compare(APPLICATION_CREDENTIAL_NAME, value);
}
}
44 changes: 43 additions & 1 deletion src/loading/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use super::config::from_config;
use super::env::from_env;
use crate::client::AuthenticatedClient;
use crate::common::IdOrName;
use crate::identity::{Password, Scope, Token};
use crate::identity::{ApplicationCredential, Password, Scope, Token};
use crate::{AuthType, BasicAuth, Error, ErrorKind, InterfaceType, NoAuth, Session};

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
Expand All @@ -52,6 +52,14 @@ pub(crate) struct Auth {
pub(crate) username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) user_domain_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) application_credential_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) application_credential_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) application_credential_name: Option<String>,
}

/// Cloud configuration.
Expand Down Expand Up @@ -169,6 +177,38 @@ impl Auth {
Ok(id)
}

fn create_application_credential(self) -> Result<ApplicationCredential, Error> {
let auth_url = require(
self.auth_url,
"Password authentication requires an authentication URL",
)?;
let app_secret = require(
self.application_credential_secret,
"Application credential requires a secret",
)?;

if let Some(app_id) = self.application_credential_id {
ApplicationCredential::new(auth_url, app_id, app_secret)
} else if let Some(app_name) = self.application_credential_name {
if self.username.is_some() && self.user_id.is_none() {
return Err(Error::new(
ErrorKind::InvalidConfig,
"Specifying Application Credential by name currently requires specifying the user ID",
));
}
let user_id = require(
self.user_id,
"Application Credential authentication by name requires the user ID",
)?;
ApplicationCredential::with_user_id(auth_url, app_name, app_secret, user_id)
} else {
Err(Error::new(
ErrorKind::InvalidConfig,
"Application Credential requires an id or a name",
))
}
}

fn create_auth(self, auth_type: Option<String>) -> Result<Arc<dyn AuthType>, Error> {
let auth_type = auth_type.unwrap_or_else(|| {
if self.token.is_some() {
Expand All @@ -185,6 +225,8 @@ impl Auth {
Arc::new(self.create_token_auth()?)
} else if auth_type == "http_basic" {
Arc::new(self.create_basic_auth()?)
} else if auth_type == "v3applicationcredential" {
Arc::new(self.create_application_credential()?)
} else if auth_type == "none" {
Arc::new(self.create_none_auth()?)
} else {
Expand Down
24 changes: 24 additions & 0 deletions src/loading/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,30 @@ hK9jLBzNvo8qzKqaGfnGieuLeXCqFDA=
.unwrap();
}

#[test]
fn test_from_config_application_credential() {
let clouds = to_yaml(
r#"clouds:
cloud_name:
auth_type: v3applicationcredential
auth:
auth_url: http://url1
application_credential_id: appid1
application_credential_secret: appidsecret1
region_name: region1"#,
);

let _ = from_files(
"cloud_name",
clouds,
with_one_key("public-clouds"),
with_one_key("clouds"),
)
.unwrap()
.create_session_config()
.unwrap();
}

#[test]
fn test_inject_profiles_error() {
let mut clouds_data = to_yaml(
Expand Down
Loading
Loading