Add per-domain OAuth (Google, GitHub) provider support#12702
Add per-domain OAuth (Google, GitHub) provider support#12702Damans227 wants to merge 34 commits intoapache:mainfrom
Conversation
…entication checks
…nid' in columns and details
…nhance user verification
…ls for improved readability and consistency
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 16931 |
|
@blueorangutan package |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16993 |
…ize domain resolution in OAuth2AuthManager
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17012 |
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17016 |
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 17017 |
|
[SF] Trillian Build Failed (tid-15566) |
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 17019 |
|
[SF] Trillian Build Failed (tid-15568) |
|
[SF] Trillian Build Failed (tid-15569) |
There was a problem hiding this comment.
Pull request overview
Adds support for configuring OAuth providers (Google/GitHub) per domain, with global fallback, and wires domain context through the UI and backend authentication flow.
Changes:
- UI: Adds an “External” OAuth login tab with domain input and provider discovery.
- Backend: Adds
domain_idto OAuth providers, introduces domain-aware DAO lookups with global fallback, and resolves domain from API params for login/verification. - Tests/Schema: Updates unit tests for new method signatures and adds DB schema migration for per-domain providers.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/views/dashboard/VerifyOauth.vue | Sends domain context when verifying OAuth codes. |
| ui/src/views/auth/Login.vue | Adds OAuth tab w/ domain-based provider discovery and domain-aware OAuth URLs. |
| ui/src/config/section/config.js | Exposes domain fields and domainid arg in OAuth provider config UI. |
| plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java | Updates mocks for domain-aware provider lookup. |
| plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java | Updates stubbing for domain resolution and new verify method signature; adjusts authenticator setup test. |
| plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java | Refactors test setup and populates domain-related response fields. |
| plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java | Updates calls to domain-aware methods/signatures. |
| plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java | Updates tests for domain-scoped enablement and list/register changes. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java | Adds domainId field to provider entity. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java | Adds domain-aware verification and provider lookup w/ fallback. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java | Adds domain-aware verification and access-token retrieval. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java | Implements domain + global fallback queries and domain-inclusive listing. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java | Adds domain-aware DAO APIs. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java | Adds domain UUID/name fields in API response. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java | Adds domain params and resolves domainId before verification. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java | Returns domain info and checks enabled state in domain scope. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java | Accepts domainId and returns domain info in response. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java | Enforces OAuth enabled check in resolved domain scope. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java | Adds domain filtering/visibility logic and domain-aware enabled evaluation. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java | Uses domain-scoped enablement and domain-aware verification. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java | Adds domain resolution, domain-aware list/register logic, and domain-aware verification call. |
| plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java | Makes enablement domain-scoped and updates method signatures for domain awareness. |
| framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java | Adds strictScope option to avoid fallback when desired. |
| engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql | Adds domain_id to oauth_provider and uniqueness/indexing. |
| api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java | Adds domain-aware methods for verification and code-to-email. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| public OauthProviderVO findByProviderAndDomain(String provider, Long domainId) { | ||
| SearchCriteria<OauthProviderVO> sc = oauthProviderSearchByProviderAndDomain.create(); | ||
| sc.setParameters("provider", provider); | ||
|
|
||
| sc.setParameters("domainId", domainId); | ||
| return findOneBy(sc); | ||
| } |
There was a problem hiding this comment.
Using an EQ filter with a null domainId typically generates domain_id = NULL, which won’t match global rows (NULL) in SQL. This can break global provider lookup and allow duplicate global registrations. Consider handling domainId == null with an IS NULL criterion (or a separate SearchBuilder/query path) instead of setting an EQ parameter to null.
| ALTER TABLE `cloud`.`oauth_provider` ADD CONSTRAINT `fk_oauth_provider__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`); | ||
| ALTER TABLE `cloud`.`oauth_provider` ADD INDEX `i_oauth_provider__domain_id`(`domain_id`); | ||
|
|
||
| ALTER TABLE `cloud`.`oauth_provider` ADD UNIQUE KEY `uk_oauth_provider__provider_domain` (`provider`, `domain_id`); |
There was a problem hiding this comment.
In MySQL, UNIQUE(provider, domain_id) does not prevent multiple rows where domain_id is NULL (NULLs are treated as distinct), so multiple 'global' providers with the same provider can still be inserted. If global uniqueness is required, consider a schema approach that normalizes NULL (e.g., a generated column like coalesce(domain_id, 0) with a unique index) or store a non-NULL sentinel for global and index that.
| ALTER TABLE `cloud`.`oauth_provider` ADD UNIQUE KEY `uk_oauth_provider__provider_domain` (`provider`, `domain_id`); | |
| ALTER TABLE `cloud`.`oauth_provider` ADD COLUMN `domain_id_norm` bigint unsigned AS (COALESCE(`domain_id`, 0)) STORED, ADD UNIQUE KEY `uk_oauth_provider__provider_domain` (`provider`, `domain_id_norm`); |
| protected boolean isOAuthPluginEnabled() { | ||
| return OAuth2IsPluginEnabled.value(); | ||
| protected boolean isOAuthPluginEnabled(Long domainId) { | ||
| return Boolean.TRUE.equals(OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainId, true)); |
There was a problem hiding this comment.
Passing strictScope = true disables fallback to parent/global scope (it returns null when no domain-scoped value exists). That contradicts the PR goal of 'global fallback' and can unexpectedly disable OAuth in domains that rely on global config. Consider using the default valueInScope(scope, id) (or strictScope = false) for enablement checks where fallback is desired.
| return Boolean.TRUE.equals(OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainId, true)); | |
| return Boolean.TRUE.equals(OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainId)); |
| private String provider; | ||
|
|
||
| @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, | ||
| description = "List OAuth providers for a specific domain. Use -1 for global providers only.") |
There was a problem hiding this comment.
The parameter is declared as CommandType.UUID but the description instructs using -1, which is not a UUID and may be rejected by request validation/parsing. Consider modeling the global-only filter via a separate boolean/string parameter (or making this parameter a STRING) rather than overloading a UUID-typed field with -1.
| description = "List OAuth providers for a specific domain. Use -1 for global providers only.") | |
| description = "List OAuth providers for a specific domain.") |
| int totalEnabledCount = responses.size(); | ||
| if (!domainRequested && !isAuthenticated) { | ||
| List<OauthProviderVO> allProviders = _oauth2mgr.listOauthProviders(null, null, null); | ||
| for (OauthProviderVO domainProvider : allProviders) { | ||
| if (domainProvider.getDomainId() != null && domainProvider.isEnabled() | ||
| && Boolean.TRUE.equals(OAuth2AuthManager.OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainProvider.getDomainId(), true)) | ||
| && authenticatorPluginNames.contains(domainProvider.getProvider())) { | ||
| totalEnabledCount++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ListResponse<OauthProviderResponse> response = new ListResponse<>(); | ||
| response.setResponses(responses, resultList.size()); | ||
| response.setResponses(responses, totalEnabledCount); |
There was a problem hiding this comment.
ListResponse#setResponses(items, count) typically expects count to reflect the number of returned items, not the number of enabled providers across other (non-returned) domains. Returning a count larger than responses.size() can break pagination/clients and is semantically confusing. If the UI needs 'total enabled providers across all domains', consider returning that as a separate field/metric rather than overloading count.
| try { | ||
| cmd.setAuthenticators(authenticators); | ||
| } catch (AssertionError e) { | ||
| // ComponentContext is not available in unit test environment | ||
| } |
There was a problem hiding this comment.
Catching and ignoring AssertionError can mask genuine failures and make tests flaky/less trustworthy. Prefer refactoring setAuthenticators (or the test setup) so it doesn’t depend on ComponentContext/assertions in unit tests, or explicitly skip the test with an assumption when the required context isn’t available.
| public Long resolveDomainId(Map<String, Object[]> params) { | ||
| final String[] domainIdArray = (String[])params.get(ApiConstants.DOMAIN_ID); | ||
| if (ArrayUtils.isNotEmpty(domainIdArray)) { | ||
| String domainUuid = domainIdArray[0]; | ||
| if (GLOBAL_DOMAIN_FILTER.equals(domainUuid)) { | ||
| return GLOBAL_DOMAIN_ID; | ||
| } | ||
| DomainVO domain = _domainDao.findByUuid(domainUuid); | ||
| if (Objects.nonNull(domain)) { | ||
| return domain.getId(); | ||
| } | ||
| } | ||
| final String[] domainArray = (String[])params.get(ApiConstants.DOMAIN); | ||
| if (ArrayUtils.isNotEmpty(domainArray)) { | ||
| String path = domainArray[0]; | ||
| if (StringUtils.isNotEmpty(path)) { | ||
| if (!path.startsWith("/")) { | ||
| path = "/" + path; | ||
| } | ||
| if (!path.endsWith("/")) { | ||
| path += "/"; | ||
| } | ||
| DomainVO domain = _domainDao.findDomainByPath(path); | ||
| if (Objects.nonNull(domain)) { | ||
| return domain.getId(); | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The new resolveDomainId logic adds non-trivial behavior (UUID lookup, special -1 handling, and domain-path normalization). Adding unit tests for these branches would help prevent regressions (e.g., path formatting and the -1 global filter behavior).
|
@blueorangutan package |
|
@Damans227 a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 17026 |
Description
Add per-domain OAuth provider support. Allows OAuth providers (Google, GitHub) to be configured at the domain level with global fallback.
Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
How Has This Been Tested?
Manual testing with GitHub OAuth provider configured at domain level and global level, verifying domain-specific lookup with global fallback.