Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Added support for multi-tenant authentication #563

Closed
wants to merge 98 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
2443280
changes for multi-tenant auth
andrew-slutsky Jan 28, 2021
93c3fcd
Adding changes for multi-tenant auth
andrew-slutsky Jan 28, 2021
669e728
changes for multi-tenant auth
andrew-slutsky Jan 28, 2021
3f53c1c
changes for multi-tenant auth
andrew-slutsky Jan 28, 2021
e2e8569
Merge remote-tracking branch 'origin/main' into main_multi_tenant
andrew-slutsky Jan 29, 2021
8356243
saving change
andrew-slutsky Feb 3, 2021
77ba272
adding changes to deploy.py
andrew-slutsky Feb 12, 2021
3e79876
changes to enable support for multi-tenant authentication
andrew-slutsky Feb 12, 2021
e7021a1
changes to app function to support multi-tenant auth
andrew-slutsky Feb 12, 2021
f8fda80
removing to compile
andrew-slutsky Feb 12, 2021
ac95cd3
Merge branch 'main' into main_multi_tenant
andrew-slutsky Feb 12, 2021
72f3d6e
temp removing agent changes
andrew-slutsky Feb 12, 2021
c2d1793
fixing python from lint
andrew-slutsky Feb 12, 2021
11ccd1d
removing pdb
andrew-slutsky Feb 12, 2021
2a14b57
typo
andrew-slutsky Feb 12, 2021
aadb0fe
typo
andrew-slutsky Feb 12, 2021
8233e66
typo
andrew-slutsky Feb 12, 2021
2d90809
typo
andrew-slutsky Feb 12, 2021
aa15f10
typo
andrew-slutsky Feb 12, 2021
fefc9d4
formatting
andrew-slutsky Feb 12, 2021
a1acf00
updating after feedback
andrew-slutsky Feb 14, 2021
987e876
updating app function resource for agent
andrew-slutsky Feb 14, 2021
69f98b2
rename var
andrew-slutsky Feb 14, 2021
ec9ede0
reformatting with tool: black
andrew-slutsky Feb 14, 2021
210f167
typos
andrew-slutsky Feb 14, 2021
ff74d1a
typoes
andrew-slutsky Feb 14, 2021
14f3108
typos
andrew-slutsky Feb 14, 2021
fdf1ed0
outputs
andrew-slutsky Feb 14, 2021
c45630e
fixing lint issue
andrew-slutsky Feb 14, 2021
7dfad83
lint changes
andrew-slutsky Feb 14, 2021
9595b6b
reverting
andrew-slutsky Feb 14, 2021
ffe118a
ran utility black
andrew-slutsky Feb 14, 2021
3553a08
making changes for lint
andrew-slutsky Feb 14, 2021
6874719
typo
andrew-slutsky Feb 14, 2021
d88fbdb
removing info self.tenant
andrew-slutsky Feb 14, 2021
a5aa3f4
revert
andrew-slutsky Feb 14, 2021
499f50d
agent
andrew-slutsky Feb 14, 2021
3a9067a
new build
andrew-slutsky Feb 14, 2021
7cbe7a4
build
andrew-slutsky Feb 14, 2021
a1dc8c9
build
andrew-slutsky Feb 14, 2021
0578941
build
andrew-slutsky Feb 14, 2021
0b329cb
build
andrew-slutsky Feb 14, 2021
bc15097
build
andrew-slutsky Feb 14, 2021
d3deb9a
build
andrew-slutsky Feb 14, 2021
93f16b9
build
andrew-slutsky Feb 14, 2021
cd19cdf
build
andrew-slutsky Feb 14, 2021
262cecf
build onefuzz
andrew-slutsky Feb 14, 2021
7f5eebe
build onefuzz
andrew-slutsky Feb 14, 2021
79c42c3
build onefuzz
andrew-slutsky Feb 14, 2021
a6b4625
build onefuzz
andrew-slutsky Feb 14, 2021
ab3b65b
build onefuzz
andrew-slutsky Feb 14, 2021
25d6827
commenting out cargo
andrew-slutsky Feb 14, 2021
4ae1ddc
build
andrew-slutsky Feb 15, 2021
5c9fcb0
format check
andrew-slutsky Feb 15, 2021
8944153
build
andrew-slutsky Feb 15, 2021
db98c73
build
andrew-slutsky Feb 15, 2021
3fd1b2b
build
andrew-slutsky Feb 15, 2021
7c9c0d1
build
andrew-slutsky Feb 15, 2021
3a6b0ed
build
andrew-slutsky Feb 15, 2021
14d5cd6
build
andrew-slutsky Feb 15, 2021
c408410
Update auth.rs
andrew-slutsky Feb 15, 2021
c21ec1d
build
andrew-slutsky Feb 15, 2021
7d48e77
build
andrew-slutsky Feb 15, 2021
194fbe0
build
andrew-slutsky Feb 15, 2021
c1f0b61
build
andrew-slutsky Feb 15, 2021
c612e20
build
andrew-slutsky Feb 15, 2021
7e740d6
build
andrew-slutsky Feb 16, 2021
f9d1eca
asdfasdfsadfasdffasdfasdasdffsda
andrew-slutsky Feb 17, 2021
387a9a3
build
andrew-slutsky Feb 17, 2021
45307b2
build
andrew-slutsky Feb 17, 2021
687ea3d
build
andrew-slutsky Feb 17, 2021
2e98181
build
andrew-slutsky Feb 18, 2021
b50ad94
build
andrew-slutsky Feb 18, 2021
6420970
build
andrew-slutsky Feb 18, 2021
13e515c
build
andrew-slutsky Feb 18, 2021
1a63ca0
build
andrew-slutsky Feb 18, 2021
9c6bc01
build
andrew-slutsky Feb 18, 2021
22497d2
build
andrew-slutsky Feb 18, 2021
9264f4e
build
andrew-slutsky Feb 18, 2021
fdb37a4
build
andrew-slutsky Feb 18, 2021
04e88db
build
andrew-slutsky Feb 18, 2021
552da77
build
andrew-slutsky Feb 18, 2021
1e1c525
build
andrew-slutsky Feb 18, 2021
9743c87
build
andrew-slutsky Feb 18, 2021
ec0085c
Update src/agent/onefuzz-supervisor/src/auth.rs
andrew-slutsky Feb 23, 2021
d0d8f68
build
andrew-slutsky Feb 23, 2021
59be78d
build
andrew-slutsky Feb 23, 2021
b823b74
build
andrew-slutsky Feb 24, 2021
5e0bd10
build
andrew-slutsky Feb 24, 2021
1075624
build
andrew-slutsky Feb 24, 2021
f6f13a0
build
andrew-slutsky Feb 24, 2021
4562ae8
build
andrew-slutsky Feb 24, 2021
5c1e7ae
build
andrew-slutsky Feb 24, 2021
e02c1f6
build
andrew-slutsky Feb 24, 2021
be548fc
build
andrew-slutsky Feb 24, 2021
365b35e
build
andrew-slutsky Feb 24, 2021
3c9dc6f
build
andrew-slutsky Feb 24, 2021
bffadd9
build
andrew-slutsky Feb 24, 2021
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
50 changes: 42 additions & 8 deletions src/agent/onefuzz-supervisor/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,48 @@ pub struct ClientCredentials {
client_id: Uuid,
client_secret: Secret<String>,
resource: String,
multi_tenant_domain: Option<String>,
tenant: String,
}

impl ClientCredentials {
pub fn new(client_id: Uuid, client_secret: String, resource: String, tenant: String) -> Self {
pub fn new(
client_id: Uuid,
client_secret: String,
resource: String,
multi_tenant_domain: Option<String>,
tenant: String,
) -> Self {
let client_secret = client_secret.into();

Self {
client_id,
client_secret,
resource,
multi_tenant_domain,
tenant,
}
}

pub async fn access_token(&self) -> Result<AccessToken> {
let (authority, resource) = if let Some(domain) = &self.multi_tenant_domain {
let url = Url::parse(&self.resource.clone())?;
let host = url.host_str().ok_or_else(|| {
anyhow::format_err!("resource URL does not have a host string: {}", url)
})?;
let instance: Vec<&str> = host.split('.').collect();
(
andrew-slutsky marked this conversation as resolved.
Show resolved Hide resolved
String::from("common"),
format!("https://{}/{}/", &domain, instance[0]),
)
} else {
(self.tenant.clone(), self.resource.clone())
};

let mut url = Url::parse("https://login.microsoftonline.com")?;
url.path_segments_mut()
.expect("Authority URL is cannot-be-a-base")
.extend(&[&self.tenant, "oauth2", "v2.0", "token"]);
.extend(&[&authority.clone(), "oauth2", "v2.0", "token"]);

let response = reqwest::Client::new()
.post(url)
Expand All @@ -115,8 +137,8 @@ impl ClientCredentials {
("client_id", self.client_id.to_hyphenated().to_string()),
("client_secret", self.client_secret.expose_ref().to_string()),
("grant_type", "client_credentials".into()),
("tenant", self.tenant.clone()),
("scope", format!("{}.default", self.resource)),
("tenant", authority),
("scope", format!("{}.default", resource)),
])
.send_retry_default()
.await?
Expand Down Expand Up @@ -147,20 +169,32 @@ impl From<ClientAccessTokenBody> for AccessToken {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct ManagedIdentityCredentials {
resource: String,
multi_tenant_domain: Option<String>,
}

const MANAGED_IDENTITY_URL: &str =
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01";

impl ManagedIdentityCredentials {
pub fn new(resource: String) -> Self {
Self { resource }
pub fn new(resource: String, multi_tenant_domain: Option<String>) -> Self {
Self {
resource,
multi_tenant_domain,
}
}

fn url(&self) -> Url {
let mut url = Url::parse(MANAGED_IDENTITY_URL).unwrap();
url.query_pairs_mut()
.append_pair("resource", &self.resource);
let resource = if let Some(domain) = &self.multi_tenant_domain {
let uri = Url::parse(&self.resource).unwrap();
Copy link
Contributor

@gdhuper gdhuper Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion, as pointed out before, this unwrap may panic if it's an invalid url. Since the return type is not Result<>, an alternative could be using expect, which will still panic but allows you to pass a string error message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a better alternative, but still panics, which we want to avoid.

I suggest doing some validation earlier. For example, you could validate and precompute multi_tenant_domain in the ctor (ManagedIdentityCredentials::new()), which would then return a Result.

@anslutsk, using some stand-in values, could you share what we expect resource and a valid managed_identity_url to look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put in some temporary println statements to provide in context here.

In the case where OneFuzz is deployed in single tenant mode, the config.json file contains an entry for "multi_tenant_config" which has value "null" and the self.resource String value is "https://anslutsk-26.azurewebsites.net" and thus, the pattern "if let Some(domain)" is not triggered, so the else clause is run, which executes the line: self.resource.clone().

In the case where OneFuzz is deployed in multi tenant mode, the config.json file contains the entry "multi_tenant_config" with the value "mspmecloud.onmicrosoft.com" and the self.resource String value is "https://anslutsk-26.azurewebsites.net" and so the pattern "if let Some(domain)" is triggered, so the corresponding code block is executed and the value of self.instance[0] is "anslutsk-26"

Copy link
Contributor Author

@andrew-slutsky andrew-slutsky Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there was an invalid domain in the "multi_tenant_config" json config item, for example the String value "...." the program did not crash and we get the expected error during while trying to obtain a Bearer token from IMS:

[2021-02-24T07:12:19Z ERROR onefuzz_telemetry] error running supervisor agent: 400 Bad Request: {"error":"invalid_resource","error_description":"AADSTS500011: The resource principal named https://..../anslutsk-multi-26 was not found in the tenant named 975f013f-7f24-47e8-a7d3-abc4752bf346. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.\r\nTrace ID: 0eec3338-bfa4-49f1-a7aa-7d13f9842200\r\nCorrelation ID: 02d18935-c309-4c71-8757-7a9770a98805\r\nTimestamp: 2021-02-24 07:12:19Z","error_codes":[500011],"timestamp":"2021-02-24 07:12:19Z","trace_id":"0eec3338-bfa4-49f1-a7aa-7d13f9842200","correlation_id":"02d18935-c309-4c71-8757-7a9770a98805","error_uri":"https://westus2.login.microsoft.com/error?code=500011"}

In addition, I ran an example of another "error" case:

When there was an invalid domain in the "multi_tenant_config" json config item, for example the String value "asdfasdfasdf" the program did not crash and we get the expected error during while trying to obtain a Bearer token from IMS:

[2021-02-24T07:16:33Z ERROR onefuzz_telemetry] error running supervisor agent: 400 Bad Request: {"error":"invalid_resource","error_description":"AADSTS500011: The resource principal named https://asdfasdfasdf/anslutsk-multi-26 was not found in the tenant named 975f013f-7f24-47e8-a7d3-abc4752bf346. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.\r\nTrace ID: b0c8b83c-e552-4f2f-9f77-d280785d2600\r\nCorrelation ID: 1982fe67-c50a-4e0a-ac63-186134839aae\r\nTimestamp: 2021-02-24 07:16:33Z","error_codes":[500011],"timestamp":"2021-02-24 07:16:33Z","trace_id":"b0c8b83c-e552-4f2f-9f77-d280785d2600","correlation_id":"1982fe67-c50a-4e0a-ac63-186134839aae","error_uri":"https://westus2.login.microsoft.com/error?code=500011"}

I do not see the point in adding additional error handling. Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value assigned to self.resource is taken from the config item "onefuzz_url" and if the value is invalid, the program crashes like this:

[2021-02-24T15:54:35Z ERROR onefuzz_telemetry] error loading supervisor agent config: invalid value: string "asdf", expected description() is deprecated; use Display at line 1 column 50

If the "onefuzz_url" is set to "null" the following error is displayed:

[2021-02-24T16:03:23Z ERROR onefuzz_telemetry] error loading supervisor agent config: invalid type: null, expected a string representing an URL at line 1 column 48
Error: invalid type: null, expected a string representing an URL at line 1 column 48

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary to change this section of the code, but that doesn't mean that I don't support making a change! If either of you would like to suggest a specific change then I'd be happy to test it. It would need to be pretty specific though since I don't share either of your view points.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing examples of expected/valid inputs, and your investigation into the current failure modes.

I do not see the point in adding additional error handling. Am I missing something?

We still want to change what we're doing here, because this de facto current unreachability is very non-local, and depends on the behavior of the larger system. But if we ever accidentally introduce a bug elsewhere that makes the unwrap() reachable, we want to fail in a way that will be easier to understand and fix, and get reported here.

So, the constraint here is that we must not unwrap(). We could make ManagedIdentityCredentials::url() return a Result<Url>, but that would be a bit silly, since we'd then have to change all use sites, and a non-Ok() result is actually an error that we can detect much earlier (in the constructor).

Instead, I suggest changing ManagedIdentityCredentials::new() to have the signature new(Url, Option<Url>) -> Result<Self>, taking care to generate the resource string without a trailing / in the non-multi-tenant case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test123

let host = uri.host_str().unwrap();
let instance: Vec<&str> = host.split('.').collect();
format!("https://{}/{}", domain, instance[0])
} else {
self.resource.clone()
};

url.query_pairs_mut().append_pair("resource", &resource);
url
}

Expand Down
24 changes: 20 additions & 4 deletions src/agent/onefuzz-supervisor/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub struct StaticConfig {

pub onefuzz_url: Url,

pub multi_tenant_domain: Option<String>,

pub instrumentation_key: Option<Uuid>,

pub telemetry_key: Option<Uuid>,
Expand All @@ -43,6 +45,8 @@ struct RawStaticConfig {

pub onefuzz_url: Url,

pub multi_tenant_domain: Option<String>,

pub instrumentation_key: Option<Uuid>,

pub telemetry_key: Option<Uuid>,
Expand All @@ -65,14 +69,16 @@ impl StaticConfig {
.to_string()
.trim_end_matches('/')
.to_owned();
let managed = ManagedIdentityCredentials::new(resource);
let managed =
ManagedIdentityCredentials::new(resource, config.multi_tenant_domain.clone());
managed.into()
}
};
let config = StaticConfig {
credentials,
pool_name: config.pool_name,
onefuzz_url: config.onefuzz_url,
multi_tenant_domain: config.multi_tenant_domain,
instrumentation_key: config.instrumentation_key,
telemetry_key: config.telemetry_key,
heartbeat_queue: config.heartbeat_queue,
Expand All @@ -94,6 +100,10 @@ impl StaticConfig {
let client_id = Uuid::parse_str(&std::env::var("ONEFUZZ_CLIENT_ID")?)?;
let client_secret = std::env::var("ONEFUZZ_CLIENT_SECRET")?;
let tenant = std::env::var("ONEFUZZ_TENANT")?;
let multi_tenant_domain = match std::env::var("ONEFUZZ_MULTI_TENANT_DOMAIN") {
Ok(v) => Some(v),
Err(_e) => None,
};
let onefuzz_url = Url::parse(&std::env::var("ONEFUZZ_URL")?)?;
let pool_name = std::env::var("ONEFUZZ_POOL")?;

Expand All @@ -115,15 +125,21 @@ impl StaticConfig {
None
};

let credentials =
ClientCredentials::new(client_id, client_secret, onefuzz_url.to_string(), tenant)
.into();
let credentials = ClientCredentials::new(
client_id,
client_secret,
onefuzz_url.to_string(),
multi_tenant_domain.clone(),
tenant,
)
.into();

Ok(Self {
instance_id,
credentials,
pool_name,
onefuzz_url,
multi_tenant_domain,
instrumentation_key,
telemetry_key,
heartbeat_queue,
Expand Down
4 changes: 4 additions & 0 deletions src/api-service/__app__/onefuzzlib/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ def build_pool_config(pool: Pool) -> str:
instance_id=get_instance_id(),
)

multi_tenant_domain = os.environ.get("MULTI_TENANT_DOMAIN")
if multi_tenant_domain:
config.multi_tenant_domain = multi_tenant_domain

filename = f"{pool.name}/config.json"

save_blob(
Expand Down
4 changes: 4 additions & 0 deletions src/api-service/__app__/pool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def set_config(pool: Pool) -> Pool:
),
instance_id=get_instance_id(),
)

multi_tenant_domain = os.environ.get("MULTI_TENANT_DOMAIN")
if multi_tenant_domain:
pool.config.multi_tenant_domain = multi_tenant_domain
return pool


Expand Down
19 changes: 15 additions & 4 deletions src/deployment/azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
"signedExpiry": {
"type": "string"
},
"app_func_issuer": {
"type": "string"
},
"app_func_audience": {
"type": "string"
},
"multi_tenant_domain": {
"type": "string"
},
"diagnosticsLogsLevel": {
"type": "string",
"defaultValue": "Verbose",
Expand Down Expand Up @@ -202,6 +211,10 @@
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountNameFunc'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
},
{
"name": "MULTI_TENANT_DOMAIN",
"value": "[parameters('multi_tenant_domain')]"
},
{
"name": "AzureWebJobsDisableHomepage",
"value": "true"
Expand Down Expand Up @@ -262,11 +275,9 @@
"tokenStoreEnabled": true,
"clientId": "[parameters('clientId')]",
"clientSecret": "[parameters('clientSecret')]",
"issuer": "[concat('https://sts.windows.net/', subscription().tenantId, '/')]",
"issuer": "[parameters('app_func_issuer')]",
"defaultProvider": "AzureActiveDirectory",
"allowedAudiences": [
"[concat('https://', parameters('name'), '.azurewebsites.net')]"
],
"allowedAudiences": ["[parameters('app_func_audience')]"],
"isAadAutoProvisioned": false
}
},
Expand Down
Loading