Config Refactor Round 2. (#2771)

* Config Refactor Round 2.

* Adding docs.

* Fix file formatting.

* Removing.

* fixing imports.

* Removing.

* Fixing cli access token retrieval.

* Fixing authority check.

* Small edits.

* Removing duplicate.

* Adding uuid check.

* Possible to override with existing params.

* Allowing flags to override storage.

* Trying to fix config params.?

* Fixing.

* Set endpoint params via app function.

* Checking changes to params.

* Make tenant_domain default.

* Remove endoint params from models.

* UPdating docs.

* Setting

* Removing hardcoded values.

* Typo.

* Removing endpoint upload.

* Typo.

* Fixing typos.

* Fix error message about aad tenant.

* Responding to comments.

* Update src/ApiService/ApiService/UserCredentials.cs

Co-authored-by: Marc Greisen <mgreisen@microsoft.com>

---------

Co-authored-by: Marc Greisen <mgreisen@microsoft.com>
This commit is contained in:
Noah McGregor Harper
2023-01-31 23:03:38 +00:00
committed by GitHub
parent 3d2fb65c14
commit f402304084
16 changed files with 269 additions and 96 deletions

View File

@ -0,0 +1,34 @@
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace Microsoft.OneFuzz.Service.Functions;
public class Config {
private readonly ILogTracer _log;
private readonly IOnefuzzContext _context;
public Config(ILogTracer log, IOnefuzzContext context) {
_log = log;
_context = context;
}
[Function("Config")]
public Async.Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET")] HttpRequestData req) {
return Get(req);
}
public async Async.Task<HttpResponseData> Get(HttpRequestData req) {
_log.Info($"getting endpoint config parameters");
var endpointParams = new ConfigResponse(
Authority: _context.ServiceConfiguration.Authority,
ClientId: _context.ServiceConfiguration.CliAppId,
TenantDomain: _context.ServiceConfiguration.TenantDomain);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(endpointParams);
return response;
}
}

View File

@ -159,6 +159,12 @@ public record ScalesetResponse(
Nodes: null); Nodes: null);
} }
public record ConfigResponse(
string? Authority,
string? ClientId,
string? TenantDomain
) : BaseResponse();
public class BaseResponseConverter : JsonConverter<BaseResponse> { public class BaseResponseConverter : JsonConverter<BaseResponse> {
public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return null; return null;

View File

@ -26,7 +26,9 @@ public interface IServiceConfig {
public string? DiagnosticsAzureBlobContainerSasUrl { get; } public string? DiagnosticsAzureBlobContainerSasUrl { get; }
public string? DiagnosticsAzureBlobRetentionDays { get; } public string? DiagnosticsAzureBlobRetentionDays { get; }
public string? CliAppId { get; }
public string? Authority { get; }
public string? TenantDomain { get; }
public string? MultiTenantDomain { get; } public string? MultiTenantDomain { get; }
public ResourceIdentifier? OneFuzzDataStorage { get; } public ResourceIdentifier? OneFuzzDataStorage { get; }
public ResourceIdentifier? OneFuzzFuncStorage { get; } public ResourceIdentifier? OneFuzzFuncStorage { get; }
@ -97,7 +99,9 @@ public class ServiceConfiguration : IServiceConfig {
public string? DiagnosticsAzureBlobContainerSasUrl { get => GetEnv("DIAGNOSTICS_AZUREBLOBCONTAINERSASURL"); } public string? DiagnosticsAzureBlobContainerSasUrl { get => GetEnv("DIAGNOSTICS_AZUREBLOBCONTAINERSASURL"); }
public string? DiagnosticsAzureBlobRetentionDays { get => GetEnv("DIAGNOSTICS_AZUREBLOBRETENTIONINDAYS"); } public string? DiagnosticsAzureBlobRetentionDays { get => GetEnv("DIAGNOSTICS_AZUREBLOBRETENTIONINDAYS"); }
public string? CliAppId { get => GetEnv("CLI_APP_ID"); }
public string? Authority { get => GetEnv("AUTHORITY"); }
public string? TenantDomain { get => GetEnv("TENANT_DOMAIN"); }
public string? MultiTenantDomain { get => GetEnv("MULTI_TENANT_DOMAIN"); } public string? MultiTenantDomain { get => GetEnv("MULTI_TENANT_DOMAIN"); }
public ResourceIdentifier? OneFuzzDataStorage { public ResourceIdentifier? OneFuzzDataStorage {

View File

@ -92,7 +92,7 @@ public class UserCredentials : IUserCredentials {
} else { } else {
var tenantsStr = allowedTenants.OkV is null ? "null" : String.Join(';', allowedTenants.OkV!); var tenantsStr = allowedTenants.OkV is null ? "null" : String.Join(';', allowedTenants.OkV!);
_log.Error($"issuer not from allowed tenant. issuer: {token.Issuer:Tag:Issuer} - tenants: {tenantsStr:Tag:Tenants}"); _log.Error($"issuer not from allowed tenant. issuer: {token.Issuer:Tag:Issuer} - tenants: {tenantsStr:Tag:Tenants}");
return OneFuzzResult<UserAuthInfo>.Error(ErrorCode.INVALID_REQUEST, new[] { "unauthorized AAD issuer" }); return OneFuzzResult<UserAuthInfo>.Error(ErrorCode.INVALID_REQUEST, new[] { "unauthorized AAD issuer. If multi-tenant auth is failing, make sure to include all tenant_ids in the `allowed_aad_tenants` list in the instance_config. To see the current instance_config, run `onefuzz instance_config get`. " });
} }
} else { } else {
_log.Error($"Failed to get allowed tenants due to {allowedTenants.ErrorV:Tag:Error}"); _log.Error($"Failed to get allowed tenants due to {allowedTenants.ErrorV:Tag:Error}");

View File

@ -26,7 +26,7 @@ public class ConfigOperations : Orm<InstanceConfig>, IConfigOperations {
private static readonly InstanceConfigCacheKey _key = new(); // singleton key private static readonly InstanceConfigCacheKey _key = new(); // singleton key
public Task<InstanceConfig> Fetch() public Task<InstanceConfig> Fetch()
=> _cache.GetOrCreateAsync(_key, async entry => { => _cache.GetOrCreateAsync(_key, async entry => {
entry = entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(10)); // cached for 10 minutes entry = entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(1)); // cached for 1 minute
var key = _context.ServiceConfiguration.OneFuzzInstanceName ?? throw new Exception("Environment variable ONEFUZZ_INSTANCE_NAME is not set"); var key = _context.ServiceConfiguration.OneFuzzInstanceName ?? throw new Exception("Environment variable ONEFUZZ_INSTANCE_NAME is not set");
return await GetEntityAsync(key, key); return await GetEntityAsync(key, key);
}); });

View File

@ -25,6 +25,11 @@ public sealed class TestServiceConfiguration : IServiceConfig {
public string? OneFuzzTelemetry => "TestOneFuzzTelemetry"; public string? OneFuzzTelemetry => "TestOneFuzzTelemetry";
public string? CliAppId => "TestGuid";
public string? Authority => "TestAuthority";
public string? TenantDomain => "TestDomain";
public string? MultiTenantDomain => null; public string? MultiTenantDomain => null;
public string? OneFuzzInstanceName => "UnitTestInstance"; public string? OneFuzzInstanceName => "UnitTestInstance";

View File

@ -41,8 +41,9 @@ from .ssh import build_ssh_command, ssh_connect, temp_file
UUID_EXPANSION = TypeVar("UUID_EXPANSION", UUID, str) UUID_EXPANSION = TypeVar("UUID_EXPANSION", UUID, str)
DEFAULT = BackendConfig( DEFAULT = BackendConfig(
authority="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", authority="",
client_id="72f1562a-8c0c-41ea-beb9-fa2b71c80134", client_id="",
tenant_domain="",
) )
# This was generated randomly and should be preserved moving forwards # This was generated randomly and should be preserved moving forwards
@ -122,6 +123,10 @@ class Endpoint:
as_params: bool = False, as_params: bool = False,
alternate_endpoint: Optional[str] = None, alternate_endpoint: Optional[str] = None,
) -> A: ) -> A:
# Retrieve Auth Parameters
self._req_config_params()
response = self._req_base( response = self._req_base(
method, method,
data=data, data=data,
@ -153,6 +158,33 @@ class Endpoint:
return [model.parse_obj(x) for x in response] return [model.parse_obj(x) for x in response]
def _req_config_params(
self,
) -> None:
if self.onefuzz._backend.config.endpoint is None:
raise Exception("Endpoint Not Configured")
endpoint = self.onefuzz._backend.config.endpoint
response = self.onefuzz._backend.session.request(
"GET", endpoint + "/api/config"
)
logging.debug(response.json())
endpoint_params = responses.Config.parse_obj(response.json())
logging.debug(self.onefuzz._backend.config.authority)
# Will override values in storage w/ provided values for SP use
if self.onefuzz._backend.config.client_id == "":
self.onefuzz._backend.config.client_id = endpoint_params.client_id
if self.onefuzz._backend.config.authority == "":
self.onefuzz._backend.config.authority = endpoint_params.authority
if self.onefuzz._backend.config.tenant_domain == "":
self.onefuzz._backend.config.tenant_domain = endpoint_params.tenant_domain
self.onefuzz._backend.save_config()
def _disambiguate( def _disambiguate(
self, self,
name: str, name: str,
@ -1862,7 +1894,9 @@ class Onefuzz:
self.logger.debug("set config") self.logger.debug("set config")
if reset: if reset:
self._backend.config = BackendConfig(authority="", client_id="") self._backend.config = BackendConfig(
authority="", client_id="", tenant_domain=""
)
if endpoint is not None: if endpoint is not None:
# The normal path for calling the API always uses the oauth2 workflow, # The normal path for calling the API always uses the oauth2 workflow,

View File

@ -95,7 +95,7 @@ class BackendConfig(BaseModel):
client_id: str client_id: str
endpoint: Optional[str] endpoint: Optional[str]
features: Set[str] = Field(default_factory=set) features: Set[str] = Field(default_factory=set)
tenant_domain: Optional[str] tenant_domain: str
class Backend: class Backend:
@ -181,7 +181,7 @@ class Backend:
if not self.config.endpoint: if not self.config.endpoint:
raise Exception("endpoint not configured") raise Exception("endpoint not configured")
if self.config.tenant_domain: if "https://login.microsoftonline.com/common" in self.config.authority:
endpoint = urlparse(self.config.endpoint).netloc.split(".")[0] endpoint = urlparse(self.config.endpoint).netloc.split(".")[0]
scopes = [ scopes = [
f"api://{self.config.tenant_domain}/{endpoint}/.default", f"api://{self.config.tenant_domain}/{endpoint}/.default",

View File

@ -8,6 +8,9 @@ param clientSecret string
param signedExpiry string param signedExpiry string
param app_func_issuer string param app_func_issuer string
param app_func_audiences array param app_func_audiences array
param cli_app_id string
param authority string
param tenant_domain string
param multi_tenant_domain string param multi_tenant_domain string
param enable_remote_debugging bool = false param enable_remote_debugging bool = false
param enable_profiler bool = false param enable_profiler bool = false
@ -239,6 +242,9 @@ module functionSettings 'bicep-templates/function-settings.bicep' = {
fuzz_storage_resource_id: storage.outputs.FuzzId fuzz_storage_resource_id: storage.outputs.FuzzId
keyvault_name: keyVaultName keyvault_name: keyVaultName
monitor_account_name: operationalInsights.outputs.monitorAccountName monitor_account_name: operationalInsights.outputs.monitorAccountName
cli_app_id: cli_app_id
authority: authority
tenant_domain: tenant_domain
multi_tenant_domain: multi_tenant_domain multi_tenant_domain: multi_tenant_domain
enable_profiler: enable_profiler enable_profiler: enable_profiler
app_config_endpoint: featureFlags.outputs.AppConfigEndpoint app_config_endpoint: featureFlags.outputs.AppConfigEndpoint

View File

@ -8,6 +8,9 @@ param app_insights_key string
@secure() @secure()
param func_sas_url string param func_sas_url string
param cli_app_id string
param authority string
param tenant_domain string
param multi_tenant_domain string param multi_tenant_domain string
@secure() @secure()
@ -52,6 +55,9 @@ resource functionSettings 'Microsoft.Web/sites/config@2021-03-01' = {
APPINSIGHTS_APPID: app_insights_app_id APPINSIGHTS_APPID: app_insights_app_id
ONEFUZZ_TELEMETRY: telemetry ONEFUZZ_TELEMETRY: telemetry
AzureWebJobsStorage: func_sas_url AzureWebJobsStorage: func_sas_url
CLI_APP_ID: cli_app_id
AUTHORITY: authority
TENANT_DOMAIN: tenant_domain
MULTI_TENANT_DOMAIN: multi_tenant_domain MULTI_TENANT_DOMAIN: multi_tenant_domain
AzureWebJobsDisableHomepage: 'true' AzureWebJobsDisableHomepage: 'true'
AzureSignalRConnectionString: signal_r_connection_string AzureSignalRConnectionString: signal_r_connection_string

View File

@ -23,7 +23,6 @@ param diagnostics_log_level string
param log_retention int param log_retention int
param linux_fx_version string param linux_fx_version string
var siteconfig = (use_windows) ? { var siteconfig = (use_windows) ? {
} : { } : {
linuxFxVersion: linux_fx_version linuxFxVersion: linux_fx_version
@ -75,6 +74,7 @@ resource funcAuthSettings 'Microsoft.Web/sites/config@2021-03-01' = {
globalValidation: { globalValidation: {
unauthenticatedClientAction: 'RedirectToLoginPage' unauthenticatedClientAction: 'RedirectToLoginPage'
requireAuthentication: true requireAuthentication: true
excludedPaths: [ '/api/config' ]
} }
httpSettings: { httpSettings: {
requireHttps: true requireHttps: true

View File

@ -1,6 +1,12 @@
{ {
"tenant_id": "72f988bf-86f1-41af-91ab-2d7cd011db47",
"tenant_domain": "azurewebsites.net",
"multi_tenant_domain": "",
"cli_client_id": "72f1562a-8c0c-41ea-beb9-fa2b71c80134",
"proxy_nsg_config": { "proxy_nsg_config": {
"allowed_ips": ["*"], "allowed_ips": [
"*"
],
"allowed_service_tags": [] "allowed_service_tags": []
} }
} }

View File

@ -43,8 +43,9 @@ from azure.storage.blob import (
from msrest.serialization import TZ_UTC from msrest.serialization import TZ_UTC
from deploylib.configuration import ( from deploylib.configuration import (
Config,
InstanceConfigClient, InstanceConfigClient,
NetworkSecurityConfig, NsgRule,
parse_rules, parse_rules,
update_admins, update_admins,
update_allowed_aad_tenants, update_allowed_aad_tenants,
@ -61,7 +62,6 @@ from deploylib.registration import (
get_application, get_application,
get_service_principal, get_service_principal,
get_signed_in_user, get_signed_in_user,
get_tenant_id,
query_microsoft_graph, query_microsoft_graph,
register_application, register_application,
set_app_audience, set_app_audience,
@ -74,10 +74,6 @@ from deploylib.registration import (
USER_READ_PERMISSION = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" USER_READ_PERMISSION = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
MICROSOFT_GRAPH_APP_ID = "00000003-0000-0000-c000-000000000000" MICROSOFT_GRAPH_APP_ID = "00000003-0000-0000-c000-000000000000"
ONEFUZZ_CLI_AUTHORITY = (
"https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"
)
COMMON_AUTHORITY = "https://login.microsoftonline.com/common"
TELEMETRY_NOTICE = ( TELEMETRY_NOTICE = (
"Telemetry collection on stats and OneFuzz failures are sent to Microsoft. " "Telemetry collection on stats and OneFuzz failures are sent to Microsoft. "
"To disable, delete the ONEFUZZ_TELEMETRY application setting in the " "To disable, delete the ONEFUZZ_TELEMETRY application setting in the "
@ -139,7 +135,7 @@ class Client:
location: str, location: str,
application_name: str, application_name: str,
owner: str, owner: str,
nsg_config: str, config: str,
client_id: Optional[str], client_id: Optional[str],
client_secret: Optional[str], client_secret: Optional[str],
app_zip: str, app_zip: str,
@ -167,7 +163,7 @@ class Client:
self.location = location self.location = location
self.application_name = application_name self.application_name = application_name
self.owner = owner self.owner = owner
self.nsg_config = nsg_config self.config = config
self.app_zip = app_zip self.app_zip = app_zip
self.tools = tools self.tools = tools
self.instance_specific = instance_specific self.instance_specific = instance_specific
@ -180,10 +176,6 @@ class Client:
"client_id": client_id, "client_id": client_id,
"client_secret": client_secret, "client_secret": client_secret,
} }
if self.multi_tenant_domain:
authority = COMMON_AUTHORITY
else:
authority = ONEFUZZ_CLI_AUTHORITY
self.migrations = migrations self.migrations = migrations
self.export_appinsights = export_appinsights self.export_appinsights = export_appinsights
self.admins = admins self.admins = admins
@ -196,9 +188,15 @@ class Client:
self.host_dotnet_on_windows = host_dotnet_on_windows self.host_dotnet_on_windows = host_dotnet_on_windows
self.enable_profiler = enable_profiler self.enable_profiler = enable_profiler
self.rules: List[NsgRule] = []
self.tenant_id = ""
self.tenant_domain = ""
self.authority = ""
self.cli_config: Dict[str, Union[str, UUID]] = { self.cli_config: Dict[str, Union[str, UUID]] = {
"client_id": self.cli_app_id, "client_id": "",
"authority": authority, "authority": "",
} }
machine = platform.machine() machine = platform.machine()
@ -305,7 +303,7 @@ class Client:
# The url to access the instance # The url to access the instance
# This also represents the legacy identifier_uris of the application # This also represents the legacy identifier_uris of the application
# registration # registration
if self.multi_tenant_domain: if self.multi_tenant_domain != "":
return "https://%s/%s" % (self.multi_tenant_domain, self.application_name) return "https://%s/%s" % (self.multi_tenant_domain, self.application_name)
else: else:
return "https://%s.azurewebsites.net" % self.application_name return "https://%s.azurewebsites.net" % self.application_name
@ -316,14 +314,14 @@ class Client:
# to be from an approved domain The format of this value is derived # to be from an approved domain The format of this value is derived
# from the default value proposed by azure when creating an application # from the default value proposed by azure when creating an application
# registration api://{guid}/... # registration api://{guid}/...
if self.multi_tenant_domain: if self.multi_tenant_domain != "":
return "api://%s/%s" % (self.multi_tenant_domain, self.application_name) return "api://%s/%s" % (self.multi_tenant_domain, self.application_name)
else: else:
return "api://%s.azurewebsites.net" % self.application_name return "api://%s.azurewebsites.net" % self.application_name
def get_signin_audience(self) -> str: def get_signin_audience(self) -> str:
# https://docs.microsoft.com/en-us/azure/active-directory/develop/supported-accounts-validation # https://docs.microsoft.com/en-us/azure/active-directory/develop/supported-accounts-validation
if self.multi_tenant_domain: if self.multi_tenant_domain != "":
return "AzureADMultipleOrgs" return "AzureADMultipleOrgs"
else: else:
return "AzureADMyOrg" return "AzureADMyOrg"
@ -382,7 +380,7 @@ class Client:
else: else:
self.update_existing_app_registration(app, app_roles) self.update_existing_app_registration(app, app_roles)
if self.multi_tenant_domain and app["signInAudience"] == "AzureADMyOrg": if self.multi_tenant_domain != "" and app["signInAudience"] == "AzureADMyOrg":
set_app_audience( set_app_audience(
app["id"], app["id"],
"AzureADMultipleOrgs", "AzureADMultipleOrgs",
@ -419,13 +417,10 @@ class Client:
OnefuzzAppRole.CliClient, OnefuzzAppRole.CliClient,
self.get_subscription_id(), self.get_subscription_id(),
) )
if self.multi_tenant_domain:
authority = COMMON_AUTHORITY
else:
authority = app_info.authority
self.cli_config = { self.cli_config = {
"client_id": app_info.client_id, "client_id": app_info.client_id,
"authority": authority, "authority": self.authority,
} }
else: else:
logger.error( logger.error(
@ -437,14 +432,10 @@ class Client:
else: else:
onefuzz_cli_app = cli_app onefuzz_cli_app = cli_app
authorize_application(uuid.UUID(onefuzz_cli_app["appId"]), app["appId"]) authorize_application(uuid.UUID(onefuzz_cli_app["appId"]), app["appId"])
if self.multi_tenant_domain:
authority = COMMON_AUTHORITY
else:
tenant_id = get_tenant_id(self.get_subscription_id())
authority = "https://login.microsoftonline.com/%s" % tenant_id
self.cli_config = { self.cli_config = {
"client_id": onefuzz_cli_app["appId"], "client_id": onefuzz_cli_app["appId"],
"authority": authority, "authority": self.authority,
} }
# ensure replyURLs is set properly # ensure replyURLs is set properly
@ -641,7 +632,7 @@ class Client:
# Add --custom_domain value to Allowed token audiences setting # Add --custom_domain value to Allowed token audiences setting
if self.custom_domain: if self.custom_domain:
if self.multi_tenant_domain: if self.multi_tenant_domain != "":
root_domain = self.multi_tenant_domain root_domain = self.multi_tenant_domain
else: else:
root_domain = "%s.azurewebsites.net" % self.application_name root_domain = "%s.azurewebsites.net" % self.application_name
@ -653,7 +644,7 @@ class Client:
app_func_audiences.extend(custom_domains) app_func_audiences.extend(custom_domains)
if self.multi_tenant_domain: if self.multi_tenant_domain != "":
# clear the value in the Issuer Url field: # clear the value in the Issuer Url field:
# https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient-enterpriseapi-multitenant # https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient-enterpriseapi-multitenant
app_func_issuer = "" app_func_issuer = ""
@ -680,6 +671,9 @@ class Client:
"clientSecret": {"value": self.results["client_secret"]}, "clientSecret": {"value": self.results["client_secret"]},
"app_func_issuer": {"value": app_func_issuer}, "app_func_issuer": {"value": app_func_issuer},
"signedExpiry": {"value": expiry}, "signedExpiry": {"value": expiry},
"cli_app_id": {"value": self.cli_app_id},
"authority": {"value": self.authority},
"tenant_domain": {"value": self.tenant_domain},
"multi_tenant_domain": multi_tenant_domain, "multi_tenant_domain": multi_tenant_domain,
"workbookData": {"value": self.workbook_data}, "workbookData": {"value": self.workbook_data},
"enable_remote_debugging": {"value": self.host_dotnet_on_windows}, "enable_remote_debugging": {"value": self.host_dotnet_on_windows},
@ -778,6 +772,38 @@ class Client:
table_service = TableService(account_name=name, account_key=key) table_service = TableService(account_name=name, account_key=key)
migrate(table_service, self.migrations) migrate(table_service, self.migrations)
def parse_config(self) -> None:
logger.info("parsing config: %s", self.config)
if self.config:
with open(self.config, "r") as template_handle:
config_template = json.load(template_handle)
try:
config = Config(config_template)
self.rules = parse_rules(config)
## Values provided via the CLI will override what's in the config.json
if self.authority == "":
self.authority = (
"https://login.microsoftonline.com/" + config.tenant_id
)
if self.tenant_domain == "":
self.tenant_domain = config.tenant_domain
if self.multi_tenant_domain == "":
self.multi_tenant_domain = config.multi_tenant_domain
if self.cli_app_id == "":
self.cli_app_id = config.cli_client_id
except Exception as ex:
logging.info(
"An Exception was encountered while parsing config file: %s", ex
)
raise Exception(
"config and sub-values were not properly included in config."
)
def set_instance_config(self) -> None: def set_instance_config(self) -> None:
logger.info("setting instance config") logger.info("setting instance config")
name = self.results["deploy"]["func_name"]["value"] name = self.results["deploy"]["func_name"]["value"]
@ -787,26 +813,7 @@ class Client:
config_client = InstanceConfigClient(table_service, self.application_name) config_client = InstanceConfigClient(table_service, self.application_name)
if self.nsg_config: update_nsg(config_client, self.rules)
logger.info("deploying arm template: %s", self.nsg_config)
with open(self.nsg_config, "r") as template_handle:
config_template = json.load(template_handle)
try:
config = NetworkSecurityConfig(config_template)
rules = parse_rules(config)
except Exception as ex:
logging.info(
"An Exception was encountered while parsing nsg_config file: %s", ex
)
raise Exception(
"proxy_nsg_config and sub-values were not properly included in config."
+ "Please submit a configuration resembling"
+ " { 'proxy_nsg_config': { 'allowed_ips': [], 'allowed_service_tags': [] } }"
)
update_nsg(config_client, rules)
if self.admins: if self.admins:
update_admins(config_client, self.admins) update_admins(config_client, self.admins)
@ -1136,18 +1143,11 @@ class Client:
"config", "config",
"--endpoint", "--endpoint",
f"https://{self.application_name}.azurewebsites.net", f"https://{self.application_name}.azurewebsites.net",
"--authority",
str(self.cli_config["authority"]),
"--client_id",
str(self.cli_config["client_id"]),
] ]
if "client_secret" in self.cli_config: if "client_secret" in self.cli_config:
cmd += ["--client_secret", "YOUR_CLIENT_SECRET_HERE"] cmd += ["--client_secret", "YOUR_CLIENT_SECRET_HERE"]
if self.multi_tenant_domain:
cmd += ["--tenant_domain", str(self.multi_tenant_domain)]
as_str = " ".join(cmd) as_str = " ".join(cmd)
logger.info(f"Update your CLI config via: {as_str}") logger.info(f"Update your CLI config via: {as_str}")
@ -1174,6 +1174,7 @@ def lower_case(arg: str) -> str:
def main() -> None: def main() -> None:
rbac_only_states = [ rbac_only_states = [
("parse_config", Client.parse_config),
("check_region", Client.check_region), ("check_region", Client.check_region),
("rbac", Client.setup_rbac), ("rbac", Client.setup_rbac),
("eventgrid", Client.remove_eventgrid), ("eventgrid", Client.remove_eventgrid),
@ -1200,7 +1201,7 @@ def main() -> None:
parser.add_argument("resource_group") parser.add_argument("resource_group")
parser.add_argument("application_name", type=lower_case) parser.add_argument("application_name", type=lower_case)
parser.add_argument("owner") parser.add_argument("owner")
parser.add_argument("nsg_config") parser.add_argument("config")
parser.add_argument( parser.add_argument(
"--bicep-template", "--bicep-template",
type=arg_file, type=arg_file,
@ -1272,7 +1273,7 @@ def main() -> None:
parser.add_argument( parser.add_argument(
"--multi_tenant_domain", "--multi_tenant_domain",
type=str, type=str,
default=None, default="",
help="enable multi-tenant authentication with this tenant domain", help="enable multi-tenant authentication with this tenant domain",
) )
parser.add_argument( parser.add_argument(
@ -1299,7 +1300,7 @@ def main() -> None:
parser.add_argument( parser.add_argument(
"--cli_app_id", "--cli_app_id",
type=str, type=str,
default="72f1562a-8c0c-41ea-beb9-fa2b71c80134", default="",
help="CLI App Registration to be used during deployment.", help="CLI App Registration to be used during deployment.",
) )
parser.add_argument( parser.add_argument(
@ -1337,7 +1338,7 @@ def main() -> None:
location=args.location, location=args.location,
application_name=args.application_name, application_name=args.application_name,
owner=args.owner, owner=args.owner,
nsg_config=args.nsg_config, config=args.config,
client_id=args.client_id, client_id=args.client_id,
client_secret=args.client_secret, client_secret=args.client_secret,
app_zip=args.app_zip, app_zip=args.app_zip,

View File

@ -48,12 +48,17 @@ class InstanceConfigClient:
self.enable_storage_client_logging() self.enable_storage_client_logging()
class NetworkSecurityConfig: class Config:
cli_client_id: str
tenant_id: str
tenant_domain: str
multi_tenant_domain: str
allowed_ips: List[str] allowed_ips: List[str]
allowed_service_tags: List[str] allowed_service_tags: List[str]
def __init__(self, config: Any): def __init__(self, config: Any):
self.parse_nsg_json(config) self.parse_nsg_json(config)
self.parse_endpoint_json(config)
def parse_nsg_json(self, config: Any) -> None: def parse_nsg_json(self, config: Any) -> None:
if not isinstance(config, Dict): if not isinstance(config, Dict):
@ -107,6 +112,66 @@ class NetworkSecurityConfig:
self.allowed_ips = proxy_config["allowed_ips"] self.allowed_ips = proxy_config["allowed_ips"]
self.allowed_service_tags = proxy_config["allowed_service_tags"] self.allowed_service_tags = proxy_config["allowed_service_tags"]
def parse_endpoint_json(self, config: Any) -> None:
if "cli_client_id" not in config:
raise Exception(
"CLI client_id not provided as valid key. Please Provide Valid Config."
)
if (
not isinstance(config["cli_client_id"], str)
or config["cli_client_id"] == ""
):
raise Exception(
"client_id is not a string. Please provide valid client_id."
)
try:
UUID(config["cli_client_id"])
except ValueError:
raise Exception(
"client_id is not a valid UUID. Please provide valid client_id."
)
if "tenant_id" not in config:
raise Exception(
"tenant_id not provided as valid key. Please provide valid config."
)
if not isinstance(config["tenant_id"], str) or config["tenant_id"] == "":
raise Exception(
"tenant_id is not a string. Please provide valid tenant_id."
)
if "tenant_domain" not in config:
raise Exception(
"tenant_domain not provided as valid key. Please provide valid config."
)
if (
not isinstance(config["tenant_domain"], str)
or config["tenant_domain"] == ""
):
raise Exception(
"tenant_domain is not a string. Please provide valid tenant_domain."
)
if "multi_tenant_domain" not in config:
raise Exception(
"multi_tenant_domain not provided as valid key. Please provide valid config. If the instance is not multi-tenant, please provide an empty string."
)
if not isinstance(config["multi_tenant_domain"], str):
raise Exception(
"multi_tenant_domain is not a string. Please provide valid multi_tenant_domain. If the instance is not multi-tenant, please provide an empty string."
)
self.cli_client_id = config["cli_client_id"]
self.tenant_id = config["tenant_id"]
self.tenant_domain = config["tenant_domain"]
self.multi_tenant_domain = config["multi_tenant_domain"]
class NsgRule: class NsgRule:
rule: str rule: str
@ -175,7 +240,7 @@ def update_admins(config_client: InstanceConfigClient, admins: List[UUID]) -> No
) )
def parse_rules(proxy_config: NetworkSecurityConfig) -> List[NsgRule]: def parse_rules(proxy_config: Config) -> List[NsgRule]:
allowed_ips = proxy_config.allowed_ips allowed_ips = proxy_config.allowed_ips
allowed_service_tags = proxy_config.allowed_service_tags allowed_service_tags = proxy_config.allowed_service_tags

View File

@ -6,7 +6,7 @@
import unittest import unittest
from typing import Any from typing import Any
from deploylib.configuration import NetworkSecurityConfig from deploylib.configuration import Config
class DeployTests(unittest.TestCase): class DeployTests(unittest.TestCase):
@ -15,33 +15,33 @@ class DeployTests(unittest.TestCase):
# Test Dictionary # Test Dictionary
invalid_config: Any = "" invalid_config: Any = ""
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Empty Dic # Test Empty Dic
invalid_config = {} invalid_config = {}
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Invalid Outer Keys # Test Invalid Outer Keys
invalid_config = {"": ""} invalid_config = {"": ""}
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Inner Dictionary # Test Inner Dictionary
invalid_config = {"proxy_nsg_config": ""} invalid_config = {"proxy_nsg_config": ""}
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Inner Keys # Test Inner Keys
invalid_config = {"proxy_nsg_config": {}} invalid_config = {"proxy_nsg_config": {}}
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Inner Keys # Test Inner Keys
invalid_config = {"proxy_nsg_config": {"allowed_ips": ""}} invalid_config = {"proxy_nsg_config": {"allowed_ips": ""}}
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test Inner Dict Values (lists) # Test Inner Dict Values (lists)
invalid_config = { invalid_config = {
"proxy_nsg_config": {"allowed_ips": [], "allowed_service_tags": ""} "proxy_nsg_config": {"allowed_ips": [], "allowed_service_tags": ""}
} }
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
# Test List Values # Test List Values
invalid_config = { invalid_config = {
"proxy_nsg_config": { "proxy_nsg_config": {
@ -50,19 +50,19 @@ class DeployTests(unittest.TestCase):
} }
} }
with self.assertRaises(Exception): with self.assertRaises(Exception):
NetworkSecurityConfig(invalid_config) Config(invalid_config)
## Test Valid Configs ## Test Valid Configs
# Test Empty Lists # Test Empty Lists
valid_config: Any = { valid_config: Any = {
"proxy_nsg_config": {"allowed_ips": [], "allowed_service_tags": []} "proxy_nsg_config": {"allowed_ips": [], "allowed_service_tags": []}
} }
NetworkSecurityConfig(valid_config) Config(valid_config)
# Test Wild Card Lists # Test Wild Card Lists
valid_config = { valid_config = {
"proxy_nsg_config": {"allowed_ips": ["*"], "allowed_service_tags": []} "proxy_nsg_config": {"allowed_ips": ["*"], "allowed_service_tags": []}
} }
NetworkSecurityConfig(valid_config) Config(valid_config)
# Test IPs Lists # Test IPs Lists
valid_config = { valid_config = {
"proxy_nsg_config": { "proxy_nsg_config": {
@ -70,7 +70,7 @@ class DeployTests(unittest.TestCase):
"allowed_service_tags": [], "allowed_service_tags": [],
} }
} }
NetworkSecurityConfig(valid_config) Config(valid_config)
# Test Tags Lists # Test Tags Lists
valid_config = { valid_config = {
"proxy_nsg_config": { "proxy_nsg_config": {
@ -78,7 +78,7 @@ class DeployTests(unittest.TestCase):
"allowed_service_tags": ["Internet"], "allowed_service_tags": ["Internet"],
} }
} }
NetworkSecurityConfig(valid_config) Config(valid_config)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -52,6 +52,12 @@ class Info(BaseResponse):
insights_instrumentation_key: Optional[str] insights_instrumentation_key: Optional[str]
class Config(BaseResponse):
authority: str
client_id: str
tenant_domain: str
class ContainerInfoBase(BaseResponse): class ContainerInfoBase(BaseResponse):
name: str name: str
metadata: Optional[Dict[str, str]] metadata: Optional[Dict[str, str]]