Skip to content
Open
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
8 changes: 7 additions & 1 deletion .github/playwright/impact-map.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@
"playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts",
"playwright/e2e/Features/TestSuitePipelineRedeploy.spec.ts",
"playwright/e2e/Features/TierDropdown.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts",
"playwright/e2e/Flow/CustomizeWidgets.spec.ts",
"playwright/e2e/Flow/ExploreDiscovery.spec.ts",
"playwright/e2e/Flow/LineageSettings.spec.ts",
Expand Down Expand Up @@ -2060,6 +2061,7 @@
"playwright/e2e/Features/TeamsHierarchy.spec.ts",
"playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts",
"playwright/e2e/Features/TierDropdown.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts",
"playwright/e2e/Features/UserProfileOnlineStatus.spec.ts",
"playwright/e2e/Features/Workflows/NoOpWorkflowNodeConfig.spec.ts",
"playwright/e2e/Features/Workflows/RecognizerFeedbackSchemaNodeLock.spec.ts",
Expand Down Expand Up @@ -3138,6 +3140,7 @@
"playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts",
"playwright/e2e/Features/TestSuitePipelineRedeploy.spec.ts",
"playwright/e2e/Features/TierDropdown.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts",
"playwright/e2e/Features/Topic.spec.ts",
"playwright/e2e/Features/UserProfileOnlineStatus.spec.ts",
"playwright/e2e/Features/Workflows/NoOpWorkflowNodeConfig.spec.ts",
Expand Down Expand Up @@ -3435,6 +3438,7 @@
"playwright/e2e/Features/LandingPageWidgets/DomainWidgetFilter.spec.ts",
"playwright/e2e/Features/Permissions/DataProductPermissions.spec.ts",
"playwright/e2e/Features/SampleDataDomainDataProduct.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts",
"playwright/e2e/Flow/IngestionBot.spec.ts",
"playwright/e2e/Pages/DataContractInheritance.spec.ts",
"playwright/e2e/Pages/DataMarketplace.spec.ts",
Expand Down Expand Up @@ -3597,6 +3601,7 @@
"playwright/e2e/Features/Tasks/TaskNavigation.spec.ts",
"playwright/e2e/Features/TeamSubscriptions.spec.ts",
"playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts",
"playwright/e2e/Features/Topic.spec.ts",
"playwright/e2e/Features/Workflows/NoOpWorkflowNodeConfig.spec.ts",
"playwright/e2e/Features/Workflows/RecognizerFeedbackSchemaNodeLock.spec.ts",
Expand Down Expand Up @@ -4407,7 +4412,8 @@
"openmetadata-ui/src/main/resources/ui/playwright/utils/tier.ts"
],
"specs": [
"playwright/e2e/Features/TierDropdown.spec.ts"
"playwright/e2e/Features/TierDropdown.spec.ts",
"playwright/e2e/Features/TierWidget.spec.ts"
]
},
{
Expand Down
10 changes: 10 additions & 0 deletions bootstrap/sql/migrations/native/2.0.2/mysql/schemaChanges.sql
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,13 @@ UPDATE change_event_consumers
SET json = JSON_SET(json, '$.startingTimestamp', CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED))
WHERE extension = 'eventSubscription.Offset'
AND JSON_EXTRACT(json, '$.startingTimestamp') IS NULL;

-- OpenMetadata issues this JWT itself; a non-positive lifetime makes every login token expire
-- immediately. Repair persisted values accepted by older SSO forms before auth configuration loads.
UPDATE openmetadata_settings
SET json = JSON_SET(json, '$.oidcConfiguration.tokenValidity', 3600)
WHERE configType = 'authenticationConfiguration'
AND JSON_TYPE(JSON_EXTRACT(json, '$.oidcConfiguration.tokenValidity')) IN ('INTEGER', 'DOUBLE')
AND CAST(
JSON_UNQUOTE(JSON_EXTRACT(json, '$.oidcConfiguration.tokenValidity')) AS DECIMAL(65, 10)
) <= 0;
12 changes: 12 additions & 0 deletions bootstrap/sql/migrations/native/2.0.2/postgres/schemaChanges.sql
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,15 @@ SET json = jsonb_set(
to_jsonb((EXTRACT(EPOCH FROM now()) * 1000)::bigint))
WHERE extension = 'eventSubscription.Offset'
AND json ->> 'startingTimestamp' IS NULL;

-- OpenMetadata issues this JWT itself; a non-positive lifetime makes every login token expire
-- immediately. Repair persisted values accepted by older SSO forms before auth configuration loads.
UPDATE openmetadata_settings
SET json = jsonb_set(
json,
'{oidcConfiguration,tokenValidity}',
to_jsonb(3600),
false)
WHERE configtype = 'authenticationConfiguration'
AND jsonb_typeof(json #> '{oidcConfiguration,tokenValidity}') = 'number'
AND (json #>> '{oidcConfiguration,tokenValidity}')::numeric <= 0;
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Jdbi jdbi() {
}

private void initializeSchema() {
createFlywayTable("v004__create_db_connection_info.sql", "openmetadata_settings");
createTable("1.3.0", "change_event_consumers");
createTable("1.13.0", "rdf_index_job");
createTable("1.13.0", "rdf_index_partition");
Expand All @@ -86,11 +87,32 @@ void applyReleaseMigration() {
}

private void createTable(final String version, final String table) {
final String dialect = backend == Backend.MYSQL ? "mysql" : "postgres";
final Path path =
repositoryRoot()
.resolve(
"bootstrap/sql/migrations/native/"
+ version
+ "/"
+ dialect
+ "/schemaChanges.sql");
createTable(path, table);
}

private void createFlywayTable(final String migration, final String table) {
final String driver =
backend == Backend.MYSQL ? "com.mysql.cj.jdbc.Driver" : "org.postgresql.Driver";
final Path path =
repositoryRoot().resolve("bootstrap/sql/migrations/flyway/" + driver + "/" + migration);
createTable(path, table);
}

private void createTable(final Path path, final String table) {
final String create =
migrationStatements(version).stream()
migrationStatements(path).stream()
.filter(statement -> statement.contains("CREATE TABLE IF NOT EXISTS " + table + " ("))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Missing migration for " + table));
.orElseThrow(() -> new IllegalStateException("Missing table migration for " + table));
jdbi.useHandle(handle -> handle.execute(create));
}

Expand All @@ -104,6 +126,10 @@ private List<String> migrationStatements(final String version) {
+ "/"
+ dialect
+ "/schemaChanges.sql");
return migrationStatements(path);
}

private List<String> migrationStatements(final Path path) {
return MigrationFile.parseSQLFile(
path.toFile(), backend == Backend.MYSQL ? ConnectionType.MYSQL : ConnectionType.POSTGRES);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
import org.openmetadata.service.security.AuthenticationCodeFlowHandler;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.JwtFilter;
import org.openmetadata.service.security.OidcTokenValidity;
import org.openmetadata.service.security.SecurityUtil;
import org.openmetadata.service.security.auth.LoginAttemptCache;
import org.openmetadata.service.security.auth.validator.Auth0Validator;
Expand Down Expand Up @@ -353,6 +354,8 @@ public Response createOrUpdate(Settings setting) {

try {
updateSetting(setting);
} catch (BadRequestException ex) {
throw ex;
} catch (Exception ex) {
LOG.error(FAILED_TO_UPDATE_SETTINGS, ex.getMessage());
return Response.status(500, INTERNAL_SERVER_ERROR_WITH_REASON + ex.getMessage()).build();
Expand All @@ -369,6 +372,8 @@ public Response createOrUpdate(Settings setting) {
public Response createNewSetting(Settings setting) {
try {
updateSetting(setting);
} catch (BadRequestException ex) {
throw ex;
} catch (Exception ex) {
LOG.error(FAILED_TO_UPDATE_SETTINGS, ex.getMessage());
return Response.status(500, INTERNAL_SERVER_ERROR_WITH_REASON + ex.getMessage()).build();
Expand Down Expand Up @@ -484,6 +489,8 @@ public void updateSetting(Settings setting) {
String updatedJson = prepareSettingForUpdate(setting);
dao.insertSettings(setting.getConfigType().toString(), updatedJson);
settingUpdated(setting.getConfigType());
} catch (BadRequestException ex) {
throw ex;
} catch (Exception ex) {
LOG.error("Failing in Updating Setting.", ex);
throw new CustomExceptionMessage(
Expand All @@ -504,7 +511,7 @@ private void updateSettingIfCurrent(Settings setting, String expectedJson) {
"Setting changed while the JSON Patch was being applied");
}
settingUpdated(setting.getConfigType());
} catch (PreconditionFailedException ex) {
} catch (BadRequestException | PreconditionFailedException ex) {
throw ex;
} catch (Exception ex) {
LOG.error("Failing in Updating Setting.", ex);
Expand Down Expand Up @@ -551,6 +558,10 @@ private String prepareSettingForUpdate(Settings setting) {
} else if (setting.getConfigType() == SettingsType.AUTHENTICATION_CONFIGURATION) {
AuthenticationConfiguration authConfig =
JsonUtils.convertValue(setting.getConfigValue(), AuthenticationConfiguration.class);
if (authConfig.getOidcConfiguration() != null
&& !OidcTokenValidity.isValid(authConfig.getOidcConfiguration().getTokenValidity())) {
throw new BadRequestException(OidcTokenValidity.VALIDATION_MESSAGE);
}
setting.setConfigValue(authConfig);
} else if (setting.getConfigType() == SettingsType.AUTHORIZER_CONFIGURATION) {
AuthorizerConfiguration authorizerConfig =
Expand Down Expand Up @@ -1701,13 +1712,18 @@ private FieldError validateAuthenticationConfigurationBaseFields(
private FieldError validateOidcConfiguration(
AuthenticationConfiguration authConfig, AuthorizerConfiguration authzConfig) {
try {
OidcClientConfig oidcConfig = authConfig.getOidcConfiguration();
FieldError tokenValidityError = validateOidcTokenValidity(oidcConfig);
if (tokenValidityError != null) {
return tokenValidityError;
}

String clientType = String.valueOf(authConfig.getClientType()).toLowerCase();
if ("confidential".equals(clientType)) {
if (authConfig.getOidcConfiguration() == null) {
if (oidcConfig == null) {
return ValidationErrorBuilder.createFieldError(
FieldPaths.OIDC_CLIENT_ID, "OIDC configuration is required");
}
OidcClientConfig oidcConfig = authConfig.getOidcConfiguration();

if (nullOrEmpty(oidcConfig.getId())) {
return ValidationErrorBuilder.createFieldError(
Expand Down Expand Up @@ -1821,6 +1837,15 @@ private FieldError validateOidcConfiguration(
}
}

@VisibleForTesting
static FieldError validateOidcTokenValidity(OidcClientConfig oidcConfig) {
if (oidcConfig != null && !OidcTokenValidity.isValid(oidcConfig.getTokenValidity())) {
return ValidationErrorBuilder.createFieldError(
FieldPaths.OIDC_TOKEN_VALIDITY, OidcTokenValidity.VALIDATION_MESSAGE);
}
return null;
}

/**
* Re-derives publicKeyUrls from the OIDC discovery document for confidential clients, where the
* field is not user-editable. Runs on every save and validate so that changing discoveryUri does
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,14 @@ private void initializeFields() {
validatePrincipalClaimsMapping(claimsMapping);
this.teamClaimMapping = authenticationConfiguration.getJwtTeamClaimMapping();
this.principalDomain = authorizerConfiguration.getPrincipalDomain();
this.tokenValidity = authenticationConfiguration.getOidcConfiguration().getTokenValidity();
Integer configuredTokenValidity =
authenticationConfiguration.getOidcConfiguration().getTokenValidity();
if (!OidcTokenValidity.isValid(configuredTokenValidity)) {
LOG.warn(
"OIDC token validity must be positive; using the {} second default",
OidcTokenValidity.DEFAULT_VALIDITY_SECONDS);
}
this.tokenValidity = OidcTokenValidity.resolveOrDefault(configuredTokenValidity);
this.maxAge = authenticationConfiguration.getOidcConfiguration().getMaxAge();
this.promptType = authenticationConfiguration.getOidcConfiguration().getPrompt();
this.clientAuthentication = getClientAuthentication(client.getConfiguration());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2026 Collate.
* 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.
*/
package org.openmetadata.service.security;

public final class OidcTokenValidity {
public static final int DEFAULT_VALIDITY_SECONDS = 3600;
public static final String VALIDATION_MESSAGE = "OIDC token validity must be at least 1 second";

private OidcTokenValidity() {}

public static boolean isValid(Integer validitySeconds) {
return validitySeconds != null && validitySeconds > 0;
}

public static int resolveOrDefault(Integer validitySeconds) {
return isValid(validitySeconds) ? validitySeconds : DEFAULT_VALIDITY_SECONDS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static class FieldPaths {
public static final String OIDC_SERVER_URL =
"authenticationConfiguration.oidcConfiguration.serverUrl";
public static final String OIDC_SCOPE = "authenticationConfiguration.oidcConfiguration.scope";
public static final String OIDC_TOKEN_VALIDITY =
"authenticationConfiguration.oidcConfiguration.tokenValidity";
public static final String OIDC_CALLBACK_URL =
"authenticationConfiguration.oidcConfiguration.callbackUrl";
public static final String OIDC_TENANT = "authenticationConfiguration.oidcConfiguration.tenant";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026 Collate.
* 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.
*/
package org.openmetadata.service.jdbi3;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;
import org.openmetadata.schema.security.client.OidcClientConfig;
import org.openmetadata.schema.system.FieldError;
import org.openmetadata.service.util.ValidationErrorBuilder.FieldPaths;

class SystemRepositoryOidcTokenValidityTest {

@Test
void reportsZeroAsAFieldValidationError() {
FieldError error =
SystemRepository.validateOidcTokenValidity(new OidcClientConfig().withTokenValidity(0));

assertEquals(FieldPaths.OIDC_TOKEN_VALIDITY, error.getField());
assertEquals("OIDC token validity must be at least 1 second", error.getError());
}

@Test
void acceptsPositiveValidityAndAbsentOptionalOidcConfiguration() {
assertNull(
SystemRepository.validateOidcTokenValidity(new OidcClientConfig().withTokenValidity(3600)));
assertNull(SystemRepository.validateOidcTokenValidity(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.configuration.GlossaryTermRelationSettings;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.security.client.OidcClientConfig;
import org.openmetadata.schema.settings.Settings;
import org.openmetadata.schema.settings.SettingsType;
import org.openmetadata.schema.utils.JsonUtils;
Expand All @@ -34,6 +36,7 @@
import org.openmetadata.service.migration.MigrationValidationClient;
import org.openmetadata.service.resources.settings.SettingsCache;
import org.openmetadata.service.secrets.masker.PasswordEntityMasker;
import org.openmetadata.service.security.OidcTokenValidity;

class SystemRepositoryPatchSettingTest {
private static final String SETTING_NAME = SettingsType.GLOSSARY_TERM_RELATION_SETTINGS.value();
Expand Down Expand Up @@ -301,6 +304,24 @@ void putEmailSettingPreservesOmittedPassword() {
assertEquals(PasswordEntityMasker.PASSWORD_MASK, responseConfig.getPassword());
}

@Test
void putAuthenticationSettingRejectsInvalidOidcTokenValidityAsBadRequest() {
Settings update =
new Settings()
.withConfigType(SettingsType.AUTHENTICATION_CONFIGURATION)
.withConfigValue(
new AuthenticationConfiguration()
.withOidcConfiguration(new OidcClientConfig().withTokenValidity(0)));

BadRequestException failure =
assertThrows(BadRequestException.class, () -> systemRepository.createOrUpdate(update));

assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), failure.getResponse().getStatus());
assertEquals(OidcTokenValidity.VALIDATION_MESSAGE, failure.getMessage());
verify(systemDAO, never()).insertSettings(anyString(), anyString());
settingsCacheMock.verifyNoInteractions();
}

private JsonPatch appendRelationTypePatch() {
return Json.createPatchBuilder()
.test("/relationTypes", Json.createArrayBuilder().build())
Expand Down
Loading
Loading