From 0c39eda8d2dd02ff351b5d7387d52eda3918fdee Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 5 Dec 2018 09:32:53 -0200 Subject: [PATCH] [KECLOAK-8237] - Openshift Client Storage --- dependencies/server-all/pom.xml | 4 + .../feature-packs/server-feature-pack/pom.xml | 4 + .../openshift-restclient-java/main/module.xml | 34 + .../keycloak-services/main/module.xml | 3 + .../cache/infinispan/UserCacheSession.java | 2 +- .../keycloak/models/jpa/JpaUserProvider.java | 2 +- pom.xml | 10 + .../models/utils/KeycloakModelUtils.java | 7 +- .../AbstractReadOnlyClientScopeAdapter.java | 88 +++ .../java/org/keycloak/models/ClientModel.java | 11 + .../org/keycloak/models/ClientScopeModel.java | 2 - services/pom.xml | 4 + .../freemarker/model/ApplicationsBean.java | 62 +- .../managers/AuthenticationManager.java | 2 +- .../resources/LoginActionsService.java | 4 +- .../util/DefaultClientSessionContext.java | 2 +- .../OpenshiftClientStorageProvider.java | 88 +++ ...OpenshiftClientStorageProviderFactory.java | 147 ++++ .../openshift/OpenshiftSAClientAdapter.java | 477 ++++++++++++ ...torage.client.ClientStorageProviderFactory | 18 + .../openshift/OpenshiftClientStorageTest.java | 288 +++++++ .../openshift/client-storage/api-v1.json | 495 ++++++++++++ .../openshift/client-storage/api.json | 12 + .../client-storage/apis-response.json | 20 + .../client-storage/apis-route-v1.json | 37 + .../client-storage/namespace-default.json | 25 + .../client-storage/oapi-response.json | 12 + .../openshift/client-storage/oapi-v1.json | 732 ++++++++++++++++++ .../client-storage/openshift-version.json | 11 + .../client-storage/route-response.json | 44 ++ .../sa-oauth-redirect-reference.json | 15 + .../client-storage/sa-oauth-redirect-uri.json | 17 + .../openshift/client-storage/sa-system.json | 12 + .../account/messages/messages_en.properties | 6 + .../login/messages/messages_en.properties | 8 +- 35 files changed, 2673 insertions(+), 32 deletions(-) create mode 100644 distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/com/openshift/openshift-restclient-java/main/module.xml create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientScopeAdapter.java create mode 100644 services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProvider.java create mode 100644 services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/storage/openshift/OpenshiftSAClientAdapter.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api-v1.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-response.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-route-v1.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/namespace-default.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-response.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-v1.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/openshift-version.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/route-response.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-reference.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-uri.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-system.json diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 3d8477ded3..2fbc934299 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -446,6 +446,10 @@ guice no_aop + + com.openshift + openshift-restclient-java + diff --git a/distribution/feature-packs/server-feature-pack/pom.xml b/distribution/feature-packs/server-feature-pack/pom.xml index 3935c48016..b5d0e6762d 100644 --- a/distribution/feature-packs/server-feature-pack/pom.xml +++ b/distribution/feature-packs/server-feature-pack/pom.xml @@ -784,6 +784,10 @@ + + com.openshift + openshift-restclient-java + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/com/openshift/openshift-restclient-java/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/com/openshift/openshift-restclient-java/main/module.xml new file mode 100644 index 0000000000..7b1ffe138c --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/com/openshift/openshift-restclient-java/main/module.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index 055f30fa96..746c6f4609 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -44,6 +44,9 @@ + + + diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index c0bdb5ea69..7e4ddad5b4 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -718,7 +718,7 @@ public class UserCacheSession implements UserCache { consentModel.setLastUpdatedDate(cachedConsent.getLastUpdatedDate()); for (String clientScopeId : cachedConsent.getClientScopeIds()) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, clientScopeId); + ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, client, clientScopeId); if (clientScope != null) { consentModel.addGrantedClientScope(clientScope); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index f9d51b8088..96802df690 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -305,7 +305,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { Collection grantedClientScopeEntities = entity.getGrantedClientScopes(); if (grantedClientScopeEntities != null) { for (UserConsentClientScopeEntity grantedClientScope : grantedClientScopeEntities) { - ClientScopeModel grantedClientScopeModel = KeycloakModelUtils.findClientScopeById(realm, grantedClientScope.getScopeId()); + ClientScopeModel grantedClientScopeModel = KeycloakModelUtils.findClientScopeById(realm, client, grantedClientScope.getScopeId()); if (grantedClientScopeModel != null) { model.addGrantedClientScope(grantedClientScopeModel); } diff --git a/pom.xml b/pom.xml index 2b3dfbe6b8..695246471b 100755 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,9 @@ 7.11.0.Final + + 6.1.3.Final + 2.0.0-M21 1.0.0-M33 @@ -1172,6 +1175,13 @@ ${project.version} + + + com.openshift + openshift-restclient-java + ${version.com.openshift.openshift-restclient-java} + + org.keycloak keycloak-saml-as7-modules diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 1046d8dbd4..d62e262554 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -628,9 +628,14 @@ public final class KeycloakModelUtils { * Lookup clientScope OR client by id. Method is useful if you know just ID, but you don't know * if underlying model is clientScope or client */ - public static ClientScopeModel findClientScopeById(RealmModel realm, String clientScopeId) { + public static ClientScopeModel findClientScopeById(RealmModel realm, ClientModel client, String clientScopeId) { ClientScopeModel clientScope = realm.getClientScopeById(clientScopeId); + if (clientScope == null) { + // as fallback we try to resolve dynamic scopes + clientScope = client.getDynamicClientScope(clientScopeId); + } + if (clientScope != null) { return clientScope; } else { diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientScopeAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientScopeAdapter.java new file mode 100644 index 0000000000..b459130236 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientScopeAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.storage.client; + +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RoleModel; + +public abstract class AbstractReadOnlyClientScopeAdapter implements ClientScopeModel { + + @Override + public void setName(String name) { + + } + + @Override + public void setDescription(String description) { + + } + + @Override + public void setProtocol(String protocol) { + + } + + @Override + public void setAttribute(String name, String value) { + + } + + @Override + public void removeAttribute(String name) { + + } + + @Override + public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) { + return null; + } + + @Override + public void removeProtocolMapper(ProtocolMapperModel mapping) { + + } + + @Override + public void updateProtocolMapper(ProtocolMapperModel mapping) { + + } + + @Override + public void addScopeMapping(RoleModel role) { + + } + + @Override + public void deleteScopeMapping(RoleModel role) { + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientScopeModel)) return false; + + ClientScopeModel that = (ClientScopeModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 43e740ae19..1ffce08013 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java @@ -177,6 +177,17 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot */ Map getClientScopes(boolean defaultScope, boolean filterByProtocol); + /** + *

Returns a {@link ClientScopeModel} associated with this client. + * + *

This method is used as a fallback in order to let clients to resolve a {@code scope} dynamically which is not listed as default or optional scope when calling {@link #getClientScopes(boolean, boolean)}. + * + * @param scope the scope name + * @return the client scope + */ + default ClientScopeModel getDynamicClientScope(String scope) { + return null; + } /** * Time in seconds since epoc diff --git a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java index d40d0e01c2..441b591b23 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java @@ -92,6 +92,4 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon default void setIncludeInTokenScope(boolean includeInTokenScope) { setAttribute(INCLUDE_IN_TOKEN_SCOPE, String.valueOf(includeInTokenScope)); } - - } diff --git a/services/pom.xml b/services/pom.xml index 868565357d..b20a1e56ce 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -178,6 +178,10 @@ + + com.openshift + openshift-restclient-java + diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java index 625580d575..3a5aa86c1f 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java @@ -32,6 +32,7 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.storage.StorageId; import java.util.ArrayList; import java.util.HashSet; @@ -52,28 +53,18 @@ public class ApplicationsBean { Set offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user); - List realmClients = realm.getClients(); - for (ClientModel client : realmClients) { - // Don't show bearerOnly clients - if (client.isBearerOnly()) { - continue; - } - + for (ClientModel client : getApplications(session, realm, user)) { Set availableRoles = new HashSet<>(); - if (client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID) - || client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) { - if (!AdminPermissions.realms(session, realm, user).isAdmin()) continue; - } else { - // Construct scope parameter with all optional scopes to see all potentially available roles - Set allClientScopes = new HashSet<>(client.getClientScopes(true, true).values()); - allClientScopes.addAll(client.getClientScopes(false, true).values()); - allClientScopes.add(client); + // Construct scope parameter with all optional scopes to see all potentially available roles + Set allClientScopes = new HashSet<>(client.getClientScopes(true, true).values()); + allClientScopes.addAll(client.getClientScopes(false, true).values()); + allClientScopes.add(client); - availableRoles = TokenManager.getAccess(user, client, allClientScopes); - } - List realmRolesAvailable = new LinkedList(); - MultivaluedHashMap resourceRolesAvailable = new MultivaluedHashMap(); + availableRoles = TokenManager.getAccess(user, client, allClientScopes); + + List realmRolesAvailable = new LinkedList<>(); + MultivaluedHashMap resourceRolesAvailable = new MultivaluedHashMap<>(); processRoles(availableRoles, realmRolesAvailable, resourceRolesAvailable); List orderedScopes = new ArrayList<>(); @@ -94,12 +85,39 @@ public class ApplicationsBean { additionalGrants.add("${offlineToken}"); } - ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, client, - clientScopesGranted, additionalGrants); - applications.add(appEntry); + applications.add(new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, client, clientScopesGranted, additionalGrants)); } } + private Set getApplications(KeycloakSession session, RealmModel realm, UserModel user) { + Set clients = new HashSet<>(); + + for (ClientModel client : realm.getClients()) { + // Don't show bearerOnly clients + if (client.isBearerOnly()) { + continue; + } + + if (client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID) + || client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) { + if (!AdminPermissions.realms(session, realm, user).isAdmin()) continue; + } + + clients.add(client); + } + + List consents = session.users().getConsents(realm, user.getId()); + + for (UserConsentModel consent : consents) { + ClientModel client = consent.getClient(); + + if (!new StorageId(client.getId()).isLocal()) { + clients.add(client); + } + } + return clients; + } + private void processRoles(Set inputRoles, List realmRoles, MultivaluedHashMap clientRoles) { for (RoleModel role : inputRoles) { if (role.getContainer() instanceof RealmModel) { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index eac4cb7607..0406c441d6 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -988,7 +988,7 @@ public class AuthenticationManager { List clientScopesToDisplay = new LinkedList<>(); for (String clientScopeId : authSession.getClientScopes()) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, clientScopeId); + ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, authSession.getClient(), clientScopeId); if (clientScope == null || !clientScope.isDisplayOnConsentScreen()) { continue; diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index e955d3fdb5..44adc2d847 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -845,8 +845,8 @@ public class LoginActionsService { boolean updateConsentRequired = false; for (String clientScopeId : authSession.getClientScopes()) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, clientScopeId); - if (clientScope != null) { + ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, client, clientScopeId); + if (clientScope != null && clientScope.isDisplayOnConsentScreen()) { if (!grantedConsent.isClientScopeGranted(clientScope)) { grantedConsent.addGrantedClientScope(clientScope); updateConsentRequired = true; diff --git a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java index 74d14a6b9d..758b415692 100644 --- a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java +++ b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java @@ -199,7 +199,7 @@ public class DefaultClientSessionContext implements ClientSessionContext { private Set loadClientScopes() { Set clientScopes = new HashSet<>(); for (String scopeId : clientScopeIds) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(clientSession.getClient().getRealm(), scopeId); + ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(clientSession.getClient().getRealm(), getClientSession().getClient(), scopeId); if (clientScope != null) { if (isClientScopePermittedForUser(clientScope)) { clientScopes.add(clientScope); diff --git a/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProvider.java b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProvider.java new file mode 100644 index 0000000000..41ec8144d1 --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.storage.openshift; + +import java.util.regex.Matcher; + +import com.openshift.restclient.IClient; +import com.openshift.restclient.NotFoundException; +import com.openshift.restclient.model.IResource; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderModel; + +/** + * @author Pedro Igor + */ +public class OpenshiftClientStorageProvider implements ClientStorageProvider { + + private final KeycloakSession session; + private final ClientStorageProviderModel providerModel; + private final IClient client; + + OpenshiftClientStorageProvider(KeycloakSession session, ClientStorageProviderModel providerModel, IClient client) { + this.session = session; + this.providerModel = providerModel; + this.client = client; + } + + @Override + public ClientModel getClientById(String id, RealmModel realm) { + StorageId storageId = new StorageId(id); + if (!storageId.getProviderId().equals(providerModel.getId())) return null; + String clientId = storageId.getExternalId(); + return getClientByClientId(clientId, realm); + } + + @Override + public ClientModel getClientByClientId(String clientId, RealmModel realm) { + Matcher matcher = OpenshiftClientStorageProviderFactory.SERVICE_ACCOUNT_PATTERN.matcher(clientId); + IResource resource = null; + + if (matcher.matches()) { + resource = getServiceAccount(matcher.group(2), matcher.group(1)); + } else { + String defaultNamespace = providerModel.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_DEFAULT_NAMESPACE); + + if (defaultNamespace != null) { + resource = getServiceAccount(clientId, defaultNamespace); + } + } + + if (resource == null) { + return null; + } + + return new OpenshiftSAClientAdapter(clientId, resource, client, session, realm, providerModel); + } + + @Override + public void close() { + + } + + private IResource getServiceAccount(String name, String namespace) { + try { + return client.get("ServiceAccount", name, namespace); + } catch (NotFoundException nfe) { + return null; + } + } +} diff --git a/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProviderFactory.java b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProviderFactory.java new file mode 100644 index 0000000000..59e05884ac --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftClientStorageProviderFactory.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.storage.openshift; + +import static org.keycloak.storage.CacheableStorageProviderModel.CACHE_POLICY; + +import java.util.List; +import java.util.regex.Pattern; + +import com.openshift.restclient.ClientBuilder; +import com.openshift.restclient.IClient; +import org.keycloak.common.Profile; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.storage.CacheableStorageProviderModel; +import org.keycloak.storage.client.ClientStorageProviderFactory; +import org.keycloak.storage.client.ClientStorageProviderModel; + +/** + * @author Pedro Igor + */ +public class OpenshiftClientStorageProviderFactory implements ClientStorageProviderFactory, EnvironmentDependentProviderFactory { + + public static final String PROVIDER_ID = "openshift-oauth-client"; + static final Pattern SERVICE_ACCOUNT_PATTERN = Pattern.compile("system:serviceaccount:([^:]+):([^:]+)"); + public static final String CONFIG_PROPERTY_ACCESS_TOKEN = "openshift.access_token"; + public static final String CONFIG_PROPERTY_OPENSHIFT_URI = "openshift.uri"; + public static final String CONFIG_PROPERTY_DEFAULT_NAMESPACE = "openshift.namespace.default"; + public static final String CONFIG_PROPERTY_REQUIRE_USER_CONSENT = "user.consent.require"; + public static final String CONFIG_PROPERTY_DISPLAY_SCOPE_CONSENT_TEXT= "user.consent.scope.consent.text"; + + private final List CONFIG_PROPERTIES; + private IClient client; + + public OpenshiftClientStorageProviderFactory() { + CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() + .property().name(CONFIG_PROPERTY_ACCESS_TOKEN) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Access Token") + .helpText("Bearer token that will be used to invoke on Openshift api server. Must have privilege to lookup oauth clients, service accounts, and invoke on token review interface") + .add() + .property().name(CONFIG_PROPERTY_OPENSHIFT_URI) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Openshift URL") + .helpText("Openshift api server URL base endpoint.") + .add() + .property().name(CONFIG_PROPERTY_DEFAULT_NAMESPACE) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Default Namespace") + .helpText("The default namespace to use when the server is not able to resolve the namespace from the client identifier. Useful when clients in Openshift don't have names with the following pattern: " + SERVICE_ACCOUNT_PATTERN.pattern()) + .add() + .property().name(CONFIG_PROPERTY_REQUIRE_USER_CONSENT) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .defaultValue("true") + .label("Require User Consent") + .helpText("If set to true, clients from this storage will ask the end-user for any scope requested during the authorization flow") + .add() + .property().name(CONFIG_PROPERTY_DISPLAY_SCOPE_CONSENT_TEXT) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .defaultValue("true") + .label("Display Scopes Consent Text") + .helpText("If set to true, the consent page will display texts from the message bundle for scopes. Otherwise, the scope name will be displayed.") + .add() + .build(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public OpenshiftClientStorageProvider create(KeycloakSession session, ComponentModel model) { + ClientStorageProviderModel providerModel = createProviderModel(model); + IClient client = getClient(providerModel); + + if (client != null) { + return new OpenshiftClientStorageProvider(session, providerModel, client); + } + + client.getAuthorizationContext().setToken(providerModel.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_ACCESS_TOKEN)); + + return new OpenshiftClientStorageProvider(session, providerModel, client); + } + + @Override + public String getHelpText() { + return "Openshift OAuth Client Adapter"; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException { + config.getConfig().putSingle(CACHE_POLICY, CacheableStorageProviderModel.CachePolicy.NO_CACHE.name()); + } + + @Override + public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) { + if (!oldModel.get(CONFIG_PROPERTY_OPENSHIFT_URI).equals(newModel.get(CONFIG_PROPERTY_OPENSHIFT_URI))) { + client = null; + } else { + getClient(createProviderModel(newModel)).getAuthorizationContext().setToken(newModel.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_ACCESS_TOKEN)); + } + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.OPENSHIFT_INTEGRATION); + } + + private IClient getClient(ClientStorageProviderModel providerModel) { + synchronized (this) { + if (client == null) { + client = new ClientBuilder(providerModel.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_OPENSHIFT_URI)).build(); + } + } + + return client; + } + + private ClientStorageProviderModel createProviderModel(ComponentModel model) { + return new ClientStorageProviderModel(model); + } +} diff --git a/services/src/main/java/org/keycloak/storage/openshift/OpenshiftSAClientAdapter.java b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftSAClientAdapter.java new file mode 100644 index 0000000000..d8748fbb87 --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/openshift/OpenshiftSAClientAdapter.java @@ -0,0 +1,477 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.storage.openshift; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.openshift.restclient.IClient; +import com.openshift.restclient.model.IResource; +import com.openshift.restclient.model.route.IRoute; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; +import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.storage.client.AbstractReadOnlyClientScopeAdapter; +import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter; +import org.keycloak.storage.client.ClientStorageProviderModel; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public final class OpenshiftSAClientAdapter extends AbstractReadOnlyClientStorageAdapter { + + private static final String ANNOTATION_OAUTH_REDIRECT_URI = "serviceaccounts.openshift.io/oauth-redirecturi"; + private static final String ANNOTATION_OAUTH_REDIRECT_REFERENCE = "serviceaccounts.openshift.io/oauth-redirectreference"; + private static final Pattern ROLE_SCOPE_PATTERN = Pattern.compile("role:([^:]+):([^:!]+)(:[!])?"); + private static final Set OPTIONAL_SCOPES = Stream.of("user:info", "user:check-access").collect(Collectors.toSet()); + private static final Set DEFAULT_PROTOCOL_MAPPERS = createDefaultProtocolMappers(); + + private static Set createDefaultProtocolMappers() { + Set mappers = new HashSet<>(); + + ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper("username", "username", "preferred_username", "string", true, true, UserPropertyMapper.PROVIDER_ID); + + mapper.setId(KeycloakModelUtils.generateId()); + + mappers.add(mapper); + + return mappers; + } + + private final IResource resource; + private final String clientId; + private final IClient client; + private final ClientRepresentation defaultConfig = new ClientRepresentation(); + + public OpenshiftSAClientAdapter(String clientId, IResource resource, IClient client, KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) { + super(session, realm, component); + this.resource = resource; + this.clientId = clientId; + this.client = client; + } + + @Override + public String getClientId() { + return clientId; + } + + @Override + public String getName() { + return resource.getName(); + } + + @Override + public String getDescription() { + return getConfigOrDefault(() -> defaultConfig.getDescription(), defaultConfig::setDescription, new StringBuilder().append(resource.getKind()).append(" ").append(resource.getName()).append(" from namespace ").append(resource.getNamespace().getName()).toString()); + } + + @Override + public boolean isEnabled() { + return getConfigOrDefault(() -> defaultConfig.isEnabled(), defaultConfig::setEnabled, true); + } + + @Override + public Set getWebOrigins() { + return new HashSet<>(getConfigOrDefault(() -> defaultConfig.getWebOrigins(), defaultConfig::setWebOrigins, Collections.emptyList())); + } + + @Override + public Set getRedirectUris() { + return new HashSet<>(getConfigOrDefault((Supplier>) () -> defaultConfig.getRedirectUris(), + uris -> defaultConfig.setRedirectUris(uris), + (Supplier>) () -> resource.getAnnotations().entrySet().stream() + .filter((entry) -> entry.getKey().startsWith(ANNOTATION_OAUTH_REDIRECT_URI) || entry.getKey().startsWith(ANNOTATION_OAUTH_REDIRECT_REFERENCE)) + .map(entry -> { + if (entry.getKey().startsWith(ANNOTATION_OAUTH_REDIRECT_URI)) { + return entry.getValue(); + } else { + Map values; + + try { + values = JsonSerialization.readValue(entry.getValue(), Map.class); + } catch (IOException e) { + throw new RuntimeException("Failed to parse annotation [" + ANNOTATION_OAUTH_REDIRECT_REFERENCE + "]", e); + } + + Map reference = (Map) values.get("reference"); + String kind = (String) reference.get("kind"); + + if (!"Route".equals(kind)) { + throw new IllegalArgumentException("Only route references are supported for " + ANNOTATION_OAUTH_REDIRECT_REFERENCE); + } + + String name = (String) reference.get("name"); + IRoute route = client.get(kind, name, resource.getNamespace().getName()); + + StringBuilder url = new StringBuilder(route.getURL()); + + if (url.charAt(url.length() - 1) != '/') { + url.append('/'); + } + + return url.append('*').toString(); + } + }).collect(Collectors.toList()))); + } + + @Override + public String getManagementUrl() { + return null; + } + + @Override + public String getRootUrl() { + return null; + } + + @Override + public String getBaseUrl() { + return null; + } + + @Override + public boolean isBearerOnly() { + return false; + } + + @Override + public int getNodeReRegistrationTimeout() { + return 0; + } + + @Override + public String getClientAuthenticatorType() { + return null; + } + + @Override + public boolean validateSecret(String secret) { + //TODO: do we want SAs as confidential clients and enable client credentials grant and resource owner grant ? + return false; + } + + @Override + public String getSecret() { + //TODO: check if validate secret is enough, don't see a reason to return SAs secret + return null; + } + + @Override + public String getRegistrationToken() { + return null; + } + + @Override + public String getProtocol() { + //TODO: set login protocol, always oidc + return OIDCLoginProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public String getAuthenticationFlowBindingOverride(String binding) { + return null; + } + + @Override + public Map getAuthenticationFlowBindingOverrides() { + return Collections.emptyMap(); + } + + @Override + public boolean isFrontchannelLogout() { + return false; + } + + @Override + public boolean isFullScopeAllowed() { + return false; + } + + @Override + public boolean isPublicClient() { + return true; + } + + @Override + public boolean isConsentRequired() { + return component.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, true); + } + + @Override + public boolean isDisplayOnConsentScreen() { + return false; + } + + @Override + public boolean isStandardFlowEnabled() { + return true; + } + + @Override + public boolean isImplicitFlowEnabled() { + return false; + } + + @Override + public boolean isDirectAccessGrantsEnabled() { + return false; + } + + @Override + public boolean isServiceAccountsEnabled() { + return false; + } + + @Override + public Map getClientScopes(boolean defaultScope, boolean filterByProtocol) { + if (defaultScope) { + return Collections.emptyMap(); + } + + Map scopes = new HashMap<>(); + + for (String scope : OPTIONAL_SCOPES) { + scopes.put(scope, createClientScope(scope)); + } + + return scopes; + } + + @Override + public ClientScopeModel getDynamicClientScope(String scope) { + if (OPTIONAL_SCOPES.contains(scope)) { + return createClientScope(scope); + } + + Matcher matcher = ROLE_SCOPE_PATTERN.matcher(scope); + + if (matcher.matches()) { + String namespace = matcher.group(2); + + if (resource.getNamespace().getName().equals(namespace)) { + return createClientScope(scope); + } + } + + return null; + } + + @Override + public int getNotBefore() { + return 0; + } + + @Override + public Set getProtocolMappers() { + return getConfigOrDefault(() -> { + List mappers = defaultConfig.getProtocolMappers(); + + if (mappers == null) { + return null; + } + + Set model = new HashSet<>(); + + for (ProtocolMapperRepresentation mapper : mappers) { + model.add(RepresentationToModel.toModel(mapper)); + } + + return model; + }, (Consumer>) mappers -> { + defaultConfig.setProtocolMappers(mappers.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList())); + }, (Supplier>) () -> DEFAULT_PROTOCOL_MAPPERS); + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + return getProtocolMappers().stream().filter(protocolMapperModel -> id.equals(protocolMapperModel.getId())).findAny().get(); + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + return getProtocolMappers().stream().filter(protocolMapperModel -> name.equals(protocolMapperModel.getName())).findAny().get(); + } + + @Override + public Set getScopeMappings() { + return Collections.emptySet(); + } + + @Override + public Set getRealmScopeMappings() { + return Collections.emptySet(); + } + + @Override + public boolean hasScope(RoleModel role) { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientModel)) return false; + + ClientModel that = (ClientModel) o; + return that.getId().equals(getId()); + } + + private V getConfigOrDefault(Supplier valueSupplier, Consumer valueConsumer, Supplier defaultValue) { + V value = valueSupplier.get(); + + if (value != null) { + return value; + } + + value = defaultValue.get(); + + if (valueConsumer != null) { + valueConsumer.accept(value); + } + + return value; + } + + private V getConfigOrDefault(Supplier valueSupplier, Consumer valueConsumer, V defaultValue) { + return getConfigOrDefault(valueSupplier, valueConsumer, (Supplier) () -> defaultValue); + } + + private ClientScopeModel createClientScope(String scope) { + ClientScopeModel managedScope = realm.getClientScopes().stream().filter(scopeModel -> scopeModel.getName().equals(scope)) + .findAny().orElse(null); + + if (managedScope != null) { + return managedScope; + } + + Map attributes = new HashMap<>(); + + attributes.put(ClientScopeModel.DISPLAY_ON_CONSENT_SCREEN, Boolean.valueOf(isConsentRequired()).toString()); + + if (component.get(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_DISPLAY_SCOPE_CONSENT_TEXT, Boolean.TRUE)) { + StringBuilder consentText = new StringBuilder("${openshift.scope."); + + if (scope.indexOf(':') != -1) { + consentText.append(scope.replaceFirst(":", "_")); + } + + attributes.put(ClientScopeModel.CONSENT_SCREEN_TEXT, consentText.append("}").toString()); + } else { + attributes.put(ClientScopeModel.CONSENT_SCREEN_TEXT, scope); + } + + return new AbstractReadOnlyClientScopeAdapter() { + @Override + public String getId() { + return scope; + } + + @Override + public String getName() { + return scope; + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public String getDescription() { + return scope; + } + + @Override + public String getProtocol() { + return OIDCLoginProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Set getProtocolMappers() { + return DEFAULT_PROTOCOL_MAPPERS; + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + return null; + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + return null; + } + + @Override + public Set getScopeMappings() { + return Collections.emptySet(); + } + + @Override + public Set getRealmScopeMappings() { + return Collections.emptySet(); + } + + @Override + public boolean hasScope(RoleModel role) { + return false; + } + }; + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory new file mode 100644 index 0000000000..1ac631b9cb --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory @@ -0,0 +1,18 @@ +# +# * Copyright 2018 Red Hat, Inc. and/or its affiliates +# * and other contributors as indicated by the @author tags. +# * +# * 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. +# + +org.keycloak.storage.openshift.OpenshiftClientStorageProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java new file mode 100644 index 0000000000..b038102cb2 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java @@ -0,0 +1,288 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.keycloak.testsuite.openshift; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.keycloak.common.Profile.Feature.OPENSHIFT_INTEGRATION; +import static org.keycloak.testsuite.ProfileAssume.assumeFeatureEnabled; +import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; + +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.util.Arrays; + +import io.undertow.Undertow; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.admin.client.resource.ComponentResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.common.util.StreamUtil; +import org.keycloak.events.Details; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.openshift.OpenshiftClientStorageProviderFactory; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ConsentPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * Test that clients can override auth flows + * + * @author Pedro Igor + */ +public final class OpenshiftClientStorageTest extends AbstractTestRealmKeycloakTest { + + private static Undertow OPENSHIFT_API_SERVER; + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + private LoginPage loginPage; + + @Page + private AppPage appPage; + + @Page + private ConsentPage consentPage; + + @Page + private ErrorPage errorPage; + + private String userId; + private String clientStorageId; + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(UserResource.class) + .addPackages(true, "org.keycloak.testsuite"); + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @BeforeClass + public static void onBeforeClass() { + OPENSHIFT_API_SERVER = Undertow.builder().addHttpListener(8880, "localhost", new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + String uri = exchange.getRequestURI(); + + if (uri.endsWith("/version/openshift") || uri.endsWith("/version")) { + writeResponse("openshift-version.json", exchange); + } else if (uri.endsWith("/oapi")) { + writeResponse("oapi-response.json", exchange); + } else if (uri.endsWith("/apis")) { + writeResponse("apis-response.json", exchange); + } else if (uri.endsWith("/api")) { + writeResponse("api.json", exchange); + } else if (uri.endsWith("/api/v1")) { + writeResponse("api-v1.json", exchange); + } else if (uri.endsWith("/oapi/v1")) { + writeResponse("oapi-v1.json", exchange); + } else if (uri.contains("/apis/route.openshift.io/v1")) { + writeResponse("apis-route-v1.json", exchange); + } else if (uri.endsWith("/api/v1/namespaces/default")) { + writeResponse("namespace-default.json", exchange); + } else if (uri.endsWith("/oapi/v1/namespaces/default/routes/proxy")) { + writeResponse("route-response.json", exchange); + } else if (uri.contains("/serviceaccounts/system")) { + writeResponse("sa-system.json", exchange); + } else if (uri.contains("/serviceaccounts/")) { + writeResponse(uri.substring(uri.lastIndexOf('/') + 1) + ".json", exchange); + } + } + + private void writeResponse(String file, HttpServerExchange exchange) throws IOException { + exchange.getResponseSender().send(StreamUtil.readString(getClass().getResourceAsStream("/openshift/client-storage/" + file))); + } + }).build(); + + OPENSHIFT_API_SERVER.start(); + } + + @AfterClass + public static void onAfterClass() { + OPENSHIFT_API_SERVER.stop(); + } + + @Before + public void onBefore() { + assumeFeatureEnabled(OPENSHIFT_INTEGRATION); + ComponentRepresentation provider = new ComponentRepresentation(); + + provider.setName("openshift-client-storage"); + provider.setProviderId(OpenshiftClientStorageProviderFactory.PROVIDER_ID); + provider.setProviderType(ClientStorageProvider.class.getName()); + provider.setConfig(new MultivaluedHashMap<>()); + provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_OPENSHIFT_URI, "http://localhost:8880"); + provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_ACCESS_TOKEN, "token"); + provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_DEFAULT_NAMESPACE, "default"); + provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, "true"); + + Response resp = adminClient.realm("test").components().add(provider); + resp.close(); + clientStorageId = ApiUtil.getCreatedId(resp); + getCleanup().addComponentId(clientStorageId); + } + + @Before + public void clientConfiguration() { + userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(); + } + + @Test + public void testCodeGrantFlowWithServiceAccountUsingOAuthRedirectReference() { + String clientId = "system:serviceaccount:default:sa-oauth-redirect-reference"; + testCodeGrantFlow(clientId, "https://myapp.org/callback", () -> assertSuccessfulResponseWithoutConsent(clientId)); + } + + @Test + public void failCodeGrantFlowWithServiceAccountUsingOAuthRedirectReference() throws Exception { + testCodeGrantFlow("system:serviceaccount:default:sa-oauth-redirect-reference", "http://myapp.org/callback", () -> assertEquals(OAuthErrorException.INVALID_REDIRECT_URI, events.poll().getError())); + } + + @Test + public void testCodeGrantFlowWithServiceAccountUsingOAuthRedirectUri() { + String clientId = "system:serviceaccount:default:sa-oauth-redirect-uri"; + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId)); + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth/second", () -> assertSuccessfulResponseWithoutConsent(clientId)); + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth/third", () -> assertSuccessfulResponseWithoutConsent(clientId)); + } + + @Test + public void testCodeGrantFlowWithUserConsent() { + String clientId = "system:serviceaccount:default:sa-oauth-redirect-uri"; + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithConsent(clientId), "user:info user:check-access"); + + ComponentResource component = testRealm().components().component(clientStorageId); + ComponentRepresentation representation = component.toRepresentation(); + + representation.getConfig().put(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, Arrays.asList("false")); + component.update(representation); + + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId), "user:info user:check-access"); + + representation.getConfig().put(OpenshiftClientStorageProviderFactory.CONFIG_PROPERTY_REQUIRE_USER_CONSENT, Arrays.asList("true")); + component.update(representation); + + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithoutConsent(clientId, Details.CONSENT_VALUE_PERSISTED_CONSENT), "user:info user:check-access"); + + testRealm().users().get(userId).revokeConsent(clientId); + + testCodeGrantFlow(clientId, "http://localhost:8180/auth/realms/master/app/auth", () -> assertSuccessfulResponseWithConsent(clientId), "user:info user:check-access"); + } + + @Test + public void failCodeGrantFlowWithServiceAccountUsingOAuthRedirectUri() throws Exception { + testCodeGrantFlow("system:serviceaccount:default:sa-oauth-redirect-uri", "http://myapp.org/callback", () -> assertEquals(OAuthErrorException.INVALID_REDIRECT_URI, events.poll().getError())); + } + + private void testCodeGrantFlow(String clientId, String expectedRedirectUri, Runnable assertThat) { + testCodeGrantFlow(clientId, expectedRedirectUri, assertThat, null); + } + + private void testCodeGrantFlow(String clientId, String expectedRedirectUri, Runnable assertThat, String scope) { + if (scope != null) { + oauth.scope(scope); + } + oauth.clientId(clientId); + oauth.redirectUri(expectedRedirectUri); + driver.navigate().to(oauth.getLoginFormUrl()); + loginPage.assertCurrent(); + + try { + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + } catch (Exception ignore) { + + } + + assertThat.run(); + } + + private void assertSuccessfulResponseWithoutConsent(String clientId) { + assertSuccessfulResponseWithoutConsent(clientId, null); + } + + private void assertSuccessfulResponseWithoutConsent(String clientId, String consentDetail) { + AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().client(clientId).detail(Details.REDIRECT_URI, oauth.getRedirectUri()).detail(Details.USERNAME, "test-user@localhost"); + + if (consentDetail != null) { + expectedEvent.detail(Details.CONSENT, Details.CONSENT_VALUE_PERSISTED_CONSENT); + } + + expectedEvent.assertEvent(); + assertSuccessfulRedirect(); + } + + private void assertSuccessfulResponseWithConsent(String clientId) { + consentPage.assertCurrent(); + driver.getPageSource().contains("user:info"); + driver.getPageSource().contains("user:check-access"); + consentPage.confirm(); + events.expectLogin().client(clientId).detail(Details.REDIRECT_URI, oauth.getRedirectUri()).detail(Details.USERNAME, "test-user@localhost").detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED).assertEvent(); + assertSuccessfulRedirect("user:info", "user:check-access"); + } + + private void assertSuccessfulRedirect(String... expectedScopes) { + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, null); + String accessToken = tokenResponse.getAccessToken(); + Assert.assertNotNull(accessToken); + + try { + AccessToken token = new JWSInput(accessToken).readJsonContent(AccessToken.class); + + for (String expectedScope : expectedScopes) { + token.getScope().contains(expectedScope); + } + } catch (Exception e) { + fail("Failed to parse access token"); + e.printStackTrace(); + } + + Assert.assertNotNull(tokenResponse.getRefreshToken()); + oauth.doLogout(tokenResponse.getRefreshToken(), null); + events.clear(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api-v1.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api-v1.json new file mode 100644 index 0000000000..ca080238da --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api-v1.json @@ -0,0 +1,495 @@ +{ + "kind": "APIResourceList", + "groupVersion": "v1", + "resources": [ + { + "name": "bindings", + "singularName": "", + "namespaced": true, + "kind": "Binding", + "verbs": [ + "create" + ] + }, + { + "name": "componentstatuses", + "singularName": "", + "namespaced": false, + "kind": "ComponentStatus", + "verbs": [ + "get", + "list" + ], + "shortNames": [ + "cs" + ] + }, + { + "name": "configmaps", + "singularName": "", + "namespaced": true, + "kind": "ConfigMap", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "cm" + ] + }, + { + "name": "endpoints", + "singularName": "", + "namespaced": true, + "kind": "Endpoints", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ep" + ] + }, + { + "name": "events", + "singularName": "", + "namespaced": true, + "kind": "Event", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ev" + ] + }, + { + "name": "limitranges", + "singularName": "", + "namespaced": true, + "kind": "LimitRange", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "limits" + ] + }, + { + "name": "namespaces", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "ns" + ] + }, + { + "name": "namespaces/finalize", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "update" + ] + }, + { + "name": "namespaces/status", + "singularName": "", + "namespaced": false, + "kind": "Namespace", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "nodes", + "singularName": "", + "namespaced": false, + "kind": "Node", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "no" + ] + }, + { + "name": "nodes/proxy", + "singularName": "", + "namespaced": false, + "kind": "Node", + "verbs": [] + }, + { + "name": "nodes/status", + "singularName": "", + "namespaced": false, + "kind": "Node", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "persistentvolumeclaims", + "singularName": "", + "namespaced": true, + "kind": "PersistentVolumeClaim", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "pvc" + ] + }, + { + "name": "persistentvolumeclaims/status", + "singularName": "", + "namespaced": true, + "kind": "PersistentVolumeClaim", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "persistentvolumes", + "singularName": "", + "namespaced": false, + "kind": "PersistentVolume", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "pv" + ] + }, + { + "name": "persistentvolumes/status", + "singularName": "", + "namespaced": false, + "kind": "PersistentVolume", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "pods", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "po" + ], + "categories": [ + "all" + ] + }, + { + "name": "pods/attach", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [] + }, + { + "name": "pods/binding", + "singularName": "", + "namespaced": true, + "kind": "Binding", + "verbs": [ + "create" + ] + }, + { + "name": "pods/eviction", + "singularName": "", + "namespaced": true, + "group": "policy", + "version": "v1beta1", + "kind": "Eviction", + "verbs": [ + "create" + ] + }, + { + "name": "pods/exec", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [] + }, + { + "name": "pods/log", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "get" + ] + }, + { + "name": "pods/portforward", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [] + }, + { + "name": "pods/proxy", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [] + }, + { + "name": "pods/status", + "singularName": "", + "namespaced": true, + "kind": "Pod", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "podtemplates", + "singularName": "", + "namespaced": true, + "kind": "PodTemplate", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "replicationcontrollers", + "singularName": "", + "namespaced": true, + "kind": "ReplicationController", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "rc" + ], + "categories": [ + "all" + ] + }, + { + "name": "replicationcontrollers/scale", + "singularName": "", + "namespaced": true, + "group": "autoscaling", + "version": "v1", + "kind": "Scale", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "replicationcontrollers/status", + "singularName": "", + "namespaced": true, + "kind": "ReplicationController", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "resourcequotas", + "singularName": "", + "namespaced": true, + "kind": "ResourceQuota", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "quota" + ] + }, + { + "name": "resourcequotas/status", + "singularName": "", + "namespaced": true, + "kind": "ResourceQuota", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "secrets", + "singularName": "", + "namespaced": true, + "kind": "Secret", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "serviceaccounts", + "singularName": "", + "namespaced": true, + "kind": "ServiceAccount", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "sa" + ] + }, + { + "name": "services", + "singularName": "", + "namespaced": true, + "kind": "Service", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "svc" + ], + "categories": [ + "all" + ] + }, + { + "name": "services/proxy", + "singularName": "", + "namespaced": true, + "kind": "Service", + "verbs": [] + }, + { + "name": "services/status", + "singularName": "", + "namespaced": true, + "kind": "Service", + "verbs": [ + "get", + "patch", + "update" + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api.json new file mode 100644 index 0000000000..f266b12ec3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/api.json @@ -0,0 +1,12 @@ +{ + "kind": "APIVersions", + "versions": [ + "v1" + ], + "serverAddressByClientCIDRs": [ + { + "clientCIDR": "0.0.0.0/0", + "serverAddress": "localhost:8880" + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-response.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-response.json new file mode 100644 index 0000000000..2dacc20d1b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-response.json @@ -0,0 +1,20 @@ +{ + "kind": "APIGroupList", + "apiVersion": "v1", + "groups": [ + { + "name": "route.openshift.io", + "versions": [ + { + "groupVersion": "route.openshift.io/v1", + "version": "v1" + } + ], + "preferredVersion": { + "groupVersion": "route.openshift.io/v1", + "version": "v1" + }, + "serverAddressByClientCIDRs": null + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-route-v1.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-route-v1.json new file mode 100644 index 0000000000..5efef9a65c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/apis-route-v1.json @@ -0,0 +1,37 @@ +{ + "kind": "APIResourceList", + "apiVersion": "v1", + "groupVersion": "route.openshift.io/v1", + "resources": [ + { + "name": "routes", + "singularName": "", + "namespaced": true, + "kind": "Route", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "categories": [ + "all" + ] + }, + { + "name": "routes/status", + "singularName": "", + "namespaced": true, + "kind": "Route", + "verbs": [ + "get", + "patch", + "update" + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/namespace-default.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/namespace-default.json new file mode 100644 index 0000000000..2f81fcf09c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/namespace-default.json @@ -0,0 +1,25 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "default", + "selfLink": "/api/v1/namespaces/default", + "uid": "cb37acb7-d084-11e8-aea9-5254001e7d16", + "resourceVersion": "977", + "creationTimestamp": "2018-10-15T14:15:39Z", + "annotations": { + "openshift.io/sa.scc.mcs": "s0:c1,c0", + "openshift.io/sa.scc.supplemental-groups": "1000000000/10000", + "openshift.io/sa.scc.uid-range": "1000000000/10000" + } + }, + "spec": { + "finalizers": [ + "kubernetes", + "openshift.io/origin" + ] + }, + "status": { + "phase": "Active" + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-response.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-response.json new file mode 100644 index 0000000000..f802005ab9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-response.json @@ -0,0 +1,12 @@ +{ + "kind": "APIVersions", + "versions": [ + "v1" + ], + "serverAddressByClientCIDRs": [ + { + "clientCIDR": "0.0.0.0/0", + "serverAddress": "192.168.121.194:8443" + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-v1.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-v1.json new file mode 100644 index 0000000000..6f5e16a6b9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/oapi-v1.json @@ -0,0 +1,732 @@ +{ + "kind": "APIResourceList", + "groupVersion": "v1", + "resources": [ + { + "name": "appliedclusterresourcequotas", + "singularName": "", + "namespaced": true, + "kind": "AppliedClusterResourceQuota", + "verbs": [ + "get", + "list" + ] + }, + { + "name": "buildconfigs", + "singularName": "", + "namespaced": true, + "kind": "BuildConfig", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "bc" + ] + }, + { + "name": "buildconfigs/instantiate", + "singularName": "", + "namespaced": true, + "kind": "BuildRequest", + "verbs": [ + "create" + ] + }, + { + "name": "buildconfigs/instantiatebinary", + "singularName": "", + "namespaced": true, + "kind": "BinaryBuildRequestOptions", + "verbs": [] + }, + { + "name": "buildconfigs/webhooks", + "singularName": "", + "namespaced": true, + "kind": "Build", + "verbs": [] + }, + { + "name": "builds", + "singularName": "", + "namespaced": true, + "kind": "Build", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "builds/clone", + "singularName": "", + "namespaced": true, + "kind": "BuildRequest", + "verbs": [ + "create" + ] + }, + { + "name": "builds/details", + "singularName": "", + "namespaced": true, + "kind": "Build", + "verbs": [ + "update" + ] + }, + { + "name": "builds/log", + "singularName": "", + "namespaced": true, + "kind": "BuildLog", + "verbs": [ + "get" + ] + }, + { + "name": "clusternetworks", + "singularName": "", + "namespaced": false, + "kind": "ClusterNetwork", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "clusterresourcequotas", + "singularName": "", + "namespaced": false, + "kind": "ClusterResourceQuota", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "clusterquota" + ] + }, + { + "name": "clusterresourcequotas/status", + "singularName": "", + "namespaced": false, + "kind": "ClusterResourceQuota", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "clusterrolebindings", + "singularName": "", + "namespaced": false, + "kind": "ClusterRoleBinding", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update" + ] + }, + { + "name": "clusterroles", + "singularName": "", + "namespaced": false, + "kind": "ClusterRole", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update" + ] + }, + { + "name": "deploymentconfigs", + "singularName": "", + "namespaced": true, + "kind": "DeploymentConfig", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "dc" + ] + }, + { + "name": "deploymentconfigs/instantiate", + "singularName": "", + "namespaced": true, + "kind": "DeploymentRequest", + "verbs": [ + "create" + ] + }, + { + "name": "deploymentconfigs/log", + "singularName": "", + "namespaced": true, + "kind": "DeploymentLog", + "verbs": [ + "get" + ] + }, + { + "name": "deploymentconfigs/rollback", + "singularName": "", + "namespaced": true, + "kind": "DeploymentConfigRollback", + "verbs": [ + "create" + ] + }, + { + "name": "deploymentconfigs/scale", + "singularName": "", + "namespaced": true, + "group": "extensions", + "version": "v1beta1", + "kind": "Scale", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "deploymentconfigs/status", + "singularName": "", + "namespaced": true, + "kind": "DeploymentConfig", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "egressnetworkpolicies", + "singularName": "", + "namespaced": true, + "kind": "EgressNetworkPolicy", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "groups", + "singularName": "", + "namespaced": false, + "kind": "Group", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "hostsubnets", + "singularName": "", + "namespaced": false, + "kind": "HostSubnet", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "identities", + "singularName": "", + "namespaced": false, + "kind": "Identity", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "images", + "singularName": "", + "namespaced": false, + "kind": "Image", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "imagesignatures", + "singularName": "", + "namespaced": false, + "kind": "ImageSignature", + "verbs": [ + "create", + "delete" + ] + }, + { + "name": "imagestreamimages", + "singularName": "", + "namespaced": true, + "kind": "ImageStreamImage", + "verbs": [ + "get" + ], + "shortNames": [ + "isimage" + ] + }, + { + "name": "imagestreamimports", + "singularName": "", + "namespaced": true, + "kind": "ImageStreamImport", + "verbs": [ + "create" + ] + }, + { + "name": "imagestreammappings", + "singularName": "", + "namespaced": true, + "kind": "ImageStreamMapping", + "verbs": [ + "create" + ] + }, + { + "name": "imagestreams", + "singularName": "", + "namespaced": true, + "kind": "ImageStream", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ], + "shortNames": [ + "is" + ] + }, + { + "name": "imagestreams/secrets", + "singularName": "", + "namespaced": true, + "kind": "SecretList", + "verbs": [ + "get" + ] + }, + { + "name": "imagestreams/status", + "singularName": "", + "namespaced": true, + "kind": "ImageStream", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "imagestreamtags", + "singularName": "", + "namespaced": true, + "kind": "ImageStreamTag", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update" + ], + "shortNames": [ + "istag" + ] + }, + { + "name": "localresourceaccessreviews", + "singularName": "", + "namespaced": true, + "kind": "LocalResourceAccessReview", + "verbs": [ + "create" + ] + }, + { + "name": "localsubjectaccessreviews", + "singularName": "", + "namespaced": true, + "kind": "LocalSubjectAccessReview", + "verbs": [ + "create" + ] + }, + { + "name": "netnamespaces", + "singularName": "", + "namespaced": false, + "kind": "NetNamespace", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "oauthaccesstokens", + "singularName": "", + "namespaced": false, + "kind": "OAuthAccessToken", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "oauthauthorizetokens", + "singularName": "", + "namespaced": false, + "kind": "OAuthAuthorizeToken", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "oauthclientauthorizations", + "singularName": "", + "namespaced": false, + "kind": "OAuthClientAuthorization", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "oauthclients", + "singularName": "", + "namespaced": false, + "kind": "OAuthClient", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "podsecuritypolicyreviews", + "singularName": "", + "namespaced": true, + "kind": "PodSecurityPolicyReview", + "verbs": [ + "create" + ] + }, + { + "name": "podsecuritypolicyselfsubjectreviews", + "singularName": "", + "namespaced": true, + "kind": "PodSecurityPolicySelfSubjectReview", + "verbs": [ + "create" + ] + }, + { + "name": "podsecuritypolicysubjectreviews", + "singularName": "", + "namespaced": true, + "kind": "PodSecurityPolicySubjectReview", + "verbs": [ + "create" + ] + }, + { + "name": "processedtemplates", + "singularName": "", + "namespaced": true, + "kind": "Template", + "verbs": [ + "create" + ] + }, + { + "name": "projectrequests", + "singularName": "", + "namespaced": false, + "kind": "ProjectRequest", + "verbs": [ + "create", + "list" + ] + }, + { + "name": "projects", + "singularName": "", + "namespaced": false, + "kind": "Project", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "resourceaccessreviews", + "singularName": "", + "namespaced": true, + "kind": "ResourceAccessReview", + "verbs": [ + "create" + ] + }, + { + "name": "rolebindingrestrictions", + "singularName": "", + "namespaced": true, + "kind": "RoleBindingRestriction", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "rolebindings", + "singularName": "", + "namespaced": true, + "kind": "RoleBinding", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update" + ] + }, + { + "name": "roles", + "singularName": "", + "namespaced": true, + "kind": "Role", + "verbs": [ + "create", + "delete", + "get", + "list", + "patch", + "update" + ] + }, + { + "name": "routes", + "singularName": "", + "namespaced": true, + "kind": "Route", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "routes/status", + "singularName": "", + "namespaced": true, + "kind": "Route", + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "name": "selfsubjectrulesreviews", + "singularName": "", + "namespaced": true, + "kind": "SelfSubjectRulesReview", + "verbs": [ + "create" + ] + }, + { + "name": "subjectaccessreviews", + "singularName": "", + "namespaced": true, + "kind": "SubjectAccessReview", + "verbs": [ + "create" + ] + }, + { + "name": "subjectrulesreviews", + "singularName": "", + "namespaced": true, + "kind": "SubjectRulesReview", + "verbs": [ + "create" + ] + }, + { + "name": "templates", + "singularName": "", + "namespaced": true, + "kind": "Template", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "name": "useridentitymappings", + "singularName": "", + "namespaced": false, + "kind": "UserIdentityMapping", + "verbs": [ + "create", + "delete", + "get", + "patch", + "update" + ] + }, + { + "name": "users", + "singularName": "", + "namespaced": false, + "kind": "User", + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/openshift-version.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/openshift-version.json new file mode 100644 index 0000000000..aa6cd2776e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/openshift-version.json @@ -0,0 +1,11 @@ +{ + "major": "3", + "minor": "10+", + "gitVersion": "v3.10.0+2084755-68", + "gitCommit": "2084755", + "gitTreeState": "", + "buildDate": "2018-10-30T09:01:17Z", + "goVersion": "", + "compiler": "", + "platform": "" +}ssss \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/route-response.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/route-response.json new file mode 100644 index 0000000000..8810603fe0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/route-response.json @@ -0,0 +1,44 @@ +{ + "kind": "Route", + "apiVersion": "v1", + "metadata": { + "name": "proxy", + "namespace": "default", + "selfLink": "/oapi/v1/namespaces/default/routes/proxy", + "uid": "3bf12cd8-d14a-11e8-82c2-5254001e7d16", + "resourceVersion": "45934", + "creationTimestamp": "2018-10-16T13:48:59Z", + "annotations": { + "openshift.io/host.generated": "true" + } + }, + "spec": { + "host": "myapp.org", + "to": { + "kind": "Service", + "name": "proxy", + "weight": 100 + }, + "tls": { + "termination": "reencrypt", + "destinationCACertificate": "-----BEGIN COMMENT-----\nThis is an empty PEM file created to provide backwards compatibility\nfor reencrypt routes that have no destinationCACertificate. This \ncontent will only appear for routes accessed via /oapi/v1/routes.\n-----END COMMENT-----\n" + }, + "wildcardPolicy": "None" + }, + "status": { + "ingress": [ + { + "host": "myapp.org", + "routerName": "router", + "conditions": [ + { + "type": "Admitted", + "status": "True", + "lastTransitionTime": "2018-10-16T13:49:00Z" + } + ], + "wildcardPolicy": "None" + } + ] + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-reference.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-reference.json new file mode 100644 index 0000000000..56a52c49b5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-reference.json @@ -0,0 +1,15 @@ +{ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "proxy-with-redirect-reference", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/serviceaccounts/proxy", + "uid": "3befc25c-d14a-11e8-8666-5254001e7d16", + "resourceVersion": "205225", + "creationTimestamp": "2018-10-16T13: 48:59Z", + "annotations": { + "serviceaccounts.openshift.io/oauth-redirectreference.primary": "{\"kind\":\"OAuthRedirectReference\",\"apiVersion\":\"v1\",\"reference\":{\"kind\":\"Route\",\"name\":\"proxy\"}}" + } + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-uri.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-uri.json new file mode 100644 index 0000000000..afc9b8f601 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-oauth-redirect-uri.json @@ -0,0 +1,17 @@ +{ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "proxy-with-redirect-uri", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/serviceaccounts/proxy", + "uid": "3befc25c-d14a-11e8-8666-5254001e7d16", + "resourceVersion": "205225", + "creationTimestamp": "2018-10-16T13: 48:59Z", + "annotations": { + "serviceaccounts.openshift.io/oauth-redirecturi.first": "http://localhost:8180/auth/realms/master/app/auth", + "serviceaccounts.openshift.io/oauth-redirecturi.second": "http://localhost:8180/auth/realms/master/app/auth/second", + "serviceaccounts.openshift.io/oauth-redirecturi.third": "http://localhost:8180/auth/realms/master/app/auth/third" + } + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-system.json b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-system.json new file mode 100644 index 0000000000..e118fd7ff3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/openshift/client-storage/sa-system.json @@ -0,0 +1,12 @@ +{ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "system", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/serviceaccounts/proxy", + "uid": "3befc25c-d14a-11e8-8666-5254001e7d16", + "resourceVersion": "205225", + "creationTimestamp": "2018-10-16T13: 48:59Z" + } +} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 90c7f3389d..7d2d4acaa8 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -346,3 +346,9 @@ addTeam=Add team to share your resource with myPermissions=My Permissions waitingforApproval=Waiting for approval anyPermission=Any Permission + +# Openshift messages +openshift.scope.user_info=User information +openshift.scope.user_check-access=User access information +openshift.scope.user_full=Full Access +openshift.scope.list-projects=List projects \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 747ca5ca5d..7ab1421ed5 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -311,4 +311,10 @@ console-update-password=Update of your password is required. console-verify-email=You are required to verify your email address. An email has been sent to {0} that contains a verification code. Please enter this code into the input below. console-email-code=Email Code: console-accept-terms=Accept Terms? [y/n]: -console-accept=y \ No newline at end of file +console-accept=y + +# Openshift messages +openshift.scope.user_info=User information +openshift.scope.user_check-access=User access information +openshift.scope.user_full=Full Access +openshift.scope.list-projects=List projects \ No newline at end of file