From 27db71e9c2d972dc8e2238dac79f84d54ab8461a Mon Sep 17 00:00:00 2001 From: Andrea Vibelli Date: Tue, 17 Dec 2024 16:47:50 +0100 Subject: [PATCH] chore: separate Errata and Pyxis caches and use new config for svc principal --- .../sbom/config/features/FeatureConfig.java | 2 + .../features/KerberosServiceConfig.java | 47 +++ .../feature/sbom/errata/ErrataClient.java | 4 +- .../ReleaseAdvisoryEventsListener.java | 10 +- .../ErrataCachingKerberosClientSupport.java | 311 ++++++++++++++++++ .../ErrataKrb5ClientRequestFilter.java | 42 +++ .../sbom/kerberos/ErrataTicketCache.java | 65 ++++ ...=> PyxisCachingKerberosClientSupport.java} | 15 +- ...java => PyxisKrb5ClientRequestFilter.java} | 7 +- ...TicketCache.java => PyxisTicketCache.java} | 2 +- .../feature/sbom/pyxis/PyxisClient.java | 24 +- .../src/main/resources/application-dev.yaml | 4 + .../src/main/resources/application-prod.yaml | 4 + .../src/main/resources/application-test.yaml | 4 + service/src/main/resources/application.yaml | 4 + .../ReleaseAdvisoryEventsListenerTest.java | 9 +- 16 files changed, 528 insertions(+), 26 deletions(-) create mode 100644 service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/KerberosServiceConfig.java create mode 100644 service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataCachingKerberosClientSupport.java create mode 100644 service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataKrb5ClientRequestFilter.java create mode 100644 service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataTicketCache.java rename service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/{CachingKerberosClientSupport.java => PyxisCachingKerberosClientSupport.java} (95%) rename service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/{Krb5ClientRequestFilter.java => PyxisKrb5ClientRequestFilter.java} (88%) rename service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/{TicketCache.java => PyxisTicketCache.java} (98%) diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/FeatureConfig.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/FeatureConfig.java index b616969f9..f0e005f8c 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/FeatureConfig.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/FeatureConfig.java @@ -29,4 +29,6 @@ public interface FeatureConfig { UmbConfig umb(); + + KerberosServiceConfig kerberos(); } diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/KerberosServiceConfig.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/KerberosServiceConfig.java new file mode 100644 index 000000000..407ac2289 --- /dev/null +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/config/features/KerberosServiceConfig.java @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual 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.jboss.sbomer.service.feature.sbom.config.features; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * @author Andrea Vibelli + */ +@ApplicationScoped +@ConfigMapping(prefix = "sbomer.features.kerberos") +public interface KerberosServiceConfig { + + @WithDefault("false") + @WithName("enabled") + boolean isEnabled(); + + ErrataServiceConfig errata(); + + PyxisServiceConfig pyxis(); + + interface ErrataServiceConfig { + String servicePrincipalName(); + } + + interface PyxisServiceConfig { + String servicePrincipalName(); + } +} diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/ErrataClient.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/ErrataClient.java index 6fee1520e..fc7e66c9c 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/ErrataClient.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/ErrataClient.java @@ -37,7 +37,7 @@ import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataProduct; import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataRelease; import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataVariant; -import org.jboss.sbomer.service.feature.sbom.kerberos.Krb5ClientRequestFilter; +import org.jboss.sbomer.service.feature.sbom.kerberos.ErrataKrb5ClientRequestFilter; import io.quarkus.rest.client.reactive.ClientExceptionMapper; import io.smallrye.reactive.messaging.annotations.Blocking; @@ -60,7 +60,7 @@ @ClientHeaderParam(name = "User-Agent", value = "SBOMer") @RegisterRestClient(configKey = "errata") @Path("/api/v1") -@RegisterProvider(Krb5ClientRequestFilter.class) +@RegisterProvider(ErrataKrb5ClientRequestFilter.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface ErrataClient { diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/event/release/ReleaseAdvisoryEventsListener.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/event/release/ReleaseAdvisoryEventsListener.java index 452042551..28652d677 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/event/release/ReleaseAdvisoryEventsListener.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/errata/event/release/ReleaseAdvisoryEventsListener.java @@ -42,13 +42,11 @@ import org.jboss.sbomer.core.features.sbom.utils.SbomUtils; import org.jboss.sbomer.service.feature.sbom.errata.ErrataClient; import org.jboss.sbomer.service.feature.sbom.errata.dto.Errata; -import org.jboss.sbomer.service.feature.sbom.errata.dto.Errata.Details; import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataBuildList; import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataBuildList.BuildItem; import org.jboss.sbomer.service.feature.sbom.errata.dto.ErrataBuildList.ProductVersionEntry; import org.jboss.sbomer.service.feature.sbom.errata.event.AdvisoryEventUtils; import org.jboss.sbomer.service.feature.sbom.k8s.model.SbomGenerationStatus; -import org.jboss.sbomer.service.feature.sbom.model.RandomStringIdGenerator; import org.jboss.sbomer.service.feature.sbom.model.RequestEvent; import org.jboss.sbomer.service.feature.sbom.model.Sbom; import org.jboss.sbomer.service.feature.sbom.model.SbomGenerationRequest; @@ -117,7 +115,7 @@ public void onReleaseAdvisoryEvent(@ObservesAsync AdvisoryReleaseEvent event) { Map> advisoryBuildDetails = getAdvisoryBuildDetails( advisoryRequestConfig.getAdvisoryId()); V1Beta1RequestRecord advisoryManifestsRecord = sbomService - .searchLastSuccessfulAdvisoryRequestRecord(advisoryRequestConfig.getAdvisoryId()); + .searchLastSuccessfulAdvisoryRequestRecord(requestEvent.getId(), advisoryRequestConfig.getAdvisoryId()); if (erratum.getDetails().get().getContentTypes().contains("docker")) { @@ -491,7 +489,9 @@ private Map> mapProductVersionToCPEs( private List getRepositoriesDetails(String nvr) { - PyxisRepositoryDetails repositoriesDetails = pyxisClient.getRepositoriesDetails(nvr); + log.debug("Getting repositories details from Pyxis for NVR '{}'", nvr); + PyxisRepositoryDetails repositoriesDetails = pyxisClient + .getRepositoriesDetails(nvr, PyxisClient.REPOSITORIES_DETAILS_INCLUDES); return repositoriesDetails.getData() .stream() .flatMap(dataSection -> dataSection.getRepositories().stream()) @@ -515,7 +515,7 @@ private List getRepositoriesDetails(String nvr) { * accessible without authentication */ private PyxisRepository getRepository(String registry, String repository) { - return pyxisClient.getRepository(registry, repository); + return pyxisClient.getRepository(registry, repository, PyxisClient.REPOSITORIES_REGISTRY_INCLUDES); } } diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataCachingKerberosClientSupport.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataCachingKerberosClientSupport.java new file mode 100644 index 000000000..d8ea6384e --- /dev/null +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataCachingKerberosClientSupport.java @@ -0,0 +1,311 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual 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.jboss.sbomer.service.feature.sbom.kerberos; + +import static javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; + +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.jboss.sbomer.service.feature.sbom.config.features.KerberosServiceConfig; + +import io.quarkiverse.kerberos.client.KerberosCallbackHandler; +import io.quarkiverse.kerberos.client.KerberosClientConfig; +import io.quarkiverse.kerberos.client.UserPrincipalSubjectFactory; +import io.quarkus.runtime.configuration.ConfigurationException; + +@ApplicationScoped +@Slf4j +public class ErrataCachingKerberosClientSupport { + + private static final String KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + + // See http://oid-info.com/get/1.2.840.113554.1.2.2 + private static final String KERBEROS_OID = "1.2.840.113554.1.2.2"; + // See http://oid-info.com/get/1.3.6.1.5.5.2 + private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + + private static final String DEFAULT_LOGIN_CONTEXT_NAME = "KDC"; + + private final Instance callbackHandler; + + private final Instance userPrincipalSubjectFactory; + + private final KerberosClientConfig kerberosConfig; + + private final KerberosServiceConfig kerberosServiceConfig; + + private final String realKeytabPath; + + @Inject + ErrataTicketCache ticketCache; + + @Inject + public ErrataCachingKerberosClientSupport( + Instance callbackHandler, + Instance userPrincipalSubjectFactory, + KerberosClientConfig kerberosConfig, + KerberosServiceConfig kerberosServiceConfig) { + this.callbackHandler = callbackHandler; + this.userPrincipalSubjectFactory = userPrincipalSubjectFactory; + this.kerberosConfig = kerberosConfig; + this.kerberosServiceConfig = kerberosServiceConfig; + if (callbackHandler.isResolvable() && callbackHandler.isAmbiguous()) { + throw new IllegalStateException("Multiple " + KerberosCallbackHandler.class + " beans registered"); + } + if (userPrincipalSubjectFactory.isResolvable() && userPrincipalSubjectFactory.isAmbiguous()) { + throw new IllegalStateException("Multiple " + UserPrincipalSubjectFactory.class + " beans registered"); + } + String realKeytabPath = null; + if (kerberosConfig.keytabPath().isPresent()) { + URL keytabUrl = Thread.currentThread() + .getContextClassLoader() + .getResource(kerberosConfig.keytabPath().get()); + if (keytabUrl != null) { + realKeytabPath = keytabUrl.toString(); + } else { + Path filePath = Paths.get(kerberosConfig.keytabPath().get()); + if (Files.exists(filePath)) { + realKeytabPath = filePath.toAbsolutePath().toString(); + } + } + if (realKeytabPath == null) { + throw new ConfigurationException( + "Keytab file is not available at " + kerberosConfig.keytabPath().get()); + } + } + this.realKeytabPath = realKeytabPath; + } + + public String getServiceTicket() { + log.debug("Getting a new service ticket..."); + return getServiceTicket(getCompleteUserPrincipalName()); + } + + public String getServiceTicket(String completeUserPrincipalName) { + log.debug("Getting a new service ticket for user principal name '{}'...", completeUserPrincipalName); + try { + Subject userPrincipalSubject = getUserPrincipalSubject(completeUserPrincipalName); + if (userPrincipalSubject == null) { + log.debug("User Principal Subject is null"); + throw new RuntimeException(); + } + + return getServiceTicket(userPrincipalSubject, completeUserPrincipalName); + } catch (LoginException ex) { + log.debug("Login exception: %s", ex.getMessage()); + throw new RuntimeException(ex); + } + } + + public String getServiceTicket(Subject userPrincipalSubject, String completeUserPrincipalName) { + log.debug("Getting a new service ticket for user principal subject ..."); + + try { + return Subject.doAs(userPrincipalSubject, new PrivilegedExceptionAction() { + @Override + public String run() throws Exception { + /* + * The getNegotiateToken method calls context.initSecContext, which uses the stored KerberosTicket + * to create a token suitable for the Kerberos or SPNEGO negotiation protocol. The initSecContext + * does not necessarily contact the Kerberos server; it simply builds the token based on the cached + * ticket. context.initSecContext will reach out to the Kerberos server only if the ticket is + * missing or if a renewal is required. This means we don't need any further caching here. + */ + GSSContext context = createServiceContext(); + return getNegotiateToken(context, new byte[0]); + } + }); + } catch (PrivilegedActionException ex) { + Throwable ex2 = ex.getCause() != null ? ex.getCause() : ex; + log.debug("PrivilegedAction failure: %s", ex2.getMessage()); + throw new RuntimeException(ex); + } catch (Throwable ex) { + Throwable ex2 = ex.getCause() != null ? ex.getCause() : ex; + log.debug("Authentication failure: %s", ex2.getMessage()); + throw new RuntimeException(ex2); + } + } + + public Subject getUserPrincipalSubject() throws LoginException { + return getUserPrincipalSubject(getCompleteUserPrincipalName()); + } + + public Subject getUserPrincipalSubject(String completeUserPrincipalName) throws LoginException { + log.debug("Getting user principal subject for name '{}'...", completeUserPrincipalName); + if (userPrincipalSubjectFactory.isResolvable()) { + Subject subject = userPrincipalSubjectFactory.get().getSubjectForUserPrincipal(completeUserPrincipalName); + if (subject != null) { + return subject; + } + } + SubjectTgtPair cachedSubjectTgtPair = ticketCache.getTgt(completeUserPrincipalName); + if (cachedSubjectTgtPair != null) { + if (!cachedSubjectTgtPair.isExpired()) { + log.info( + "Subject for user principal name '{}' was found in the cache, reusing it!", + completeUserPrincipalName); + return cachedSubjectTgtPair.getSubject(); + } else { + log.info( + "Subject for user principal name '{}' was found in the cache but is expired, need to create a new one.", + completeUserPrincipalName); + ticketCache.invalidateTgt(completeUserPrincipalName); + } + } else { + log.info( + "Subject for user principal name '{}' was NOT found in the cache, creating a new one.", + completeUserPrincipalName); + } + + String loginContextName = kerberosConfig.loginContextName().orElse(DEFAULT_LOGIN_CONTEXT_NAME); + Configuration config = DEFAULT_LOGIN_CONTEXT_NAME.equals(loginContextName) + ? new DefaultJAASConfiguration(completeUserPrincipalName) + : null; + final LoginContext lc = new LoginContext( + loginContextName, + new Subject(), + // callback is not required if a keytab is used + getCallback(completeUserPrincipalName), + config); + lc.login(); + + Subject subject = lc.getSubject(); + ticketCache.cacheTgt(completeUserPrincipalName, subject); + return subject; + } + + public GSSContext createServiceContext() throws GSSException { + Oid oid = new Oid(kerberosConfig.useSpnegoOid() ? SPNEGO_OID : KERBEROS_OID); + GSSManager gssManager = GSSManager.getInstance(); + GSSName serverName = gssManager.createName(kerberosServiceConfig.errata().servicePrincipalName(), null); + return gssManager.createContext(serverName, oid, null, GSSContext.DEFAULT_LIFETIME); + } + + public String getNegotiateToken(GSSContext context, byte[] token) throws GSSException { + token = context.initSecContext(token, 0, token.length); + return Base64.getEncoder().encodeToString(token); + } + + protected CallbackHandler getCallback(String completeUserPrincipalName) { + if (callbackHandler.isResolvable()) { + return callbackHandler.get(); + } + if (kerberosConfig.userPrincipalPassword().isPresent()) { + return new UsernamePasswordCBH( + completeUserPrincipalName, + kerberosConfig.userPrincipalPassword().get().toCharArray()); + } + return null; + } + + private class DefaultJAASConfiguration extends Configuration { + String completeUserPrincipalName; + + public DefaultJAASConfiguration(String completeUserPrincipalName) { + this.completeUserPrincipalName = completeUserPrincipalName; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + if (!DEFAULT_LOGIN_CONTEXT_NAME.equals(name)) { + throw new IllegalArgumentException("Unexpected name '" + name + "'"); + } + // See + // https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/module/Krb5LoginModule.html + AppConfigurationEntry[] entries = new AppConfigurationEntry[1]; + Map options = new HashMap<>(); + if (kerberosConfig.debug()) { + options.put("debug", "true"); + } + // No need to refresh the krb5config file, won't be changed frequently + options.put("refreshKrb5Config", "false"); + options.put("storeKey", "true"); + options.put("isInitiator", "true"); + if (realKeytabPath != null) { + options.put("useKeyTab", "true"); + options.put("keyTab", realKeytabPath); + options.put("principal", completeUserPrincipalName); + } + entries[0] = new AppConfigurationEntry(KRB5_LOGIN_MODULE, REQUIRED, options); + + return entries; + } + + } + + private static class UsernamePasswordCBH implements CallbackHandler { + private final String username; + private final char[] password; + + private UsernamePasswordCBH(final String username, final char[] password) { + this.username = username; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback current : callbacks) { + if (current instanceof NameCallback) { + NameCallback ncb = (NameCallback) current; + ncb.setName(username); + } else if (current instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) current; + pcb.setPassword(password); + } else { + throw new UnsupportedCallbackException(current); + } + } + } + } + + protected String getCompleteUserPrincipalName() { + return kerberosConfig.userPrincipalName() + + (kerberosConfig.userPrincipalRealm().isPresent() ? "@" + kerberosConfig.userPrincipalRealm().get() + : ""); + } +} diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataKrb5ClientRequestFilter.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataKrb5ClientRequestFilter.java new file mode 100644 index 000000000..e625e82d6 --- /dev/null +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataKrb5ClientRequestFilter.java @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual 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.jboss.sbomer.service.feature.sbom.kerberos; + +import java.io.IOException; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ErrataKrb5ClientRequestFilter implements ClientRequestFilter { + + private static final String AUTHORIZATION = "Authorization"; + private static final String NEGOTIATE = "Negotiate"; + + @Inject + ErrataCachingKerberosClientSupport kerberosClientSupport; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + String serviceTicket = kerberosClientSupport.getServiceTicket(); + requestContext.getHeaders().add(AUTHORIZATION, NEGOTIATE + " " + serviceTicket); + } + +} \ No newline at end of file diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataTicketCache.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataTicketCache.java new file mode 100644 index 000000000..f9f4bf1a2 --- /dev/null +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/ErrataTicketCache.java @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual 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.jboss.sbomer.service.feature.sbom.kerberos; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosTicket; + +import jakarta.enterprise.context.ApplicationScoped; +import lombok.extern.slf4j.Slf4j; + +@ApplicationScoped +@Slf4j +public class ErrataTicketCache { + + // Ticket-Granting Ticket (TGT) Cache + private final ConcurrentMap tgtCache = new ConcurrentHashMap<>(); + + public SubjectTgtPair getTgt(String userPrincipal) { + SubjectTgtPair cached = tgtCache.get(userPrincipal); + if (cached != null) { + log.debug("Got Subject-TGT from the cache"); + } + return cached; + } + + public void invalidateTgt(String userPrincipal) { + tgtCache.remove(userPrincipal); + } + + public void cacheTgt(String userPrincipal, Subject subject) { + KerberosTicket ticket = extractTgtKerberosTicket(subject); + if (ticket != null) { + SubjectTgtPair pair = new SubjectTgtPair(ticket, subject); + tgtCache.put(userPrincipal, pair); + } + } + + public static KerberosTicket extractTgtKerberosTicket(Subject subject) { + for (KerberosTicket ticket : subject.getPrivateCredentials(KerberosTicket.class)) { + if (ticket.getServer().getName().startsWith("krbtgt")) { + return ticket; + } + } + return null; + } + +} \ No newline at end of file diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/CachingKerberosClientSupport.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisCachingKerberosClientSupport.java similarity index 95% rename from service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/CachingKerberosClientSupport.java rename to service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisCachingKerberosClientSupport.java index e604429dd..6dfa0bbc2 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/CachingKerberosClientSupport.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisCachingKerberosClientSupport.java @@ -51,6 +51,7 @@ import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; +import org.jboss.sbomer.service.feature.sbom.config.features.KerberosServiceConfig; import io.quarkiverse.kerberos.client.KerberosCallbackHandler; import io.quarkiverse.kerberos.client.KerberosClientConfig; @@ -59,7 +60,7 @@ @ApplicationScoped @Slf4j -public class CachingKerberosClientSupport { +public class PyxisCachingKerberosClientSupport { private static final String KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; @@ -76,19 +77,23 @@ public class CachingKerberosClientSupport { private final KerberosClientConfig kerberosConfig; + private final KerberosServiceConfig kerberosServiceConfig; + private final String realKeytabPath; @Inject - TicketCache ticketCache; + PyxisTicketCache ticketCache; @Inject - public CachingKerberosClientSupport( + public PyxisCachingKerberosClientSupport( Instance callbackHandler, Instance userPrincipalSubjectFactory, - KerberosClientConfig kerberosConfig) { + KerberosClientConfig kerberosConfig, + KerberosServiceConfig kerberosServiceConfig) { this.callbackHandler = callbackHandler; this.userPrincipalSubjectFactory = userPrincipalSubjectFactory; this.kerberosConfig = kerberosConfig; + this.kerberosServiceConfig = kerberosServiceConfig; if (callbackHandler.isResolvable() && callbackHandler.isAmbiguous()) { throw new IllegalStateException("Multiple " + KerberosCallbackHandler.class + " beans registered"); } @@ -217,7 +222,7 @@ public Subject getUserPrincipalSubject(String completeUserPrincipalName) throws public GSSContext createServiceContext() throws GSSException { Oid oid = new Oid(kerberosConfig.useSpnegoOid() ? SPNEGO_OID : KERBEROS_OID); GSSManager gssManager = GSSManager.getInstance(); - GSSName serverName = gssManager.createName(kerberosConfig.servicePrincipalName(), null); + GSSName serverName = gssManager.createName(kerberosServiceConfig.pyxis().servicePrincipalName(), null); return gssManager.createContext(serverName, oid, null, GSSContext.DEFAULT_LIFETIME); } diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/Krb5ClientRequestFilter.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisKrb5ClientRequestFilter.java similarity index 88% rename from service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/Krb5ClientRequestFilter.java rename to service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisKrb5ClientRequestFilter.java index 4cd536cc3..f34e4b5af 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/Krb5ClientRequestFilter.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisKrb5ClientRequestFilter.java @@ -22,19 +22,20 @@ import jakarta.inject.Inject; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; +import lombok.extern.slf4j.Slf4j; -public class Krb5ClientRequestFilter implements ClientRequestFilter { +@Slf4j +public class PyxisKrb5ClientRequestFilter implements ClientRequestFilter { private static final String AUTHORIZATION = "Authorization"; private static final String NEGOTIATE = "Negotiate"; @Inject - CachingKerberosClientSupport kerberosClientSupport; + PyxisCachingKerberosClientSupport kerberosClientSupport; @Override public void filter(ClientRequestContext requestContext) throws IOException { String serviceTicket = kerberosClientSupport.getServiceTicket(); requestContext.getHeaders().add(AUTHORIZATION, NEGOTIATE + " " + serviceTicket); } - } \ No newline at end of file diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/TicketCache.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisTicketCache.java similarity index 98% rename from service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/TicketCache.java rename to service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisTicketCache.java index 20b663259..add17155d 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/TicketCache.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/kerberos/PyxisTicketCache.java @@ -28,7 +28,7 @@ @ApplicationScoped @Slf4j -public class TicketCache { +public class PyxisTicketCache { // Ticket-Granting Ticket (TGT) Cache private final ConcurrentMap tgtCache = new ConcurrentHashMap<>(); diff --git a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/pyxis/PyxisClient.java b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/pyxis/PyxisClient.java index 5ec82e37d..4cf1f86cd 100644 --- a/service/src/main/java/org/jboss/sbomer/service/feature/sbom/pyxis/PyxisClient.java +++ b/service/src/main/java/org/jboss/sbomer/service/feature/sbom/pyxis/PyxisClient.java @@ -26,7 +26,7 @@ import org.jboss.sbomer.core.errors.ForbiddenException; import org.jboss.sbomer.core.errors.NotFoundException; import org.jboss.sbomer.core.errors.UnauthorizedException; -import org.jboss.sbomer.service.feature.sbom.kerberos.Krb5ClientRequestFilter; +import org.jboss.sbomer.service.feature.sbom.kerberos.PyxisKrb5ClientRequestFilter; import org.jboss.sbomer.service.feature.sbom.pyxis.dto.PyxisRepository; import org.jboss.sbomer.service.feature.sbom.pyxis.dto.PyxisRepositoryDetails; @@ -38,6 +38,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -48,20 +49,31 @@ @ClientHeaderParam(name = "User-Agent", value = "SBOMer") @RegisterRestClient(configKey = "pyxis") @Path("/v1") -@RegisterProvider(Krb5ClientRequestFilter.class) +@RegisterProvider(PyxisKrb5ClientRequestFilter.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface PyxisClient { + public static List REPOSITORIES_DETAILS_INCLUDES = List.of( + "data.repositories.registry", + "data.repositories.repository", + "data.repositories.tags", + "data.repositories.published"); + public static List REPOSITORIES_REGISTRY_INCLUDES = List + .of("_id", "registry", "repository", "requires_terms"); + @GET - @Path("/images/nvr/{nvr}?include=data.repositories.registry&include=data.repositories.repository&include=data.repositories.tags&include=data.repositories.published") - public PyxisRepositoryDetails getRepositoriesDetails(@PathParam("nvr") String nvr); + @Path("/images/nvr/{nvr}") + public PyxisRepositoryDetails getRepositoriesDetails( + @PathParam("nvr") String nvr, + @QueryParam("include") List includes); @GET - @Path("/repositories/registry/{registry}/repository/{repository}?include=_id&include=registry&include=repository&include=requires_terms") + @Path("/repositories/registry/{registry}/repository/{repository}") public PyxisRepository getRepository( @PathParam("registry") String registry, - @PathParam("repository") String repository); + @PathParam("repository") String repository, + @QueryParam("include") List includes); @ClientExceptionMapper @Blocking diff --git a/service/src/main/resources/application-dev.yaml b/service/src/main/resources/application-dev.yaml index d7ee9451a..bbf9dbe1f 100644 --- a/service/src/main/resources/application-dev.yaml +++ b/service/src/main/resources/application-dev.yaml @@ -91,3 +91,7 @@ sbomer: enabled: false kerberos: enabled: false + errata: + service-principal-name: errata-service-principal + pyxis: + service-principal-name: pyxis-service-principal diff --git a/service/src/main/resources/application-prod.yaml b/service/src/main/resources/application-prod.yaml index edd04d9aa..233a01c24 100644 --- a/service/src/main/resources/application-prod.yaml +++ b/service/src/main/resources/application-prod.yaml @@ -47,6 +47,10 @@ sbomer: enabled: true kerberos: enabled: true + errata: + service-principal-name: errata-service-principal + pyxis: + service-principal-name: pyxis-service-principal quarkus: oidc: diff --git a/service/src/main/resources/application-test.yaml b/service/src/main/resources/application-test.yaml index 09c5379e5..e24993f8d 100644 --- a/service/src/main/resources/application-test.yaml +++ b/service/src/main/resources/application-test.yaml @@ -81,3 +81,7 @@ sbomer: enabled: false kerberos: enabled: false + errata: + service-principal-name: errata-service-principal + pyxis: + service-principal-name: pyxis-service-principal diff --git a/service/src/main/resources/application.yaml b/service/src/main/resources/application.yaml index a7830f631..e33da8bea 100644 --- a/service/src/main/resources/application.yaml +++ b/service/src/main/resources/application.yaml @@ -192,6 +192,10 @@ sbomer: enabled: false kerberos: enabled: false + errata: + service-principal-name: errata-service-principal + pyxis: + service-principal-name: pyxis-service-principal controller: generation-request: diff --git a/service/src/test/java/org/jboss/sbomer/service/test/unit/feature/sbom/errata/ReleaseAdvisoryEventsListenerTest.java b/service/src/test/java/org/jboss/sbomer/service/test/unit/feature/sbom/errata/ReleaseAdvisoryEventsListenerTest.java index cbba99aa0..0b5fce1e1 100644 --- a/service/src/test/java/org/jboss/sbomer/service/test/unit/feature/sbom/errata/ReleaseAdvisoryEventsListenerTest.java +++ b/service/src/test/java/org/jboss/sbomer/service/test/unit/feature/sbom/errata/ReleaseAdvisoryEventsListenerTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -237,7 +238,7 @@ void testReleaseErrataWithSingleDockerBuild() throws IOException { when(statsService.getStats()) .thenReturn(Stats.builder().withVersion("ReleaseAdvisoryEventsListenerTest_1.0.0").build()); - when(pyxisClient.getRepositoriesDetails("ruby-25-container-1-260.1733408998")).thenReturn(repositoriesDetails); + when(pyxisClient.getRepositoriesDetails(anyString(), anyList())).thenReturn(repositoriesDetails); when(generationRequestRepository.findById(anyString())).thenAnswer(invocation -> { String generationId = invocation.getArgument(0); @@ -248,7 +249,7 @@ void testReleaseErrataWithSingleDockerBuild() throws IOException { when(sbomService.get("A14FF4DDB7DB47D")).thenReturn(firstManifest); when(sbomService.get("2A5F7CA4166C470")).thenReturn(indexManifest); when(sbomService.save(any(Sbom.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(sbomService.searchLastSuccessfulAdvisoryRequestRecord(anyString())) + when(sbomService.searchLastSuccessfulAdvisoryRequestRecord(anyString(), anyString())) .thenReturn(latestAdvisoryRequestManifest); AdvisoryReleaseEvent event = AdvisoryReleaseEvent.builder() @@ -472,7 +473,7 @@ void testReleaseErrataWithMultiDockerBuilds() throws IOException { when(statsService.getStats()) .thenReturn(Stats.builder().withVersion("ReleaseAdvisoryEventsListenerTest_1.0.0").build()); - when(pyxisClient.getRepositoriesDetails(anyString())) + when(pyxisClient.getRepositoriesDetails(anyString(), anyList())) .thenAnswer(invocation -> pyxisRepositories.get(invocation.getArgument(0))); when(generationRequestRepository.findById(anyString())).thenAnswer(invocation -> { @@ -483,7 +484,7 @@ void testReleaseErrataWithMultiDockerBuilds() throws IOException { when(sbomService.get(anyString())).thenAnswer(invocation -> sboms.get(invocation.getArgument(0))); when(sbomService.save(any(Sbom.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(sbomService.searchLastSuccessfulAdvisoryRequestRecord(anyString())) + when(sbomService.searchLastSuccessfulAdvisoryRequestRecord(anyString(), anyString())) .thenReturn(latestAdvisoryRequestManifest); AdvisoryReleaseEvent event = AdvisoryReleaseEvent.builder()