From a37e9bca9eb97fe46cf405982db47eaf35c31a94 Mon Sep 17 00:00:00 2001 From: a-langer Date: Wed, 17 Jul 2024 19:24:44 +0700 Subject: [PATCH] Updating all libraries to Java 11 and image to 3.70.0-java11-ubi --- .env | 2 +- Dockerfile | 2 +- README.md | 20 ++- etc/logback/logback.xml | 12 +- nexus-docker/pom.xml | 23 ++- nexus-pac4j-plugin/pom.xml | 35 +++- nexus-pac4j-plugin/src/main/config/shiro.ini | 18 +- .../github/alanger/nexus/bootstrap/Main.java | 4 +- .../Pac4jAuthenticationListener.java | 4 +- .../nexus/bootstrap/Pac4jSecurityFilter.java | 2 +- .../nexus/plugin/Pac4jCallbackLogic.java | 43 ++++- .../nexus/plugin/realm/NexusPac4jRealm.java | 2 - .../plugin/realm/Pac4jPrincipalName.java | 130 ++++++++++++++ .../nexus/plugin/realm/Pac4jRealmName.java | 162 ++++++++++++++++++ .../plugin/rest/NugetApiKeyResource.java | 120 ++++++++++--- pom.xml | 8 +- 16 files changed, 511 insertions(+), 76 deletions(-) create mode 100644 nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jPrincipalName.java create mode 100644 nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jRealmName.java diff --git a/.env b/.env index 3c17720..cfa23ad 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ LOGGING_MAX_SIZE="${LOGGING_MAX_SIZE:-10M}" LOGGING_COUNT_FILES="${LOGGING_COUNT_FILES:-10}" ## Nexus -NEXUS_IMAGE="${NEXUS_IMAGE:-ghcr.io/a-langer/nexus-sso:3.70.0}" +NEXUS_IMAGE="${NEXUS_IMAGE:-ghcr.io/a-langer/nexus-sso:3.70.0-java11-ubi}" NEXUS_USER="${NEXUS_USER:-nexus}" NEXUS_GROUP="${NEXUS_GROUP:-nexus}" NEXUS_DATA="${NEXUS_DATA:-./nexus_data}" diff --git a/Dockerfile b/Dockerfile index b24d142..24d52f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # docker rmi $(docker images -f "dangling=true" -q) # docker run --user=0:0 --rm -it -p 8081:8081/tcp sonatype/nexus3:3.37.3 /bin/bash -ARG NEXUS_BASE_IMAGE="sonatype/nexus3:3.70.0" +ARG NEXUS_BASE_IMAGE="sonatype/nexus3:3.70.0-java11-ubi" FROM $NEXUS_BASE_IMAGE USER root diff --git a/README.md b/README.md index e214ae0..5bfe564 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![license](https://img.shields.io/badge/license-EPL1-brightgreen.svg)](https://github.com/a-langer/nexus-sso/blob/main/LICENSE "License of source code") [![image](https://ghcr-badge.deta.dev/a-langer/nexus-sso/latest_tag?trim=major&label=latest)][0] -[![image-size](https://ghcr-badge.deta.dev/a-langer/nexus-sso/size?tag=3.70.0)][0] +[![image-size](https://ghcr-badge.deta.dev/a-langer/nexus-sso/size?tag=3.70.0-java11-ubi)][0] [![JitPack](https://jitpack.io/v/a-langer/nexus-sso.svg)][1] Patch for [Nexus OSS][2] with authorization via [SSO][9] and [tokens][10]. By default this features available only in PRO version ([see comparison][5]), but this patch provides them an alternative implementation without violating the license. @@ -23,14 +23,16 @@ Solution implement as Docker [container][0] (based on [official image][3] with S ## Supported features and examples of usage -> **Note**: Since version `3.61.0` for SSO and User Tokens, it is enough to have following [realms][8.1] in the order listed: -> -> 1. "**Local Authenticating Realm**" - built-in realm used by default. -> 2. "**SSO Pac4j Realm**" - single sign-on realm uses an external Identity Provider (IdP). -> 3. "**SSO Token Realm**" - realm allows you to use user tokens instead of a password. -> 4. "**Docker Bearer Token Realm**" - required to access Docker repositories through a Docker client (must be below the "**SSO Token Realm**"). -> -> Other realms are not required and may lead to conflicts. +> **Note**: Since version `3.70.0-java11-ubi` image and all libraries have been updated to Java 11. See [release notes](https://help.sonatype.com/en/sonatype-nexus-repository-3-70-0-release-notes.html) for more information. + +Since version `3.61.0` for using SSO and User Tokens, it is enough to have following [realms][8.1] in the order listed: + +1. "**Local Authenticating Realm**" - built-in realm used by default. +2. "**SSO Pac4j Realm**" - single sign-on realm uses an external Identity Provider (IdP). +3. "**SSO Token Realm**" - realm allows you to use user tokens instead of a password. +4. "**Docker Bearer Token Realm**" - required to access Docker repositories through a Docker client (must be below the "**SSO Token Realm**"). + +Other realms are not required and may lead to conflicts. List of features this patch adds: diff --git a/etc/logback/logback.xml b/etc/logback/logback.xml index d2c37b6..c57dd50 100644 --- a/etc/logback/logback.xml +++ b/etc/logback/logback.xml @@ -160,8 +160,9 @@ - - + + + @@ -176,17 +177,16 @@ - + + + - - - diff --git a/nexus-docker/pom.xml b/nexus-docker/pom.xml index 85d3bd9..9b58b5b 100644 --- a/nexus-docker/pom.xml +++ b/nexus-docker/pom.xml @@ -14,8 +14,8 @@ pom - sonatype/nexus3:${nexus.base.version} - ghcr.io/a-langer/nexus-sso:${project.version} + sonatype/nexus3:${nexus.base.version}-java11-ubi + ghcr.io/a-langer/nexus-sso:${project.version}-java11-ubi 0.3.2-fixed @@ -88,10 +88,12 @@ match="ARG NEXUS_PLUGIN_VERSION=(.*)" replace='ARG NEXUS_PLUGIN_VERSION="${nexus.plugin.version}"' /> - Set size?tag=${project.version} to README.md + + + Set size?tag=${image.tag} to README.md + replace='[![image-size](https://ghcr-badge.deta.dev/a-langer/nexus-sso/size?tag=${image.tag})][0]' /> Set ANSIBLEGALAXY_VERSION="${ansiblegalaxy.version}" to Dockerfile + + + ant-contrib + ant-contrib + 1.0b3 + + + ant + ant + + + + org.codehaus.mojo diff --git a/nexus-pac4j-plugin/pom.xml b/nexus-pac4j-plugin/pom.xml index 3e0dada..9703831 100644 --- a/nexus-pac4j-plugin/pom.xml +++ b/nexus-pac4j-plugin/pom.xml @@ -36,23 +36,33 @@ - + io.buji buji-pac4j - 4.1.1 + 8.0.0 org.apache.shiro * + + org.slf4j + * + - + + + org.pac4j + javaee-pac4j + 7.1.0 + + org.pac4j pac4j-core - 3.9.0 + 5.6.0 org.slf4j @@ -60,11 +70,11 @@ - + org.pac4j pac4j-saml - 3.9.0 + 5.6.0 @@ -158,6 +168,19 @@ com.fasterxml.woodstox woodstox-core + + + org.bouncycastle + bcprov-jdk15on + + + org.springframework + spring-beans + + + com.fasterxml.jackson.core + jackson-databind + xalan diff --git a/nexus-pac4j-plugin/src/main/config/shiro.ini b/nexus-pac4j-plugin/src/main/config/shiro.ini index af7e5c2..3171c61 100644 --- a/nexus-pac4j-plugin/src/main/config/shiro.ini +++ b/nexus-pac4j-plugin/src/main/config/shiro.ini @@ -24,6 +24,7 @@ Authenticate_Modal_Dialog_Message =
Accessing API Key requires validation o # pac4jRealm = com.github.alanger.nexus.plugin.realm.NexusPac4jRealm ## SAML buji-pac4j https://github.com/bujiio/buji-pac4j/blob/master/src/main/resources/buji-pac4j-default.ini +## SAML buji-pac4j-demo https://github.com/pac4j/buji-pac4j-demo/blob/master/src/main/resources/shiro.ini authorizationGenerator = org.pac4j.core.authorization.generator.FromAttributesAuthorizationGenerator authorizationGenerator.roleAttributes = ${PAC4J_ROLE_ATTRS:-roles} authorizationGenerator.permissionAttributes = ${PAC4J_PERMISSION_ATTRS:-permission} @@ -88,18 +89,27 @@ tokenRealm.authenticationCachingEnabled = true ; securityManager.realms = $iniRealm, $pac4jRealm, $tokenRealm ## Pac4j filters -callbackFilter = io.buji.pac4j.filter.CallbackFilter +callbackFilter = org.pac4j.jee.filter.CallbackFilter callbackFilter.config = $config callbackFilter.defaultUrl = /${NEXUS_CONTEXT:-} callbackFilter.defaultClient = SAML2Client +callbackFilter.renewSession = false +# Required since buji-pac4j:8.0.0 (or use pac4jToShiroBridge) +; callbackLogic = com.github.alanger.nexus.plugin.Pac4jCallbackLogic +; config.callbackLogic = $callbackLogic +; callbackFilter.callbackLogic = $callbackLogic -saml2SecurityFilter = io.buji.pac4j.filter.SecurityFilter +# Required since buji-pac4j:8.0.0 (or use callbackLogic) +pac4jToShiroBridge = io.buji.pac4j.bridge.Pac4jShiroBridge +pac4jToShiroBridge.config = $config + +saml2SecurityFilter = org.pac4j.jee.filter.SecurityFilter saml2SecurityFilter.config = $config saml2SecurityFilter.clients = SAML2Client -pac4jLogout = io.buji.pac4j.filter.LogoutFilter +pac4jLogout = org.pac4j.jee.filter.LogoutFilter pac4jLogout.config = $config -; pac4jCentralLogout = io.buji.pac4j.filter.LogoutFilter +; pac4jCentralLogout = org.pac4j.jee.filter.LogoutFilter ; pac4jCentralLogout.config = $config ; pac4jCentralLogout.localLogout = false ; pac4jCentralLogout.centralLogout = true diff --git a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Main.java b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Main.java index b11a31d..ec66b4a 100644 --- a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Main.java +++ b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Main.java @@ -37,7 +37,7 @@ import com.github.alanger.shiroext.http.MockHttpServletRequest; import com.github.alanger.shiroext.http.MockHttpServletResponse; import com.github.alanger.shiroext.realm.jdbc.JdbcRealmName; -import com.github.alanger.shiroext.realm.pac4j.Pac4jRealmName; +import com.github.alanger.nexus.plugin.realm.NexusPac4jRealm; /** * Main object of script initialization. @@ -221,7 +221,7 @@ public void initObjects() { objects.put("echoRealm", echoRealm); // Realm for authorization by SAML/SSO - Pac4jRealmName pac4jRealm = (Pac4jRealmName) objects.getOrDefault("pac4jRealm", DI.getInstance().pac4jRealm); + NexusPac4jRealm pac4jRealm = (NexusPac4jRealm) objects.getOrDefault("pac4jRealm", DI.getInstance().pac4jRealm); pac4jRealm.setName("pac4jRealm"); objects.put("pac4jRealm", pac4jRealm); diff --git a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jAuthenticationListener.java b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jAuthenticationListener.java index 7ce2f0a..e4153c5 100644 --- a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jAuthenticationListener.java +++ b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jAuthenticationListener.java @@ -29,8 +29,8 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.subject.support.WebDelegatingSubject; -import com.github.alanger.shiroext.realm.pac4j.Pac4jPrincipalName; -import com.github.alanger.shiroext.realm.pac4j.Pac4jRealmName; +import com.github.alanger.nexus.plugin.realm.Pac4jPrincipalName; +import com.github.alanger.nexus.plugin.realm.Pac4jRealmName; import com.github.alanger.shiroext.realm.RealmUtils; import com.github.alanger.shiroext.realm.ICommonRole; diff --git a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jSecurityFilter.java b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jSecurityFilter.java index e9ee516..a488e93 100644 --- a/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jSecurityFilter.java +++ b/nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jSecurityFilter.java @@ -7,7 +7,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import io.buji.pac4j.filter.SecurityFilter; +import org.pac4j.jee.filter.SecurityFilter; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; diff --git a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Pac4jCallbackLogic.java b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Pac4jCallbackLogic.java index 0e3e60c..04271d7 100644 --- a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Pac4jCallbackLogic.java +++ b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Pac4jCallbackLogic.java @@ -2,24 +2,53 @@ import org.pac4j.core.config.Config; import org.pac4j.core.context.WebContext; +import org.pac4j.core.context.session.SessionStore; +import org.pac4j.core.engine.DefaultCallbackLogic; import org.pac4j.core.http.adapter.HttpActionAdapter; -import io.buji.pac4j.engine.ShiroCallbackLogic; +import io.buji.pac4j.profile.ShiroProfileManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Debug only. + * Required since buji-pac4j:8.0.0, add to in shiro.ini: + * + *
+ * callbackLogic = com.github.alanger.nexus.plugin.Pac4jCallbackLogic
+ * config.callbackLogic = $callbackLogic
+ * callbackFilter.callbackLogic = $callbackLogic
+ * 
+ * + * Or use {@link io.buji.pac4j.bridge.Pac4jShiroBridge}: + *
+ * pac4jToShiroBridge = io.buji.pac4j.bridge.Pac4jShiroBridge
+ * pac4jToShiroBridge.config = $config
+ * 
+ * + * @see https://github.com/bujiio/buji-pac4j/blob/8.0.x/src/main/resources/buji-pac4j-default.ini + * @see https://github.com/pac4j/buji-pac4j-demo/blob/8.0.x/src/main/resources/shiro.ini */ -public class Pac4jCallbackLogic extends ShiroCallbackLogic { +public class Pac4jCallbackLogic extends DefaultCallbackLogic { + + private static final Logger logger = LoggerFactory.getLogger(Pac4jCallbackLogic.class); + + public Pac4jCallbackLogic() { + super(); + this.setProfileManagerFactory(ShiroProfileManager::new); + } @Override - public R perform(C context, Config config, HttpActionAdapter httpActionAdapter, String inputDefaultUrl, - Boolean inputSaveInSession, Boolean inputMultiProfile, Boolean inputRenewSession, String client) { + public Object perform(WebContext webContext, SessionStore sessionStore, Config config, + HttpActionAdapter httpActionAdapter, String inputDefaultUrl, Boolean inputRenewSession, + String defaultClient) { try { - return super.perform(context, config, httpActionAdapter, inputDefaultUrl, inputSaveInSession, inputMultiProfile, - inputRenewSession, client); + return super.perform(webContext, sessionStore, config, httpActionAdapter, inputDefaultUrl, + inputRenewSession, defaultClient); } catch (final Exception e) { // Verbose error from org.opensaml.xmlsec.signature.support.SignatureValidator logger.trace("Callback perform error:", e); throw e; } } + } diff --git a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/NexusPac4jRealm.java b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/NexusPac4jRealm.java index f052497..4b03101 100644 --- a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/NexusPac4jRealm.java +++ b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/NexusPac4jRealm.java @@ -16,8 +16,6 @@ import org.slf4j.LoggerFactory; import org.sonatype.nexus.security.NexusSimpleAuthenticationInfo; import org.sonatype.nexus.security.RealmCaseMapping; -import com.github.alanger.shiroext.realm.pac4j.Pac4jPrincipalName; -import com.github.alanger.shiroext.realm.pac4j.Pac4jRealmName; import io.buji.pac4j.token.Pac4jToken; /** diff --git a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jPrincipalName.java b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jPrincipalName.java new file mode 100644 index 0000000..acf09da --- /dev/null +++ b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jPrincipalName.java @@ -0,0 +1,130 @@ +package com.github.alanger.nexus.plugin.realm; + +import java.io.Serializable; +import java.security.Principal; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.github.alanger.shiroext.realm.IPrincipalName; +import com.github.alanger.shiroext.realm.IUserPrefix; + +import org.pac4j.core.profile.AnonymousProfile; +import org.pac4j.core.profile.UserProfile; +import org.pac4j.core.util.CommonHelper; + +/** + * Moved from shiro-ext library for recompile with + * {@link org.pac4j.core.profile.UserProfile} as interface instead of class. + * + * @since buji-pac4j:5.0.0 + * @since Nexus:3.70.0 + * @see https://github.com/bujiio/buji-pac4j/blob/master/src/main/java/io/buji/pac4j/subject/Pac4jPrincipal.java + */ +public class Pac4jPrincipalName implements Principal, Serializable, IUserPrefix, IPrincipalName { + + private final List profiles; + private final boolean byName; + private String userPrefix = ""; + private String principalNameAttribute; + + public Pac4jPrincipalName(final List profiles) { + this(profiles, null, false); + } + + public Pac4jPrincipalName(final List profiles, String principalNameAttribute, + boolean byName) { + this.profiles = profiles; + this.principalNameAttribute = CommonHelper.isBlank(principalNameAttribute) ? null + : principalNameAttribute.trim(); + this.byName = byName; + } + + public Pac4jPrincipalName(final List profiles, String principalNameAttribute) { + this.profiles = profiles; + this.principalNameAttribute = CommonHelper.isBlank(principalNameAttribute) ? null + : principalNameAttribute.trim(); + this.byName = !CommonHelper.isBlank(principalNameAttribute); + } + + @Override + public String getUserPrefix() { + return userPrefix; + } + + @Override + public void setUserPrefix(String userPrefix) { + this.userPrefix = userPrefix; + } + + @Override + public String getPrincipalNameAttribute() { + return principalNameAttribute; + } + + @Override + public void setPrincipalNameAttribute(String principalNameAttribute) { + this.principalNameAttribute = principalNameAttribute; + } + + public boolean isByName() { + return byName; + } + + public UserProfile getProfile() { + return flatIntoOneProfile(this.profiles).get(); + } + + // Compatibility with buji-pac4j 4.1.1 + public static Optional flatIntoOneProfile(final Collection profiles) { + final Optional profile = profiles.stream().filter(p -> p != null && !(p instanceof AnonymousProfile)) + .findFirst(); + if (profile.isPresent()) { + return profile; + } else { + return profiles.stream().filter(Objects::nonNull).findFirst(); + } + } + + public List getProfiles() { + return this.profiles; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Pac4jPrincipalName that = (Pac4jPrincipalName) o; + return profiles != null ? profiles.equals(that.profiles) : that.profiles == null; + } + + @Override + public int hashCode() { + return profiles != null ? profiles.hashCode() : 0; + } + + @Override + public String getName() { + final UserProfile profile = this.getProfile(); + if (null == principalNameAttribute) { + return profile.getId(); + } + final Object attrValue = profile.getAttribute(principalNameAttribute); + return (null == attrValue) ? null : getUserPrefix() + String.valueOf(attrValue).replaceAll("(^\\[)|(\\]$)", ""); + } + + @Override + public String toString() { + if (isByName()) { + String name = getName(); + return name != null ? name : getUserPrefix() + getProfile().getId(); + } else { + return CommonHelper.toNiceString(this.getClass(), "profiles", getProfiles()); + } + } + +} diff --git a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jRealmName.java b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jRealmName.java new file mode 100644 index 0000000..68fbb60 --- /dev/null +++ b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jRealmName.java @@ -0,0 +1,162 @@ +package com.github.alanger.nexus.plugin.realm; + +import static com.github.alanger.shiroext.realm.RealmUtils.asList; +import static com.github.alanger.shiroext.realm.RealmUtils.filterBlackOrWhite; + +import io.buji.pac4j.realm.Pac4jRealm; +import io.buji.pac4j.token.Pac4jToken; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.pac4j.core.profile.UserProfile; + +import java.util.*; + +import com.github.alanger.shiroext.realm.ICommonPermission; +import com.github.alanger.shiroext.realm.ICommonRole; +import com.github.alanger.shiroext.realm.IFilterPermission; +import com.github.alanger.shiroext.realm.IFilterRole; +import com.github.alanger.shiroext.realm.IPrincipalName; +import com.github.alanger.shiroext.realm.IUserPrefix; + +/** + * Moved from shiro-ext library for recompile with + * {@link org.pac4j.core.profile.UserProfile} as interface instead of class. + * + * @since buji-pac4j:5.0.0 + * @since Nexus:3.70.0 + * @see https://github.com/bujiio/buji-pac4j/blob/master/src/main/java/io/buji/pac4j/realm/Pac4jRealm.java + */ +public class Pac4jRealmName extends Pac4jRealm + implements ICommonPermission, ICommonRole, IUserPrefix, IPrincipalName, IFilterRole, IFilterPermission { + + private String commonRole = null; + private String commonPermission = null; + private String userPrefix = ""; + + private String roleWhiteList; + private String roleBlackList; + private String permissionWhiteList; + private String permissionBlackList; + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) + throws AuthenticationException { + + final Pac4jToken token = (Pac4jToken) authenticationToken; + + // Compatibility with buji-pac4j 4.1.1 + final List profiles = token.getProfiles(); + + final Pac4jPrincipalName principal = new Pac4jPrincipalName(profiles, getPrincipalNameAttribute()); + principal.setUserPrefix(getUserPrefix()); + final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName()); + return new SimpleAuthenticationInfo(principalCollection, profiles.hashCode()); + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) { + final Set roles = new HashSet<>(); + final Set permissions = new HashSet<>(); + final Pac4jPrincipalName principal = principals.oneByType(Pac4jPrincipalName.class); + if (principal != null) { + roles.addAll(asList(commonRole)); + permissions.addAll(asList(commonPermission)); + + // Compatibility with buji-pac4j 4.1.1 + final List profiles = principal.getProfiles(); + for (final UserProfile profile : profiles) { + if (profile != null) { + roles.addAll(profile.getRoles()); + profile.addRoles(asList(commonRole)); + + permissions.addAll(profile.getPermissions()); + profile.addPermissions(asList(commonPermission)); + } + } + } + + final SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); + filterBlackOrWhite(roles, roleWhiteList, roleBlackList); + simpleAuthorizationInfo.addRoles(roles); + filterBlackOrWhite(permissions, permissionWhiteList, permissionBlackList); + simpleAuthorizationInfo.addStringPermissions(permissions); + return simpleAuthorizationInfo; + } + + @Override + public String getCommonRole() { + return commonRole; + } + + @Override + public void setCommonRole(String commonRole) { + this.commonRole = commonRole; + } + + @Override + public String getCommonPermission() { + return commonPermission; + } + + @Override + public void setCommonPermission(String commonPermission) { + this.commonPermission = commonPermission; + } + + @Override + public String getUserPrefix() { + return userPrefix; + } + + @Override + public void setUserPrefix(String userPrefix) { + this.userPrefix = userPrefix; + } + + @Override + public String getRoleWhiteList() { + return roleWhiteList; + } + + @Override + public void setRoleWhiteList(String roleWhiteList) { + this.roleWhiteList = roleWhiteList; + } + + @Override + public String getRoleBlackList() { + return roleBlackList; + } + + @Override + public void setRoleBlackList(String roleBlackList) { + this.roleBlackList = roleBlackList; + } + + @Override + public String getPermissionWhiteList() { + return permissionWhiteList; + } + + @Override + public void setPermissionWhiteList(String permissionWhiteList) { + this.permissionWhiteList = permissionWhiteList; + } + + @Override + public String getPermissionBlackList() { + return permissionBlackList; + } + + @Override + public void setPermissionBlackList(String permissionBlackList) { + this.permissionBlackList = permissionBlackList; + } + +} diff --git a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyResource.java b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyResource.java index 4e184f8..4ca544c 100644 --- a/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyResource.java +++ b/nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyResource.java @@ -1,8 +1,14 @@ package com.github.alanger.nexus.plugin.rest; import com.google.common.base.Preconditions; +import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx; +import com.orientechnologies.orient.core.record.impl.ODocument; +import com.orientechnologies.orient.core.sql.OCommandSQL; +import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery; + import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.List; import java.util.Optional; import javax.inject.Inject; import javax.inject.Named; @@ -20,11 +26,9 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.subject.PrincipalCollection; import org.sonatype.goodies.common.ComponentSupport; -import org.sonatype.nexus.common.entity.AbstractEntity; import org.sonatype.nexus.common.wonderland.AuthTicketService; import org.sonatype.nexus.orient.DatabaseInstance; import org.sonatype.nexus.orient.DatabaseInstanceNames; -import org.sonatype.nexus.orient.entity.EntityAdapter; import org.sonatype.nexus.rest.NotCacheable; import org.sonatype.nexus.rest.Resource; import org.sonatype.nexus.rest.WebApplicationMessageException; @@ -39,11 +43,14 @@ * Nuget API key implementation for SSO. *

* GET: /service/rest/internal/nuget-api-key?authToken=XXXXX + * + * @see org.sonatype.nexus.internal.security.apikey.orient.OrientApiKeyStore + * @see org.sonatype.nexus.internal.security.apikey.orient.OrientApiKeyEntityAdapter */ @Named @Singleton @Path(NugetApiKeyResource.RESOURCE_URI) -@Produces({"application/json"}) +@Produces({ "application/json" }) public class NugetApiKeyResource extends ComponentSupport implements Resource { public static final String RESOURCE_URI = "/internal/nuget-api-key"; @@ -57,28 +64,24 @@ public class NugetApiKeyResource extends ComponentSupport implements Resource { private final SecurityHelper securityHelper; - // org.sonatype.nexus.internal.security.apikey.orient.OrientApiKeyEntityAdapter - private final EntityAdapter entityAdapter; - private final Provider databaseInstance; @Inject - public NugetApiKeyResource(Provider apiKeyStore, AuthTicketService authTicketService, SecurityHelper securityHelper, - EntityAdapter entityAdapter, + public NugetApiKeyResource(Provider apiKeyStore, AuthTicketService authTicketService, + SecurityHelper securityHelper, @Named(DatabaseInstanceNames.SECURITY) final Provider databaseInstance) { super(); this.apiKeyStore = Preconditions.checkNotNull(apiKeyStore); this.authTicketService = Preconditions.checkNotNull(authTicketService); this.securityHelper = Preconditions.checkNotNull(securityHelper); - this.entityAdapter = Preconditions.checkNotNull(entityAdapter); this.databaseInstance = Preconditions.checkNotNull(databaseInstance); - log.trace("NugetApiKeyResource apiKeyStore: {}, authTicketService: {}, securityHelper: {}, entityAdapter: {}, databaseInstance: {}", - apiKeyStore, authTicketService, securityHelper, entityAdapter, databaseInstance); + log.trace("apiKeyStore: {}, authTicketService: {}, securityHelper: {}, databaseInstance: {}", + apiKeyStore, authTicketService, securityHelper, databaseInstance); } @GET - @RequiresPermissions({"nexus:apikey:read"}) + @RequiresPermissions({ "nexus:apikey:read" }) @Validate @NotCacheable public NugetApiKeyXO readKey(@NotNull @Valid @QueryParam("authToken") String base64AuthToken) { @@ -86,11 +89,19 @@ public NugetApiKeyXO readKey(@NotNull @Valid @QueryParam("authToken") String bas PrincipalCollection principals = this.securityHelper.subject().getPrincipals(); // Read by principals - char[] apiKey = this.apiKeyStore.get().getApiKey(DOMAIN, principals).map(ApiKey::getApiKey).orElse(null); + char[] apiKey = null; + try { + apiKey = this.apiKeyStore.get().getApiKey(DOMAIN, principals).map(ApiKey::getApiKey).orElse(null); + } catch (Exception e) { + log.trace("Error read apiKey from KeyStore by principal {}: {}", principals.getPrimaryPrincipal(), e); + deleteApiKey(principals); + } // Read by primary principal or create a new if (apiKey == null) { - apiKey = finfApiKey(principals).map(ApiKey::getApiKey).orElseGet(() -> this.apiKeyStore.get().createApiKey(DOMAIN, principals)); + log.trace("Find apiKey by primary principal or create a new"); + apiKey = finfApiKey(principals).map(ApiKey::getApiKey) + .orElseGet(() -> this.apiKeyStore.get().createApiKey(DOMAIN, principals)); } log.trace("Read apiKey for principal {} = {}", principals.getPrimaryPrincipal(), apiKey != null ? "***" : null); @@ -99,41 +110,96 @@ public NugetApiKeyXO readKey(@NotNull @Valid @QueryParam("authToken") String bas @DELETE @RequiresAuthentication - @RequiresPermissions({"nexus:apikey:delete"}) + @RequiresPermissions({ "nexus:apikey:delete" }) @Validate public NugetApiKeyXO resetKey(@NotNull @Valid @QueryParam("authToken") String base64AuthToken) { validateAuthToken(base64AuthToken); PrincipalCollection principals = this.securityHelper.subject().getPrincipals(); // Delete by principals - this.apiKeyStore.get().deleteApiKey(DOMAIN, principals); - - // Delete by entity, see org.sonatype.nexus.internal.security.apikey.orient.OrientApiKeyStore#deleteApiKey - inTxRetry(databaseInstance).run(db -> { - entityAdapter.deleteEntity(db, (AbstractEntity) finfApiKey(principals).orElse(null)); - }); + deleteApiKey(principals); char[] apiKey = this.apiKeyStore.get().createApiKey(DOMAIN, principals); - log.trace("Reset apiKey for principal {} = {}", principals.getPrimaryPrincipal(), apiKey != null ? "***" : null); + log.trace("Reset apiKey for principal {} = {}", principals.getPrimaryPrincipal(), + apiKey != null ? "***" : null); return new NugetApiKeyXO(apiKey); } - // ApiKey by primary principal + /** + * Find ApiKey by primary principal + *

+ * Since version buji-pac4j:5.0.0 {@link org.pac4j.core.profile.UserProfile } is + * interface and deserialization is not possible because it was previously + * serialized as a class. Trying to read the API key results in an error: + * + *

+     * java.io.InvalidClassException: org.pac4j.core.profile.UserProfile; local class incompatible
+     * 
+ * + * For compatibility with old API keys, operations are performed in the database + * via SQL. + * + * @since buji-pac4j:5.0.0 + * @since Nexus:3.70.0 + */ private Optional finfApiKey(PrincipalCollection principals) { - for (ApiKey ak : apiKeyStore.get().browse(DOMAIN)) { - if (principals.getPrimaryPrincipal().toString().equals(ak.getPrincipals().getPrimaryPrincipal().toString())) { - return Optional.of(ak); + + // Since version buji-pac4j:5.0.0 is not possible + // @formatter:off + // for (ApiKey ak : apiKeyStore.get().browse(DOMAIN)) { + // if (principals.getPrimaryPrincipal().toString() + // .equals(ak.getPrincipals().getPrimaryPrincipal().toString())) { + // return Optional.of(ak); + // } + // } + // @formatter:on + + String apiKey = null; + try (ODatabaseDocumentTx db = databaseInstance.get().acquire()) { + List result = db.query( + new OSQLSynchQuery("select * from api_key where domain = ? and primary_principal = ?"), + DOMAIN, principals.getPrimaryPrincipal().toString() // + ); + + if (result != null && !result.isEmpty()) { + apiKey = result.get(0).field("api_key"); } } + + if (apiKey != null) { + return Optional.of(this.apiKeyStore.get().newApiKey(DOMAIN, principals, apiKey.toCharArray())); + } + return Optional.empty(); } + /** + * Delete ApiKey by primary principal + * + * @since buji-pac4j:5.0.0 + * @since Nexus:3.70.0 + */ + private void deleteApiKey(PrincipalCollection principals) { + try { + // May not delete without error + this.apiKeyStore.get().deleteApiKey(DOMAIN, principals); + } catch (Exception e) { + log.trace("Error delete apiKey from KeyStore by principal {}: {}", principals.getPrimaryPrincipal(), e); + log.trace("Invalid apiKey will be deleted from database"); + } finally { + inTxRetry(databaseInstance).run(db -> { + db.command(new OCommandSQL("delete from api_key where domain = ? and primary_principal = ?")) + .execute(DOMAIN, principals.getPrimaryPrincipal().toString()); + }); + } + } + private void validateAuthToken(String base64AuthToken) { String authToken = new String(Base64.getDecoder().decode(base64AuthToken), StandardCharsets.UTF_8); if (!this.authTicketService.redeemTicket(authToken)) { throw new WebApplicationMessageException(Response.Status.FORBIDDEN, "Invalid authentication ticket"); } - } + } diff --git a/pom.xml b/pom.xml index f01ba39..d40486b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,8 @@ UTF-8 UTF-8 - 1.8 - 1.8 + 11 + 11 ${project.version} 03 ${nexus.base.version}-${nexus.extension.version} @@ -257,7 +257,7 @@ - + org.apache.maven.plugins maven-enforcer-plugin @@ -271,7 +271,7 @@ - 1.8 + ${maven.compiler.target} org.bouncycastle:bcprov-jdk15on org.eclipse.jetty:jetty-server