All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.microcks.operator.KeycloakHelper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The Microcks Authors.
 *
 * 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 io.github.microcks.operator;

import io.github.microcks.client.ApiClient;
import io.github.microcks.client.ApiException;
import io.github.microcks.client.KeycloakClient;
import io.github.microcks.client.api.ConfigApi;
import io.github.microcks.client.model.KeycloakConfig;
import io.github.microcks.operator.api.base.v1alpha1.Microcks;
import io.github.microcks.operator.base.resources.KeycloakConfigSecretDependentResource;
import io.github.microcks.operator.base.resources.KeycloakServiceDependentResource;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.jboss.logging.Logger;

import java.io.IOException;
import java.util.Base64;
import java.util.Iterator;
import java.util.Map;

/**
 * A helper class for retrieving a Keycloak Service Account information and then
 * authenticating to a Keycloak server.
 * @author laurent
 */
public class KeycloakHelper {

   /** Get a JBoss logging logger. */
   private final Logger logger = Logger.getLogger(getClass());

   private static final String KUBE_SVC_DNS_SUFFIX = ".svc.cluster.local";
   private static final String OIDC_TOKEN_ENDPOINT_SUFFIX = "/protocol/openid-connect/token";

   final KubernetesClient client;
   final ObjectMapper mapper;

   /**
    * Default constructor with injected Kubernetes client.
    * @param client A Kubernetes client for interacting with the cluster
    */
   public KeycloakHelper(KubernetesClient client) {
      this.client = client;
      this.mapper = new ObjectMapper();
   }

   /**
    * Retrive the Keycloak configuration from a Microcks instance.
    * @param completeMicrocks The Microcks instance to get Keycloak configuration from
    * @return KeyloakConfig instance
    * @throws ApiException If an error occurs while connecting to Microcks API
    */
   public KeycloakConfig getKeycloakConfig(Microcks completeMicrocks) throws ApiException {
      ApiClient apiClient = new ApiClient();
      apiClient.updateBaseUri("http://" + completeMicrocks.getMetadata().getName() + "."
            + completeMicrocks.getMetadata().getNamespace() + KUBE_SVC_DNS_SUFFIX + ":8080/api");
      logger.infof("Connecting to Microcks on '%s'", apiClient.getBaseUri());

      ConfigApi configApi = new ConfigApi(apiClient);
      return configApi.getKeycloakConfig();
   }

   /**
    * Get an OAuth token for a given resource metadata and Microcks instance. Depending on Keycloak config
    * and the target Microcks instance this will either use a service account secret or a default service account.
    * It will then authenticate to the Keycloak server token endpoint and return the OAuth token.
    * @param resourceMetadata The metadata of the resource that relates to a Microcks instance
    * @param completeMicrocks The Microcks instance that relates to the resource
    * @return An OAuth token than can be used as a bearer when calling Microcks API secured endpoints
    * @throws UnsatisfiedRequirementException If a configuration requirement is not satisfied
    * @throws ApiException If an error occurs while connecting to Microcks API
    * @throws IOException If the underlying http connection fails
    */
   public String getOAuthToken(ObjectMeta resourceMetadata, Microcks completeMicrocks)
         throws UnsatisfiedRequirementException, ApiException, IOException {
      KeycloakConfig keycloakConfig = getKeycloakConfig(completeMicrocks);

      String oauthToken;
      if (Boolean.TRUE.equals(keycloakConfig.getEnabled())) {
         ServiceAccountAndCredentials saAndCredentials = getServiceAccountAndCredentials(resourceMetadata, completeMicrocks);

         String keycloakEndpoint = getKeycloakEndpoint(completeMicrocks, keycloakConfig);
         logger.infof("Using keycloakEndpoints: %s", keycloakEndpoint);
         oauthToken = KeycloakClient.connectAndGetOAuthToken(
               saAndCredentials.getServiceAccountName(),
               saAndCredentials.getServiceAccountCredentials(),
               keycloakEndpoint);
         logger.info("Authentication to Keycloak server succeed!");

      } else {
         oauthToken = "";
         logger.info("Keycloak protection is not enabled, using a fake token");
      }

      return oauthToken;
   }

   /** */
   private ServiceAccountAndCredentials getServiceAccountAndCredentials(ObjectMeta resourceMetadata, Microcks completeMicrocks) throws UnsatisfiedRequirementException {
      Map annotations = resourceMetadata.getAnnotations();
      String serviceAccountSecret = annotations.get(MicrocksOperatorConfig.SERVICE_ACCOUNT_SECRET_SELECTOR);

      String serviceAccountName = KeycloakConfigSecretDependentResource.OPERATOR_SERVICE_ACCOUNT;
      String serviceAccountCredentials = null;

      if (serviceAccountSecret == null) {
         if (!completeMicrocks.getSpec().getKeycloak().isInstall()) {
            logger.errorf("No service account secret provided for accessing Keycloak from '%s'", resourceMetadata.getName());
            throw new UnsatisfiedRequirementException("No service account secret provided for accessing Keycloak. You must specify a "
                  + MicrocksOperatorConfig.SERVICE_ACCOUNT_SECRET_SELECTOR + " annotation.");
         }

         // We must read the service account secret from the Keycloak realm configuration.
         logger.infof("Using default service account secret to access Keycloak for '%s'", resourceMetadata.getName());
         logger.infof("Looking for '%s' secret in '%s' namespace", KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks),
               resourceMetadata.getNamespace());
         Secret saSecret = client.secrets().inNamespace(resourceMetadata.getNamespace())
               .withName(KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks)).get();

         if (saSecret == null) {
            logger.errorf("No '%s' secret found in '%s' namespace for getting Keycloak info",
                  KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks),
                  resourceMetadata.getName());
            throw new UnsatisfiedRequirementException("No '"
                  + KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks) + "' secret found in '"
                  + resourceMetadata.getName() + "' namespace for getting Keycloak info");
         }

         // Get base64 encoded realm config that needs to be decoded.
         String realmConfig = saSecret.getData().get(KeycloakConfigSecretDependentResource.REALM_CONFIG_KEY);
         realmConfig = new String(Base64.getDecoder().decode(realmConfig));

         serviceAccountCredentials = extractServiceAccountCredentialsFromRealmConfig(completeMicrocks, realmConfig);
      } else {
         // We must read the service account name and credentials from the provided secret.
         logger.infof("Using '%s' service account secret to access Keycloak '%s'", serviceAccountSecret, resourceMetadata.getName());
         Secret saSecret = client.secrets().inNamespace(resourceMetadata.getNamespace())
               .withName(serviceAccountSecret).get();

         serviceAccountName = saSecret.getStringData().get("service-account-name");
         serviceAccountCredentials = saSecret.getStringData().get("service-account-credentials");
      }

      return new ServiceAccountAndCredentials(serviceAccountName, serviceAccountCredentials);
   }

   /** */
   private String extractServiceAccountCredentialsFromRealmConfig(Microcks completeMicrocks, String realmConfig) throws UnsatisfiedRequirementException {
      String serviceAccountCredentials = null;

      JsonNode jsonRealmConfig;
      try {
         jsonRealmConfig = mapper.readTree(realmConfig);
      } catch (JsonProcessingException jpe) {
         logger.errorf("Error while parsing realm configuration: %s", jpe.getMessage());
         throw new UnsatisfiedRequirementException("Error while parsing '"
               + KeycloakConfigSecretDependentResource.REALM_CONFIG_KEY + "' in "
               + KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks));
      }
      if (jsonRealmConfig.has("applications")) {
         JsonNode applicationsNode = jsonRealmConfig.get("applications");
         Iterator applications = applicationsNode.elements();
         while (applications.hasNext()) {
            JsonNode application = applications.next();
            if (KeycloakConfigSecretDependentResource.OPERATOR_SERVICE_ACCOUNT.equals(application.path("name").asText())) {
               serviceAccountCredentials = application.get("secret").asText();
               break;
            }
         }
      }

      // Throw an exception if we didn't find the service account credentials.
      if (serviceAccountCredentials == null) {
         logger.errorf("%s does not contains secret for service account '%s'",
               KeycloakConfigSecretDependentResource.REALM_CONFIG_KEY,
               KeycloakConfigSecretDependentResource.OPERATOR_SERVICE_ACCOUNT);
         throw new UnsatisfiedRequirementException("No service account secret found in '"
               + KeycloakConfigSecretDependentResource.REALM_CONFIG_KEY + "' in "
               + KeycloakConfigSecretDependentResource.getSecretName(completeMicrocks));
      }
      return serviceAccountCredentials;
   }

   /** */
   private String getKeycloakEndpoint(Microcks completeCR, KeycloakConfig config) {
      if (completeCR.getSpec().getKeycloak().isInstall()) {
         return "http://" + KeycloakServiceDependentResource.getServiceName(completeCR) + "." + completeCR.getMetadata().getNamespace()
               + KUBE_SVC_DNS_SUFFIX + ":8080/realms/" + config.getRealm() + OIDC_TOKEN_ENDPOINT_SUFFIX;
      } else if (completeCR.getSpec().getKeycloak().getPrivateUrl() != null) {
         return completeCR.getSpec().getKeycloak().getPrivateUrl() + "/realms/"
               + config.getRealm() + OIDC_TOKEN_ENDPOINT_SUFFIX;
      }
      return config.getAuthServerUrl() + "/realms/" + config.getRealm() + OIDC_TOKEN_ENDPOINT_SUFFIX;
   }

   /** Thin wrapper class to hold service account name and credentials. */
   private static class ServiceAccountAndCredentials {
      final String serviceAccountName;
      final String serviceAccountCredentials;

      public ServiceAccountAndCredentials(String serviceAccountName, String serviceAccountCredentials) {
         this.serviceAccountName = serviceAccountName;
         this.serviceAccountCredentials = serviceAccountCredentials;
      }

      public String getServiceAccountName() {
         return serviceAccountName;
      }
      public String getServiceAccountCredentials() {
         return serviceAccountCredentials;
      }
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy