Skip to content
93 changes: 93 additions & 0 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,23 @@
"issuer": {
"type": "string",
"description": "The expected issuer (iss) claim of incoming JWT tokens."
},
"roles-path": {
"type": "string",
"description": "Path to the JWT claim that contains custom roles. Supports dot notation and bracket notation. Supported for Custom authentication."
},
"roles-format": {
"type": "string",
"description": "Format of the JWT claim that contains custom roles. Supported for Custom authentication.",
"enum": [
"array",
"string",
"delimited-string"
]
},
"roles-delimiter": {
"type": "string",
"description": "Delimiter used when roles-format is delimited-string. Supported for Custom authentication."
}
},
"required": [ "audience", "issuer" ]
Expand All @@ -479,6 +496,82 @@
},
"then": { "required": [ "jwt" ] },
"else": { "properties": { "jwt": false } }
},
{
"$comment": "Custom JWT role extraction settings default to the standard DAB roles claim shape.",
"if": {
"properties": {
"provider": {
"const": "Custom"
}
},
"required": [ "provider" ]
},
"then": {
"properties": {
"jwt": {
"properties": {
"roles-path": {
"default": "roles"
},
"roles-format": {
"default": "array"
},
"roles-delimiter": {
"default": " "
}
}
}
}
}
},
{
"$comment": "Custom JWT role extraction settings are only supported for the Custom provider.",
"if": {
"properties": {
"provider": {
"not": { "const": "Custom" }
}
},
"required": [ "provider" ]
},
"then": {
"properties": {
"jwt": {
"properties": {
"roles-path": false,
"roles-format": false,
"roles-delimiter": false
}
}
}
}
},
{
"$comment": "roles-delimiter is valid only when Custom JWT roles-format is delimited-string.",
"if": {
"properties": {
"provider": {
"const": "Custom"
},
"jwt": {
"required": [ "roles-delimiter" ]
}
},
"required": [ "provider", "jwt" ]
},
"then": {
"properties": {
"jwt": {
"properties": {
"roles-format": {
"const": "delimited-string"
}
},
"required": [ "roles-format" ]
}
}
}
}
]
}
Expand Down
47 changes: 47 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,53 @@ public void TestUpdateAuthenticationJwtIssuerHostSettings(string updatedJwtIssue
Assert.AreEqual(updatedJwtIssuerValue.ToString(), runtimeConfig.Runtime.Host.Authentication.Jwt.Issuer);
}

[TestMethod]
public void TestUpdateAuthenticationJwtRolesSettingsForCustomProvider()
{
SetupFileSystemWithInitialConfig(INITIAL_CONFIG);

ConfigureOptions options = new(
runtimeHostAuthenticationProvider: "Custom",
runtimeHostAuthenticationJwtAudience: "updatedAudience",
runtimeHostAuthenticationJwtIssuer: "updatedIssuer",
runtimeHostAuthenticationJwtRolesPath: "realm_access.roles",
runtimeHostAuthenticationJwtRolesFormat: "delimited-string",
runtimeHostAuthenticationJwtRolesDelimiter: ",",
config: TEST_RUNTIME_CONFIG_FILE
);
bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);

Assert.IsTrue(isSuccess);
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig));
Assert.AreEqual("realm_access.roles", runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.RolesPath);
Assert.AreEqual("delimited-string", runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.RolesFormat);
Assert.AreEqual(",", runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.RolesDelimiter);
}

[DataTestMethod]
[DataRow("EntraID", "roles", "array", null, DisplayName = "Role settings fail for non-Custom provider.")]
[DataRow("Custom", "groups[0]", "array", null, DisplayName = "Invalid rolesPath fails.")]
[DataRow("Custom", "", "array", null, DisplayName = "Blank rolesPath fails.")]
[DataRow("Custom", "roles", "semicolon-delimited", null, DisplayName = "Invalid rolesFormat fails.")]
[DataRow("Custom", "roles", "array", ",", DisplayName = "rolesDelimiter rejected for array format.")]
public void TestUpdateAuthenticationJwtRolesSettingsValidationFailure(string authenticationProvider, string rolesPath, string rolesFormat, string rolesDelimiter)
{
SetupFileSystemWithInitialConfig(INITIAL_CONFIG);

ConfigureOptions options = new(
runtimeHostAuthenticationProvider: authenticationProvider,
runtimeHostAuthenticationJwtRolesPath: rolesPath,
runtimeHostAuthenticationJwtRolesFormat: rolesFormat,
runtimeHostAuthenticationJwtRolesDelimiter: rolesDelimiter,
config: TEST_RUNTIME_CONFIG_FILE
);
bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);

Assert.IsFalse(isSuccess);
Assert.AreEqual(INITIAL_CONFIG, _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE));
}

/// <summary>
/// Test to update the current depth limit for GraphQL and removal the depth limit using -1.
/// When runtime.graphql.depth-limit has an initial value of 8.
Expand Down
36 changes: 36 additions & 0 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,42 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled);
}

[TestMethod]
public void TestInitializingCustomJwtRoleSettings()
{
string[] args =
{
"init",
"-c",
TEST_RUNTIME_CONFIG_FILE,
"--connection-string",
SAMPLE_TEST_CONN_STRING,
"--database-type",
"mssql",
"--auth.provider",
"Custom",
"--auth.audience",
"dab-api",
"--auth.issuer",
"https://issuer.example.com",
"--auth.roles-path",
"scope",
"--auth.roles-format",
"delimited-string",
"--auth.roles-delimiter",
","
};
Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);

Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));

JwtOptions? jwt = runtimeConfig.Runtime?.Host?.Authentication?.Jwt;
Assert.IsNotNull(jwt);
Assert.AreEqual("scope", jwt.RolesPath);
Assert.AreEqual("delimited-string", jwt.RolesFormat);
Assert.AreEqual(",", jwt.RolesDelimiter);
}

/// <summary>
/// Test to validate the usage of --graphql.multiple-mutations.create.enabled option of the init command for all database types.
///
Expand Down
15 changes: 15 additions & 0 deletions src/Cli/Commands/ConfigureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public ConfigureOptions(
string? runtimeHostAuthenticationProvider = null,
string? runtimeHostAuthenticationJwtAudience = null,
string? runtimeHostAuthenticationJwtIssuer = null,
string? runtimeHostAuthenticationJwtRolesPath = null,
string? runtimeHostAuthenticationJwtRolesFormat = null,
string? runtimeHostAuthenticationJwtRolesDelimiter = null,
int? runtimeHostMaxResponseSizeMb = null,
string? azureKeyVaultEndpoint = null,
AKVRetryPolicyMode? azureKeyVaultRetryPolicyMode = null,
Expand Down Expand Up @@ -171,6 +174,9 @@ public ConfigureOptions(
RuntimeHostAuthenticationProvider = runtimeHostAuthenticationProvider;
RuntimeHostAuthenticationJwtAudience = runtimeHostAuthenticationJwtAudience;
RuntimeHostAuthenticationJwtIssuer = runtimeHostAuthenticationJwtIssuer;
RuntimeHostAuthenticationJwtRolesPath = runtimeHostAuthenticationJwtRolesPath;
RuntimeHostAuthenticationJwtRolesFormat = runtimeHostAuthenticationJwtRolesFormat;
RuntimeHostAuthenticationJwtRolesDelimiter = runtimeHostAuthenticationJwtRolesDelimiter;
RuntimeHostMaxResponseSizeMb = runtimeHostMaxResponseSizeMb;
// Azure Key Vault
AzureKeyVaultEndpoint = azureKeyVaultEndpoint;
Expand Down Expand Up @@ -366,6 +372,15 @@ public ConfigureOptions(
[Option("runtime.host.authentication.jwt.issuer", Required = false, HelpText = "Configure the entity that issued the Jwt Token.")]
public string? RuntimeHostAuthenticationJwtIssuer { get; }

[Option("runtime.host.authentication.jwt.roles-path", Required = false, HelpText = "Configure the path to the roles claim in the raw JWT payload JSON.")]
public string? RuntimeHostAuthenticationJwtRolesPath { get; }

[Option("runtime.host.authentication.jwt.roles-format", Required = false, HelpText = "Configure the format used to parse the roles claim.")]
public string? RuntimeHostAuthenticationJwtRolesFormat { get; }

[Option("runtime.host.authentication.jwt.roles-delimiter", Required = false, HelpText = "Configure the delimiter used when roles-format is delimited-string.")]
public string? RuntimeHostAuthenticationJwtRolesDelimiter { get; }

[Option("runtime.host.max-response-size-mb", Required = false, HelpText = "Maximum response size in megabytes. Use -1 for maximum engine limit. Default: 158.")]
public int? RuntimeHostMaxResponseSizeMb { get; }

Expand Down
15 changes: 15 additions & 0 deletions src/Cli/Commands/InitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public InitOptions(
string authenticationProvider,
string? audience = null,
string? issuer = null,
string? rolesPath = null,
string? rolesFormat = null,
string? rolesDelimiter = null,
string restPath = RestRuntimeOptions.DEFAULT_PATH,
string? runtimeBaseRoute = null,
bool restDisabled = false,
Expand Down Expand Up @@ -57,6 +60,9 @@ public InitOptions(
AuthenticationProvider = authenticationProvider;
Audience = audience;
Issuer = issuer;
RolesPath = rolesPath;
RolesFormat = rolesFormat;
RolesDelimiter = rolesDelimiter;
RestPath = restPath;
RuntimeBaseRoute = runtimeBaseRoute;
RestDisabled = restDisabled;
Expand Down Expand Up @@ -105,6 +111,15 @@ public InitOptions(
[Option("auth.issuer", Required = false, HelpText = "Specify the party that issued the jwt token.")]
public string? Issuer { get; }

[Option("auth.roles-path", Required = false, HelpText = "Path to the roles claim in the raw JWT payload JSON for Custom authentication.")]
public string? RolesPath { get; }

[Option("auth.roles-format", Required = false, HelpText = "Format used to parse the roles claim for Custom authentication.")]
public string? RolesFormat { get; }

[Option("auth.roles-delimiter", Required = false, HelpText = "Delimiter used when auth.roles-format is delimited-string for Custom authentication.")]
public string? RolesDelimiter { get; }

[Option("rest.path", Default = RestRuntimeOptions.DEFAULT_PATH, Required = false, HelpText = "Specify the REST endpoint's default prefix.")]
public string RestPath { get; }

Expand Down
Loading
Loading