├── etc ├── nginx │ ├── tls │ │ └── .gitkeep │ ├── common_location.conf │ ├── _ssl.conf │ ├── nginx.conf │ └── docker_location.conf ├── h2db │ └── .h2.server.properties ├── logback │ └── logback-access.xml ├── jetty │ ├── nexus-web.xml │ └── jetty-sso.xml ├── nexus-default.properties └── orientdb │ └── orientdb-server-config.xml ├── nexus_data └── .gitkeep ├── docs ├── Okta-Nexus-SAML.png ├── Migration.md ├── Patch.md ├── Tokens.md ├── Nginx.md └── Docker.md ├── nexus-pac4j-plugin └── src │ └── main │ ├── config │ ├── samlKeystore.jks │ ├── metadata.xml │ ├── metadata-keycloak.xml │ ├── sp-metadata.xml │ └── shiro.ini │ ├── java │ └── com │ │ └── github │ │ └── alanger │ │ └── nexus │ │ └── plugin │ │ ├── InitResourceXO.java │ │ ├── rest │ │ ├── NugetApiKeyXO.java │ │ └── NugetApiKeyResource.java │ │ ├── InitResource.java │ │ ├── Pac4jCallbackLogic.java │ │ ├── realm │ │ ├── TokenUserManager.java │ │ ├── Pac4jPrincipalName.java │ │ ├── Pac4jUserManager.java │ │ ├── Pac4jRealmName.java │ │ └── NexusTokenRealm.java │ │ ├── datastore │ │ ├── EncryptedString.java │ │ └── H2TcpConsole.java │ │ ├── ui │ │ └── NonTransitiveSearchComponent.java │ │ ├── Init.java │ │ ├── apikey │ │ ├── ApiTokenService.java │ │ └── ApiKeySanitizer.java │ │ ├── resources │ │ ├── UiPac4jPluginDescriptor.java │ │ └── nexus-sso-customize.vm.js │ │ └── DI.java │ └── groovy │ └── com │ └── github │ └── alanger │ └── nexus │ └── bootstrap │ ├── GroovyStringLookup.java │ ├── SubjectFilter.java │ ├── ReloadCongiguration.java │ ├── AnonymousFilter.java │ ├── Pac4jSecurityFilter.java │ ├── Pac4jAuthenticationListener.java │ ├── DebugFilter.java │ └── QuotaFilter.java ├── .gitignore ├── _compose.override_prod.yml ├── compose-keycloak.yml ├── .dockerignore ├── _compose.override.yml ├── .env ├── compose.yml ├── nexus-docker └── migrator.sh ├── Dockerfile ├── nexus-repository-services └── pom.xml └── README.md /etc/nginx/tls/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nexus_data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/Okta-Nexus-SAML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-langer/nexus-sso/HEAD/docs/Okta-Nexus-SAML.png -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/config/samlKeystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-langer/nexus-sso/HEAD/nexus-pac4j-plugin/src/main/config/samlKeystore.jks -------------------------------------------------------------------------------- /etc/h2db/.h2.server.properties: -------------------------------------------------------------------------------- 1 | #H2 Server Properties 2 | #Fri Dec 06 22:49:46 NOVT 2024 3 | webSSL=false 4 | webAllowOthers=true 5 | webPort=2480 6 | 0=Nexus H2 (File)|org.h2.Driver|jdbc\:h2\:/nexus-data/db/nexus| 7 | 1=Nexus H2 (TCP)|org.h2.Driver|jdbc\:h2\:tcp\://nexus\:2424/nexus| 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .settings 4 | .classpath 5 | .project 6 | .vscode 7 | nexus-public 8 | old 9 | target 10 | bin 11 | etc/orient 12 | etc/fabric 13 | etc/karaf 14 | compose.override.yml 15 | *_dev 16 | *_data 17 | *_prod 18 | *\.jar 19 | *\.kar 20 | *\.tar 21 | *\.gz 22 | *\.zip 23 | *\.log 24 | *\.png 25 | *\.crt 26 | *\.key 27 | ssl.conf 28 | *Debug\.java 29 | * copy* 30 | * - Copy* 31 | * — копия* 32 | *_SAVE* 33 | *_TEST* 34 | *.versionsBackup -------------------------------------------------------------------------------- /_compose.override_prod.yml: -------------------------------------------------------------------------------- 1 | # See docs/Docker.md, change these settings for your production environment, ex.: 2 | services: 3 | nexus: 4 | volumes: 5 | # All file volumes from compose.yml are inherited here, the following are added to them 6 | - ${NEXUS_ETC}/sso/config/shiro.ini:/opt/sonatype/nexus/etc/sso/config/shiro.ini:ro 7 | - ${NEXUS_ETC}/sso/config/metadata.xml:/opt/sonatype/nexus/etc/sso/config/metadata.xml:ro 8 | - ${NEXUS_ETC}/sso/config/sp-metadata.xml:/opt/sonatype/nexus/etc/sso/config/sp-metadata.xml:ro 9 | env_file: 10 | - .env_prod 11 | 12 | nginx: 13 | env_file: 14 | - .env_prod 15 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/InitResourceXO.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin; 2 | 3 | import com.google.common.base.Preconditions; 4 | import io.swagger.annotations.ApiModelProperty; 5 | 6 | /** 7 | * Init message json object. 8 | */ 9 | public class InitResourceXO { 10 | 11 | @ApiModelProperty("message") 12 | private String message; 13 | 14 | public InitResourceXO() {} 15 | 16 | public InitResourceXO(String message) { 17 | this.message = Preconditions.checkNotNull(message); 18 | } 19 | 20 | public String getMessage() { 21 | return this.message; 22 | } 23 | 24 | public void setMessage(String message) { 25 | this.message = Preconditions.checkNotNull(message); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /etc/nginx/common_location.conf: -------------------------------------------------------------------------------- 1 | # CORS headers for allow XMLHttpRequest https://issues.sonatype.org/browse/NEXUS-12710 2 | set $origin $http_origin; 3 | if ($origin = '') { 4 | set $origin '*'; 5 | } 6 | add_header Access-Control-Allow-Origin "$origin" always; 7 | add_header Access-Control-Allow-Credentials 'true' always; 8 | add_header Access-Control-Allow-Methods 'HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS' always; 9 | add_header Access-Control-Allow-Headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; 10 | 11 | # Protect /rewrite-endpoint/?conf=etc/urlrewrite.xml 12 | location ~ ^(/rewrite.*|/service/rest/rewrite.*)$ { 13 | return 403; 14 | } 15 | 16 | # Protect /service/rest/v1/script/* 17 | location ~ /service/rest/v1/script.*$ { 18 | return 403; 19 | } 20 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyXO.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.rest; 2 | 3 | import com.google.common.base.Preconditions; 4 | import io.swagger.annotations.ApiModelProperty; 5 | 6 | /** 7 | * Api key json object. 8 | */ 9 | public class NugetApiKeyXO { 10 | 11 | @ApiModelProperty("nugetApiKey") 12 | private String nugetApiKey; 13 | 14 | public NugetApiKeyXO() {} 15 | 16 | public NugetApiKeyXO(char[] nugetApiKey) { 17 | this.nugetApiKey = new String(Preconditions.checkNotNull(nugetApiKey)); 18 | } 19 | 20 | public NugetApiKeyXO(String nugetApiKey) { 21 | this.nugetApiKey = Preconditions.checkNotNull(nugetApiKey); 22 | } 23 | 24 | public String getApiKey() { 25 | return this.nugetApiKey; 26 | } 27 | 28 | public void setApiKey(String nugetApiKey) { 29 | this.nugetApiKey = Preconditions.checkNotNull(nugetApiKey); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /etc/nginx/_ssl.conf: -------------------------------------------------------------------------------- 1 | # See https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=old&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6 2 | listen 443 ssl http2; 3 | listen [::]:443 ssl http2; 4 | 5 | ssl_certificate /etc/nginx/tls/site.crt; 6 | ssl_certificate_key /etc/nginx/tls/site.key; 7 | 8 | ssl_session_timeout 1d; 9 | ssl_session_cache shared:SSL:10m; 10 | ssl_session_tickets off; 11 | ssl_protocols TLSv1.2 TLSv1.3; 12 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA; 13 | ssl_prefer_server_ciphers on; 14 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/GroovyStringLookup.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.script.ScriptEngine; 6 | 7 | import org.apache.commons.text.lookup.StringLookup; 8 | 9 | import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; 10 | 11 | public class GroovyStringLookup implements StringLookup { 12 | 13 | public static final GroovyStringLookup INSTANCE = new GroovyStringLookup(); 14 | 15 | private GroovyStringLookup() { 16 | } 17 | 18 | @Override 19 | public String lookup(final String script) { 20 | if (script == null) { 21 | return null; 22 | } 23 | try { 24 | final ScriptEngine scriptEngine = new GroovyScriptEngineImpl(); 25 | return Objects.toString(scriptEngine.eval(script), null); 26 | } catch (final Exception e) { 27 | throw new IllegalArgumentException( 28 | String.format("Error in Groovy script engine evaluating script [%s].", script), e); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /compose-keycloak.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/keycloak/keycloak-containers/tree/main/docker-compose-examples 2 | # Keycloak recaptcha on regisration https://www.keycloak.org/docs/latest/server_admin/#_recaptcha 3 | # Keycloak recaptcha on login https://github.com/raptor-group/keycloak-login-recaptcha 4 | # docker compose -f compose-keycloak.yml up --remove-orphans 5 | version: "3.9" 6 | 7 | x-container: &container 8 | restart: ${RESTART_POLICY:-unless-stopped} 9 | env_file: 10 | - .env 11 | 12 | x-logging: &logging 13 | driver: "json-file" 14 | options: 15 | max-size: ${LOGGING_MAX_SIZE:-5M} 16 | max-file: ${LOGGING_COUNT_FILES:-10} 17 | 18 | services: 19 | postgres: 20 | <<: *container 21 | image: ${POSTGRES_IMAGE:-postgres:14} 22 | volumes: 23 | - "postgres_data:/var/lib/postgresql/data" 24 | logging: 25 | <<: *logging 26 | 27 | keycloak: 28 | <<: *container 29 | image: ${KEYCLOAK_IMAGE:-jboss/keycloak:16.1.1} 30 | ports: 31 | - 8080:8080 32 | depends_on: 33 | - postgres 34 | logging: 35 | <<: *logging 36 | 37 | volumes: 38 | postgres_data: 39 | # postgres_data: { driver: local, driver_opts: { type: 'none', o: 'bind', device: '${POSTGRES_DATA}' } } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | *_dev 3 | *.jks 4 | *.tar 5 | *.gz 6 | *.zip 7 | *.log 8 | *.png 9 | * copy* 10 | * - Copy* 11 | *_SAVE* 12 | etc/*/*_SAVE* 13 | etc/*/*_TEST* 14 | etc/*/* copy* 15 | etc/*/*.crt 16 | nexus-pac4j-plugin/src/main/*/*_SAVE 17 | nexus-pac4j-plugin/src/main/*/*_SAVE* 18 | nexus-pac4j-plugin/src/main/*/*_TEST* 19 | nexus-pac4j-plugin/src/main/*/* copy* 20 | nexus-pac4j-plugin/src/main/*/* copy 21 | nexus-pac4j-plugin/src/main/*/*.crt 22 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/*_SAVE 23 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/*_SAVE* 24 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/*_TEST 25 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/*_TEST* 26 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/* copy* 27 | nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/* copy 28 | nexus-pac4j-plugin/src/main/config/*_SAVE 29 | nexus-pac4j-plugin/src/main/config/*_SAVE* 30 | nexus-pac4j-plugin/src/main/config/*_TEST 31 | nexus-pac4j-plugin/src/main/config/*_TEST* 32 | nexus-pac4j-plugin/src/main/config/* copy* 33 | nexus-pac4j-plugin/src/main/config/* copy 34 | -------------------------------------------------------------------------------- /docs/Migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | Since version [`3.70.1-java11-ubi`][0] your need migrate from legacy [OrientDB][1] to [H2DB][2]. Don't worry, this version make migration automatically, just update the image version and run the container (see [migrator.sh](../nexus-docker/migrator.sh) for more information). 4 | 5 | > **WARN**: Versions [`3.71.0`](https://help.sonatype.com/en/download.html#download-sonatype-nexus-repository-database-migrator) and above of the Database Migrator utility only support migrating between `H2` and `PostgreSQL`. 6 | 7 | Of course, you can perform the migration yourself following the instructions below: 8 | 9 | 1. [Sonatype Nexus Repository 3.70.0 was the final release to include our legacy OrientDB](https://help.sonatype.com/en/upgrading-to-nexus-repository-3-71-0-and-beyond.html). 10 | 2. [3.71.0 and beyond do not support OrientDB, Java 8, or Java 11](https://help.sonatype.com/en/sonatype-nexus-repository-3-71-0-release-notes.html). 11 | 3. [Migrating From OrientDB to H2](https://help.sonatype.com/en/orient-3-70-java-8-or-11.html). 12 | 4. [Database Migrator Utility for 3.70.x](https://help.sonatype.com/en/orientdb-downloads.html). 13 | 14 | [0]: https://help.sonatype.com/en/sonatype-nexus-repository-3-70-0-release-notes.html "Nexus Repository 3.70.0 - 3.70.1 Release Notes" 15 | [1]: http://orientdb.org/docs/2.2.x/ "OrientDB" 16 | [2]: https://www.h2database.com/html/main.html "H2 Database Engine" 17 | -------------------------------------------------------------------------------- /etc/logback/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /favicon 7 | /static/ 8 | /service/outreach/ 9 | /service/rest/v1/status 10 | /service/extdirect/poll/rapture_State_get 11 | /service/extdirect/poll/coreui_Repository_readStatus 12 | 13 | NEUTRAL 14 | DENY 15 | 16 | ${karaf.data}/log/request.log 17 | true 18 | 19 | %clientHost %l %user [%date] "%requestURL" %statusCode %header{Content-Length} %bytesSent %elapsedTime "%header{User-Agent}" [%thread] 20 | 21 | 22 | ${karaf.data}/log/request-%d{yyyy-MM-dd}.log.gz 23 | 90 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/Patch.md: -------------------------------------------------------------------------------- 1 | # Patch features configuration 2 | 3 | Additional features implemented in this patch. 4 | 5 | ## Non-transitive privileges in group repositories 6 | 7 | **Non-transitive privileges in group repositories** - by default group repository privileges in Nexus are transitive (all or nothing), this [property](../etc/nexus-default.properties) enables mode of non-transitive privileges (only what is allowed): 8 | 9 | ```properties 10 | nexus.sso.group.nontransitive.privileges.enabled=true 11 | ``` 12 | 13 | > **Note**: 14 | > 15 | > * It is sufficient for a user to have the "browse" or "read" privilege (either one) to read files from the repository. 16 | > * Privileges must be granted to the repository itself and to the group repository in which it is a member. 17 | 18 | ## Jetty Rewrite Handler 19 | 20 | [Jetty Rewrite Handler][1] is used to route HTTP requests within the application and can be further configured using [jetty-sso.xml](../etc/jetty/jetty-sso.xml) (for example override or protect API endpoint). Also it supports hot-reload, to apply the any settings of plugin without restarting the container, run the command: 21 | 22 | ```bash 23 | docker compose exec -- nexus curl -k http://localhost:8081/rewrite-status 24 | ``` 25 | 26 | > **Note**: Hot-reload not working for environment variables defined in [.env](../.env), this changes take effect only after the container is restarted. 27 | 28 | [1]: https://eclipse.dev/jetty/documentation/jetty-9/index.html "Jetty Rewrite Handler" 29 | -------------------------------------------------------------------------------- /etc/jetty/nexus-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sonatype Nexus 5 | 6 | 7 | org.sonatype.nexus.bootstrap.osgi.BootstrapListener 8 | 9 | 10 | 11 | nexusFilter 12 | org.sonatype.nexus.bootstrap.osgi.DelegatingFilter 13 | 14 | 15 | nexusFilter 16 | /* 17 | REQUEST 18 | ERROR 19 | 20 | 21 | 25 | 26 | 27 | org.eclipse.jetty.servlet.Default.dirAllowed 28 | false 29 | 30 | 31 | 32 | 33 | shiroext-engine-class 34 | org.codehaus.groovy.jsr223.GroovyScriptEngineImpl 35 | 36 | 37 | 38 | /error.html 39 | 40 | 41 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/InitResource.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin; 2 | 3 | import org.sonatype.goodies.common.ComponentSupport; 4 | import org.sonatype.nexus.rest.NotCacheable; 5 | import org.sonatype.nexus.rest.Resource; 6 | import java.sql.Timestamp; 7 | import javax.inject.Inject; 8 | import javax.inject.Named; 9 | import javax.inject.Singleton; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.Produces; 13 | import javax.ws.rs.QueryParam; 14 | 15 | /** 16 | * Reload endpoint "/service/rest/rewrite-status". 17 | *

18 | * docker compose exec -- nexus curl -sSfkI http://localhost:8081/rewrite-status/?conf=etc/sso/config/urlrewrite.xml 19 | */ 20 | @Named 21 | @Singleton 22 | @Path("/rewrite-status") 23 | @Produces({"application/json"}) 24 | public class InitResource extends ComponentSupport implements Resource { 25 | 26 | private final Init init; 27 | 28 | @Inject 29 | public InitResource(@Named Init init) { 30 | super(); 31 | this.init = init; 32 | log.trace("InitResource Init object: {}", init); 33 | } 34 | 35 | // Parameter "conf" for compatibility with UrlRewriteFilter 36 | @GET 37 | @NotCacheable 38 | public InitResourceXO reload(@QueryParam("conf") String conf) throws Exception { 39 | String message = "Reload disabled"; 40 | if (init.isTraceEnabled()) { 41 | message = "Reloaded " + new Timestamp(System.currentTimeMillis()); 42 | init.doStart(); 43 | } 44 | return new InitResourceXO(message); 45 | } 46 | 47 | @GET 48 | @NotCacheable 49 | public InitResourceXO reload() throws Exception { 50 | return reload(null); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /_compose.override.yml: -------------------------------------------------------------------------------- 1 | # This is development environment, for production see _compose.override_prod.yml 2 | services: 3 | nexus: 4 | # Disable analytics if required https://help.sonatype.com/en/in-product-analytics-capability.html 5 | environment: 6 | - INSTALL4J_ADD_VM_PARAMS=${INSTALL4J_ADD_VM_PARAMS} -D#nexus.analytics.enabled=false -Dnexus.scripts.allowCreation=true -Dnexus.datastore.enabled=true 7 | -Dnexus.sso.h2.tcpListenerEnabled=true -Dnexus.sso.h2.tcpListenerPort=2424 -Dnexus.h2.httpListenerEnabled=true -Dnexus.h2.httpListenerPort=2480 8 | # Disabling "Analyze Application" https://stackoverflow.com/a/41726259/19707292 9 | extra_hosts: 10 | - clm.sonatype.com:0.0.0.0 11 | - rhc.sonatype.com:0.0.0.0 12 | - rhc-pro.sonatype.com:0.0.0.0 13 | dns: 14 | - ${NETWORK_DNS_RESOLVER_1:-8.8.8.8} 15 | - ${NETWORK_DNS_RESOLVER_2:-4.4.4.4} 16 | - ${NETWORK_DNS_RESOLVER_3:-192.168.0.1} 17 | volumes: 18 | - ${NEXUS_ETC}/logback:/opt/sonatype/nexus/etc/logback:ro 19 | - ${NEXUS_ETC}/jetty/nexus-web.xml:/opt/sonatype/nexus/etc/jetty/nexus-web.xml:ro 20 | - ${NEXUS_ETC}/jetty/jetty-sso.xml:/opt/sonatype/nexus/etc/jetty/jetty-sso.xml:ro 21 | - ${NEXUS_ETC}/nexus-default.properties:/opt/sonatype/nexus/etc/nexus-default.properties:ro 22 | - ./nexus-pac4j-plugin/src/main/config:/opt/sonatype/nexus/etc/sso/config:ro 23 | - ./nexus-pac4j-plugin/src/main/groovy:/opt/sonatype/nexus/etc/sso/script:ro 24 | - ${NEXUS_ETC}/h2db:/opt/sonatype/nexus/etc/h2db:ro # H2DB console config 25 | ports: 26 | - ${NEXUS_HTTP_PORT:-8081}:8081 # Nexus: http://localhost:8081/ (remove it from production environment) 27 | - ${DBCONSOLE_TCP_PORT:-2424}:2424 # H2DB: tcp://localhost:2424 (remove it from production environment) 28 | - ${DBCONSOLE_HTTP_PORT:-2481}:2480 # H2DB: http://localhost:2481 (remove it from production environment) 29 | -------------------------------------------------------------------------------- /docs/Tokens.md: -------------------------------------------------------------------------------- 1 | # User Auth Tokens 2 | 3 | [User Auth Tokens][0] - are applied when security policies do not allow the users password to be used, such as for storing in plain text (in settings Docker, Maven and etc.) or combined with [SAML/SSO](./SAML.md). Each user can set a personal token that can be used instead of a password. The creation of tokens is implemented through the "NuGet API Key" menu (privilegies `nx-apikey-all` required), however, the tokens themselves apply to all types of repositories. Example of usage user token: 4 | 5 | * Enable "**SSO Token Realm**" (above "**Docker Bearer Token Realm**") in the server administration panel. 6 | * Go to menu "Nexus -> Manage your user account -> NuGet API Key", press "Access API key". 7 | * Type your **username** if using SSO login, otherwise type password, then press "Authenticate". 8 | * Copy "Your NuGet API Key", press "Close" and "Sign out". 9 | * To validate a token: press "Sign in", type your username and token instead of password. 10 | * Also, a pair of username+token can be used for authorization in Maven, Docker, Pip, etc., example for HTTP basic authorization - `Authorization: Basic `. 11 | 12 | ## Debug 13 | 14 | To enable debugging, add the following lines to the [shiro.ini](../nexus-pac4j-plugin/src/main/config/shiro.ini): 15 | 16 | ```ini 17 | # Disable authentication caching 18 | tokenRealm.authenticationCachingEnabled = false 19 | ``` 20 | 21 | And following lines to the [logback.xml](../etc/logback/logback.xml) file (output will be to `${NEXUS_DATA}/log/nexus.log`): 22 | 23 | ```xml 24 | 25 | 26 | 27 | 28 | ``` 29 | 30 | [0]: https://help.sonatype.com/en/user-tokens.html "Nexus PRO tokens" 31 | -------------------------------------------------------------------------------- /etc/nexus-default.properties: -------------------------------------------------------------------------------- 1 | ## DO NOT EDIT - CUSTOMIZATIONS BELONG IN $data-dir/etc/nexus.properties 2 | ## 3 | # Jetty section 4 | application-port=8081 5 | application-host=0.0.0.0 6 | nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-requestlog.xml,${jetty.etc}/jetty-sso.xml 7 | nexus-context-path=/${NEXUS_CONTEXT} 8 | 9 | # Nexus section https://github.com/sonatype/nexus-public/blob/main/assemblies/nexus-base-overlay/src/main/resources/overlay/etc/nexus-default.properties 10 | nexus-edition=nexus-oss-edition 11 | nexus-features=nexus-oss-feature 12 | 13 | nexus.upgrade.warnOnMissingDependencies=true 14 | nexus.hazelcast.discovery.isEnabled=false 15 | 16 | # https://support.sonatype.com/hc/en-us/articles/360049884673#rhc 17 | nexus.ossindex.plugin.enabled=false 18 | # nexus.skipDefaultRepositories=true 19 | 20 | # https://support.sonatype.com/hc/en-us/articles/360045220393 21 | # https://baykara.medium.com/how-to-automate-nexus-setup-process-5755183bc322 22 | # nexus.scripts.allowCreation=true 23 | 24 | # https://support.sonatype.com/hc/en-us/articles/213464978-How-to-avoid-Could-not-download-page-bundle-messages 25 | 26 | # https://issues.sonatype.org/browse/NEXUS-18850 27 | # https://github.com/sonatype/nexus-public/blob/main/components/nexus-security/src/main/java/org/sonatype/nexus/security/authc/AntiCsrfHelper.java 28 | # nexus.security.anticsrftoken.enabled=false 29 | 30 | # https://help.sonatype.com/en/in-product-analytics-capability.html 31 | # https://github.com/sonatype/nexus-public/blob/main/components/nexus-rapture/src/main/java/org/sonatype/nexus/rapture/internal/RaptureWebResourceBundle.java 32 | # nexus.analytics.enabled=false 33 | 34 | # https://help.sonatype.com/en/orient-3-70-java-8-or-11.html 35 | nexus.datastore.enabled=true 36 | 37 | # https://support.sonatype.com/hc/en-us/articles/213467158-How-to-reset-a-forgotten-admin-password-in-Sonatype-Nexus-Repository-3 38 | # nexus.h2.httpListenerEnabled=true 39 | # nexus.h2.httpListenerPort=2480 40 | 41 | # Only SSO plugin 42 | # nexus.sso.h2.tcpListenerEnabled=true 43 | # nexus.sso.h2.tcpListenerPort=2424 44 | nexus.sso.group.nontransitive.privileges.enabled=true 45 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Pac4jCallbackLogic.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin; 2 | 3 | import org.pac4j.core.config.Config; 4 | import org.pac4j.core.context.WebContext; 5 | import org.pac4j.core.context.session.SessionStore; 6 | import org.pac4j.core.engine.DefaultCallbackLogic; 7 | import org.pac4j.core.http.adapter.HttpActionAdapter; 8 | import io.buji.pac4j.profile.ShiroProfileManager; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * Required since buji-pac4j:8.0.0, add to in shiro.ini: 15 | * 16 | *

17 |  * callbackLogic = com.github.alanger.nexus.plugin.Pac4jCallbackLogic
18 |  * config.callbackLogic = $callbackLogic
19 |  * callbackFilter.callbackLogic = $callbackLogic
20 |  * 
21 | * 22 | * Or use {@link io.buji.pac4j.bridge.Pac4jShiroBridge}: 23 | *
24 |  * pac4jToShiroBridge = io.buji.pac4j.bridge.Pac4jShiroBridge
25 |  * pac4jToShiroBridge.config = $config
26 |  * 
27 | * 28 | * @see https://github.com/bujiio/buji-pac4j/blob/8.0.x/src/main/resources/buji-pac4j-default.ini 29 | * @see https://github.com/pac4j/buji-pac4j-demo/blob/8.0.x/src/main/resources/shiro.ini 30 | */ 31 | public class Pac4jCallbackLogic extends DefaultCallbackLogic { 32 | 33 | private static final Logger logger = LoggerFactory.getLogger(Pac4jCallbackLogic.class); 34 | 35 | public Pac4jCallbackLogic() { 36 | super(); 37 | this.setProfileManagerFactory(ShiroProfileManager::new); 38 | } 39 | 40 | @Override 41 | public Object perform(WebContext webContext, SessionStore sessionStore, Config config, 42 | HttpActionAdapter httpActionAdapter, String inputDefaultUrl, Boolean inputRenewSession, 43 | String defaultClient) { 44 | try { 45 | return super.perform(webContext, sessionStore, config, httpActionAdapter, inputDefaultUrl, 46 | inputRenewSession, defaultClient); 47 | } catch (final Exception e) { 48 | // Verbose error from org.opensaml.xmlsec.signature.support.SignatureValidator 49 | logger.trace("Callback perform error:", e); 50 | throw e; 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/TokenUserManager.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.realm; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import javax.inject.Singleton; 6 | import org.eclipse.sisu.Description; 7 | 8 | import org.sonatype.nexus.common.event.EventManager; 9 | import org.sonatype.nexus.security.config.SecurityConfigurationManager; 10 | import org.sonatype.nexus.security.user.RoleMappingUserManager; 11 | import org.sonatype.nexus.security.user.User; 12 | import org.sonatype.nexus.security.user.UserNotFoundException; 13 | 14 | /** 15 | * User manager for SSO Token realm. 16 | * 17 | * @since 3.70.1-02 18 | * @see org.sonatype.nexus.security.internal.UserManagerImpl 19 | */ 20 | @Singleton 21 | @Named(TokenUserManager.SOURCE) 22 | @Description("Pac4jToken") 23 | public class TokenUserManager extends Pac4jUserManager { 24 | 25 | public static final String SOURCE = "pac4jToken"; 26 | 27 | @Inject 28 | public TokenUserManager(EventManager eventManager, SecurityConfigurationManager configuration, RoleMappingUserManager defaultUserManager) { 29 | super(eventManager, configuration, defaultUserManager); 30 | } 31 | 32 | //-- org.sonatype.nexus.security.user.UserManager --// 33 | 34 | @Override 35 | public String getSource() { 36 | return SOURCE; 37 | } 38 | 39 | @Override 40 | public String getAuthenticationRealmName() { 41 | return NexusTokenRealm.NAME; 42 | } 43 | 44 | @Override 45 | public User addUser(User user, String password) { 46 | throw new UnsupportedOperationException("SSO/Token users can't add"); 47 | } 48 | 49 | @Override 50 | public User updateUser(User user) throws UserNotFoundException { 51 | throw new UnsupportedOperationException("SSO/Token users can't update"); 52 | } 53 | 54 | @Override 55 | public void deleteUser(String userId) throws UserNotFoundException { 56 | throw new UnsupportedOperationException("SSO/Token users can't delete"); 57 | } 58 | 59 | @Override 60 | public void changePassword(String userId, String newPassword) throws UserNotFoundException { 61 | throw new UnsupportedOperationException("SSO/Token users can't change passwords"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/datastore/EncryptedString.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.datastore; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import org.sonatype.nexus.crypto.LegacyCipherFactory; // since 3.75.1 6 | import org.sonatype.nexus.crypto.LegacyCipherFactory.PbeCipher; // since 3.75.1 7 | import com.fasterxml.jackson.core.Base64Variant; 8 | import com.fasterxml.jackson.core.Base64Variants; 9 | 10 | import static java.nio.charset.StandardCharsets.UTF_8; 11 | 12 | /** 13 | * This class should be used if you need to search in database on the encrypted string. 14 | * 15 | * @see org.sonatype.nexus.datastore.mybatis.MyBatisDataStore#prepare 16 | * @see org.sonatype.nexus.datastore.mybatis.handlers.EncryptedStringTypeHandler 17 | * @see org.sonatype.nexus.datastore.mybatis.MyBatisCipher 18 | * 19 | * @see org.sonatype.nexus.crypto.secrets.EncryptDecryptService 20 | * @see org.sonatype.nexus.crypto.internal.PbeCipherFactory 21 | * @see org.sonatype.nexus.crypto.internal.PbeCipherFactory.PbeCipher 22 | */ 23 | @Named 24 | public class EncryptedString { 25 | 26 | public static final Base64Variant BASE_64 = Base64Variants.getDefaultVariant(); 27 | 28 | // Hidden bean org.sonatype.nexus.datastore.mybatis.MyBatisCipher 29 | private final PbeCipher databaseCipher; 30 | 31 | @Inject 32 | public EncryptedString(final LegacyCipherFactory pbeCipherFactory, 33 | @Named("${nexus.mybatis.cipher.password:-changeme}") final String password, 34 | @Named("${nexus.mybatis.cipher.salt:-changeme}") final String salt, 35 | @Named("${nexus.mybatis.cipher.iv:-0123456789ABCDEF}") final String iv) throws Exception { 36 | this.databaseCipher = pbeCipherFactory.create(password, salt, iv); 37 | } 38 | 39 | public final PbeCipher cipher() { 40 | return this.databaseCipher; 41 | } 42 | 43 | /** 44 | * Encrypt string using database cipher + Base64. 45 | */ 46 | public final String encrypt(final String value) { 47 | return BASE_64.encode(cipher().encrypt(value.getBytes(UTF_8))); 48 | } 49 | 50 | /** 51 | * Decrypt string using Base64 + database cipher. 52 | */ 53 | public final String decrypt(final String value) { 54 | return value != null ? new String(cipher().decrypt(BASE_64.decode(value)), UTF_8) : null; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # user nginx; 2 | # worker_processes auto; 3 | 4 | # error_log /var/log/nginx/error.log notice; 5 | # pid /var/run/nginx.pid; 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | # error_log logs/error.log error; 12 | # access_log logs/data-access.log combined; 13 | error_log stderr error; 14 | access_log off; 15 | 16 | proxy_send_timeout 30s; 17 | proxy_read_timeout 60s; 18 | proxy_buffering off; 19 | keepalive_timeout 5 5; 20 | tcp_nodelay on; 21 | client_max_body_size 0; 22 | chunked_transfer_encoding on; 23 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=nexus:100m inactive=30d max_size=2g; 24 | 25 | proxy_http_version 1.1; 26 | proxy_set_header Connection ""; 27 | proxy_set_header X-Forwarded-For $remote_addr; 28 | proxy_set_header Host $host; 29 | proxy_set_header X-Forwarded-Proto $scheme; 30 | 31 | upstream nexus-node { 32 | server nexus:8081 max_fails=0; 33 | keepalive 150; 34 | keepalive_timeout 60s; 35 | keepalive_time 1h; 36 | keepalive_requests 1000; 37 | } 38 | 39 | map $upstream_http_location $upstream_docker_version { 40 | "~^(http(s)?:/)?(/[-_:0-9a-z\.]+)?/(?v1|v2)/([-_0-9a-z\.]+)/(.*)$" $version; 41 | } 42 | map $upstream_http_location $upstream_docker_repo_name { 43 | "~^(http(s)?:/)?(/[-_:0-9a-z\.]+)?/(v1|v2)/(?[-_0-9a-z\.]+)/(.*)$" $repo_name; 44 | } 45 | map $upstream_http_location $upstream_docker_rest_uri { 46 | "~^(http(s)?:/)?(/[-_:0-9a-z\.]+)?/(v1|v2)/([-_0-9a-z\.]+)/(?.*)$" $rest_uri; 47 | } 48 | 49 | map $uri $docker_repo_name_in { 50 | "~^/(v1|v2)/(?[-_0-9a-z\.]+)/(.*)$" $repo_name; 51 | } 52 | 53 | map $upstream_docker_repo_name:$docker_repo_name_in $response_header_location { 54 | "~^(.*):\1$" $upstream_http_location; 55 | default /$upstream_docker_version/$docker_repo_name_in/$upstream_docker_repo_name/$upstream_docker_rest_uri; 56 | } 57 | 58 | include common.conf*; 59 | 60 | server { 61 | listen 80; 62 | server_name nexus; 63 | 64 | include non_ssl.conf*; 65 | include common_location.conf*; 66 | include docker_location.conf*; 67 | 68 | location / { 69 | proxy_pass http://nexus-node/; 70 | } 71 | } 72 | 73 | server { 74 | listen 80 deferred; 75 | server_name nexus_ssl; 76 | 77 | include ssl.conf*; 78 | include common_location.conf*; 79 | include docker_location.conf*; 80 | 81 | location / { 82 | proxy_pass http://nexus-node/; 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/SubjectFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import static java.lang.String.format; 4 | import static java.nio.charset.StandardCharsets.UTF_8; 5 | import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; 6 | 7 | import java.io.IOException; 8 | 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.ServletRequest; 12 | import javax.servlet.ServletResponse; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | 16 | import org.apache.shiro.SecurityUtils; 17 | import org.apache.shiro.subject.Subject; 18 | 19 | /* 20 | * Filter by subject name. 21 | */ 22 | public class SubjectFilter extends QuotaFilter { 23 | 24 | private String namePattern = "admin"; 25 | 26 | public SubjectFilter() { 27 | setMethods("PUT,POST,DELETE,MOVE,PROPPATCH"); 28 | setResponseStatus(403); 29 | } 30 | 31 | @Override 32 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 33 | throws IOException, ServletException { 34 | HttpServletRequest request = (HttpServletRequest) req; 35 | HttpServletResponse response = (HttpServletResponse) resp; 36 | 37 | boolean isRecord = methods.contains(request.getMethod()); 38 | 39 | if (request.getAttribute(getClass().getCanonicalName()) != null || !isRecord) { 40 | chain.doFilter(request, response); 41 | return; 42 | } 43 | request.setAttribute(getClass().getCanonicalName(), true); 44 | request.setCharacterEncoding(UTF_8.name()); 45 | response.setCharacterEncoding(UTF_8.name()); 46 | 47 | Subject subject = SecurityUtils.getSubject(); 48 | String userName = String.valueOf(subject.getPrincipal()); 49 | 50 | boolean allowed = userName.matches(namePattern); 51 | 52 | if (!allowed) { 53 | String msg = format("User %s is forbidden method %s to %s", userName, request.getMethod(), getRepoName(request)); 54 | logger.trace(msg); 55 | response.setStatus(getResponseStatus()); 56 | response.setHeader(ERROR_MESSAGE, msg); 57 | request.setAttribute(ERROR_MESSAGE, msg); 58 | writeJsonMessage(response, msg); 59 | return; 60 | } 61 | 62 | chain.doFilter(request, response); 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | // none 68 | } 69 | 70 | public String getNamePattern() { 71 | return namePattern; 72 | } 73 | 74 | public void setNamePattern(String namePattern) { 75 | this.namePattern = namePattern; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /docs/Nginx.md: -------------------------------------------------------------------------------- 1 | # Nginx configuration 2 | 3 | ## Docker Repository Reverse Proxy 4 | 5 | **Docker Repository Reverse Proxy** - this [Nginx configuration](../etc/nginx/docker_location.conf) implements a proxy strategy to use Docker registries without additional ports or hostnames (while the [official documentation][1] only suggests two proxy strategies: "Port Mapping" and "Host Mapping"). To apply the proxy strategy, required pre-configuration of Nexus (see [gistcomment-4188452][2]): 6 | 7 | * After deployment, three Docker registries need to be created: 8 | 9 | * `docker-login` - uses to check authorization, it is recommended to choose type "group" or "hosted". To allow anonymous access, enable "Allow anonymous docker pull". 10 | * `docker-group` (optional) - choose type "group", uses to look up images in docker registries. CLI searches will be performed on all registries added to this group (assuming the user has read permissions or the "Allow anonymous docker pull" option is enabled). 11 | * `docker-root` (optional) - is used to pull an image from the Docker registry hosted in the Nexus root, i.e. without a given repository name. Can be of any type, for host your own images required the "hosted" type. Image names in this repository must not contain a slash (for example, myhost/myimage:latest). 12 | 13 | * After authorization, working with docker registries is controlled by Nexus permissions. For example, if you don't give a user permission to write to the "super-secret-docker-hosted-repo" registry, they can log in, but they can't push images to that registry. 14 | * Example of usage for host "https://nexus_host" and registry "my-hosted-registry": 15 | 16 | ```bash 17 | # Download an image "alpine" from a public registry 18 | docker pull alpine:latest 19 | # Change tag of image "alpine" 20 | docker tag alpine:latest nexus_host/my-hosted-registry/alpine:latest 21 | # Log in to the local registry 22 | docker login nexus_host -u $username -p $password_or_token 23 | # Pushing image "alpine" to registry "my-hosted-registry" 24 | docker push nexus_host/my-hosted-registry/alpine:latest 25 | # Search image "alpine" in hosted registry "my-hosted-registry" 26 | docker search nexus_host/my-hosted-registry/alpine:latest 27 | # Pulling image "alpine" from hosted registry "my-hosted-registry" 28 | docker pull nexus_host/my-hosted-registry/alpine:latest 29 | ``` 30 | 31 | ## Nginx SSL 32 | 33 | Nginx SSL is pre-configured, to enable it, need copy file [_ssl.conf](../etc/nginx/_ssl.conf) to `ssl.conf` and pass to directory `${NEXUS_ETC}/nginx/tls/` two files: 34 | 35 | * `site.crt` - PEM certificate of domain name. 36 | * `site.key` - key for certificate. 37 | 38 | [1]: https://help.sonatype.com/en/docker-repository-reverse-proxy-strategies.html "Docker reverse proxy" 39 | [2]: https://gist.github.com/abdennour/74c5de79e57a47f3351217d674238da8?permalink_comment_id=4188452#gistcomment-4188452 "Nginx for Docker registry" 40 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/config/metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MIIDqDCCApCgAwIBAgIGAX+Uod4qMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG 13 | A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU 14 | MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0yNDk2MzM4NDEcMBoGCSqGSIb3DQEJ 15 | ARYNaW5mb0Bva3RhLmNvbTAeFw0yMjAzMTYyMTI3MzBaFw0zMjAzMTYyMTI4MzBaMIGUMQswCQYD 16 | VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG 17 | A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0yNDk2MzM4NDEc 18 | MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 19 | ggEBAL/oEryhfQu5Qr9+RKlYOqcsFrOEwCILV5p5bvujFmgRy4K3Cjv4OowMsMQF1Oln/lq6XZzX 20 | wjl0G+JzYzPfqvrE6zpzQsNIcLjBwBRSA670meh3z1ZrGVNoT/nVWcIegHO5EAjElSHi1eZsKvMZ 21 | YLy59fEXzJ759r00kVMv9vir2Dp3Q0Gx39/eFGH0hVNN42saqFGUR8IJesBFptUZyUQSdi1E6Qoq 22 | qzvSlO8L3z1qrjAUtWi2f7mPcK70IkW+/rgnZ5NZJxK8rFet1nuJIs9yee4trHMRRtezM2YMuR7q 23 | 4ZtOHYhUgwyvHnFsnovszJkM9/e3eHz+6iMKuCts+LcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA 24 | Y2pC/66Ha4m/lNx+IP3Ena171s2qIpYMADubNsyK5ZPHi2VBTVN8mGd+DrUMKNAtfYTecOmsaXEo 25 | 2zdhg8IM3RuWZPiP2fRxnLQHHUTWg5S/ATQ9gPPfKEAV+xrcLn2Z0JW4Tj4IMzMgQ444mKqwSJxz 26 | BpVIcbUAkDifkYDULM4tDMpOC2OmCyXfpv4f3XfXCc4yPi2h5QOFvHcSo9auIxQeosdddqlo2iOo 27 | bfLAmqaqKQhKlHyN9CFHwokDOd6lSt95g9EsJ4x6s4M2KqJWmFzq96vZJRzsWIj4XXVqoBZaeQur 28 | mpV2TI55uqdIrPgNXAjCUSGk+UfUVsj1XZWcWw== 29 | 30 | 31 | 32 | 33 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | TZ="${TZ:-Asia/Novosibirsk}" 2 | RESTART_POLICY="${RESTART_POLICY:-unless-stopped}" 3 | LOGGING_MAX_SIZE="${LOGGING_MAX_SIZE:-10M}" 4 | LOGGING_COUNT_FILES="${LOGGING_COUNT_FILES:-10}" 5 | 6 | ## Nexus 7 | NEXUS_IMAGE="${NEXUS_IMAGE:-ghcr.io/a-langer/nexus-sso:3.75.1-java17-ubi}" 8 | NEXUS_USER="${NEXUS_USER:-nexus}" 9 | NEXUS_GROUP="${NEXUS_GROUP:-nexus}" 10 | NEXUS_DATA="${NEXUS_DATA:-./nexus_data}" 11 | NEXUS_ETC="${NEXUS_ETC:-./etc}" 12 | NEXUS_HOME="/opt/sonatype/nexus" 13 | NEXUS_MEM_RESERVATION="${NEXUS_MEM_RESERVATION:-512m}" 14 | NEXUS_MEM_LIMIT="${NEXUS_MEM_LIMIT:-3000m}" 15 | INSTALL4J_ADD_VM_PARAMS="-Xms${NEXUS_MEM_RESERVATION} -Xmx${NEXUS_MEM_LIMIT} -Djdk.security.allowNonCaAnchor=true -Djava.util.prefs.userRoot=/nexus-data/javaprefs" 16 | JAVA_MIN_MEM="${NEXUS_MEM_RESERVATION}" 17 | JAVA_MAX_MEM="${NEXUS_MEM_LIMIT}" 18 | JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8" 19 | 20 | ## Pac4j 21 | PAC4J_INI_SCAN_PERIOD="${PAC4J_INI_SCAN_PERIOD:-0}" 22 | PAC4J_ROLE_ATTRS="${PAC4J_ROLE_ATTRS:-roles}" 23 | PAC4J_PERMISSION_ATTRS="${PAC4J_PERMISSION_ATTRS:-permission}" 24 | PAC4J_PRINCIPAL_NAME_ATTR="${PAC4J_PRINCIPAL_NAME_ATTR:-username}" 25 | PAC4J_COMMON_ROLE="${PAC4J_COMMON_ROLE:-nx-authenticated, nx-public}" 26 | PAC4J_COMMON_PERMISSION="${PAC4J_COMMON_PERMISSION:-nexus:apikey:*, nexus:sso-user:read, nexus:repository-view:docker:docker-login:read}" 27 | PAC4J_PROFILE_ATTRS="${PAC4J_PROFILE_ATTRS:-firstName:firstName, lastName:lastName, email:email}" 28 | PAC4J_KEYSTORE="${PAC4J_KEYSTORE:-etc/sso/config/samlKeystore.jks}" 29 | PAC4J_KEYSTORE_PASSWORD="${PAC4J_KEYSTORE_PASSWORD:-pac4j-demo-passwd}" 30 | PAC4J_KEYSTORE_KEY_PASSWORD="${PAC4J_KEYSTORE_KEY_PASSWORD:-pac4j-demo-passwd}" 31 | PAC4J_IDENTITY_PROVIDER_METADATA="${PAC4J_IDENTITY_PROVIDER_METADATA:-etc/sso/config/metadata.xml}" 32 | PAC4J_AUTHENTICATION_LIFETIME="${PAC4J_AUTHENTICATION_LIFETIME:-3600}" 33 | PAC4J_BASE_URL="${PAC4J_BASE_URL:-http://localhost}" 34 | PAC4J_SERVICE_PROVIDER_METADATA="${PAC4J_SERVICE_PROVIDER_METADATA:-etc/sso/config/sp-metadata.xml}" 35 | PAC4J_TOKEN_COMMON_ROLE="${PAC4J_TOKEN_COMMON_ROLE:-nx-authenticated-token, nx-public}" 36 | PAC4J_TOKEN_COMMON_PERMISSION="${PAC4J_TOKEN_COMMON_PERMISSION:-nexus:sso-user:read, nexus:repository-view:docker:docker-login:read}" 37 | 38 | ## Keycloak 39 | # KEYCLOAK_IMAGE="jboss/keycloak:16.1.1" 40 | # DB_VENDOR="POSTGRES" 41 | # DB_ADDR="postgres" 42 | # DB_DATABASE="keycloak" 43 | # DB_USER="keycloak" 44 | # DB_SCHEMA="public" 45 | # DB_PASSWORD="password" 46 | # KEYCLOAK_USER="admin" 47 | # KEYCLOAK_PASSWORD="123456" 48 | # JDBC_PARAMS="ssl=false" 49 | # JAVA_OPTS="-server -Xms512m -Xmx2048m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" 50 | # KEYCLOAK_LOGLEVEL="ERROR" 51 | # ROOT_LOGLEVEL="ERROR" 52 | 53 | ## Postgres 54 | # POSTGRES_IMAGE="postgres:14" 55 | # POSTGRES_DATA="./postgres_data_dev" 56 | # POSTGRES_DB="keycloak" 57 | # POSTGRES_USER="keycloak" 58 | # POSTGRES_PASSWORD="password" 59 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/ReloadCongiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import javax.servlet.ServletContext; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class ReloadCongiguration extends Thread { 12 | 13 | public static final String NEED_RELOAD = "shiro_need_reload"; 14 | 15 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 16 | private final String urlRewriteStatusPath; 17 | private final File config; 18 | private final long interval; 19 | private final ServletContext servletContext; 20 | 21 | private long lastModified = 0L; 22 | 23 | private volatile boolean stopped; 24 | 25 | public ReloadCongiguration(String urlRewriteStatusPath, File config, long interval, ServletContext servletContext) { 26 | this.urlRewriteStatusPath = urlRewriteStatusPath; 27 | this.config = config; 28 | setLastModified(config.lastModified()); 29 | this.interval = interval; 30 | this.servletContext = servletContext; 31 | } 32 | 33 | public void setLastModified(long lastModified) { 34 | this.lastModified = lastModified; 35 | } 36 | 37 | @Override 38 | public void interrupt() { 39 | stopped = true; 40 | super.interrupt(); 41 | } 42 | 43 | @Override 44 | public boolean isInterrupted() { 45 | return stopped ? stopped : super.isInterrupted(); 46 | } 47 | 48 | @Override 49 | public void run() { 50 | if (isInterrupted() || Thread.interrupted() || Thread.currentThread().isInterrupted()) 51 | return; 52 | logger.trace("Run: config = {}, lastModified = {}", config, this.lastModified); 53 | if (config != null) { 54 | if (config.lastModified() > this.lastModified || Boolean.TRUE.equals(servletContext.getAttribute(NEED_RELOAD))) { 55 | try { 56 | this.swapUrlRewriteConfig(); 57 | setLastModified(config.lastModified()); 58 | servletContext.removeAttribute(NEED_RELOAD); 59 | } catch (Exception e) { 60 | logger.error("Ini reload error", e); 61 | } 62 | } 63 | try { 64 | sleep(interval); 65 | } catch (InterruptedException e) { 66 | logger.warn("Ini reload sleep error", e); 67 | Thread.currentThread().interrupt(); 68 | return; 69 | } 70 | run(); 71 | } 72 | } 73 | 74 | public void swapUrlRewriteConfig() throws Exception { 75 | URL url = new URL(this.urlRewriteStatusPath); 76 | HttpURLConnection con = (HttpURLConnection) url.openConnection(); 77 | con.setRequestMethod("GET"); 78 | con.setConnectTimeout(5000); 79 | con.setReadTimeout(10000); 80 | int status = con.getResponseCode(); 81 | logger.trace("swapUrlRewriteConfig status: {}", status); 82 | if (status != 200) { 83 | throw new IOException("GET in " + this.urlRewriteStatusPath + " returned code " + status); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /etc/nginx/docker_location.conf: -------------------------------------------------------------------------------- 1 | # See https://gist.github.com/abdennour/74c5de79e57a47f3351217d674238da8?permalink_comment_id=4188452#gistcomment-4188452 2 | 3 | location ~ ^/api/(.*) { 4 | rewrite ^/api/(.*)$ /$1$is_args$args last; 5 | } 6 | 7 | # Global auth, ex.: docker login nexus_host -u admin -p XXXXXXXX 8 | location ~ ^/(v1|v2)/(|token)$ { 9 | proxy_pass http://nexus-node/repository/docker-login/$1/$2$is_args$args; 10 | } 11 | 12 | # Global search, ex.: docker search nexus_host/myrepo/myslug/image:latest 13 | location ~ ^/(v1|v2)/(_ping|_catalog|search)$ { 14 | set $ver $1; 15 | set $dest $2; 16 | set $repo 'docker-root'; # Repository name by default: docker search nexus_host/image:latest 17 | set $repo_replace '$host/'; 18 | 19 | # Set default limit if not specified 20 | if ( $arg_n = '' ) { 21 | set $arg_n 1; 22 | } 23 | 24 | # Repository name from query: docker search nexus_host/myrepo/myslug/image:latest 25 | if ( $arg_q ~* "^(.*?)(%2F|\/)(.+$)$" ) { # %2F or / 26 | set $repo $1; # Repository name 27 | set $args q=$3&n=$arg_n; 28 | set $repo_replace '$host/$repo/'; # Add repository name to host 29 | } 30 | 31 | proxy_pass "http://nexus-node/repository/${repo}/${ver}/${dest}${is_args}${args}"; 32 | 33 | sub_filter_types application/json; 34 | sub_filter_once off; 35 | sub_filter '$host/' '$repo_replace'; 36 | 37 | error_page 400 404 500 = @search_fallback; # No fallback if 200 and 'num_results: 0' 38 | proxy_intercept_errors on; 39 | recursive_error_pages on; 40 | } 41 | 42 | # Fallback search in 'docker-group' if 404 43 | location @search_fallback { 44 | set $repo 'docker-group'; 45 | set $args q=$arg_q&n=$arg_n; 46 | set $repo_replace '$host/$repo/'; # Add repository name to host 47 | proxy_pass "http://nexus-node/repository/${repo}/${ver}/${dest}${is_args}${args}"; 48 | sub_filter_types application/json; 49 | sub_filter_once off; 50 | sub_filter '$host/' '$repo_replace'; 51 | } 52 | 53 | # Pushing to hosted docker-root repo, ex.: docker push nexus_host/image:latest 54 | location ~ ^/(v1|v2)/([-_0-9a-z\.]+)/blobs/uploads/$ { 55 | proxy_pass http://nexus-node/repository/docker-root/$1/$2/blobs/uploads/$is_args$args; 56 | proxy_hide_header Location; 57 | add_header Location $response_header_location always; 58 | } 59 | 60 | # Pulling from hosted docker-root repo, ex.: docker pull nexus_host/image:latest 61 | location ~ ^/(v1|v2)/([-_0-9a-z\.]+)/(blobs/sha256.*|manifests/.*)$ { 62 | proxy_pass http://nexus-node/repository/docker-root/$1/$2/$3$is_args$args; 63 | proxy_hide_header Location; 64 | add_header Location $response_header_location always; 65 | } 66 | 67 | # Pushing to specific repo, ex.: docker push nexus_host/myrepo/image:latest 68 | location ~ ^/(v1|v2)/([-_0-9a-z\.]+)/(.*)/blobs/uploads/$ { 69 | proxy_pass http://nexus-node/repository/$2/$1/$3/blobs/uploads/$is_args$args; 70 | proxy_hide_header Location; 71 | add_header Location $response_header_location always; 72 | } 73 | 74 | # Pulling from specific repo, ex.: docker pull nexus_host/myrepo/image:latest 75 | location ~ ^/(v1|v2)/([-_0-9a-z\.]+)/(.*)$ { 76 | proxy_pass http://nexus-node/repository/$2/$1/$3$is_args$args; 77 | proxy_hide_header Location; 78 | add_header Location $response_header_location always; 79 | } 80 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | x-container: &container 2 | restart: ${RESTART_POLICY:-unless-stopped} 3 | env_file: 4 | - .env 5 | 6 | x-logging: &logging 7 | driver: "json-file" 8 | options: 9 | max-size: ${LOGGING_MAX_SIZE:-5M} 10 | max-file: ${LOGGING_COUNT_FILES:-10} 11 | 12 | # docker compose up -d --wait --wait-timeout 45 13 | services: 14 | # docker compose exec -- nexus curl -k http://localhost:8081/rewrite-status 15 | # docker compose exec -- nexus bash 16 | # docker compose run --rm nexus bash 17 | nexus: 18 | <<: *container 19 | image: ${NEXUS_IMAGE} 20 | user: ${NEXUS_USER:-nexus}:${NEXUS_GROUP:-nexus} 21 | cpus: ${NEXUS_CPUS:-4} 22 | mem_limit: ${NEXUS_MEM_LIMIT:-3000m} 23 | mem_reservation: ${NEXUS_MEM_RESERVATION:-512m} 24 | volumes: 25 | - ${NEXUS_DATA}:/nexus-data 26 | logging: 27 | <<: *logging 28 | healthcheck: 29 | test: curl --fail http://localhost:8081/service/rest/v1/status || exit 1 30 | start_period: ${HEAL_START_PERIOD:-60s} 31 | interval: ${HEAL_INTERVAL:-30s} 32 | timeout: ${HEAL_TIMEOUT:-2s} 33 | retries: ${HEAL_RETRIES:-5} 34 | 35 | # docker compose exec -- nginx nginx -s reload; 36 | nginx: 37 | <<: *container 38 | image: ${NGINX_IMAGE:-nginx:1.23.3} 39 | user: ${NGINX_USER:-0}:${NGINX_GROUP:-0} 40 | cpus: ${NGINX_CPUS:-2} 41 | mem_limit: ${NGINX_MEM_LIMIT:-256m} 42 | mem_reservation: ${NGINX_MEM_RESERVATION:-64m} 43 | ports: 44 | - ${NGINX_HTTP_PORT:-80}:80 45 | - ${NGINX_HTTPS_PORT:-443}:443 46 | depends_on: 47 | nexus: 48 | condition: service_healthy 49 | volumes: 50 | - ${NEXUS_ETC:-./etc}/nginx:/etc/nginx/:ro 51 | logging: 52 | <<: *logging 53 | 54 | # docker compose --profile debug up 55 | # docker compose --profile debug up -d dbconsole 56 | # docker compose --profile debug rm -sf dbconsole 57 | dbconsole: 58 | <<: *container 59 | image: ${DBCONSOLE_IMAGE:-$NEXUS_IMAGE} 60 | user: ${DBCONSOLE_USER:-${NEXUS_USER:-nexus}}:${DBCONSOLE_GROUP:-${NEXUS_GROUP:-nexus}} 61 | cpus: ${DBCONSOLE_CPUS:-2} 62 | mem_limit: ${DBCONSOLE_MEM_LIMIT:-128m} 63 | mem_reservation: ${DBCONSOLE_MEM_RESERVATION:-64m} 64 | environment: 65 | - DBCONSOLE_NAMES=${DBCONSOLE_NAMES:-localhost} # The comma-separated list of external names https://h2database.com/javadoc/org/h2/tools/Server.html#main-java.lang.String...- 66 | volumes: 67 | - ${NEXUS_ETC}/h2db:/opt/sonatype/nexus/etc/h2db:ro # H2DB console config 68 | - ${NEXUS_DATA}:/nexus-data # H2DB file (accessible only if Nexus not running) 69 | ports: 70 | # H2 web console: http://localhost:2480 -> jdbc:h2:tcp://nexus:2424/nexus -> empty login/pass 71 | - "${DBCONSOLE_HTTP_PORT:-2480}:2480" 72 | profiles: 73 | - debug 74 | logging: 75 | <<: *logging 76 | entrypoint: ["bash", "-c", 77 | "java -cp $$NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.Server -web -webPort 2480 -webAllowOthers -webExternalNames $$DBCONSOLE_NAMES -ifExists -properties /opt/sonatype/nexus/etc/h2db/" 78 | ] 79 | 80 | networks: 81 | default: 82 | driver: ${NETWORK_DRIVER:-bridge} 83 | ipam: 84 | config: 85 | - subnet: ${NETWORK_SUBNET:-172.30.0.0/16} 86 | -------------------------------------------------------------------------------- /nexus-docker/migrator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail; 4 | 5 | # Logging 6 | function log() { 7 | msg="`date '+%Y/%m/%d %H:%M:%S'` $1"; 8 | echo "$msg" >> "${2:-$logFile}" && echo "$1"; 9 | } 10 | 11 | # Only for migration from legacy OrientDB to H2DB https://help.sonatype.com/en/download.html#download-sonatype-nexus-repository-database-migrator 12 | if [[ ! -f "/nexus-data/db/nexus.mv.db" && ! -z $(find /nexus-data/db -path "/*/database.ocf") ]]; then 13 | 14 | # Docs https://help.sonatype.com/en/orient-3-70-java-8-or-11.html 15 | 16 | # Prepare migrator directory 17 | bakVer=$(date '+%Y-%m-%d-%H-%M-%S')-${PLUG_VERSION} 18 | migratorDir="/nexus-data/migrator-${bakVer}" 19 | mkdir -p "${migratorDir}" 20 | logFile="${migratorDir}/migrator.log" 21 | 22 | # Check DB files 23 | if [[ -d "/nexus-data/db/logs" ]]; then 24 | log "Healthcheck log directory /nexus-data/db/logs already exists, exiting!" $logFile 25 | exit 1; 26 | fi 27 | log "Run database healthcheck before migration, see logs in /nexus-data/db/logs" $logFile 28 | cd /nexus-data/db && java -jar ${NEXUS_HOME}/nexus-db-migrator-*.jar --healthcheck -y 29 | log "Database healthcheck completed successfully, see logs in /nexus-data/db/logs" $logFile 30 | 31 | # 1. Perform a full backup using normal backup procedures. 32 | # https://orientdb.org/docs/3.1.x/console/Console-Command-Backup.html 33 | # https://github.com/sonatype/nexus-public/blob/release-3.70.1-02/components/nexus-orient/src/main/java/org/sonatype/nexus/orient/DatabaseManagerSupport.java 34 | cd "${migratorDir}" 35 | log "Perform a full backup of databases: component, config, security" $logFile 36 | for dbPath in /nexus-data/db/{component,config,security}; do 37 | dbName=$(basename $dbPath); 38 | fileName=${dbName}-${bakVer}; 39 | log "Backup db ${dbName} to ${fileName}.bak" $logFile 40 | java -Xmx512m -jar /opt/sonatype/nexus/lib/support/nexus-orient-console.jar \ 41 | "CONNECT PLOCAL:/nexus-data/db/${dbName} admin admin; backup database ./${fileName}.bak -compressionLevel=3 -bufferSize=16384;" > ./${fileName}.log 42 | if [ $? -ne 0 ]; then 43 | log "Error ${$?}" $logFile; exit $?; 44 | fi 45 | done 46 | 47 | # 2. Copy the backup to a clean working location on a different filesystem so that any extraction doesn’t impact the existing production system. 48 | # 3. Shut down Nexus Repository. 49 | 50 | # 4. Run the following command from the clean working location containing your database backup. 51 | log "Perform migration, see logs in ${migratorDir}/logs" $logFile 52 | java -jar ${NEXUS_HOME}/nexus-db-migrator-*.jar --migration_type=h2 -y 53 | log "Migration completed successfully, see logs in ${migratorDir}/logs" $logFile 54 | 55 | # 5. Copy the resultant nexus.mv.db file to your $data-dir/db directory. 56 | log "Backup OrientDB dirctory to /nexus-data/db_${bakVer}" $logFile 57 | mv -f /nexus-data/db /nexus-data/db_${bakVer} && mkdir -p /nexus-data/db 58 | log "Copy the resultant nexus.mv.db file to /nexus-data/db" $logFile 59 | cp ./nexus.mv.db /nexus-data/db 60 | 61 | # 6. Edit the $data-dir/etc/nexus.properties file and add the following line: 62 | # nexus.datastore.enabled=true 63 | export INSTALL4J_ADD_VM_PARAMS="${INSTALL4J_ADD_VM_PARAMS} -Dnexus.datastore.enabled=true" 64 | fi 65 | 66 | # 7. Start Nexus Repository. 67 | cd /opt/sonatype/nexus 68 | exec ./bin/nexus run 69 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/config/metadata-keycloak.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | qMpXA1IiGC9rHa2xVQkU-uOBuiWr0NN-S-nMYLrutdE 9 | 10 | MIICmzCCAYMCBgF/taB6xzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIwMzIzMDcxMjQ3WhcNMzIwMzIzMDcxNDI3WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmGjoiiL2iA42dyEvAoNpsozWjBR0uGv4il3OmZ/Dxu4ihsRRumkIaTeYbzh7kQeFQbgJwwZ7TfpwUJCIi/iM2Z500BenzNc40AbXw6+vsukL8HH+xsl9eq82KH9LpnH+37EOrcpcxmOFDx3/FaWP4PR3JkDV4ZLA5QFaOd5YjgmtEB3yEfYJ0bz5gBY4uQ8CoHwogR7j9DLU+yUaf7S2ltEXEO0/1UzK4XKbPsthiRxyAv8JHFL+RTql1+ainfLSlXW//9CKSqDK7LXQzVDIBUTiGPfEynDyEqnRGrs3Qo/jlWSw/9AkC7Gqft+nGpiWX2yOhWQfpk6yghU8JhSGTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIrGooPMZ7gLTWYsFrGupTAYH8/M0Ro5Y/mfc+BUhfbOHxMyCZBsClSSKqKO9nMpYCa4A/Z0wfVfWOgpspnrdzar02cAeD9LuyUV+Nqucqn40M5ZiiRFgu3FhlzUK/cJnnyuVjeqdXD0ORI5jvxqdkLggtzBIy34SCIRfdnDzr+nwZSh0gnhI+KWEjWyCTHmN0Z6+QwJiNlV8oJ3etvg6glYtAaov2n/HlDXbfigVDtlX56yxkVvp1fpxsGSFJQpeeWTZCf7YtxnPUDzcq3YW5LNu6vAPV7qKH8OpVOqX/VuPHxRjZZhh+IlRsYTVYmV6I5G3nN3H9aN0QrkNdm9OIw= 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | urn:oasis:names:tc:SAML:2.0:nameid-format:persistent 19 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 20 | urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified 21 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image: 2 | # https://github.com/sonatype/docker-nexus3 3 | # https://hub.docker.com/r/sonatype/nexus3 4 | # docker build --progress=plain --no-cache -t /sonatype/nexus3:3.70.1-java11-ubi . 5 | # docker rmi $(docker images -f "dangling=true" -q) 6 | # docker run --user=0:0 --rm -it -p 8081:8081/tcp sonatype/nexus3:3.70.1-java11-ubi /bin/bash 7 | 8 | ARG NEXUS_BASE_IMAGE="sonatype/nexus3:3.75.1-java17-ubi" 9 | FROM $NEXUS_BASE_IMAGE 10 | USER root 11 | 12 | ARG NEXUS_PLUGIN_VERSION="3.75.1-01" 13 | ENV PLUG_VERSION="${NEXUS_PLUGIN_VERSION}" 14 | ENV NEXUS_PLUGINS="${NEXUS_HOME}/system" 15 | 16 | # Add nexus-pac4j-plugin.jar 17 | RUN rm -rf ${NEXUS_PLUGINS}/com/github/alanger/nexus/plugin/nexus-pac4j-plugin/ 18 | COPY nexus-pac4j-plugin/target/nexus-pac4j-plugin-*.jar ${NEXUS_PLUGINS}/com/github/alanger/nexus/plugin/nexus-pac4j-plugin/${PLUG_VERSION}/nexus-pac4j-plugin-${PLUG_VERSION}.jar 19 | RUN chmod -R 644 ${NEXUS_PLUGINS}/com/github/alanger/nexus/plugin/nexus-pac4j-plugin/${PLUG_VERSION}/nexus-pac4j-plugin-${PLUG_VERSION}.jar && \ 20 | echo "reference\:file\:com/github/alanger/nexus/plugin/nexus-pac4j-plugin/${PLUG_VERSION}/nexus-pac4j-plugin-${PLUG_VERSION}.jar = 200" >> /opt/sonatype/nexus/etc/karaf/startup.properties 21 | 22 | # Override nexus-repository-services.jar 23 | RUN rm -rf ${NEXUS_PLUGINS}/org/sonatype/nexus/nexus-repository-services/ 24 | COPY nexus-repository-services/target/nexus-repository-services-*.jar ${NEXUS_PLUGINS}/org/sonatype/nexus/nexus-repository-services/${PLUG_VERSION}/nexus-repository-services-${PLUG_VERSION}.jar 25 | RUN chmod -R 644 ${NEXUS_PLUGINS}/org/sonatype/nexus/nexus-repository-services/${PLUG_VERSION}/nexus-repository-services-${PLUG_VERSION}.jar 26 | 27 | # Add SSO configs 28 | COPY etc/nexus-default.properties /opt/sonatype/nexus/etc/nexus-default.properties 29 | COPY etc/jetty/nexus-web.xml /opt/sonatype/nexus/etc/jetty/nexus-web.xml 30 | COPY etc/jetty/jetty-sso.xml /opt/sonatype/nexus/etc/jetty/jetty-sso.xml 31 | COPY etc/h2db/.h2.server.properties /opt/sonatype/nexus/etc/h2db/.h2.server.properties 32 | COPY nexus-pac4j-plugin/src/main/config/ /opt/sonatype/nexus/etc/sso/config/ 33 | COPY nexus-pac4j-plugin/src/main/groovy/ /opt/sonatype/nexus/etc/sso/script/ 34 | RUN chown nexus:nexus -R /opt/sonatype/nexus/etc/sso/ 35 | 36 | # Add nexus-repository-ansiblegalaxy.jar, see https://github.com/l3ender/nexus-repository-ansiblegalaxy/issues/25 37 | # ARG ANSIBLEGALAXY_VERSION="0.3.3" 38 | # RUN rm -rf ${NEXUS_PLUGINS}/org/sonatype/nexus/plugins/nexus-repository-ansiblegalaxy/ 39 | # COPY nexus-docker/target/nexus-repository-ansiblegalaxy-*.jar ${NEXUS_PLUGINS}/org/sonatype/nexus/plugins/nexus-repository-ansiblegalaxy/${ANSIBLEGALAXY_VERSION}/nexus-repository-ansiblegalaxy-${ANSIBLEGALAXY_VERSION}.jar 40 | # RUN chmod -R 644 ${NEXUS_PLUGINS}/org/sonatype/nexus/plugins/nexus-repository-ansiblegalaxy/${ANSIBLEGALAXY_VERSION}/nexus-repository-ansiblegalaxy-${ANSIBLEGALAXY_VERSION}.jar 41 | # RUN echo "reference\:file\:org/sonatype/nexus/plugins/nexus-repository-ansiblegalaxy/${ANSIBLEGALAXY_VERSION}/nexus-repository-ansiblegalaxy-${ANSIBLEGALAXY_VERSION}.jar = 200" >> /opt/sonatype/nexus/etc/karaf/startup.properties 42 | 43 | # Add nexus-db-migrator.jar, see https://help.sonatype.com/en/sonatype-nexus-repository-3-71-0-release-notes.html 44 | # COPY nexus-docker/target/nexus-db-migrator-*.jar ${NEXUS_HOME} 45 | # COPY nexus-docker/migrator.sh ${NEXUS_HOME} 46 | # CMD [ "/opt/sonatype/nexus/migrator.sh" ] 47 | 48 | ENV INSTALL4J_ADD_VM_PARAMS="-Xms512m -Xmx2048m -Djava.util.prefs.userRoot=/nexus-data/javaprefs" 49 | 50 | # Setup permissions 51 | RUN chown nexus:nexus -R /opt/sonatype/nexus 52 | USER nexus 53 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/ui/NonTransitiveSearchComponent.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.ui; 2 | 3 | import org.sonatype.nexus.coreui.ComponentXO; 4 | import org.sonatype.nexus.coreui.SearchComponent; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import javax.inject.Inject; 9 | import javax.inject.Named; 10 | import javax.inject.Singleton; 11 | import org.sonatype.nexus.common.event.EventManager; 12 | import org.sonatype.nexus.extdirect.model.LimitedPagedResponse; 13 | import org.sonatype.nexus.extdirect.model.StoreLoadParameters; 14 | import org.sonatype.nexus.repository.Repository; 15 | import org.sonatype.nexus.repository.manager.RepositoryManager; 16 | import org.sonatype.nexus.repository.search.SearchService; 17 | import org.sonatype.nexus.repository.search.query.SearchResultsGenerator; 18 | import org.sonatype.nexus.repository.security.RepositoryPermissionChecker; 19 | import org.sonatype.nexus.repository.security.RepositoryViewPermission; 20 | import org.sonatype.nexus.security.SecurityHelper; 21 | 22 | import com.codahale.metrics.annotation.ExceptionMetered; 23 | import com.codahale.metrics.annotation.Timed; 24 | import com.softwarementors.extjs.djn.config.annotations.DirectAction; 25 | import org.apache.shiro.authz.annotation.RequiresPermissions; 26 | 27 | import static com.google.common.base.Preconditions.checkNotNull; 28 | import static java.util.Collections.singletonList; 29 | 30 | // POST: /service/extdirect 31 | // {"action":"coreui_Search_NonTransitive","method":"read","data":[{"formatSearch":false,"page":1,"start":0,"limit":300,"filter":[{"property":"keyword","value":"mystr"}]}],"type":"rpc","tid":8} 32 | @Named 33 | @Singleton 34 | @DirectAction(action = NonTransitiveSearchComponent.ACTION) 35 | public class NonTransitiveSearchComponent extends SearchComponent { 36 | 37 | public static final String ACTION = "coreui_Search_NonTransitive"; 38 | 39 | private final RepositoryPermissionChecker repositoryPermissionChecker; 40 | 41 | private final SecurityHelper securityHelper; 42 | 43 | private final RepositoryManager repositoryManager; 44 | 45 | @Inject 46 | public NonTransitiveSearchComponent(RepositoryPermissionChecker repositoryPermissionChecker, 47 | SecurityHelper securityHelper, RepositoryManager repositoryManager, SearchService searchService, 48 | @Named("${nexus.searchResultsLimit:-1000}") int searchResultsLimit, 49 | SearchResultsGenerator searchResultsGenerator, EventManager eventManager) { 50 | super(searchService, searchResultsLimit, searchResultsGenerator, eventManager); 51 | 52 | this.repositoryPermissionChecker = checkNotNull(repositoryPermissionChecker); 53 | this.securityHelper = checkNotNull(securityHelper); 54 | this.repositoryManager = checkNotNull(repositoryManager); 55 | log.trace("searchService {}, searchResultsGenerator: {}, eventManager: {}", searchService, 56 | searchResultsGenerator, eventManager); 57 | } 58 | 59 | @Override 60 | @Timed 61 | @ExceptionMetered 62 | @RequiresPermissions("nexus:search:read") 63 | public LimitedPagedResponse read(StoreLoadParameters parameters) { 64 | log.trace("parameters: {}", parameters); 65 | List componentXOs = super.read(parameters).getData().stream() 66 | .filter(c -> isNonTransitive(c.getRepositoryName())).collect(Collectors.toList()); 67 | return new LimitedPagedResponse<>(parameters.getLimit(), componentXOs.size(), componentXOs, false); 68 | } 69 | 70 | // Skip group repository 71 | private boolean isNonTransitive(String repositoryName) { 72 | Repository repository = repositoryManager.softGet(repositoryName); 73 | log.trace("repository: {}", repository); 74 | return !(repository == null || "group".equals(repository.getType().getValue())); 75 | } 76 | 77 | protected boolean isViewPermission(String format, String repositoryName) { 78 | RepositoryViewPermission rvp = new RepositoryViewPermission(format, repositoryName, singletonList("browse")); 79 | return securityHelper.isPermitted(rvp)[0]; 80 | } 81 | 82 | protected boolean isViewPermission(Repository repository) { 83 | return repositoryPermissionChecker.userCanReadOrBrowse(repository); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/datastore/H2TcpConsole.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.datastore; 2 | 3 | import java.io.File; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Named; 7 | import javax.inject.Singleton; 8 | 9 | import org.sonatype.nexus.common.app.ApplicationDirectories; 10 | import org.sonatype.nexus.common.app.ManagedLifecycle; 11 | import org.sonatype.nexus.common.event.EventAware; 12 | import org.sonatype.nexus.common.node.NodeAccess; 13 | import org.sonatype.nexus.common.stateguard.Guarded; 14 | import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport; 15 | 16 | import org.h2.tools.Server; 17 | 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | import static org.sonatype.nexus.common.app.ManagedLifecycle.Phase.TASKS; 20 | import static org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport.State.STARTED; 21 | 22 | /** 23 | * Enable H2 TCP console. 24 | *

25 | * 26 | * Use internal web console instance: 27 | * 28 | *

 29 |  * nexus.h2.httpListenerEnabled=true
 30 |  * nexus.h2.httpListenerPort=2480
 31 |  * docker compose up
 32 |  * Open http://localhost:2480 -> jdbc:h2:/nexus-data/db/nexus -> emplty login/pass
 33 |  * 
34 | * 35 | * Or different web console instance: 36 | * 37 | *
 38 |  * nexus.h2.tcpListenerEnabled=true
 39 |  * nexus.h2.tcpListenerPort=2424
 40 |  * docker compose --profile debug up
 41 |  * Open http://localhost:2480 -> jdbc:h2:tcp://nexus:2424/nexus -> emplty login/pass
 42 |  * 
43 | * 44 | * Example SQL: 45 | * 46 | *
{@code
 47 |  * SELECT * FROM EMAIL_CONFIGURATION
 48 |  * SELECT * FROM QRTZ_CRON_TRIGGERS
 49 |  * SELECT * FROM QRTZ_TRIGGERS
 50 |  * SELECT * FROM SECURITY_USER
 51 |  * SELECT * FROM ROLE
 52 |  * SELECT * FROM API_KEY
 53 |  * SELECT * FROM INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'API_KEY'
 54 |  * SELECT * FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 'API_KEY'
 55 |  * }
56 | * 57 | * TODO Not working, see {@link org.h2.util.SourceCompiler}: 58 | *
{@code
 59 |  * CREATE ALIAS NX_DECRYPT AS '
 60 |  * import com.github.alanger.nexus.plugin.DI;
 61 |  * @CODE
 62 |  * String nxDecrypt(String value) throws Exception {
 63 |  *     return DI.getInstance().encryptedString.decrypt(value);
 64 |  * }
 65 |  * ';
 66 |  * DROP ALIAS "NX_DECRYPT" IF EXISTS;
 67 |  * }
68 | * 69 | * @since 3.70.1-02 70 | * @see http://www.h2database.com/html/tutorial.html#spring 71 | * @see http://h2database.com/html/tutorial.html#command_line_tools 72 | * @see https://h2database.com/html/features.html#user_defined_functions 73 | * @see org.sonatype.nexus.datastore.mybatis.internal.H2WebConsole 74 | */ 75 | @Named 76 | @Singleton 77 | @ManagedLifecycle(phase = TASKS) 78 | public class H2TcpConsole extends StateGuardLifecycleSupport implements EventAware, EventAware.Asynchronous { 79 | private final File databasesDir; 80 | 81 | private final boolean tcpListenerEnabled; 82 | 83 | private final int tcpListenerPort; 84 | 85 | private Server h2Server; 86 | 87 | @Inject 88 | public H2TcpConsole(final ApplicationDirectories applicationDirectories, // 89 | @Named("${nexus.sso.h2.tcpListenerEnabled:-false}") final boolean tcpListenerEnabled, // 90 | @Named("${nexus.sso.h2.tcpListenerPort:-2424}") final int tcpListenerPort, // 91 | final NodeAccess nodeAccess) { 92 | checkNotNull(applicationDirectories); 93 | this.tcpListenerEnabled = tcpListenerEnabled; 94 | this.tcpListenerPort = tcpListenerPort; 95 | databasesDir = applicationDirectories.getWorkDirectory("db"); 96 | } 97 | 98 | @Override 99 | protected void doStart() throws Exception { 100 | if (tcpListenerEnabled) { 101 | h2Server = Server.createTcpServer("-tcpPort", String.valueOf(tcpListenerPort), "-tcpAllowOthers", "-ifExists", "-baseDir", 102 | databasesDir.toString()).start(); 103 | log.info("Activated"); 104 | } 105 | } 106 | 107 | @Override 108 | protected void doStop() throws Exception { 109 | if (h2Server != null) { 110 | // instance shutdown 111 | h2Server.shutdown(); 112 | h2Server = null; 113 | } 114 | } 115 | 116 | @Guarded(by = STARTED) 117 | public Server getH2Server() { 118 | return h2Server; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/AnonymousFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import javax.servlet.ServletRequest; 4 | import javax.servlet.ServletResponse; 5 | 6 | import org.apache.shiro.SecurityUtils; 7 | import org.apache.shiro.subject.Subject; 8 | import org.apache.shiro.subject.PrincipalCollection; 9 | import org.apache.shiro.util.ThreadContext; 10 | import org.apache.shiro.web.servlet.AdviceFilter; 11 | import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 12 | import org.apache.shiro.mgt.DefaultSubjectDAO; 13 | import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import org.sonatype.nexus.security.anonymous.AnonymousPrincipalCollection; 18 | 19 | // https://github.com/sonatype/nexus-public/blob/main/components/nexus-security/src/main/java/org/sonatype/nexus/security/anonymous/AnonymousFilter.java 20 | // https://github.com/sonatype/nexus-public/blob/main/components/nexus-base/src/main/java/org/sonatype/nexus/internal/security/anonymous/AnonymousManagerImpl.java 21 | // https://github.com/sonatype/nexus-public/blob/main/components/nexus-security/src/main/java/org/apache/shiro/nexus/NexusSessionStorageEvaluator.java 22 | public class AnonymousFilter extends AdviceFilter { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(AnonymousFilter.class); 25 | 26 | public static final String NAME = "anonPublic"; 27 | 28 | private static final String ORIGINAL_SUBJECT = AnonymousFilter.class.getName() + ".originalSubject"; 29 | 30 | public static final String DEFAULT_USER_ID = "anonymous"; 31 | 32 | public static final String DEFAULT_REALM_NAME = "NexusAuthorizingRealm"; 33 | 34 | private boolean sessionCreationEnabled = false; 35 | 36 | private String userId = DEFAULT_USER_ID; 37 | 38 | private String realmName = DEFAULT_REALM_NAME; 39 | 40 | public AnonymousFilter() { 41 | setName(NAME); 42 | } 43 | 44 | @Override 45 | protected boolean preHandle(final ServletRequest request, final ServletResponse response) throws Exception { 46 | Subject subject = SecurityUtils.getSubject(); 47 | if (subject.getPrincipal() == null /* && manager.isEnabled() */) { 48 | request.setAttribute(ORIGINAL_SUBJECT, subject); 49 | subject = buildSubject(); 50 | ThreadContext.bind(subject); 51 | logger.trace("Bound anonymous subject: {}", subject); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | public Subject buildSubject() { 58 | logger.trace("Building anonymous subject with user-id: {}, realm-name: {}", getUserId(), getRealmName()); 59 | PrincipalCollection principals = new AnonymousPrincipalCollection(getUserId(), getRealmName()); 60 | return new Subject.Builder() 61 | .principals(principals) 62 | .authenticated(false) 63 | .sessionCreationEnabled(this.sessionCreationEnabled) 64 | .buildSubject(); 65 | } 66 | 67 | @Override 68 | public void afterCompletion(final ServletRequest request, final ServletResponse response, final Exception exception) 69 | throws Exception { 70 | Subject subject = (Subject) request.getAttribute(ORIGINAL_SUBJECT); 71 | if (subject != null) { 72 | logger.trace("Binding original subject: {}", subject); 73 | ThreadContext.bind(subject); 74 | } 75 | } 76 | 77 | public void setSessionStorageEnabled(boolean enabled) { 78 | DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); 79 | DefaultSubjectDAO subjectDAO = (DefaultSubjectDAO) securityManager.getSubjectDAO(); 80 | DefaultSessionStorageEvaluator sessionStorageEvaluator = (DefaultSessionStorageEvaluator) subjectDAO 81 | .getSessionStorageEvaluator(); 82 | sessionStorageEvaluator.setSessionStorageEnabled(enabled); 83 | } 84 | 85 | public void setSessionCreationEnabled(boolean sessionCreationEnabled) { 86 | this.sessionCreationEnabled = sessionCreationEnabled; 87 | } 88 | 89 | public String getUserId() { 90 | return userId; 91 | } 92 | 93 | public void setUserId(String userId) { 94 | this.userId = userId; 95 | } 96 | 97 | public String getRealmName() { 98 | return realmName; 99 | } 100 | 101 | public void setRealmName(String realmName) { 102 | this.realmName = realmName; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jSecurityFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import java.io.IOException; 4 | import javax.servlet.FilterChain; 5 | import javax.servlet.ServletException; 6 | import javax.servlet.ServletRequest; 7 | import javax.servlet.ServletResponse; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import org.pac4j.jee.filter.SecurityFilter; 11 | import org.apache.shiro.SecurityUtils; 12 | import org.apache.shiro.subject.Subject; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | /** 17 | * Transparent authorization filter. Skips a request if it does not meet the specified criteria. 18 | * 19 | * @see org.sonatype.nexus.security.authc.NexusAuthenticationFilter 20 | * @see org.sonatype.nexus.security.authc.apikey.ApiKeyAuthenticationFilter 21 | */ 22 | public class Pac4jSecurityFilter extends SecurityFilter { 23 | 24 | protected static Logger log = LoggerFactory.getLogger(Pac4jSecurityFilter.class); 25 | 26 | private boolean ifNotAuthenticated = true; 27 | private boolean ifNotAuthzHeader = true; 28 | private boolean ifNotXMLHttpRequest = true; 29 | private boolean ifBrowserRequest = true; 30 | 31 | public boolean isAuthenticated() { 32 | Subject subject = SecurityUtils.getSubject(); 33 | return subject.getPrincipal() != null && subject.isAuthenticated(); 34 | } 35 | 36 | public boolean isAuthzHeader(HttpServletRequest request) { 37 | String header = request.getHeader("Authorization"); 38 | return header != null && header.length() > 0; 39 | } 40 | 41 | public boolean isBrowserRequest(HttpServletRequest request) { 42 | String header = request.getHeader("User-Agent"); 43 | return header != null && header.toLowerCase().startsWith("mozilla"); 44 | } 45 | 46 | public boolean isXMLHttpRequest(HttpServletRequest request) { 47 | String header = request.getHeader("X-Requested-With"); 48 | return header != null && header.equals("XMLHttpRequest"); 49 | } 50 | 51 | @Override 52 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 53 | throws IOException, ServletException { 54 | HttpServletRequest request = (HttpServletRequest) req; 55 | HttpServletResponse response = (HttpServletResponse) resp; 56 | 57 | if (request.getAttribute(getClass().getCanonicalName()) != null) { 58 | chain.doFilter(request, response); 59 | return; 60 | } 61 | request.setAttribute(getClass().getCanonicalName(), true); 62 | 63 | boolean need = true; 64 | if (ifNotAuthenticated) { 65 | need = !isAuthenticated(); 66 | } 67 | if (need && ifNotAuthzHeader) { 68 | need = !isAuthzHeader(request); 69 | } 70 | if (need && ifNotXMLHttpRequest) { 71 | need = !isXMLHttpRequest(request); 72 | } 73 | if (need && ifBrowserRequest) { 74 | need = isBrowserRequest(request); 75 | } 76 | 77 | if (need) { 78 | try { 79 | super.doFilter(request, response, chain); 80 | } catch (IOException | ServletException e) { 81 | throw e; 82 | } catch (Exception e) { 83 | log.warn("Filter error: ", e); 84 | throw new ServletException(e); 85 | } 86 | } else { 87 | chain.doFilter(request, response); 88 | } 89 | } 90 | 91 | public boolean isIfNotAuthenticated() { 92 | return ifNotAuthenticated; 93 | } 94 | 95 | public void setIfNotAuthenticated(boolean ifNotAuthenticated) { 96 | this.ifNotAuthenticated = ifNotAuthenticated; 97 | } 98 | 99 | public boolean isIfNotAuthzHeader() { 100 | return ifNotAuthzHeader; 101 | } 102 | 103 | public void setIfNotAuthzHeader(boolean ifNotAuthzHeader) { 104 | this.ifNotAuthzHeader = ifNotAuthzHeader; 105 | } 106 | 107 | public boolean isIfNotXMLHttpRequest() { 108 | return ifNotXMLHttpRequest; 109 | } 110 | 111 | public void setIfNotXMLHttpRequest(boolean ifNotXMLHttpRequest) { 112 | this.ifNotXMLHttpRequest = ifNotXMLHttpRequest; 113 | } 114 | 115 | public boolean isIfBrowserRequest() { 116 | return ifBrowserRequest; 117 | } 118 | 119 | public void setIfBrowserRequest(boolean ifBrowserRequest) { 120 | this.ifBrowserRequest = ifBrowserRequest; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /nexus-repository-services/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | com.github.a-langer 9 | nexus-sso 10 | 3.75.1 11 | .. 12 | 13 | nexus-repository-services 14 | jar 15 | 16 | 17 | 18 | org.sonatype.nexus 19 | nexus-repository-services 20 | ${nexus.plugin.version} 21 | true 22 | 23 | 24 | org.sonatype.nexus 25 | nexus-repository-content 26 | ${nexus.plugin.version} 27 | provided 28 | true 29 | 30 | 31 | org.sonatype.nexus.plugins 32 | nexus-repository-maven 33 | ${nexus.plugin.version} 34 | provided 35 | true 36 | 37 | 38 | 39 | 40 | src/main/java 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-shade-plugin 45 | 3.4.1 46 | 47 | 48 | package 49 | 50 | shade 51 | 52 | 53 | ${project.build.directory}/${project.artifactId}-${project.version}.jar 54 | ${project.build.directory}/dependency-reduced-pom.xml 55 | true 56 | 57 | 58 | org.sonatype.nexus:nexus-repository-services 59 | com.github.a-langer:nexus-repository-services 60 | 61 | 62 | com.github.a-langer:nexus-sso 63 | com.github.a-langer:nexus-bootstrap 64 | com.github.a-langer:shiro-ext 65 | io.buji:buji-pac4j 66 | org.apache.commons:commons-configuration2 67 | com.github.paultuckey:urlrewritefilter 68 | 69 | 70 | 71 | 72 | org.sonatype.nexus:nexus-repository-services 73 | 74 | org/sonatype/nexus/repository/group/GroupFacetImpl.class 75 | org/sonatype/nexus/repository/group/GroupFacetImpl$Config.class 76 | 77 | 78 | 79 | com.github.a-langer:nexus-repository-services 80 | 81 | org/sonatype/nexus/repository/group/* 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /etc/jetty/jetty-sso.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | REQUEST 32 | 33 | 34 | 35 | 36 | ASYNC 37 | 38 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | /rewrite-status.* 65 | /service/rest/rewrite-status 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 127.0.0.1|/service/rest/rewrite-status 80 | 127.0.0.1|/rewrite-status 81 | 127.0.0.1|/service/rest/rewrite-status/* 82 | 127.0.0.1|/rewrite-status/* 83 | 84 | 85 | true 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/Docker.md: -------------------------------------------------------------------------------- 1 | # Docker Compose configuration 2 | 3 | [Docker compose](../compose.yml) configuration may be extended with [compose.override.yml][0] (for example, pass additional files to the container). Example settings for an production environment: 4 | 5 | 1. Set variables for your production environment, ex.: 6 | 7 | ```bash 8 | export NEXUS_USER=$(id -u) NEXUS_GROUP=$(id -g) 9 | export NEXUS_ETC="./etc_prod" NEXUS_DATA="./data_prod" 10 | ``` 11 | 12 | 2. Prepare Docker production configuration, ex.: 13 | 14 | ```bash 15 | cp ./_compose.override_prod.yml ./compose.override.yml 16 | cp ./.env ./.env_prod 17 | sed -i "s/NEXUS_USER=.*/NEXUS_USER=\"${NEXUS_USER}\"/" ./.env_prod 18 | sed -i "s/NEXUS_GROUP=.*/NEXUS_GROUP=\"${NEXUS_GROUP}\"/" ./.env_prod 19 | sed -i "s|NEXUS_ETC=.*|NEXUS_ETC=\"${NEXUS_ETC}\"|" ./.env_prod 20 | sed -i "s|NEXUS_DATA=.*|NEXUS_DATA=\"${NEXUS_DATA}\"|" ./.env_prod 21 | ``` 22 | 23 | 3. Prepare Nexus directories and configuration templates, ex.: 24 | 25 | ```bash 26 | mkdir -p ${NEXUS_ETC}/sso/config/ ${NEXUS_DATA} 27 | cp -rf ./etc/* ${NEXUS_ETC}/ 28 | cp -rf ./nexus-pac4j-plugin/src/main/config/* ${NEXUS_ETC}/sso/config/ 29 | ``` 30 | 31 | 4. Modify shiro.ini, metadata.xml and sp-metadata.xml, see [docs/SAML.md](./SAML.md). 32 | 5. Enable Nginx SSL configuration, ex.: 33 | 34 | ```bash 35 | cp ${NEXUS_ETC}/nginx/_ssl.conf ${NEXUS_ETC}/nginx/ssl.conf 36 | openssl req -x509 -nodes -sha256 -days 3650 -newkey rsa:2048 -keyout ${NEXUS_ETC}/nginx/tls/site.key -out ${NEXUS_ETC}/nginx/tls/site.crt 37 | ``` 38 | 39 | 6. Change others settings for your production environment, see examples in [_compose.override_prod.yml](../_compose.override_prod.yml) and [_compose.override.yml](../_compose.override.yml). 40 | 41 | ## DB console 42 | 43 | **DB console** - interface to interact with an embedded database. Available in the following modes: 44 | 45 | 1. H2 TCP server + command line: 46 | 47 | Start service with H2 TCP server: 48 | 49 | ```bash 50 | export INSTALL4J_ADD_VM_PARAMS="-Dnexus.sso.h2.tcpListenerEnabled=true -Dnexus.sso.h2.tcpListenerPort=2424" 51 | docker compose up 52 | ``` 53 | 54 | Example SQL executing from command line: 55 | 56 | ```bash 57 | docker compose exec -- nexus bash 58 | 59 | # Locking admin account 60 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.Shell -url jdbc:h2:tcp://nexus:2424/nexus <<<"update SECURITY_USER set STATUS = 'locked' WHERE ID = 'admin';" 61 | 62 | # Unlocking admin account 63 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.Shell -url jdbc:h2:tcp://nexus:2424/nexus <<<"update SECURITY_USER set STATUS = 'active' WHERE ID = 'admin';" 64 | ``` 65 | 66 | 2. H2 TCP server + Web console in different container: 67 | 68 | Start service with H2 TCP server and Web console in different container if the "debug" profile is set (does not start by default): 69 | 70 | ```bash 71 | export INSTALL4J_ADD_VM_PARAMS="-Dnexus.sso.h2.tcpListenerEnabled=true -Dnexus.sso.h2.tcpListenerPort=2424" 72 | docker compose --profile debug up 73 | ``` 74 | 75 | Open web browser: `http://localhost:2480` -> JDBC URL: `jdbc:h2:tcp://nexus:2424/nexus` -> User/Password: `empty` -> `Connect`. 76 | 77 | 3. Or use embedded Web console: 78 | 79 | Expose port `2481` in compose config before running, then: 80 | 81 | ```bash 82 | export INSTALL4J_ADD_VM_PARAMS="-Dnexus.h2.httpListenerEnabled=true -Dnexus.h2.httpListenerPort=2481" 83 | docker compose up 84 | ``` 85 | 86 | Open web browser: `http://localhost:2481` -> JDBC URL: `jdbc:h2:/nexus-data/db/nexus` -> User/Password: `empty` -> `Connect`. 87 | 88 | ## Rebuild DB 89 | 90 | If the integrity of the H2 database is compromised, follow the instruction [1] and [2]: 91 | 92 | ```bash 93 | docker compose down 94 | docker compose run -w /nexus-data/db --rm nexus bash 95 | 96 | # Backup nexus.mv.db to nexus.zip (if required) 97 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.Script -url jdbc:h2:./nexus -script nexus.zip -options compression zip 98 | 99 | # Create a dump nexus.h2.sql of the current database nexus.mv.db 100 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.Recover -db nexus -trace 101 | 102 | # Rename the corrupt database file to nexus.mv.db.bak 103 | mv nexus.mv.db nexus.mv.db.bak 104 | 105 | # Import the dump nexus.h2.sql to new nexus.mv.db 106 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.RunScript -url jdbc:h2:./nexus -script nexus.h2.sql -checkResults 107 | 108 | # Run and check logs 109 | docker compose up -d 110 | docker compose logs -f 111 | 112 | # Restore nexus.mv.db from nexus.zip (if required) 113 | java -cp $NEXUS_HOME/system/com/h2database/h2/*/h2*.jar org.h2.tools.RunScript -url jdbc:h2:./nexus -script nexus.zip -options compression zip 114 | ``` 115 | 116 | [0]: https://docs.docker.com/compose/multiple-compose-files/merge/ "Merge Compose files" 117 | [1]: https://stackoverflow.com/a/41898677 "Rebuild H2DB" 118 | [2]: https://www.h2database.com/html/tutorial.html#upgrade_backup_restore "Upgrade, Backup, and Restore" 119 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jPrincipalName.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.realm; 2 | 3 | import java.io.Serializable; 4 | import java.security.Principal; 5 | import java.util.Collection; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | 10 | import com.github.alanger.shiroext.realm.IPrincipalName; 11 | import com.github.alanger.shiroext.realm.IUserPrefix; 12 | 13 | import org.pac4j.core.profile.AnonymousProfile; 14 | import org.pac4j.core.profile.UserProfile; 15 | import org.pac4j.core.util.CommonHelper; 16 | 17 | /** 18 | * Moved from shiro-ext library for recompile with 19 | * {@link org.pac4j.core.profile.UserProfile} as interface instead of class. 20 | * 21 | * @since buji-pac4j:5.0.0 22 | * @since Nexus:3.70.0 23 | * @see https://github.com/bujiio/buji-pac4j/blob/master/src/main/java/io/buji/pac4j/subject/Pac4jPrincipal.java 24 | */ 25 | public class Pac4jPrincipalName implements Principal, Serializable, IUserPrefix, IPrincipalName { 26 | 27 | private final List profiles; 28 | private final boolean byName; 29 | private String userPrefix = ""; 30 | private String principalNameAttribute; 31 | 32 | public Pac4jPrincipalName(final List profiles) { 33 | this(profiles, null, false); 34 | } 35 | 36 | public Pac4jPrincipalName(final List profiles, String principalNameAttribute, 37 | boolean byName) { 38 | this.profiles = profiles; 39 | this.principalNameAttribute = CommonHelper.isBlank(principalNameAttribute) ? null 40 | : principalNameAttribute.trim(); 41 | this.byName = byName; 42 | } 43 | 44 | public Pac4jPrincipalName(final List profiles, String principalNameAttribute) { 45 | this.profiles = profiles; 46 | this.principalNameAttribute = CommonHelper.isBlank(principalNameAttribute) ? null 47 | : principalNameAttribute.trim(); 48 | this.byName = !CommonHelper.isBlank(principalNameAttribute); 49 | } 50 | 51 | @Override 52 | public String getUserPrefix() { 53 | return userPrefix; 54 | } 55 | 56 | @Override 57 | public void setUserPrefix(String userPrefix) { 58 | this.userPrefix = userPrefix; 59 | } 60 | 61 | @Override 62 | public String getPrincipalNameAttribute() { 63 | return principalNameAttribute; 64 | } 65 | 66 | @Override 67 | public void setPrincipalNameAttribute(String principalNameAttribute) { 68 | this.principalNameAttribute = principalNameAttribute; 69 | } 70 | 71 | public boolean isByName() { 72 | return byName; 73 | } 74 | 75 | public UserProfile getProfile() { 76 | return flatIntoOneProfile(this.profiles).get(); 77 | } 78 | 79 | // Compatibility with buji-pac4j 4.1.1 80 | public static Optional flatIntoOneProfile(final Collection profiles) { 81 | final Optional profile = profiles.stream().filter(p -> p != null && !(p instanceof AnonymousProfile)) 82 | .findFirst(); 83 | if (profile.isPresent()) { 84 | return profile; 85 | } else { 86 | return profiles.stream().filter(Objects::nonNull).findFirst(); 87 | } 88 | } 89 | 90 | public List getProfiles() { 91 | return this.profiles; 92 | } 93 | 94 | /** 95 | * Equals by string for compatibility with internal ApiKeyStore implementation 96 | * 97 | * @since 3.70.1-02 - Equals by string 98 | * @see org.sonatype.nexus.internal.security.apikey.ApiKeyStoreImpl#principalMatches 99 | */ 100 | @Override 101 | public boolean equals(Object o) { 102 | if (o instanceof String) 103 | return ((String) o).equals(getName()); 104 | if (this == o) 105 | return true; 106 | if (o == null || getClass() != o.getClass()) 107 | return false; 108 | 109 | final Pac4jPrincipalName that = (Pac4jPrincipalName) o; 110 | return profiles != null ? profiles.equals(that.profiles) : that.profiles == null; 111 | } 112 | 113 | @Override 114 | public int hashCode() { 115 | return profiles != null ? profiles.hashCode() : 0; 116 | } 117 | 118 | @Override 119 | public String getName() { 120 | final UserProfile profile = this.getProfile(); 121 | if (null == principalNameAttribute) { 122 | return profile.getId(); 123 | } 124 | final Object attrValue = profile.getAttribute(principalNameAttribute); 125 | return (null == attrValue) ? null : getUserPrefix() + String.valueOf(attrValue).replaceAll("(^\\[)|(\\]$)", ""); 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | if (isByName()) { 131 | String name = getName(); 132 | return name != null ? name : getUserPrefix() + getProfile().getId(); 133 | } else { 134 | return CommonHelper.toNiceString(this.getClass(), "profiles", getProfiles()); 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/Pac4jAuthenticationListener.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import java.util.Map; 4 | import java.util.Properties; 5 | 6 | import org.apache.shiro.authc.AuthenticationException; 7 | import org.apache.shiro.authc.AuthenticationInfo; 8 | import org.apache.shiro.authc.AuthenticationToken; 9 | import org.apache.shiro.authc.AuthenticationListener; 10 | import org.apache.shiro.subject.PrincipalCollection; 11 | import com.github.alanger.nexus.plugin.DI; 12 | import com.github.alanger.nexus.plugin.realm.NexusPac4jRealm; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | /** 17 | * SSO authentication listener. 18 | * 19 | * @deprecated Since {@code 3.70.1-02} and will be removed. 20 | * Attribute mapping moved to {@link com.github.alanger.nexus.plugin.realm.NexusPac4jRealm NexusPac4jRealm}. 21 | */ 22 | @Deprecated(since = "3.70.1-02", forRemoval = true) 23 | public class Pac4jAuthenticationListener implements AuthenticationListener { 24 | 25 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 26 | 27 | private final NexusPac4jRealm pac4jRealm; 28 | 29 | public Pac4jAuthenticationListener() { 30 | this.pac4jRealm = DI.getInstance().pac4jRealm; 31 | } 32 | 33 | @Override 34 | public void onSuccess(AuthenticationToken token, AuthenticationInfo ai) { 35 | logger.warn("Class '{}' has been deprecated since 3.70.1-02 and will be removed in next release", getClass().getCanonicalName()); 36 | } 37 | 38 | @Override 39 | public void onFailure(AuthenticationToken token, AuthenticationException ae) { 40 | // none 41 | } 42 | 43 | @Override 44 | public void onLogout(PrincipalCollection principals) { 45 | // none 46 | } 47 | 48 | /** 49 | * @deprecated Since 3.70.1-02 and will be removed. 50 | * Use {@link com.github.alanger.nexus.plugin.realm.NexusPac4jRealm#getAttrs() NexusPac4jRealm#getAttrs}. 51 | * */ 52 | @Deprecated(since = "3.70.1-02", forRemoval = true) 53 | public Map getAttrs() { 54 | logger.warn("Property 'attrs' has been deprecated since 3.70.1-02 and will be removed in next release, use 'pac4jRealm.attrs[id]'"); 55 | return this.pac4jRealm.getAttrs(); 56 | } 57 | 58 | /** 59 | * @deprecated Since 3.70.1-02 and will be removed. 60 | * Use {@link com.github.alanger.nexus.plugin.realm.NexusPac4jRealm#getMap() NexusPac4jRealm#getMap}. 61 | * */ 62 | @Deprecated(since = "3.70.1-02", forRemoval = true) 63 | public Properties getMap() { 64 | logger.warn("Property 'map' has been deprecated since 3.70.1-02 and will be removed in next release, use 'pac4jRealm.map(attrs)'"); 65 | return this.pac4jRealm.getMap(); 66 | } 67 | 68 | /** @deprecated Since 3.70.1-02 and will be removed */ 69 | @Deprecated(since = "3.70.1-02", forRemoval = true) 70 | public void setPrincipalClass(String className) throws ClassNotFoundException { 71 | logger.warn("Property 'principalClass' has been deprecated since 3.70.1-02 and will be removed in next release"); 72 | } 73 | 74 | /** @deprecated Since 3.70.1-02 and will be removed */ 75 | @Deprecated(since = "3.70.1-02", forRemoval = true) 76 | public void setRealmClass(String className) throws ClassNotFoundException { 77 | logger.warn("Property 'realmClass' has been deprecated since 3.70.1-02 and will be removed in next release"); 78 | } 79 | 80 | /** @deprecated Since 3.70.1-02 and will be removed */ 81 | @Deprecated(since = "3.70.1-02", forRemoval = true) 82 | public void setUserQuery(String userQuery) { 83 | logger.warn("Property 'useQuery' has been deprecated since 3.70.1-02 and will be removed in next release"); 84 | } 85 | 86 | /** @deprecated Since 3.70.1-02 and will be removed */ 87 | @Deprecated(since = "3.70.1-02", forRemoval = true) 88 | public void setUserUpdate(String userUpdate) { 89 | logger.warn("Property 'userUpdate' has been deprecated since 3.70.1-02 and will be removed in next release"); 90 | } 91 | 92 | /** @deprecated Since 3.70.1-02 and will be removed */ 93 | @Deprecated(since = "3.70.1-02", forRemoval = true) 94 | public void setUserInsert(String userInsert) { 95 | logger.warn("Property 'userInsert' has been deprecated since 3.70.1-02 and will be removed in next release"); 96 | } 97 | 98 | /** @deprecated Since 3.70.1-02 and will be removed */ 99 | @Deprecated(since = "3.70.1-02", forRemoval = true) 100 | public void setRoleQuery(String roleQuery) { 101 | logger.warn("Property 'roleQuery' has been deprecated since 3.70.1-02 and will be removed in next release"); 102 | } 103 | 104 | /** @deprecated Since 3.70.1-02 and will be removed */ 105 | @Deprecated(since = "3.70.1-02", forRemoval = true) 106 | public void setRoleUpdate(String roleUpdate) { 107 | logger.warn("Property 'roleUpdate' has been deprecated since 3.70.1-02 and will be removed in next release"); 108 | } 109 | 110 | /** @deprecated Since 3.70.1-02 and will be removed */ 111 | @Deprecated(since = "3.70.1-02", forRemoval = true) 112 | public void setRoleInsert(String roleInsert) { 113 | logger.warn("Property 'roleInsert' has been deprecated since 3.70.1-02 and will be removed in next release"); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/Init.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import javax.inject.Singleton; 6 | import javax.servlet.ServletContext; 7 | 8 | import org.sonatype.nexus.common.app.ManagedLifecycle; 9 | import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport; 10 | import com.google.common.base.Preconditions; 11 | import java.io.File; 12 | import java.net.URL; 13 | import org.codehaus.groovy.control.CompilerConfiguration; 14 | import org.codehaus.groovy.tools.RootLoader; 15 | import org.opensaml.xmlsec.signature.support.SignatureValidator; 16 | import groovy.lang.GroovyClassLoader; 17 | import net.shibboleth.utilities.java.support.logic.ConstraintViolationException; 18 | import java.util.Arrays; 19 | import java.util.stream.Collectors; 20 | import static org.sonatype.nexus.common.app.ManagedLifecycle.Phase.TASKS; 21 | 22 | /** 23 | * Initialization of SSO scripts and configs. 24 | */ 25 | @Singleton 26 | @Named 27 | @ManagedLifecycle(phase = TASKS) 28 | public class Init extends StateGuardLifecycleSupport { 29 | 30 | public static final String CONFIG_PATH = "configPath"; 31 | public static final String CONFIG_PATH_DEFAULT = "file:etc/sso/config/shiro.ini"; 32 | 33 | public static final String SCRIPT_DIR = "scriptDir"; 34 | public static final String SCRIPT_DIR_DEFAULT = "etc/sso/script/"; 35 | 36 | public static final String MAIN_FILE = "mainFile"; 37 | public static final String MAIN_FILE_DEFAULT = "com/github/alanger/nexus/bootstrap/Main.java"; 38 | 39 | private final ServletContext servletContext; 40 | 41 | @Inject 42 | public Init(final ServletContext servletContext) { 43 | this.servletContext = Preconditions.checkNotNull(servletContext); 44 | 45 | // Default config location 46 | if (getConfigPath() == null) { 47 | setConfigPath(CONFIG_PATH_DEFAULT); 48 | } 49 | 50 | log.trace("Init servletContext: {}", this.servletContext); 51 | } 52 | 53 | @SuppressWarnings({"java:S2637", "null"}) 54 | @Override 55 | public void doStart() throws Exception { 56 | log.trace("Init doStart()"); 57 | 58 | ClassLoader oldTccl = Thread.currentThread().getContextClassLoader(); 59 | GroovyClassLoader gcl = getGroovyClassLoader(); 60 | 61 | try { 62 | // For Shiro Object Builder 63 | Thread.currentThread().setContextClassLoader(gcl); 64 | 65 | // FIX SAMLSignatureValidationException: Signature is not trusted 66 | try { 67 | SignatureValidator.validate(null, null); 68 | } catch (ConstraintViolationException e) { 69 | // OK: Validation credential cannot be null 70 | log.debug("Initialize SPI SignatureValidationProvider done"); 71 | } 72 | 73 | String scriptDir = getInitParameter(SCRIPT_DIR) != null ? getInitParameter(SCRIPT_DIR) : SCRIPT_DIR_DEFAULT; 74 | String mainFile = getInitParameter(MAIN_FILE) != null ? getInitParameter(MAIN_FILE) : MAIN_FILE_DEFAULT; 75 | // String mainClassName = getInitParameter("mainClassName") ?: "com.github.alanger.nexus.bootstrap.Main"; 76 | 77 | File scriptPath = new File(scriptDir); 78 | log.trace("scriptPath: {}", scriptPath.getAbsolutePath()); 79 | gcl.addURL(scriptPath.toURI().toURL()); 80 | 81 | Class groovyClass = gcl.parseClass(new File(scriptPath, mainFile)); 82 | // Class groovyClass = gcl.loadClass(mainClassName, true, false, true); 83 | 84 | // com.github.alanger.nexus.bootstrap.Main 85 | log.trace("groovyClass: {}", groovyClass.getCanonicalName()); 86 | 87 | groovyClass.getDeclaredConstructor(ServletContext.class).newInstance(servletContext); 88 | } catch (Exception e) { 89 | log.error("Init doStart() error", e); 90 | throw e; 91 | } finally { 92 | Thread.currentThread().setContextClassLoader(oldTccl); 93 | } 94 | 95 | } 96 | 97 | //-- Utils --// 98 | 99 | private String getInitParameter(String name) { 100 | return servletContext != null ? servletContext.getInitParameter(name) : null; 101 | } 102 | 103 | private GroovyClassLoader getGroovyClassLoader() { 104 | GroovyClassLoader gcl = (GroovyClassLoader) servletContext.getAttribute(GroovyClassLoader.class.getCanonicalName()); 105 | // Creates a new classloader if log level = TRACE 106 | if (gcl == null || isTraceEnabled()) { 107 | CompilerConfiguration cfg = new CompilerConfiguration(); 108 | cfg.setSourceEncoding("UTF-8"); 109 | cfg.setScriptExtensions(Arrays.stream(new String[] {"groovy", "java"}).collect(Collectors.toSet())); 110 | cfg.setRecompileGroovySource(true); 111 | cfg.setMinimumRecompilationInterval(0); 112 | 113 | RootLoader rl = new RootLoader(new URL[] {}, getClass().getClassLoader()); 114 | gcl = new GroovyClassLoader(rl, cfg); 115 | servletContext.setAttribute(GroovyClassLoader.class.getCanonicalName(), gcl); 116 | } 117 | gcl.clearCache(); 118 | gcl.setShouldRecompile(true); 119 | 120 | return gcl; 121 | } 122 | 123 | public String getConfigPath() { 124 | return this.servletContext.getInitParameter(CONFIG_PATH); 125 | } 126 | 127 | public void setConfigPath(String configPath) { 128 | this.servletContext.setInitParameter(CONFIG_PATH, configPath); 129 | } 130 | 131 | public boolean isTraceEnabled() { 132 | return log.isTraceEnabled(); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/rest/NugetApiKeyResource.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.rest; 2 | 3 | import com.codahale.metrics.annotation.ExceptionMetered; 4 | import com.google.common.base.Preconditions; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Base64; 7 | import javax.inject.Inject; 8 | import javax.inject.Named; 9 | import javax.inject.Singleton; 10 | import javax.validation.Valid; 11 | import javax.validation.constraints.NotNull; 12 | import javax.ws.rs.DELETE; 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.QueryParam; 17 | import javax.ws.rs.core.Response; 18 | import org.apache.shiro.authz.annotation.RequiresAuthentication; 19 | import org.apache.shiro.authz.annotation.RequiresPermissions; 20 | import org.apache.shiro.subject.PrincipalCollection; 21 | import org.sonatype.goodies.common.ComponentSupport; 22 | import org.sonatype.nexus.common.wonderland.AuthTicketService; 23 | import org.sonatype.nexus.rest.NotCacheable; 24 | import org.sonatype.nexus.rest.Resource; 25 | import org.sonatype.nexus.rest.WebApplicationMessageException; 26 | import org.sonatype.nexus.security.SecurityHelper; 27 | import org.sonatype.nexus.security.authc.apikey.ApiKey; 28 | import org.sonatype.nexus.validation.Validate; 29 | import com.github.alanger.nexus.plugin.apikey.ApiTokenService; 30 | import com.github.alanger.nexus.plugin.realm.NexusTokenRealm; 31 | 32 | /** 33 | * Nuget API key implementation for SSO. 34 | *

35 | * GET: /service/rest/internal/nuget-api-key?authToken=XXXXX 36 | * 37 | * @see com.github.alanger.nexus.plugin.apikey.ApiTokenService 38 | */ 39 | @Named 40 | @Singleton 41 | @Path(NugetApiKeyResource.RESOURCE_URI) 42 | @Produces({"application/json"}) 43 | public class NugetApiKeyResource extends ComponentSupport implements Resource { 44 | 45 | public static final String RESOURCE_URI = "/internal/nuget-api-key"; 46 | 47 | public static final String DOMAIN = ApiTokenService.DOMAIN; 48 | 49 | private final ApiTokenService apiTokenService; 50 | 51 | private final AuthTicketService authTicketService; 52 | 53 | private final SecurityHelper securityHelper; 54 | 55 | private final NexusTokenRealm nexusTokenRealm; 56 | 57 | @Inject 58 | public NugetApiKeyResource(final ApiTokenService apiTokenService // 59 | , final AuthTicketService authTicketService // 60 | , final SecurityHelper securityHelper // 61 | , final NexusTokenRealm nexusTokenRealm) { 62 | super(); 63 | this.apiTokenService = Preconditions.checkNotNull(apiTokenService); 64 | this.authTicketService = Preconditions.checkNotNull(authTicketService); 65 | this.securityHelper = Preconditions.checkNotNull(securityHelper); 66 | this.nexusTokenRealm = Preconditions.checkNotNull(nexusTokenRealm); 67 | 68 | log.trace("NugetApiKeyResource apiTokenService: {}, authTicketService: {}, securityHelper: {}, nexusTokenRealm: {}", // 69 | apiTokenService, authTicketService, securityHelper, nexusTokenRealm); 70 | } 71 | 72 | @GET 73 | @ExceptionMetered 74 | @RequiresPermissions({"nexus:apikey:read"}) 75 | @Validate 76 | @NotCacheable 77 | public NugetApiKeyXO readKey(@NotNull @Valid @QueryParam("authToken") String base64AuthToken) { 78 | validateAuthToken(base64AuthToken); 79 | PrincipalCollection principals = this.securityHelper.subject().getPrincipals(); 80 | 81 | // Read by primary principal or create a new 82 | char[] apiKey = null; 83 | try { 84 | apiKey = apiTokenService.getApiKey(DOMAIN, principals).map(ApiKey::getApiKey) 85 | .orElseGet(() -> apiTokenService.createApiKey(DOMAIN, principals)); 86 | } catch (Exception e) { 87 | log.trace("Error read apiKey from store by principal {}: {}", principals.getPrimaryPrincipal(), e); 88 | apiTokenService.deleteApiKey(DOMAIN, principals); 89 | } 90 | 91 | log.trace("Read apiKey for principal {} = {}", principals.getPrimaryPrincipal(), 92 | (apiKey != null && apiKey.length > 0) ? "***" : null); 93 | 94 | return new NugetApiKeyXO(apiKey); 95 | } 96 | 97 | @DELETE 98 | @ExceptionMetered 99 | @RequiresAuthentication 100 | @RequiresPermissions({"nexus:apikey:delete"}) 101 | @Validate 102 | public NugetApiKeyXO resetKey(@NotNull @Valid @QueryParam("authToken") String base64AuthToken) { 103 | validateAuthToken(base64AuthToken); 104 | PrincipalCollection principals = this.securityHelper.subject().getPrincipals(); 105 | if (principals.getRealmNames().contains(nexusTokenRealm.getName())) { 106 | throw new WebApplicationMessageException(Response.Status.FORBIDDEN, "Token reset is denied when authorizing via token realm"); 107 | } 108 | 109 | // Delete by principals 110 | apiTokenService.deleteApiKey(DOMAIN, principals); 111 | 112 | char[] apiKey = apiTokenService.createApiKey(DOMAIN, principals); 113 | log.trace("Reset apiKey for principal {} = {}", principals.getPrimaryPrincipal(), 114 | (apiKey != null && apiKey.length > 0) ? "***" : null); 115 | 116 | return new NugetApiKeyXO(apiKey); 117 | } 118 | 119 | private void validateAuthToken(String base64AuthToken) { 120 | String authToken = new String(Base64.getDecoder().decode(base64AuthToken), StandardCharsets.UTF_8); 121 | if (!this.authTicketService.redeemTicket(authToken)) { 122 | throw new WebApplicationMessageException(Response.Status.FORBIDDEN, "Invalid authentication ticket"); 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jUserManager.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.realm; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import javax.inject.Inject; 6 | import javax.inject.Named; 7 | import javax.inject.Singleton; 8 | import org.eclipse.sisu.Description; 9 | import org.sonatype.nexus.common.event.EventManager; 10 | import org.sonatype.nexus.security.config.CUser; 11 | import org.sonatype.nexus.security.config.SecurityConfigurationManager; 12 | import org.sonatype.nexus.security.role.RoleIdentifier; 13 | import org.sonatype.nexus.security.user.AbstractUserManager; 14 | import org.sonatype.nexus.security.user.RoleMappingUserManager; 15 | import org.sonatype.nexus.security.user.User; 16 | import org.sonatype.nexus.security.user.UserCreatedEvent; 17 | import org.sonatype.nexus.security.user.UserNotFoundException; 18 | import org.sonatype.nexus.security.user.UserSearchCriteria; 19 | import static com.google.common.base.Preconditions.checkNotNull; 20 | 21 | /** 22 | * User manager for SSO realm. 23 | * 24 | * @see org.sonatype.nexus.security.internal.UserManagerImpl 25 | */ 26 | @Singleton 27 | @Named(Pac4jUserManager.SOURCE) 28 | @Description("Pac4j") 29 | public class Pac4jUserManager extends AbstractUserManager implements RoleMappingUserManager { 30 | 31 | public static final String SOURCE = "pac4j"; 32 | 33 | private final EventManager eventManager; 34 | 35 | private final SecurityConfigurationManager configuration; 36 | 37 | private final RoleMappingUserManager defaultUserManager; 38 | 39 | @Inject 40 | public Pac4jUserManager(final EventManager eventManager, final SecurityConfigurationManager configuration, 41 | @Named("default") final RoleMappingUserManager defaultUserManager) { 42 | this.eventManager = checkNotNull(eventManager); 43 | this.configuration = configuration; 44 | this.defaultUserManager = checkNotNull(defaultUserManager); 45 | } 46 | 47 | //-- Utils --// 48 | 49 | private CUser toUser(User user) { 50 | if (user == null) { 51 | return null; 52 | } 53 | 54 | CUser secUser = configuration.newUser(); 55 | 56 | secUser.setId(user.getUserId()); 57 | secUser.setVersion(user.getVersion()); 58 | secUser.setFirstName(user.getFirstName()); 59 | secUser.setLastName(user.getLastName()); 60 | secUser.setEmail(user.getEmailAddress()); 61 | secUser.setStatus(user.getStatus().name()); 62 | // secUser.setPassword( password )// DO NOT set the users password! 63 | 64 | return secUser; 65 | } 66 | 67 | private Set getRoleIdsFromUser(User user) { 68 | Set roles = new HashSet<>(); 69 | for (RoleIdentifier roleIdentifier : user.getRoles()) { 70 | roles.add(roleIdentifier.getRoleId()); 71 | } 72 | return roles; 73 | } 74 | 75 | //-- org.sonatype.nexus.security.user.RoleMappingUserManager --// 76 | 77 | @Override 78 | public Set getUsersRoles(final String userId, final String source) throws UserNotFoundException { 79 | return defaultUserManager.getUsersRoles(userId, source); 80 | } 81 | 82 | @Override 83 | public void setUsersRoles(String userId, String userSource, Set roleIdentifiers) throws UserNotFoundException { 84 | defaultUserManager.setUsersRoles(userId, userSource, roleIdentifiers); 85 | } 86 | 87 | //-- org.sonatype.nexus.security.user.UserManager --// 88 | 89 | @Override 90 | public String getSource() { 91 | return SOURCE; 92 | } 93 | 94 | @Override 95 | public String getAuthenticationRealmName() { 96 | return NexusPac4jRealm.NAME; 97 | } 98 | 99 | @Override 100 | public boolean supportsWrite() { 101 | return false; 102 | } 103 | 104 | @Override 105 | public Set listUsers() { 106 | return defaultUserManager.listUsers(); 107 | } 108 | 109 | @Override 110 | public Set listUserIds() { 111 | return defaultUserManager.listUserIds(); 112 | } 113 | 114 | @Override 115 | public User addUser(User user, String password) { 116 | final CUser secUser = checkNotNull(this.toUser(user)); 117 | secUser.setPassword("[" + NexusPac4jRealm.NAME + "]"); 118 | configuration.createUser(secUser, getRoleIdsFromUser(user)); 119 | eventManager.post(new UserCreatedEvent(user)); 120 | return user; 121 | } 122 | 123 | @Override 124 | public User updateUser(User user) throws UserNotFoundException { 125 | return defaultUserManager.updateUser(user); 126 | } 127 | 128 | @Override 129 | public void deleteUser(String userId) throws UserNotFoundException { 130 | defaultUserManager.deleteUser(userId); 131 | } 132 | 133 | @Override 134 | public Set searchUsers(UserSearchCriteria criteria) { 135 | return defaultUserManager.searchUsers(criteria); 136 | } 137 | 138 | @Override 139 | public User getUser(String userId) throws UserNotFoundException { 140 | return defaultUserManager.getUser(userId); 141 | } 142 | 143 | @Override 144 | public User getUser(final String userId, final Set roleIds) throws UserNotFoundException { 145 | return defaultUserManager.getUser(userId, roleIds); 146 | } 147 | 148 | @Override 149 | public void changePassword(String userId, String newPassword) throws UserNotFoundException { 150 | throw new UnsupportedOperationException("SSO/SAML users can't change passwords"); 151 | } 152 | 153 | /** @since 3.70.0 */ 154 | @Override 155 | public boolean isConfigured() { 156 | return true; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single Sign-On patch for Nexus OSS 2 | 3 | [![license](https://img.shields.io/badge/license-EPL1-brightgreen.svg)](https://github.com/a-langer/nexus-sso/blob/main/LICENSE "License of source code") 4 | [![image](https://ghcr-badge.egpl.dev//a-langer/nexus-sso/latest_tag?trim=major&label=latest)][0] 5 | [![image-size](https://ghcr-badge.egpl.dev/a-langer/nexus-sso/size?tag=3.75.1-java17-ubi)][0] 6 | [![JitPack](https://jitpack.io/v/a-langer/nexus-sso.svg)][1] 7 | 8 | Patch for [Nexus OSS][2] with authorization via [SSO][7] and [tokens][8]. By default this features available only in PRO version ([see comparison][5]), but this patch provides them an alternative implementation without violating the license. 9 | 10 | Solution implement as Docker [container][0] (based on [official image][3] with SSO patch applied) and [compose.yml](./compose.yml) config with Nginx. Example of usage: 11 | 12 | ```bash 13 | # Clone configuration and change to working directory 14 | git clone https://github.com/a-langer/nexus-sso.git 15 | cd ./nexus-sso 16 | # Copy compose.override.yml from template for you settings 17 | cp _compose.override.yml compose.override.yml 18 | # Set environment variables for container user 19 | export NEXUS_USER=$(id -u) NEXUS_GROUP=$(id -g) 20 | # Run service and open http://localhost in web browser 21 | docker compose up -d 22 | ``` 23 | 24 | ## Supported features and examples of usage 25 | 26 | > **Warn**: Since version `3.70.1-java11-ubi`: 27 | > 28 | > * Your need migrate from legacy OrientDB to H2DB. Version 3.71.0 and beyond do not support OrientDB, Java 8, or Java 11, see [Migration.md](docs/Migration.md) for more information. 29 | > * Class `com.github.alanger.nexus.bootstrap.Pac4jAuthenticationListener` has been deprecated, see [SAML.md](docs/SAML.md#attributes-mapping). 30 | > * Image released without [nexus-repository-ansiblegalaxy](https://github.com/angeloxx/nexus-repository-ansiblegalaxy), cause by plugin does not support new storage API, see [issue #25](https://github.com/l3ender/nexus-repository-ansiblegalaxy/issues/25). 31 | 32 | Since version `3.61.0` for using SSO and User Tokens, it is enough to have following [realms][6] in the order listed: 33 | 34 | 1. "**Local Authenticating Realm**" - built-in realm used by default. 35 | 2. "**SSO Pac4j Realm**" - single sign-on realm uses an external Identity Provider (IdP). 36 | 3. "**SSO Token Realm**" - realm allows you to use user tokens instead of a password. 37 | 4. "**Docker Bearer Token Realm**" - required to access Docker repositories through a Docker client (must be below the "**SSO Token Realm**"). 38 | 39 | Other realms are not required and may lead to conflicts. 40 | 41 | List of features this patch adds: 42 | 43 | * [**SAML/SSO**](./docs/SAML.md) - authentication via Single Sign-On (SSO) using a SAML identity provider such as Keycloak, Okta, ADFS and others. 44 | 45 | * [**User Auth Tokens**](./docs/Tokens.md) - are applied when security policies do not allow the users password to be used, such as for storing in plain text (in settings Docker, Maven and etc.) or combined with [SAML/SSO](./docs/SAML.md). 46 | 47 | * [**Nginx Reverse Proxy**](./docs/Nginx.md) - this Nginx configuration implements a proxy strategy to use Docker registries without additional ports or hostnames. Also provides pre-configured SSL. 48 | 49 | * [**Docker Compose**](./docs/Docker.md) - provide flexible Compose configuration and **DB console** - web interface to interact with an embedded database. 50 | 51 | * [**Patch features**](./docs/Patch.md) - additional features implemented in this patch. 52 | 53 | ## Development environment 54 | 55 | Need installed Maven and Docker with [Compose][4] and [BuildKit][4.1] plugins: 56 | 57 | 1. Change Nexus version if update required (see [Release Notes][9] and [Maven Central][10] for more information), ex.: 58 | 59 | ```bash 60 | # Set version of the current project and any child modules 61 | mvn versions:set -DnewVersion=3.71.0 62 | # Optional can set revision number of the Nexus plugins 63 | mvn versions:set-property -Dproperty=nexus.extension.version -DnewVersion=02 64 | ``` 65 | 66 | 2. Execute assembly commands: 67 | 68 | ```bash 69 | # Build docker image 70 | mvn clean install -PbuildImage 71 | # Or build only jar bundle if needed 72 | mvn clean package 73 | ``` 74 | 75 | 3. Run docker container and test it: 76 | 77 | ```bash 78 | # Run service and open http://localhost in web browser 79 | docker compose down && docker compose up 80 | ``` 81 | 82 | 4. Accept or revert modifications to the pom.xml files: 83 | 84 | ```bash 85 | # Accept modifications 86 | mvn versions:commit 87 | # Or revert modifications and rebuild docker image 88 | mvn versions:revert && mvn clean install -PbuildImage 89 | ``` 90 | 91 | [0]: https://github.com/a-langer/nexus-sso/pkgs/container/nexus-sso "Docker image with SSO patch applied" 92 | [1]: https://jitpack.io/#a-langer/nexus-sso "Maven repository for builds from source code" 93 | [2]: https://github.com/sonatype/nexus-public "Source code of Nexus OSS" 94 | [3]: https://github.com/sonatype/docker-nexus3 "Docker image Nexus OSS" 95 | [4]: https://docs.docker.com/compose/install/ "Docker plugin for defining and running multi-container Docker applications" 96 | [4.1]: https://github.com/docker/buildx "Docker plugin for capabilities with BuildKit" 97 | [5]: https://www.sonatype.com/products/repository-oss-vs-pro-features "Nexus OSS vs Nexus PRO" 98 | [6]: https://help.sonatype.com/en/realms.html "Nexus Realms" 99 | [7]: https://help.sonatype.com/en/saml.html "Nexus PRO SAML" 100 | [8]: https://help.sonatype.com/en/user-tokens.html "Nexus PRO tokens" 101 | [9]: https://github.com/sonatype/nexus-public/releases "Nexus release notes" 102 | [10]: https://mvnrepository.com/artifact/org.sonatype.nexus/nexus-bootstrap "Version of Nexus plugins in Maven Central" 103 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/apikey/ApiTokenService.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.apikey; 2 | 3 | import com.github.alanger.nexus.plugin.datastore.EncryptedString; 4 | import com.google.common.base.Preconditions; 5 | import com.google.inject.Inject; 6 | import java.time.Instant; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | import javax.inject.Named; 10 | import javax.inject.Singleton; 11 | import org.apache.shiro.authc.UsernamePasswordToken; 12 | import org.apache.shiro.subject.PrincipalCollection; 13 | import org.sonatype.goodies.common.ComponentSupport; 14 | import org.sonatype.nexus.security.authc.apikey.ApiKey; 15 | import org.sonatype.nexus.security.authc.apikey.ApiKeyService; 16 | 17 | import org.sonatype.nexus.security.config.SecurityConfiguration; 18 | 19 | /** 20 | * API Token service for "NuGet API Key". 21 | * 22 | * @since 3.70.1-02 23 | * 24 | * @see org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl 25 | * @see org.sonatype.nexus.internal.security.apikey.store.ApiKeyStoreV2Impl 26 | */ 27 | @Singleton 28 | @Named 29 | public class ApiTokenService extends ComponentSupport { 30 | 31 | public static final String DOMAIN = "NuGetApiKey"; 32 | 33 | /** 34 | * @since 3.75.1-01 used {@link org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl org.sonatype.nexus.security.authc.apikey.ApiKeyService} 35 | * instead of {@link org.sonatype.nexus.internal.security.apikey.ApiKeyStoreImpl org.sonatype.nexus.security.authc.apikey.ApiKeyStore} which was moved to 36 | * internal package {@link org.sonatype.nexus.internal.security.apikey.store.ApiKeyStoreV2Impl org.sonatype.nexus.internal.security.apikey.store.ApiKeyStore}. 37 | */ 38 | private final ApiKeyService apiKeyService; 39 | 40 | @Inject 41 | public ApiTokenService(final EncryptedString encryptedString // 42 | , final SecurityConfiguration securityConfiguration // 43 | , final ApiKeyService apiKeyService) { 44 | this.apiKeyService = Preconditions.checkNotNull(apiKeyService); 45 | log.trace("ApiTokenService: {}, apiKeyService: {}", this, apiKeyService); 46 | } 47 | 48 | public char[] createApiKey(String domain, PrincipalCollection principals) { 49 | char[] key = this.apiKeyService.createApiKey(domain, principals); 50 | return Objects.requireNonNull(key, "apiKeyStore returned null apikey for principals: " + principals); 51 | } 52 | 53 | public char[] createApiKey(PrincipalCollection principals) { 54 | return this.createApiKey(DOMAIN, principals); 55 | } 56 | 57 | public Optional findApiKey(String domain, UsernamePasswordToken token, boolean domainAsLogin) { 58 | String username = token.getUsername(); 59 | Optional key = Optional.empty(); 60 | 61 | if (username != null) { 62 | // Domain as login for "DockerToken:XXXXX" if org.sonatype.nexus.security.authc.NexusApiKeyAuthenticationToken 63 | if (domainAsLogin) { 64 | key = this.apiKeyService.getApiKeyByToken(username, token.getPassword()); 65 | if (key.isPresent()) { 66 | log.trace("findApiKey principal: {}, domain as login: {}", key.get().getPrimaryPrincipal(), username); 67 | return key; 68 | } 69 | } 70 | 71 | // Default domain for "Username:XXXXX" if org.apache.shiro.authc.UsernamePasswordToken 72 | key = this.apiKeyService.getApiKeyByToken(domain, token.getPassword()); 73 | if (key.isPresent() && username.equals(key.get().getPrimaryPrincipal())) { 74 | log.trace("findApiKey principal: {}, default domain: {}", key.get().getPrimaryPrincipal(), domain); 75 | return key; 76 | } 77 | } 78 | 79 | return key; 80 | } 81 | 82 | public Optional findApiKey(UsernamePasswordToken token, boolean domainAsLogin) { 83 | return findApiKey(DOMAIN, token, domainAsLogin); 84 | } 85 | 86 | /** For compatibility with 3.70.1-java11-ubi-BETA-3 */ 87 | public Optional findApiKey(UsernamePasswordToken token) { 88 | return findApiKey(DOMAIN, token, false); 89 | } 90 | 91 | public Optional getApiKey(String domain, PrincipalCollection principals) { 92 | return this.apiKeyService.getApiKey(domain, principals); 93 | } 94 | 95 | public Optional getApiKey(PrincipalCollection principals) { 96 | return this.getApiKey(DOMAIN, principals); 97 | } 98 | 99 | /** 100 | * Delete ApiKey by primary principal 101 | * 102 | * @since buji-pac4j 5.0.0 103 | * @since Nexus 3.70.0 104 | * 105 | * @see com.github.alanger.nexus.plugin.realm.Pac4jPrincipalName#equals(Object) 106 | */ 107 | public void deleteApiKey(String domain, PrincipalCollection principals) { 108 | // May not return an error even if the deletion failed 109 | this.apiKeyService.deleteApiKey(domain, principals); 110 | } 111 | 112 | public void deleteApiKey(PrincipalCollection principals) { 113 | this.deleteApiKey(DOMAIN, principals); 114 | } 115 | 116 | // Utils 117 | 118 | public static final long ONE_DAY_IN_MILLS = 1000L * 60L * 60L * 24L; 119 | 120 | public static boolean isExpired(ApiKey tokenRecord, int expirationDays) { 121 | if (isExpirationEnabled(expirationDays) && tokenRecord != null) { 122 | return isTokenExpired(tokenRecord, expirationDays); 123 | } 124 | return false; 125 | } 126 | 127 | public static boolean isExpirationEnabled(int expirationDays) { 128 | return expirationDays > 0; 129 | } 130 | 131 | public static boolean isTokenExpired(ApiKey userTokenRecord, int expirationDays) { 132 | long expiration = getExpirationTimeStamp(userTokenRecord, expirationDays); 133 | return (Instant.now().toEpochMilli() > expiration); 134 | } 135 | 136 | public static long getExpirationTimeStamp(ApiKey userToken, int expirationDays) { 137 | return ONE_DAY_IN_MILLS * expirationDays + userToken.getCreated().toInstant().toEpochMilli(); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/Pac4jRealmName.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.realm; 2 | 3 | import static com.github.alanger.shiroext.realm.RealmUtils.asList; 4 | import static com.github.alanger.shiroext.realm.RealmUtils.filterBlackOrWhite; 5 | 6 | import io.buji.pac4j.realm.Pac4jRealm; 7 | import io.buji.pac4j.token.Pac4jToken; 8 | import org.apache.shiro.authc.AuthenticationException; 9 | import org.apache.shiro.authc.AuthenticationInfo; 10 | import org.apache.shiro.authc.AuthenticationToken; 11 | import org.apache.shiro.authc.SimpleAuthenticationInfo; 12 | import org.apache.shiro.authz.AuthorizationInfo; 13 | import org.apache.shiro.authz.SimpleAuthorizationInfo; 14 | import org.apache.shiro.subject.PrincipalCollection; 15 | import org.apache.shiro.subject.SimplePrincipalCollection; 16 | import org.pac4j.core.profile.UserProfile; 17 | 18 | import java.util.*; 19 | 20 | import com.github.alanger.shiroext.realm.ICommonPermission; 21 | import com.github.alanger.shiroext.realm.ICommonRole; 22 | import com.github.alanger.shiroext.realm.IFilterPermission; 23 | import com.github.alanger.shiroext.realm.IFilterRole; 24 | import com.github.alanger.shiroext.realm.IPrincipalName; 25 | import com.github.alanger.shiroext.realm.IUserPrefix; 26 | 27 | /** 28 | * Moved from shiro-ext library for recompile with 29 | * {@link org.pac4j.core.profile.UserProfile} as interface instead of class. 30 | * 31 | * @since buji-pac4j:5.0.0 32 | * @since Nexus:3.70.0 33 | * @see https://github.com/bujiio/buji-pac4j/blob/master/src/main/java/io/buji/pac4j/realm/Pac4jRealm.java 34 | */ 35 | public class Pac4jRealmName extends Pac4jRealm 36 | implements ICommonPermission, ICommonRole, IUserPrefix, IPrincipalName, IFilterRole, IFilterPermission { 37 | 38 | private String commonRole = null; 39 | private String commonPermission = null; 40 | private String userPrefix = ""; 41 | 42 | private String roleWhiteList; 43 | private String roleBlackList; 44 | private String permissionWhiteList; 45 | private String permissionBlackList; 46 | 47 | @Override 48 | protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) 49 | throws AuthenticationException { 50 | 51 | final Pac4jToken token = (Pac4jToken) authenticationToken; 52 | 53 | // Compatibility with buji-pac4j 4.1.1 54 | final List profiles = token.getProfiles(); 55 | 56 | final Pac4jPrincipalName principal = new Pac4jPrincipalName(profiles, getPrincipalNameAttribute()); 57 | principal.setUserPrefix(getUserPrefix()); 58 | final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName()); 59 | return new SimpleAuthenticationInfo(principalCollection, profiles.hashCode()); 60 | } 61 | 62 | @Override 63 | protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) { 64 | final Set roles = new HashSet<>(); 65 | final Set permissions = new HashSet<>(); 66 | final Pac4jPrincipalName principal = principals.oneByType(Pac4jPrincipalName.class); 67 | if (principal != null) { 68 | roles.addAll(asList(commonRole)); 69 | permissions.addAll(asList(commonPermission)); 70 | 71 | // Compatibility with buji-pac4j 4.1.1 72 | final List profiles = principal.getProfiles(); 73 | for (final UserProfile profile : profiles) { 74 | if (profile != null) { 75 | roles.addAll(profile.getRoles()); 76 | profile.addRoles(asList(commonRole)); 77 | 78 | permissions.addAll(profile.getPermissions()); 79 | profile.addPermissions(asList(commonPermission)); 80 | } 81 | } 82 | } 83 | 84 | final SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); 85 | filterBlackOrWhite(roles, roleWhiteList, roleBlackList); 86 | simpleAuthorizationInfo.addRoles(roles); 87 | filterBlackOrWhite(permissions, permissionWhiteList, permissionBlackList); 88 | simpleAuthorizationInfo.addStringPermissions(permissions); 89 | return simpleAuthorizationInfo; 90 | } 91 | 92 | @Override 93 | public String getCommonRole() { 94 | return commonRole; 95 | } 96 | 97 | @Override 98 | public void setCommonRole(String commonRole) { 99 | this.commonRole = commonRole; 100 | } 101 | 102 | @Override 103 | public String getCommonPermission() { 104 | return commonPermission; 105 | } 106 | 107 | @Override 108 | public void setCommonPermission(String commonPermission) { 109 | this.commonPermission = commonPermission; 110 | } 111 | 112 | @Override 113 | public String getUserPrefix() { 114 | return userPrefix; 115 | } 116 | 117 | @Override 118 | public void setUserPrefix(String userPrefix) { 119 | this.userPrefix = userPrefix; 120 | } 121 | 122 | @Override 123 | public String getRoleWhiteList() { 124 | return roleWhiteList; 125 | } 126 | 127 | @Override 128 | public void setRoleWhiteList(String roleWhiteList) { 129 | this.roleWhiteList = roleWhiteList; 130 | } 131 | 132 | @Override 133 | public String getRoleBlackList() { 134 | return roleBlackList; 135 | } 136 | 137 | @Override 138 | public void setRoleBlackList(String roleBlackList) { 139 | this.roleBlackList = roleBlackList; 140 | } 141 | 142 | @Override 143 | public String getPermissionWhiteList() { 144 | return permissionWhiteList; 145 | } 146 | 147 | @Override 148 | public void setPermissionWhiteList(String permissionWhiteList) { 149 | this.permissionWhiteList = permissionWhiteList; 150 | } 151 | 152 | @Override 153 | public String getPermissionBlackList() { 154 | return permissionBlackList; 155 | } 156 | 157 | @Override 158 | public void setPermissionBlackList(String permissionBlackList) { 159 | this.permissionBlackList = permissionBlackList; 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/resources/UiPac4jPluginDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.resources; 2 | 3 | import java.util.List; 4 | 5 | import javax.annotation.Nullable; 6 | import javax.inject.Inject; 7 | import javax.inject.Named; 8 | import javax.inject.Singleton; 9 | import org.apache.shiro.SecurityUtils; 10 | import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 11 | import org.sonatype.nexus.common.app.BaseUrlHolder; 12 | import org.sonatype.nexus.common.template.TemplateHelper; 13 | import org.sonatype.nexus.common.template.TemplateParameters; 14 | import org.sonatype.nexus.ui.UiPluginDescriptorSupport; 15 | import org.sonatype.nexus.webresources.GeneratedWebResource; 16 | import org.sonatype.nexus.webresources.WebResource; 17 | import org.sonatype.nexus.webresources.WebResourceBundle; 18 | import com.github.alanger.nexus.plugin.realm.NexusPac4jRealm; 19 | import java.io.IOException; 20 | import java.net.URL; 21 | 22 | import static java.util.Arrays.asList; 23 | import static com.google.common.base.Preconditions.checkNotNull; 24 | 25 | /** 26 | * Customize Nexus UI for SSO plugin. 27 | * 28 | * @see org.sonatype.nexus.rapture.internal.UiReactPluginDescriptorImpl 29 | * @see org.sonatype.nexus.rapture.internal.RaptureWebResourceBundle 30 | * @see org.sonatype.nexus.internal.webresources.WebResourceServiceImpl 31 | */ 32 | @Named(UiPac4jPluginDescriptor.NAME) 33 | @Singleton 34 | public class UiPac4jPluginDescriptor extends UiPluginDescriptorSupport implements WebResourceBundle { 35 | 36 | public static final String HEADER_PANEL_LOGO_TEXT = "Header_Panel_Logo_Text"; 37 | public static final String SIGNIN_MODAL_DIALOG_HTML = "SignIn_Modal_Dialog_Html"; 38 | public static final String SIGNIN_MODAL_DIALOG_TOOLTIP = "SignIn_Modal_Dialog_Tooltip"; 39 | public static final String SIGNIN_SSO_ENABLED = "SignIn_SSO_Enabled"; 40 | public static final String AUTHENTICATE_MODAL_DIALOG_MESSAGE = "Authenticate_Modal_Dialog_Message"; 41 | 42 | private String headerPanelLogoText = "Nexus OSS"; 43 | private String signinModalDialogHtml = "

Sign in with SSO
"; 44 | private String signinModalDialogTooltip = "SSO Login"; 45 | private String authenticateModalDialogMessage = 46 | "
Accessing API Key requires validation of your credentials (enter your username if using SSO login).
"; 47 | 48 | public static final String NAME = "nexus-sso-customize"; 49 | public static final String PATH = "/static/sso/" + NAME + ".js"; 50 | 51 | private final List scripts; 52 | private final String scriptLocation; 53 | private final TemplateHelper templateHelper; 54 | private final DefaultWebSecurityManager securityManager; 55 | 56 | @Inject 57 | public UiPac4jPluginDescriptor(@Named("${nexus.sso.script.location:-" + NAME + ".vm.js}") final String location, 58 | final TemplateHelper templateHelper) { 59 | super(NAME); 60 | scripts = asList(PATH); 61 | this.scriptLocation = location; 62 | this.templateHelper = checkNotNull(templateHelper); 63 | this.securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); 64 | } 65 | 66 | @Nullable 67 | @Override 68 | public List getScripts(final boolean isDebug) { 69 | return scripts; 70 | } 71 | 72 | //-- WebResourceBundle interface --// 73 | 74 | @Override 75 | public List getResources() { 76 | return asList(getCustomizeJs()); 77 | } 78 | 79 | private abstract class TemplateWebResource extends GeneratedWebResource { 80 | protected byte[] render(final String template, final TemplateParameters parameters) throws IOException { 81 | log.trace("Rendering template: {}, with params: {}", template, parameters); 82 | URL url = getClass().getResource(template); 83 | return templateHelper.render(url, parameters).getBytes(); 84 | } 85 | } 86 | 87 | /** 88 | * The customize.js resource. 89 | */ 90 | private WebResource getCustomizeJs() { 91 | return new TemplateWebResource() { 92 | @Override 93 | public String getPath() { 94 | return PATH; 95 | } 96 | 97 | @Override 98 | public String getContentType() { 99 | return JAVASCRIPT; 100 | } 101 | 102 | @Override 103 | protected byte[] generate() throws IOException { 104 | return render(scriptLocation, new TemplateParameters() // 105 | .set("baseUrl", BaseUrlHolder.get()) // 106 | .set("relativePath", BaseUrlHolder.getRelativePath()) // 107 | .set(HEADER_PANEL_LOGO_TEXT, headerPanelLogoText) // 108 | .set(SIGNIN_MODAL_DIALOG_HTML, signinModalDialogHtml) // 109 | .set(SIGNIN_MODAL_DIALOG_TOOLTIP, signinModalDialogTooltip) // 110 | .set(SIGNIN_SSO_ENABLED, isSsoEnabled()) // 111 | .set(AUTHENTICATE_MODAL_DIALOG_MESSAGE, authenticateModalDialogMessage) // 112 | ); 113 | } 114 | }; 115 | } 116 | 117 | public void setHeaderPanelLogoText(String headerPanelLogoText) { 118 | if (headerPanelLogoText != null && !headerPanelLogoText.isEmpty()) 119 | this.headerPanelLogoText = headerPanelLogoText; 120 | } 121 | 122 | public void setSigninModalDialogHtml(String signinModalDialogHtml) { 123 | if (signinModalDialogHtml != null && !signinModalDialogHtml.isEmpty()) 124 | this.signinModalDialogHtml = signinModalDialogHtml; 125 | } 126 | 127 | public void setSigninModalDialogTooltip(String signinModalDialogTooltip) { 128 | if (signinModalDialogTooltip != null && !signinModalDialogTooltip.isEmpty()) 129 | this.signinModalDialogTooltip = signinModalDialogTooltip; 130 | } 131 | 132 | public void setAuthenticateModalDialogMessage(String authenticateModalDialogMessage) { 133 | if (authenticateModalDialogMessage != null && !authenticateModalDialogMessage.isEmpty()) 134 | this.authenticateModalDialogMessage = authenticateModalDialogMessage; 135 | } 136 | 137 | private boolean isSsoEnabled() { 138 | try { 139 | return this.securityManager.getRealms().stream().anyMatch(r -> r != null && r.getName().equals(NexusPac4jRealm.NAME)); 140 | } catch (NullPointerException e) { 141 | log.trace("isSsoEnabled error", e); 142 | return false; 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/config/sp-metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | MIIDWjCCAkKgAwIBAgIEUysPdDANBgkqhkiG9w0BAQUFADBvMRAwDgYDVQQGEwdVbmtub3duMRAw 28 | DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD 29 | VQQLEwdVbmtub3duMRMwEQYDVQQDEwpwYWM0ai1kZW1vMB4XDTE0MDMyMDE1NTUzMloXDTI0MDMx 30 | NzE1NTUzMlowbzEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMH 31 | VW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjETMBEGA1UEAxMKcGFj 32 | NGotZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJvk3rg2R2uAkfZ2WgiP2Sj4 33 | xGm55JdGGDTfmt6uF2U/XcD+iAVUUSwxnCf5KfM6QgzrBia/DkOLzGyOY45ixl3zxtiLXDKEpzKU 34 | EgtQj3TzR4cQd7ii/iYLV3UFkEHP75LqXzym2JkVqj1kfJz54YBUf6Lkah0V3R7hY9QuaBucC2yq 35 | XSNSQtoqcO2wUxvCGVHz+qeuEyu1ovP0fK7NxQ0KL9Bv5hcNayIEsvKcL6kARsGpt+4gbV6pHlWp 36 | cC51x6xSs/EXuPFygO2YVKjZKkwanwhC/1RWNzWYTqAx7qJX+ZcPSElTZw47iKg6oCa0nyCgWtX1 37 | NCEy3UbOl/i7ol0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAktK+L1PRLAROfsqYSuOOyszYl3uj 38 | 3pwR1l6UigP2akgzOYjsLqKozbp28eoeM6+uNROp+qSyOHuNKRSBpDPXBgd0mstz/QKXibc1oKuu 39 | DUhi4HD6w1XXdVu2zw6vcHLRCG/chS9tXMvLZlb564Uwu6ljtSvx8I0iI6av11RRKiLCRc8B2sJ0 40 | eRMnHFjevAJWpjZ0l8+Nv0AT5WkKaJPVUrmHOn5nb2axkIAw29OS77vkRPfB3gCIEat5HilM2NVK 41 | wfkkT7lzrK8XVzKGR5sivxDDTn0IxQ3MyHY6t1yOeeF72hSwUK2uV3WjFl12g9SuIjZ7OyEGsjiD 42 | CtUfZL67og== 43 | 44 | 45 | 46 | 47 | 48 | 49 | MIIDWjCCAkKgAwIBAgIEUysPdDANBgkqhkiG9w0BAQUFADBvMRAwDgYDVQQGEwdVbmtub3duMRAw 50 | DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD 51 | VQQLEwdVbmtub3duMRMwEQYDVQQDEwpwYWM0ai1kZW1vMB4XDTE0MDMyMDE1NTUzMloXDTI0MDMx 52 | NzE1NTUzMlowbzEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMH 53 | VW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjETMBEGA1UEAxMKcGFj 54 | NGotZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJvk3rg2R2uAkfZ2WgiP2Sj4 55 | xGm55JdGGDTfmt6uF2U/XcD+iAVUUSwxnCf5KfM6QgzrBia/DkOLzGyOY45ixl3zxtiLXDKEpzKU 56 | EgtQj3TzR4cQd7ii/iYLV3UFkEHP75LqXzym2JkVqj1kfJz54YBUf6Lkah0V3R7hY9QuaBucC2yq 57 | XSNSQtoqcO2wUxvCGVHz+qeuEyu1ovP0fK7NxQ0KL9Bv5hcNayIEsvKcL6kARsGpt+4gbV6pHlWp 58 | cC51x6xSs/EXuPFygO2YVKjZKkwanwhC/1RWNzWYTqAx7qJX+ZcPSElTZw47iKg6oCa0nyCgWtX1 59 | NCEy3UbOl/i7ol0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAktK+L1PRLAROfsqYSuOOyszYl3uj 60 | 3pwR1l6UigP2akgzOYjsLqKozbp28eoeM6+uNROp+qSyOHuNKRSBpDPXBgd0mstz/QKXibc1oKuu 61 | DUhi4HD6w1XXdVu2zw6vcHLRCG/chS9tXMvLZlb564Uwu6ljtSvx8I0iI6av11RRKiLCRc8B2sJ0 62 | eRMnHFjevAJWpjZ0l8+Nv0AT5WkKaJPVUrmHOn5nb2axkIAw29OS77vkRPfB3gCIEat5HilM2NVK 63 | wfkkT7lzrK8XVzKGR5sivxDDTn0IxQ3MyHY6t1yOeeF72hSwUK2uV3WjFl12g9SuIjZ7OyEGsjiD 64 | CtUfZL67og== 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 73 | urn:oasis:names:tc:SAML:2.0:nameid-format:persistent 74 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 75 | urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/DebugFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.util.Collection; 6 | import java.util.Enumeration; 7 | 8 | import javax.servlet.Filter; 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.FilterConfig; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.ServletRequest; 13 | import javax.servlet.ServletResponse; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | /** 21 | * Debug Servlet request and response. 22 | */ 23 | public class DebugFilter implements Filter { 24 | 25 | protected static Logger log = LoggerFactory.getLogger(DebugFilter.class); 26 | 27 | protected boolean printHeader = true; 28 | protected boolean printResponseHeader = false; 29 | protected boolean printAttribute = true; 30 | protected boolean printParameter = true; 31 | protected boolean printBody = false; 32 | 33 | @Override 34 | public void init(FilterConfig filterConfig) throws ServletException { 35 | // nothing 36 | } 37 | 38 | @Override 39 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 40 | throws IOException, ServletException { 41 | HttpServletRequest request = (HttpServletRequest) req; 42 | HttpServletResponse response = (HttpServletResponse) resp; 43 | 44 | printRequest(request, printHeader, printAttribute, printParameter, printBody, log); 45 | if (printResponseHeader) 46 | printResponseHeaders(response, log); 47 | 48 | if (!response.isCommitted()) 49 | chain.doFilter(request, response); 50 | } 51 | 52 | @Override 53 | public void destroy() { 54 | // nothing 55 | } 56 | 57 | public static void throwException(Throwable cause) throws ServletException, IOException { 58 | if (cause instanceof IOException) 59 | throw (IOException) cause; 60 | else if (cause instanceof ServletException) 61 | throw (ServletException) cause; 62 | else 63 | throw new ServletException(cause); 64 | } 65 | 66 | public static void printRequest(HttpServletRequest request) throws IOException { 67 | printRequest(request, true, true, true, false, log); 68 | } 69 | 70 | public static void printRequest(HttpServletRequest request, boolean printHeader, boolean printAttribute, 71 | boolean printParameter, boolean printBody, Logger log) throws IOException { 72 | log.debug("------ Time {} -----", System.currentTimeMillis()); 73 | log.debug("request: {} {}", request.getMethod(), request.getRequestURI()); 74 | 75 | if (printHeader) { 76 | printHeaders(request, log); 77 | } 78 | 79 | if (printAttribute) { 80 | printAttributes(request, log); 81 | } 82 | 83 | if (printParameter) { 84 | printParameters(request, log); 85 | } 86 | 87 | if (printBody) { 88 | printBody(request, log); 89 | } 90 | } 91 | 92 | public static void printHeaders(HttpServletRequest request, Logger log) { 93 | StringBuilder sb = new StringBuilder(""); 94 | Enumeration e = request.getHeaderNames(); 95 | while (e.hasMoreElements()) { 96 | String s = e.nextElement(); 97 | sb.append("\n " + s + " = " + request.getHeader(s)); 98 | } 99 | log.debug("headers: {}", sb); 100 | } 101 | 102 | public static void printResponseHeaders(HttpServletResponse response, Logger log) { 103 | StringBuilder sb = new StringBuilder(""); 104 | Collection e = response.getHeaderNames(); 105 | sb.append("\n status = " + response.getStatus()); 106 | sb.append("\n committed = " + response.isCommitted()); 107 | for (String s : e) { 108 | sb.append("\n " + s + " = " + response.getHeader(s)); 109 | } 110 | log.debug("response headers: {}", sb); 111 | } 112 | 113 | public static void printAttributes(HttpServletRequest request, Logger log) { 114 | StringBuilder sb = new StringBuilder(""); 115 | Enumeration e = request.getAttributeNames(); 116 | while (e.hasMoreElements()) { 117 | String s = e.nextElement(); 118 | sb.append("\n " + s + " = " + request.getAttribute(s)); 119 | } 120 | log.debug("attributes: {}", sb); 121 | } 122 | 123 | public static void printParameters(HttpServletRequest request, Logger log) { 124 | StringBuilder sb = new StringBuilder(""); 125 | Enumeration e = request.getParameterNames(); 126 | while (e.hasMoreElements()) { 127 | String s = e.nextElement(); 128 | sb.append("\n " + s + " = " + request.getParameter(s)); 129 | } 130 | log.debug("parameters: {}", sb); 131 | } 132 | 133 | public static void printBody(HttpServletRequest request, Logger log) throws IOException { 134 | StringBuilder sb = new StringBuilder(""); 135 | BufferedReader reader = request.getReader(); 136 | String line; 137 | while ((line = reader.readLine()) != null) { 138 | sb.append("\n " + line); 139 | } 140 | reader.close(); 141 | log.debug("body: {}", sb); 142 | } 143 | 144 | public void setLog(String logName) { 145 | log = LoggerFactory.getLogger(logName); 146 | } 147 | 148 | public boolean isPrintHeader() { 149 | return printHeader; 150 | } 151 | 152 | public void setPrintHeader(boolean printHeader) { 153 | this.printHeader = printHeader; 154 | } 155 | 156 | public boolean isPrintResponseHeader() { 157 | return printResponseHeader; 158 | } 159 | 160 | public void setPrintResponseHeader(boolean printResponseHeader) { 161 | this.printResponseHeader = printResponseHeader; 162 | } 163 | 164 | public boolean isPrintAttribute() { 165 | return printAttribute; 166 | } 167 | 168 | public void setPrintAttribute(boolean printAttribute) { 169 | this.printAttribute = printAttribute; 170 | } 171 | 172 | public boolean isPrintParameter() { 173 | return printParameter; 174 | } 175 | 176 | public void setPrintParameter(boolean printParameter) { 177 | this.printParameter = printParameter; 178 | } 179 | 180 | public boolean isPrintBody() { 181 | return printBody; 182 | } 183 | 184 | public void setPrintBody(boolean printBody) { 185 | this.printBody = printBody; 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /etc/orientdb/orientdb-server-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | true 113 | 114 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/resources/nexus-sso-customize.vm.js: -------------------------------------------------------------------------------- 1 | // Velocity template variable see in com.github.alanger.nexus.plugin.resources.UiPac4jPluginDescriptor 2 | // 3 | // Header_Panel_Logo_Text: "$Header_Panel_Logo_Text" 4 | // SignIn_Modal_Dialog_Html: "$SignIn_Modal_Dialog_Html" 5 | // SignIn_Modal_Dialog_Tooltip: "$SignIn_Modal_Dialog_Tooltip" 6 | // SignIn_SSO_Enabled: "$SignIn_SSO_Enabled" 7 | // Authenticate_Modal_Dialog_Message: "$Authenticate_Modal_Dialog_Message" 8 | 9 | // Wait all plugins 10 | var checkExist = setInterval(function () { 11 | var done = ((typeof NX != "undefined") && (typeof NX.I18n != "undefined") 12 | && (typeof NX.app != "undefined")); 13 | // console.log("! checkExist done: " + done); 14 | if (done) { 15 | clearInterval(checkExist); 16 | initHeader(); 17 | if ($SignIn_SSO_Enabled || false) { 18 | initLoginDialog(); 19 | initApiKeyDialog(); 20 | } 21 | } 22 | }, 10); 23 | 24 | function initHeader() { 25 | console.log("SSO Header"); 26 | // Header text, see components/nexus-rapture/src/main/resources/static/rapture/NX/app/PluginStrings.js 27 | Ext.define("NX.app.PluginStrings", { 28 | "@aggregate_priority": 90, 29 | singleton: true, 30 | requires: ["NX.I18n"], 31 | keys: { 32 | Header_Panel_Logo_Text: "$Header_Panel_Logo_Text", 33 | } 34 | }, function (a) { 35 | NX.I18n.register(a) 36 | }); 37 | } 38 | 39 | function initLoginDialog() { 40 | console.log("SSO Login dialog"); 41 | // Login dialog 42 | Ext.define("NX.view.SignIn", { 43 | extend: "NX.view.ModalDialog", 44 | alias: "widget.nx-signin", 45 | requires: ["NX.I18n"], 46 | initComponent: function () { 47 | var a = this; 48 | a.ui = "nx-inset"; 49 | a.title = NX.I18n.get("SignIn_Title"); 50 | a.setWidth(NX.view.ModalDialog.SMALL_MODAL); 51 | Ext.apply(a, { 52 | items: { 53 | xtype: "form", 54 | defaultType: "textfield", 55 | defaults: { 56 | anchor: "100%" 57 | }, 58 | items: [ 59 | { // SSO login button 60 | xtype: "button", 61 | height: 40, 62 | html: "$SignIn_Modal_Dialog_Html", 63 | ui: "nx-primary", 64 | iconCls: 'x-fa fa-sign-in-alt', 65 | id: 'signInSSO', 66 | width: '100%', 67 | tooltip: '$SignIn_Modal_Dialog_Tooltip', 68 | handler: function () { 69 | window.location.href = "./index.html"; 70 | } 71 | }, { // SSO login separator 72 | xtype: "label", 73 | width: '100%', 74 | html: '
or
' 75 | }, { 76 | name: "username", 77 | itemId: "username", 78 | emptyText: NX.I18n.get("SignIn_Username_Empty"), 79 | allowBlank: false, 80 | validateOnBlur: false 81 | }, { 82 | name: "password", 83 | itemId: "password", 84 | inputType: "password", 85 | emptyText: NX.I18n.get("SignIn_Password_Empty"), 86 | allowBlank: false, 87 | validateOnBlur: false 88 | }], 89 | buttonAlign: "left", 90 | buttons: [{ 91 | text: NX.I18n.get("SignIn_Submit_Button"), 92 | action: "signin", 93 | formBind: true, 94 | bindToEnter: true, 95 | ui: "nx-primary" 96 | }, { 97 | text: NX.I18n.get("SignIn_Cancel_Button"), 98 | handler: a.close, 99 | scope: a 100 | }] 101 | } 102 | }); 103 | a.on({ 104 | resize: function () { 105 | a.down("#username").focus() 106 | }, 107 | single: true 108 | }); 109 | a.callParent() 110 | }, 111 | addMessage: function (b) { 112 | var a = this 113 | , d = '
' + b + "

" 114 | , c = a.down("#signinMessage"); 115 | if (c) { 116 | c.html(d) 117 | } else { 118 | a.down("form").insert(0, { 119 | xtype: "component", 120 | itemId: "signinMessage", 121 | html: d 122 | }) 123 | } 124 | }, 125 | clearMessage: function () { 126 | var a = this 127 | , b = a.down("#signinMessage"); 128 | if (b) { 129 | a.down("form").remove(b) 130 | } 131 | } 132 | }); 133 | } 134 | 135 | function initApiKeyDialog() { 136 | console.log("SSO Api key dialog"); 137 | // Api key dialog (Accessing NuGet API Key) 138 | Ext.define("NX.view.Authenticate", { 139 | extend: "NX.view.ModalDialog", 140 | alias: "widget.nx-authenticate", 141 | requires: ["NX.Icons", "NX.I18n"], 142 | cls: "nx-authenticate", 143 | message: undefined, 144 | initComponent: function () { 145 | var a = this; 146 | a.ui = "nx-inset"; 147 | a.title = NX.I18n.get("Authenticate_Title"); 148 | a.setWidth(NX.view.ModalDialog.MEDIUM_MODAL); 149 | // SSO description 150 | a.message = '$Authenticate_Modal_Dialog_Message'; 151 | Ext.apply(this, { 152 | closable: false, 153 | items: { 154 | xtype: "form", 155 | defaultType: "textfield", 156 | defaults: { 157 | anchor: "100%" 158 | }, 159 | items: [{ 160 | xtype: "container", 161 | layout: "hbox", 162 | cls: "message", 163 | items: [{ 164 | xtype: "component", 165 | html: NX.Icons.img("authenticate", "x32") 166 | }, { 167 | xtype: "label", 168 | height: 48, 169 | html: "
" + a.message + "
" 170 | }] 171 | }, { 172 | name: "username", 173 | itemId: "username", 174 | emptyText: NX.I18n.get("SignIn_Username_Empty"), 175 | allowBlank: false, 176 | readOnly: true 177 | }, { 178 | name: "password", 179 | itemId: "password", 180 | inputType: "password", 181 | emptyText: NX.I18n.get("SignIn_Password_Empty"), 182 | allowBlank: false, 183 | validateOnBlur: false 184 | }], 185 | buttonAlign: "left", 186 | buttons: [{ 187 | text: NX.I18n.get("User_View_Authenticate_Submit_Button"), 188 | action: "authenticate", 189 | formBind: true, 190 | bindToEnter: true, 191 | ui: "nx-primary" 192 | }, { 193 | text: NX.I18n.get("Authenticate_Cancel_Button"), 194 | handler: function () { 195 | if (!!a.options && Ext.isFunction(a.options.failure)) { 196 | a.options.failure.call(a.options.failure, a.options) 197 | } 198 | a.close() 199 | }, 200 | scope: a 201 | }] 202 | } 203 | }); 204 | a.on({ 205 | resize: function () { 206 | a.down("#password").focus() 207 | }, 208 | single: true 209 | }); 210 | a.callParent() 211 | } 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/DI.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin; 2 | 3 | import javax.annotation.Priority; 4 | import javax.inject.Inject; 5 | import javax.inject.Named; 6 | import javax.inject.Singleton; 7 | import javax.servlet.ServletContext; 8 | import javax.sql.DataSource; 9 | 10 | import org.apache.shiro.mgt.RealmSecurityManager; 11 | import org.sonatype.nexus.blobstore.api.BlobStoreManager; 12 | import org.sonatype.nexus.blobstore.quota.BlobStoreQuotaService; 13 | import org.sonatype.nexus.common.app.ManagedLifecycle; 14 | import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport; 15 | import org.sonatype.nexus.datastore.api.DataStoreManager; 16 | import org.sonatype.nexus.repository.manager.RepositoryManager; 17 | import org.sonatype.nexus.repository.security.RepositoryPermissionChecker; 18 | import org.sonatype.nexus.security.SecurityHelper; 19 | import org.sonatype.nexus.security.config.SecurityConfiguration; 20 | import org.sonatype.nexus.security.realm.RealmManager; 21 | import org.sonatype.nexus.selector.SelectorManager; 22 | import com.github.alanger.nexus.plugin.datastore.EncryptedString; 23 | import com.github.alanger.nexus.plugin.realm.NexusPac4jRealm; 24 | import com.github.alanger.nexus.plugin.realm.NexusTokenRealm; 25 | import com.github.alanger.nexus.plugin.resources.UiPac4jPluginDescriptor; 26 | import com.google.common.base.Preconditions; 27 | 28 | import static org.sonatype.nexus.common.app.ManagedLifecycle.Phase.SERVICES; 29 | 30 | /** 31 | * Dependency injector for script environment. 32 | */ 33 | @Singleton 34 | @Named 35 | @Priority(Integer.MAX_VALUE) 36 | @ManagedLifecycle(phase = SERVICES) // After SECURITY 37 | public class DI extends StateGuardLifecycleSupport { 38 | 39 | public static final String NAME = DI.class.getCanonicalName(); 40 | 41 | private static DI INSTANCE; 42 | 43 | // org.sonatype.nexus.datastore.internal.DataStoreManagerImpl 44 | public final DataStoreManager dataStoreManager; 45 | 46 | // com.zaxxer.hikari.HikariDataSource (nexus) 47 | public final DataSource dataSource; 48 | 49 | // org.sonatype.nexus.repository.manager.internal.RepositoryManagerImpl 50 | public final RepositoryManager repositoryManager; 51 | 52 | public final RepositoryPermissionChecker repositoryPermissionChecker; 53 | 54 | public final SecurityHelper securityHelper; 55 | 56 | // org.sonatype.nexus.internal.selector.SelectorManagerImpl 57 | public final SelectorManager selectorManager; 58 | 59 | // org.sonatype.nexus.repository.internal.blobstore.BlobStoreManagerImpl 60 | public final BlobStoreManager blobStoreManager; 61 | 62 | // org.sonatype.nexus.blobstore.quota.internal.BlobStoreQuotaServiceImpl 63 | public final BlobStoreQuotaService blobStoreQuotaService; 64 | 65 | // org.sonatype.nexus.internal.security.model.SecurityConfigurationImpl ("mybatis") 66 | public final SecurityConfiguration securityConfiguration; 67 | 68 | public final NexusPac4jRealm pac4jRealm; 69 | 70 | public final NexusTokenRealm tokenRealm; 71 | 72 | public final ServletContext servletContext; 73 | 74 | // org.sonatype.nexus.security.internal.RealmManagerImpl 75 | public final RealmManager realmManager; 76 | 77 | // org.apache.shiro.nexus.NexusWebSecurityManager 78 | public final RealmSecurityManager realmSecurityManager; 79 | 80 | public final Init init; 81 | 82 | public final UiPac4jPluginDescriptor uiPac4jPluginDescriptor; 83 | 84 | public final EncryptedString encryptedString; 85 | 86 | @SuppressWarnings("java:S3010") 87 | @Inject 88 | public DI(@Named final DataStoreManager dataStoreManager // 89 | , @Named final RepositoryManager repositoryManager // 90 | , @Named final RepositoryPermissionChecker repositoryPermissionChecker // 91 | , @Named final SecurityHelper securityHelper // 92 | , @Named final SelectorManager selectorManager // 93 | , @Named final BlobStoreManager blobStoreManager // 94 | , @Named final BlobStoreQuotaService blobStoreQuotaService // 95 | , @Named final SecurityConfiguration securityConfiguration // 96 | , @Named(NexusPac4jRealm.NAME) final NexusPac4jRealm pac4jRealm // 97 | , @Named(NexusTokenRealm.NAME) final NexusTokenRealm tokenRealm // 98 | , final ServletContext servletContext // 99 | , @Named final RealmManager realmManager // 100 | , final RealmSecurityManager realmSecurityManager // 101 | , @Named final Init init // 102 | , @Named(UiPac4jPluginDescriptor.NAME) final UiPac4jPluginDescriptor uiPac4jPluginDescriptor // 103 | , @Named final EncryptedString encryptedString) { 104 | super(); 105 | 106 | this.dataStoreManager = Preconditions.checkNotNull(dataStoreManager); 107 | this.dataSource = dataStoreManager.get(DataStoreManager.DEFAULT_DATASTORE_NAME) 108 | .orElseThrow(() -> new IllegalStateException("Missing DataStore named: " + DataStoreManager.DEFAULT_DATASTORE_NAME)) 109 | .getDataSource(); 110 | this.securityConfiguration = Preconditions.checkNotNull(securityConfiguration); 111 | this.pac4jRealm = Preconditions.checkNotNull(pac4jRealm); 112 | this.tokenRealm = Preconditions.checkNotNull(tokenRealm); 113 | this.servletContext = Preconditions.checkNotNull(servletContext); 114 | this.realmManager = Preconditions.checkNotNull(realmManager); 115 | this.realmSecurityManager = Preconditions.checkNotNull(realmSecurityManager); 116 | this.init = Preconditions.checkNotNull(init); 117 | this.uiPac4jPluginDescriptor = Preconditions.checkNotNull(uiPac4jPluginDescriptor); 118 | this.encryptedString = Preconditions.checkNotNull(encryptedString); 119 | this.repositoryManager = Preconditions.checkNotNull(repositoryManager); 120 | this.repositoryPermissionChecker = Preconditions.checkNotNull(repositoryPermissionChecker); 121 | this.securityHelper = Preconditions.checkNotNull(securityHelper); 122 | this.selectorManager = Preconditions.checkNotNull(selectorManager); 123 | this.blobStoreManager = Preconditions.checkNotNull(blobStoreManager); 124 | this.blobStoreQuotaService = Preconditions.checkNotNull(blobStoreQuotaService); 125 | 126 | if (INSTANCE == null) { 127 | INSTANCE = this; 128 | this.servletContext.setAttribute(NAME, INSTANCE); 129 | } 130 | 131 | log.trace("DI dataStoreManager: {}, dataSource: {}, repositoryManager: {}, repositoryPermissionChecker: {}, securityHelper: {}" // 132 | + ", selectorManager: {}, blobStoreManager: {}, blobStoreQuotaService: {}, securityConfiguration: {}, pac4jRealm: {}, tokenRealm: {}" // 133 | + ", servletContext: {}, realmManager: {}, realmSecurityManager: {}, init: {}, uiPac4jPluginDescriptor: {}, encryptedString: {}", // 134 | dataStoreManager, dataSource, repositoryManager, repositoryPermissionChecker, securityHelper, selectorManager, 135 | blobStoreManager, blobStoreQuotaService, securityConfiguration, pac4jRealm, tokenRealm, servletContext, realmManager, 136 | realmSecurityManager, init, uiPac4jPluginDescriptor, encryptedString); 137 | } 138 | 139 | @Override 140 | protected void doStart() throws Exception { 141 | log.trace("DI doStart"); 142 | } 143 | 144 | public static DI getInstance() { 145 | return INSTANCE; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/realm/NexusTokenRealm.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.realm; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import javax.inject.Singleton; 6 | import org.apache.shiro.authc.AuthenticationException; 7 | import org.apache.shiro.authc.AuthenticationInfo; 8 | import org.apache.shiro.authc.AuthenticationToken; 9 | import org.apache.shiro.authc.DisabledAccountException; 10 | import org.apache.shiro.authc.ExpiredCredentialsException; 11 | import org.apache.shiro.authc.SimpleAuthenticationInfo; 12 | import org.apache.shiro.authc.UnknownAccountException; 13 | import org.apache.shiro.authc.UsernamePasswordToken; 14 | import org.eclipse.sisu.Description; 15 | import org.sonatype.nexus.security.SecurityHelper; 16 | import org.sonatype.nexus.security.UserPrincipalsHelper; 17 | import org.sonatype.nexus.security.authc.NexusApiKeyAuthenticationToken; 18 | import org.sonatype.nexus.security.authc.apikey.ApiKey; 19 | import org.sonatype.nexus.security.config.SecurityConfiguration; 20 | import org.sonatype.nexus.security.user.UserNotFoundException; 21 | import com.github.alanger.nexus.plugin.apikey.ApiTokenService; 22 | import com.google.common.base.Preconditions; 23 | 24 | /** 25 | * User Token realm. Each user can set a personal token that can be used instead of a password. 26 | * The creation of tokens is implemented through the "NuGet API Key" menu (privilegies 27 | * {@code nx-apikey-all} required), however, the tokens themselves apply to all types of repositories 28 | * 29 | * @see https://help.sonatype.com/en/user-tokens.html 30 | * @see org.sonatype.nexus.security.token.BearerTokenRealm 31 | */ 32 | @Singleton 33 | @Named(NexusTokenRealm.NAME) 34 | @Description("SSO Token Realm") 35 | public class NexusTokenRealm extends NexusPac4jRealm { 36 | 37 | public static final String NAME = "tokenRealm"; 38 | public static final String DOMAIN = ApiTokenService.DOMAIN; 39 | 40 | private final ApiTokenService apiTokenService; 41 | 42 | private int expirationDays = 365; // One year 43 | 44 | private boolean domainAsLogin = false; 45 | 46 | @Inject 47 | public NexusTokenRealm(final SecurityHelper securityHelper, final UserPrincipalsHelper principalsHelper, 48 | final SecurityConfiguration securityConfiguration, final ApiTokenService apiTokenService) { 49 | super(securityHelper, principalsHelper, securityConfiguration); 50 | 51 | this.apiTokenService = Preconditions.checkNotNull(apiTokenService); 52 | 53 | setName(NAME); 54 | 55 | // Cache for API token 56 | setAuthenticationCachingEnabled(true); 57 | setAuthorizationCachingEnabled(true); 58 | 59 | // Only UsernamePasswordToken 60 | setAuthenticationTokenClass(UsernamePasswordToken.class); 61 | } 62 | 63 | @Override 64 | protected void onInitDebug() { 65 | logger.trace("onInit name: {}, authenticationCachingEnabled: {}, authorizationCachingEnabled: {}, principalNameAttribute: {}" // 66 | + ", commonRole: [{}], commonPermission: [{}], permissionWhiteList: {}, permissionBlackList: {}, roleWhiteList: {}, roleBlackList: {}, expirationDays: {}", 67 | getName(), isAuthenticationCachingEnabled(), isAuthorizationCachingEnabled(), getPrincipalNameAttribute(), getCommonRole(), 68 | getCommonPermission(), getPermissionWhiteList(), getPermissionBlackList(), getRoleWhiteList(), getRoleBlackList(), 69 | getExpirationDays()); 70 | } 71 | 72 | @Override 73 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 74 | logger.trace("doGetAuthenticationInfo token: {}, {}", token, token != null ? token.getClass() : "class null"); 75 | 76 | if (token != null) { 77 | logger.debug("Looking up API key for: {}", token); 78 | 79 | UsernamePasswordToken t = toUsernamePasswordToken(token); 80 | ApiKey key = apiTokenService.findApiKey(t, domainAsLogin).orElse(null); 81 | 82 | if (key != null) { 83 | logger.debug("Found API key principal: {}, realms: {}", key.getPrimaryPrincipal(), key.getPrincipals().getRealmNames()); 84 | 85 | if (ApiTokenService.isExpired(key, expirationDays)) { 86 | logger.debug("API token for {} ({}) is expired, created: {}, expirationDays: {}", t.getUsername(), 87 | key.getPrimaryPrincipal(), key.getCreated(), expirationDays); 88 | throw new ExpiredCredentialsException("Account " + t.getUsername() + " is expired"); 89 | } 90 | try { 91 | if (this.principalsHelper.getUserStatus(key.getPrincipals()).isActive()) { 92 | logger.debug("API token has been authenticated for {} ({})", t.getUsername(), key.getPrimaryPrincipal()); 93 | return new SimpleAuthenticationInfo(key.getPrimaryPrincipal(), key.getApiKey(), getName()); 94 | } 95 | throw new DisabledAccountException("Account " + t.getUsername() + " is disabled"); 96 | } catch (UserNotFoundException e) { 97 | logger.debug("User {} ({}) not found, removing stale API token", t.getUsername(), key.getPrimaryPrincipal()); 98 | apiTokenService.deleteApiKey(DOMAIN, key.getPrincipals()); 99 | throw new UnknownAccountException("Account " + t.getUsername() + " not found", e); 100 | } 101 | } 102 | } 103 | 104 | throw new AuthenticationException("Token " + token + " is not applicable"); 105 | } 106 | 107 | /** 108 | * Token can be {@link org.apache.shiro.authc.UsernamePasswordToken} 109 | * or {@link org.sonatype.nexus.security.authc.NexusApiKeyAuthenticationToken}. 110 | * 111 | * @see org.sonatype.nexus.security.authc.NexusApiKeyAuthenticationToken 112 | * @see org.apache.shiro.authc.UsernamePasswordToken 113 | */ 114 | private UsernamePasswordToken toUsernamePasswordToken(AuthenticationToken token) { 115 | if (token instanceof UsernamePasswordToken) { 116 | return (UsernamePasswordToken) token; 117 | } else { 118 | return new UsernamePasswordToken(token.getPrincipal().toString(), (char[]) token.getCredentials()); 119 | } 120 | } 121 | 122 | @Override 123 | @SuppressWarnings("unchecked") 124 | public boolean supports(AuthenticationToken token) { 125 | return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass()) 126 | && (UsernamePasswordToken.class.isAssignableFrom(token.getClass()) 127 | || NexusApiKeyAuthenticationToken.class.isAssignableFrom(token.getClass())); 128 | } 129 | 130 | public int getExpirationDays() { 131 | return expirationDays; 132 | } 133 | 134 | /** 135 | * API Token expiration in days, by default it is 365 days. Set to '-1' for unlimited. 136 | * Example of shiro.ini: 137 | * 138 | *
139 |      * # 365 days
140 |      * tokenRealm.expirationDays = 365
141 |      * # Unlimited
142 |      * tokenRealm.expirationDays = -1
143 |      * 
144 | * 145 | * @since 3.70.1-02 146 | * @param expirationDays Number of days for which you want user tokens to remain valid 147 | */ 148 | public void setExpirationDays(int expirationDays) { 149 | this.expirationDays = expirationDays; 150 | } 151 | 152 | public boolean isDomainAsLogin() { 153 | return domainAsLogin; 154 | } 155 | 156 | /** 157 | * Alow using a domain, such as {@code NuGetApiKey} or {@code DockerToken}, as user login. 158 | * 159 | *

160 | * Example of shiro.ini, where using parent class for UsernamePasswordToken and NexusApiKeyAuthenticationToken, except Pac4jToken: 161 | * 162 | *

163 |      * tokenRealm.authenticationTokenClass = org.apache.shiro.authc.HostAuthenticationToken
164 |      * tokenRealm.domainAsLogin = true
165 |      * 
166 | * 167 | * @since 3.70.1-02 168 | * @param domainAsLogin 169 | */ 170 | public void setDomainAsLogin(boolean domainAsLogin) { 171 | this.domainAsLogin = domainAsLogin; 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/groovy/com/github/alanger/nexus/bootstrap/QuotaFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.bootstrap; 2 | 3 | import static com.github.alanger.shiroext.realm.RealmUtils.asList; 4 | import static java.nio.charset.StandardCharsets.UTF_8; 5 | import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; 6 | import static java.util.Collections.singletonList; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import javax.servlet.Filter; 16 | import javax.servlet.FilterChain; 17 | import javax.servlet.FilterConfig; 18 | import javax.servlet.ServletException; 19 | import javax.servlet.ServletRequest; 20 | import javax.servlet.ServletResponse; 21 | 22 | import org.apache.shiro.authz.Permission; 23 | import org.apache.shiro.subject.Subject; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.sonatype.nexus.blobstore.api.BlobStore; 27 | import org.sonatype.nexus.blobstore.api.BlobStoreManager; 28 | import org.sonatype.nexus.blobstore.quota.BlobStoreQuotaResult; 29 | import org.sonatype.nexus.blobstore.quota.BlobStoreQuotaService; 30 | import org.sonatype.nexus.repository.Repository; 31 | import org.sonatype.nexus.repository.manager.RepositoryManager; 32 | import org.sonatype.nexus.repository.security.RepositoryContentSelectorPermission; 33 | import org.sonatype.nexus.repository.security.RepositoryViewPermission; 34 | import org.sonatype.nexus.security.BreadActions; 35 | import org.sonatype.nexus.security.SecurityHelper; 36 | import org.sonatype.nexus.selector.SelectorManager; 37 | import com.github.alanger.nexus.plugin.DI; 38 | 39 | /** 40 | * Quota filter. 41 | * 42 | * @see https://help.sonatype.com/en/configuring-blob-stores.html#adding-a-soft-quota 43 | * @see https://help.sonatype.com/en/storage-guide.html 44 | * @see org.sonatype.nexus.repository.internal.blobstore.BlobStoreQuotaHealthCheck 45 | */ 46 | public class QuotaFilter implements Filter { 47 | 48 | public static final String REPO_NAME_ATTR = QuotaFilter.class.getCanonicalName() + ".REPO_NAME"; 49 | 50 | // All protected for fix groovy.lang.MissingPropertyException: No such property: XXXXX 51 | 52 | protected final Logger logger = LoggerFactory.getLogger(this.getClass()); 53 | 54 | protected String permission = BreadActions.ADD; // Example: read, add, delete 55 | 56 | protected List methods = asList("PUT,POST"); 57 | 58 | private int responseStatus = 507; // Insufficient Storage 59 | 60 | protected RepositoryManager repositoryManager; 61 | 62 | protected SecurityHelper securityHelper; 63 | 64 | protected SelectorManager selectorManager; 65 | 66 | protected BlobStoreManager blobStoreManager; 67 | 68 | protected BlobStoreQuotaService blobStoreQuotaService; 69 | 70 | @Override 71 | public void init(FilterConfig filterConfig) throws ServletException { 72 | this.repositoryManager = DI.getInstance().repositoryManager; 73 | this.securityHelper = DI.getInstance().securityHelper; 74 | this.selectorManager = DI.getInstance().selectorManager; 75 | this.blobStoreManager = DI.getInstance().blobStoreManager; 76 | this.blobStoreQuotaService = DI.getInstance().blobStoreQuotaService; 77 | } 78 | 79 | @Override 80 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { 81 | HttpServletRequest request = (HttpServletRequest) req; 82 | HttpServletResponse response = (HttpServletResponse) resp; 83 | 84 | boolean isPush = methods.contains(request.getMethod()); 85 | 86 | if (request.getAttribute(getClass().getCanonicalName()) != null || !isPush) { 87 | chain.doFilter(request, response); 88 | return; 89 | } 90 | request.setAttribute(getClass().getCanonicalName(), true); 91 | request.setCharacterEncoding(UTF_8.name()); 92 | response.setCharacterEncoding(UTF_8.name()); 93 | 94 | String repoName = getRepoName(request); 95 | Repository repo = repoName != null ? this.repositoryManager.get(repoName) : null; 96 | boolean pushAllowed = repo != null && userCanInRepository(repo); 97 | 98 | if (repo != null && pushAllowed && isPush) { 99 | 100 | String storeName = repo.getConfiguration().attributes("storage").get("blobStoreName", String.class); 101 | BlobStore blobStore = storeName != null ? this.blobStoreManager.get(storeName) : null; 102 | BlobStoreQuotaResult result = blobStore != null ? this.blobStoreQuotaService.checkQuota(blobStore) : null; 103 | logger.trace("repoName: {}, storeName: {}, result: {},", repoName, storeName, result); 104 | 105 | if (result != null && result.isViolation()) { 106 | String msg = result.getMessage(); 107 | logger.trace(msg); 108 | response.setStatus(responseStatus(request)); 109 | response.setHeader(ERROR_MESSAGE, msg); 110 | request.setAttribute(ERROR_MESSAGE, msg); 111 | writeJsonMessage(response, msg); 112 | return; 113 | } 114 | } 115 | 116 | chain.doFilter(request, response); 117 | } 118 | 119 | @Override 120 | public void destroy() { 121 | // nothing 122 | } 123 | 124 | protected void writeJsonMessage(HttpServletResponse response, String msg) throws IOException { 125 | if (!response.isCommitted()) { 126 | response.setContentType("text/json"); 127 | String data = new StringBuilder().append("[{'message':'") // 128 | .append(msg) // 129 | .append("','success':false,'tid':1,'action':'upload','method':'upload','type':'rpc'}]") // 130 | .toString().replace("'", "\""); // 131 | response.getWriter().write(data); 132 | response.getWriter().close(); 133 | } 134 | } 135 | 136 | protected String getRepoName(HttpServletRequest request) { 137 | String repoName = (String) request.getAttribute(REPO_NAME_ATTR); 138 | if (repoName == null) { 139 | // From URI /repository/-- 140 | // or /service/rest/internal/ui/upload/-- 141 | repoName = new File(request.getRequestURI()).getName(); 142 | } 143 | return repoName; 144 | } 145 | 146 | private int responseStatus(HttpServletRequest request) { 147 | // UI response must be 200 148 | return request.getRequestURI().startsWith("/repository/") ? getResponseStatus() : 200; 149 | } 150 | 151 | // org.sonatype.nexus.repository.security.RepositoryPermissionChecker 152 | 153 | public boolean userCanInRepository(final Repository repository) { 154 | return userHasRepositoryViewPermissionTo(permission, repository) || userHasAnyContentSelectorAccessTo(repository, permission); 155 | } 156 | 157 | private boolean userHasRepositoryViewPermissionTo(final String action, final Repository repository) { 158 | return this.securityHelper.anyPermitted(new RepositoryViewPermission(repository, action)); 159 | } 160 | 161 | private boolean userHasAnyContentSelectorAccessTo(final Repository repository, final String... actions) { 162 | Subject subject = this.securityHelper.subject(); 163 | return this.selectorManager.browse().stream() 164 | .anyMatch(selector -> this.securityHelper.anyPermitted(subject, 165 | Arrays.stream(actions) 166 | .map(action -> new RepositoryContentSelectorPermission(selector, repository, singletonList(action))) 167 | .toArray(Permission[]::new))); 168 | } 169 | 170 | // Getters and Setters 171 | 172 | public String getPermission() { 173 | return permission; 174 | } 175 | 176 | public void setPermission(String permission) { 177 | this.permission = permission; 178 | } 179 | 180 | public List getMethods() { 181 | return methods; 182 | } 183 | 184 | public void setMethods(List methods) { 185 | this.methods = methods; 186 | } 187 | 188 | public void setMethods(String methodsStr) { 189 | this.methods = asList(methodsStr); 190 | } 191 | 192 | public int getResponseStatus() { 193 | return responseStatus; 194 | } 195 | 196 | public void setResponseStatus(int responseStatus) { 197 | this.responseStatus = responseStatus; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/config/shiro.ini: -------------------------------------------------------------------------------- 1 | # Hot-reload interval in seconds 2 | scanPeriod = ${PAC4J_INI_SCAN_PERIOD:-0} 3 | # Hot-reload URL path (don't change it!) 4 | urlRewriteStatusPath = ${URLREWRITE_STATUS_PATH:-http://localhost:8081/rewrite-status} 5 | # UI variable 6 | Header_Panel_Logo_Text = Nexus OSS 7 | SignIn_Modal_Dialog_Html =
Sign in with SSO
8 | SignIn_Modal_Dialog_Tooltip = SSO Login 9 | Authenticate_Modal_Dialog_Message =
Accessing API Key requires validation of your credentials (enter your username if using SSO login).
10 | 11 | [main] 12 | ## Nexus environment objects (declared programmatically) 13 | # securityManager = org.apache.shiro.nexus.NexusWebSecurityManager 14 | # authenticator = org.sonatype.nexus.security.authc.FirstSuccessfulModularRealmAuthenticator 15 | # authorizer = org.sonatype.nexus.security.authz.ExceptionCatchingModularRealmAuthorizer 16 | # sessionManager = org.apache.shiro.nexus.NexusWebSessionManager 17 | # chainResolver = org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 18 | # LdapRealm = org.sonatype.nexus.ldap.internal.LdapRealm 19 | # securityDataSource = com.zaxxer.hikari.HikariDataSource 20 | # iniRealm = org.apache.shiro.realm.text.IniRealm 21 | # tokenRealm = com.github.alanger.nexus.plugin.realm.NexusTokenRealm 22 | # pac4jRealm = com.github.alanger.nexus.plugin.realm.NexusPac4jRealm 23 | 24 | ## SAML buji-pac4j https://github.com/bujiio/buji-pac4j/blob/master/src/main/resources/buji-pac4j-default.ini 25 | ## SAML buji-pac4j-demo https://github.com/pac4j/buji-pac4j-demo/blob/master/src/main/resources/shiro.ini 26 | authorizationGenerator = org.pac4j.core.authorization.generator.FromAttributesAuthorizationGenerator 27 | authorizationGenerator.roleAttributes = ${PAC4J_ROLE_ATTRS:-roles} 28 | authorizationGenerator.permissionAttributes = ${PAC4J_PERMISSION_ATTRS:-permission} 29 | clients = org.pac4j.core.client.Clients 30 | clients.authorizationGenerator = $authorizationGenerator 31 | config = org.pac4j.core.config.Config 32 | config.clients = $clients 33 | 34 | pac4jRealm.principalNameAttribute = ${PAC4J_PRINCIPAL_NAME_ATTR:-username} 35 | pac4jRealm.commonRole = ${PAC4J_COMMON_ROLE:-nx-authenticated, nx-public} 36 | pac4jRealm.commonPermission = ${PAC4J_COMMON_PERMISSION:-nexus:apikey:*, nexus:sso-user:read, nexus:repository-view:docker:docker-login:read} 37 | ; pac4jRealm.roleWhiteList = ^nx-.*$ 38 | ; pac4jRealm.permissionWhiteList = ^nexus:.*$ 39 | pac4jRealm.map(attrs) = ${PAC4J_PROFILE_ATTRS:-firstName:firstName, lastName:lastName, email:email} 40 | 41 | pac4jSubjectFactory = io.buji.pac4j.subject.Pac4jSubjectFactory 42 | securityManager.subjectFactory = $pac4jSubjectFactory 43 | 44 | saml2Config = org.pac4j.saml.config.SAML2Configuration 45 | saml2Config.keystorePath = ${PAC4J_KEYSTORE:-etc/sso/config/samlKeystore.jks} 46 | saml2Config.keystorePassword = ${PAC4J_KEYSTORE_PASSWORD:-pac4j-demo-passwd} 47 | saml2Config.privateKeyPassword = ${PAC4J_KEYSTORE_KEY_PASSWORD:-pac4j-demo-passwd} 48 | saml2Config.identityProviderMetadataPath = ${PAC4J_IDENTITY_PROVIDER_METADATA:-etc/sso/config/metadata.xml} 49 | saml2Config.maximumAuthenticationLifetime = ${PAC4J_AUTHENTICATION_LIFETIME:-3600} 50 | saml2Config.serviceProviderEntityId = ${PAC4J_BASE_URL:-http://localhost}/callback?client_name=SAML2Client 51 | saml2Config.serviceProviderMetadataPath = ${PAC4J_SERVICE_PROVIDER_METADATA:-etc/sso/config/sp-metadata.xml} 52 | # Force re-authenticate, see https://github.com/a-langer/nexus-sso/issues/11 53 | ; saml2Config.forceAuth = true 54 | ; saml2Config.passive = true 55 | # Disable all validation, good for testing 56 | ; saml2Config.allSignatureValidationDisabled = true 57 | 58 | saml2Client = org.pac4j.saml.client.SAML2Client 59 | saml2Client.configuration = $saml2Config 60 | 61 | clients.callbackUrl = ${PAC4J_BASE_URL:-http://localhost}/callback 62 | clients.clients = $saml2Client 63 | 64 | ## Token authc such as basic or bearer 65 | tokenRealm.commonRole = ${PAC4J_TOKEN_COMMON_ROLE:-nx-authenticated-token, nx-public} 66 | tokenRealm.commonPermission = ${PAC4J_TOKEN_COMMON_PERMISSION:-nexus:sso-user:read, nexus:repository-view:docker:docker-login:read} 67 | # Deny read and write api token 68 | tokenRealm.permissionBlackList = .*apikey.* 69 | # Deny read and write api token 70 | tokenRealm.roleBlackList = ^nx-authenticated$ 71 | tokenRealm.authenticationCachingEnabled = true 72 | tokenRealm.expirationDays = 365 73 | 74 | ## Configure session and security manager (30m = 1800000, 24h = 86400000) 75 | ; sessionManager.globalSessionTimeout = ${SHIRO_SESSION_TIMEOUT:-1800000} 76 | ; sessionManager.sessionIdCookie.name = ${SHIRO_SESSION_COOKIE_NAME:-NEXUSID} 77 | ; sessionManager.sessionIdUrlRewritingEnabled = false 78 | ; securityManager.realms = $iniRealm, $pac4jRealm, $tokenRealm 79 | 80 | ## Pac4j filters 81 | callbackFilter = org.pac4j.jee.filter.CallbackFilter 82 | callbackFilter.config = $config 83 | callbackFilter.defaultUrl = /${NEXUS_CONTEXT:-} 84 | callbackFilter.defaultClient = SAML2Client 85 | callbackFilter.renewSession = false 86 | # Required since buji-pac4j:8.0.0 (or use pac4jToShiroBridge) 87 | ; callbackLogic = com.github.alanger.nexus.plugin.Pac4jCallbackLogic 88 | ; config.callbackLogic = $callbackLogic 89 | ; callbackFilter.callbackLogic = $callbackLogic 90 | 91 | # Required since buji-pac4j:8.0.0 (or use callbackLogic) 92 | pac4jToShiroBridge = io.buji.pac4j.bridge.Pac4jShiroBridge 93 | pac4jToShiroBridge.config = $config 94 | 95 | saml2SecurityFilter = org.pac4j.jee.filter.SecurityFilter 96 | saml2SecurityFilter.config = $config 97 | saml2SecurityFilter.clients = SAML2Client 98 | 99 | pac4jLogout = org.pac4j.jee.filter.LogoutFilter 100 | pac4jLogout.config = $config 101 | ; pac4jCentralLogout = org.pac4j.jee.filter.LogoutFilter 102 | ; pac4jCentralLogout.config = $config 103 | ; pac4jCentralLogout.localLogout = false 104 | ; pac4jCentralLogout.centralLogout = true 105 | ; pac4jCentralLogout.logoutUrlPattern = ${PAC4J_BASE_URL:-http://localhost}/.* 106 | 107 | ## Shiro-ext filters 108 | ; basic = com.github.alanger.shiroext.web.BasicAuthcFilter 109 | ; logout = com.github.alanger.shiroext.web.LogoutAuthcFilter 110 | ; roles = com.github.alanger.shiroext.web.RolesAuthzFilter 111 | ; role = com.github.alanger.shiroext.web.RoleAuthzFilter 112 | ; perms = com.github.alanger.shiroext.web.PermissionsAuthzFilter 113 | ; perm = com.github.alanger.shiroext.web.PermissionAuthzFilter 114 | ; bearer = com.github.alanger.shiroext.web.BearerAuthcFilter 115 | ; bearer.applicationName = Nexus OSS 116 | ; bearer.silent = true 117 | ; bearer.principalNameAttribute = primary_principal 118 | ; public = com.github.alanger.nexus.bootstrap.AnonymousFilter 119 | ; public.userId = public 120 | dockerExtdirect = com.github.alanger.nexus.bootstrap.DockerExtdirectFilter 121 | dockerExtdirect.dockerRoot = docker-root 122 | ; quota = com.github.alanger.nexus.bootstrap.QuotaFilter 123 | ; quota.methods = PUT,POST 124 | ; subject = com.github.alanger.nexus.bootstrap.SubjectFilter 125 | ; subject.methods = PUT,POST,DELETE 126 | ; subject.namePattern = ^admin$ 127 | ; debug = com.github.alanger.nexus.bootstrap.DebugFilter 128 | ; debug.printResponseHeader = true 129 | 130 | ## Nexus filters (declared programmatically) 131 | # nx-authc = org.sonatype.nexus.security.authc.NexusAuthenticationFilter 132 | # nx-apikey-authc = org.sonatype.nexus.security.authc.apikey.ApiKeyAuthenticationFilter 133 | # nx-anonymous = org.sonatype.nexus.security.anonymous.AnonymousFilter 134 | # authcAntiCsrf = org.sonatype.nexus.security.authc.AntiCsrfFilter 135 | # nx-perms = org.sonatype.nexus.security.authz.PermissionsFilter 136 | # nx-session-authc = org.sonatype.nexus.rapture.internal.security.SessionAuthenticationFilter 137 | 138 | [users] 139 | # Only for testing 140 | ; admin2 = ${NX_ADMIN_PASSWORD:-admin2},nx-admin 141 | ; user = 123456,nx-authenticated,nx-public 142 | ; user1 = user1,nx-authenticated,nx-public 143 | ; public = ,nx-public 144 | 145 | [roles] 146 | # Only for testing 147 | ; nx-admin = * 148 | ; nx-anonymous = nexus:healthcheck:read,nexus:repository-view:*:*:browse,nexus:repository-view:*:*:read,nexus:search:read 149 | ; nx-public = nexus:healthcheck:read,nexus:search:read 150 | ; nx-authenticated = nexus:apikey:*,nexus:sso-user:read,nexus:repository-view:docker:docker-login:read 151 | ; nx-authenticated-token = nexus:sso-user:read 152 | 153 | [urls] 154 | # SAML + Pac4j 155 | /index.html = saml2SecurityFilter 156 | /callback = callbackFilter 157 | ; /pac4jCentralLogout = pac4jCentralLogout 158 | /pac4jLogout = pac4jLogout 159 | 160 | # Nexus 161 | /service/metrics/** = nx-authc, nx-anonymous, authcAntiCsrf, nx-perms 162 | /service/outreach/** = nx-anonymous 163 | /repository/** = nx-authc, nx-apikey-authc, nx-anonymous, authcAntiCsrf 164 | /service/rapture/session = nx-session-authc 165 | 166 | # Subject and Quota example 167 | ; /repository/** = saml2SecurityFilter, subject, quota, nx-authc, nx-apikey-authc, nx-anonymous, authcAntiCsrf 168 | ; /service/rest/internal/ui/upload/** = subject, quota, nx-authc, nx-anonymous, authcAntiCsrf 169 | 170 | # Deny profile editing, need permission "nexus:sso-user:write" 171 | /service/rest/internal/ui/user = rest[nexus:sso-user] 172 | 173 | # Nexus 174 | /service/rest/** = nx-authc, nx-anonymous, authcAntiCsrf 175 | 176 | # Change docker image name in usage instruction 177 | /service/extdirect = nx-authc, nx-anonymous, dockerExtdirect, authcAntiCsrf 178 | 179 | # Nexus 180 | /service/extdirect/** = nx-authc, nx-anonymous, authcAntiCsrf 181 | /** = nx-anonymous, authcAntiCsrf -------------------------------------------------------------------------------- /nexus-pac4j-plugin/src/main/java/com/github/alanger/nexus/plugin/apikey/ApiKeySanitizer.java: -------------------------------------------------------------------------------- 1 | package com.github.alanger.nexus.plugin.apikey; 2 | 3 | import java.sql.Connection; 4 | import java.sql.ResultSet; 5 | import java.sql.SQLException; 6 | import java.sql.Statement; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Optional; 9 | import javax.annotation.Priority; 10 | import javax.inject.Inject; 11 | import javax.inject.Named; 12 | import javax.inject.Singleton; 13 | import javax.sql.DataSource; 14 | import org.apache.shiro.subject.SimplePrincipalCollection; 15 | import org.sonatype.nexus.common.app.ManagedLifecycle; 16 | import org.sonatype.nexus.common.app.ManagedLifecycle.Phase; 17 | import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport; 18 | import org.sonatype.nexus.datastore.api.DataStoreManager; 19 | import org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl; 20 | import org.sonatype.nexus.kv.GlobalKeyValueStore; 21 | import org.sonatype.nexus.security.authc.apikey.ApiKey; 22 | import org.sonatype.nexus.security.authc.apikey.ApiKeyService; 23 | import org.sonatype.nexus.security.config.CUser; 24 | import org.sonatype.nexus.security.config.SecurityConfiguration; 25 | import com.github.alanger.nexus.plugin.datastore.EncryptedString; 26 | import com.google.common.base.Preconditions; 27 | 28 | /** 29 | * Fix broken API V1 tokens. 30 | * Should run once before moving api keys to v2 table, see {@link org.sonatype.nexus.internal.security.apikey.upgrade.ApiKeyToSecretsTask ApiKeyToSecretsTask}. 31 | * 32 | *
 33 |  * SELECT * FROM API_KEY
 34 |  * SELECT * FROM API_KEY_V2
 35 |  * SELECT DISTINCT PRIMARY_PRINCIPAL, * FROM API_KEY
 36 |  * DELETE API_KEY where DOMAIN = 'DockerToken'
 37 |  * 
38 | * 39 | * @since 3.75.1 40 | * 41 | * @see org.sonatype.nexus.internal.security.apikey.upgrade.ApiKeyToSecretsTask 42 | * @see org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl 43 | */ 44 | @Singleton 45 | @Named 46 | @Priority(Integer.MAX_VALUE) 47 | @ManagedLifecycle(phase = Phase.UPGRADE) // Before TASKS 48 | public class ApiKeySanitizer extends StateGuardLifecycleSupport { 49 | 50 | private final GlobalKeyValueStore kv; 51 | private final SecurityConfiguration securityConfiguration; 52 | private final EncryptedString encryptedString; 53 | 54 | // org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl 55 | private final ApiKeyService apiKeyService; 56 | 57 | // org.sonatype.nexus.datastore.internal.DataStoreManagerImpl 58 | public final DataStoreManager dataStoreManager; 59 | 60 | // com.zaxxer.hikari.HikariDataSource (nexus) 61 | private final DataSource dataSource; 62 | 63 | @Inject 64 | public ApiKeySanitizer(final GlobalKeyValueStore kv // 65 | , final SecurityConfiguration securityConfiguration // 66 | , final ApiKeyService apiKeyService // 67 | , final EncryptedString encryptedString // 68 | , final DataStoreManager dataStoreManager) { 69 | this.kv = Preconditions.checkNotNull(kv); 70 | this.securityConfiguration = Preconditions.checkNotNull(securityConfiguration); 71 | this.apiKeyService = Preconditions.checkNotNull(apiKeyService); 72 | this.encryptedString = Preconditions.checkNotNull(encryptedString); 73 | this.dataStoreManager = Preconditions.checkNotNull(dataStoreManager); 74 | this.dataSource = dataStoreManager.get(DataStoreManager.DEFAULT_DATASTORE_NAME) 75 | .orElseThrow(() -> new IllegalStateException("Missing DataStore named: " + DataStoreManager.DEFAULT_DATASTORE_NAME)) 76 | .getDataSource(); 77 | } 78 | 79 | /** @see org.sonatype.nexus.internal.security.apikey.ApiKeyServiceImpl#doStart */ 80 | @Override 81 | protected void doStart() throws Exception { 82 | boolean secretMigrationComplete = kv.getBoolean(ApiKeyServiceImpl.MIGRATION_COMPLETE).orElse(false); 83 | log.trace("ApiKeySanitizer doStart, secretMigrationComplete: {}", secretMigrationComplete); 84 | if (!secretMigrationComplete) { 85 | sanitizeBrokenTokens(); 86 | } 87 | } 88 | 89 | private void sanitizeBrokenTokens() { 90 | String domain = ApiTokenService.DOMAIN; 91 | String sql = "SELECT * FROM api_key WHERE domain = '" + domain + "'"; 92 | log.info("Get ApiKey V1 SQL: {}", sql); 93 | try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { 94 | 95 | // Delete all Docker tokens from V1 storage, it will be created when necessary 96 | try (Statement stmtDeleteDockerV1 = conn.createStatement()) { 97 | stmtDeleteDockerV1.execute("DELETE api_key WHERE domain = 'DockerToken'"); 98 | } 99 | 100 | // H2 timestamp format 101 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 102 | 103 | while (rs.next()) { 104 | String encPrincipal = rs.getString("PRIMARY_PRINCIPAL"); 105 | String principal = encryptedString.decrypt(encPrincipal); 106 | String encToken = rs.getString("TOKEN"); 107 | String token = encryptedString.decrypt(encToken); 108 | long created = rs.getTimestamp("CREATED").getTime(); 109 | String createdStr = sdf.format(created); 110 | 111 | log.info("Key for principal: {} ({}), token: {}, created: {}", principal, encPrincipal, 112 | log.isTraceEnabled() ? token : "***", createdStr); 113 | 114 | // Skip not exist user 115 | CUser user = securityConfiguration.getUser(principal); 116 | if (user == null) { 117 | sql = "DELETE FROM api_key WHERE primary_principal = '" + encPrincipal + "'"; 118 | log.info("User is null, delete all token from V1 storage: {}", sql); 119 | try (Statement stmtDeleteV1 = conn.createStatement()) { 120 | stmtDeleteV1.execute(sql); 121 | } 122 | continue; 123 | } 124 | String realmName = "[pac4jRealm]".equals(user.getPassword()) ? "pac4jRealm" : "NexusAuthorizingRealm"; 125 | log.trace("User id: {}, status: {}, password: {}, realm: {}", user.getId(), user.getStatus(), // 126 | user.getPassword(), realmName); 127 | 128 | try { 129 | // Detect broken token in V1 storage 130 | Optional apiKey = apiKeyService.getApiKeyByToken(domain, token.toCharArray()); 131 | 132 | if (apiKey.isPresent() && !(apiKey.get().getPrincipals().getPrimaryPrincipal() instanceof String)) { 133 | log.trace("Principal class: {}, collection class: {}", 134 | apiKey.get().getPrincipals().getPrimaryPrincipal().getClass(), apiKey.get().getPrincipals().getClass()); 135 | throw new IllegalStateException("Principal class not string: " // 136 | + apiKey.get().getPrincipals().getPrimaryPrincipal().getClass()); 137 | } 138 | } catch (Exception te) { 139 | try { 140 | log.info("Detected broken token in V1 storage for principal: {} ({}), cause by: {}", principal, encPrincipal, 141 | te.getMessage()); 142 | 143 | SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, realmName); 144 | 145 | sql = "DELETE FROM api_key WHERE domain = '" + domain + "' AND primary_principal = '" + encPrincipal + "'"; 146 | log.trace(" 1 Delete old broken token from V1 storage: {}", sql); 147 | try (Statement stmtDeleteV1 = conn.createStatement()) { 148 | stmtDeleteV1.execute(sql); 149 | } 150 | 151 | log.trace(" 2 Create new token in V1 and V2 storage for principal: {} ({})", principal, encPrincipal); 152 | String newKey = new String(apiKeyService.createApiKey(domain, principals)); // Creates in V1 and V2 if NOT mirgated 153 | 154 | sql = "DELETE FROM api_key_v2 WHERE domain = '" + domain + "' AND username = '" + principal + "' AND access_key = '" 155 | + newKey.replaceAll("-([a-zA-Z0-9]*)-([a-zA-Z0-9]*)$", "") + "'"; // Trim key for V2 storage format 156 | log.trace(" 3 Delete duplicate token from V2 storage: {}", sql); 157 | try (Statement stmtDeleteV2 = conn.createStatement()) { 158 | stmtDeleteV2.execute(sql); 159 | } 160 | 161 | sql = "UPDATE api_key SET token = '" + encToken + "', created = (TIMESTAMP '" + createdStr + "') WHERE domain = '" 162 | + domain + "' AND primary_principal = '" + encPrincipal + "'"; 163 | log.trace(" 4 Set old value to new token in V1 storage: {}", sql); 164 | try (Statement stmtUpdateV1 = conn.createStatement()) { 165 | stmtUpdateV1.execute(sql); 166 | } 167 | 168 | log.info("Recreated broken token in V1 storage for principal: {} ({})", principal, encPrincipal); 169 | } catch (Exception se) { 170 | log.error("Error sanitize token for principal: {}", principal, se); 171 | } 172 | } 173 | } 174 | } catch (SQLException sqle) { 175 | log.error("Error sanitizeBrokenTokens", sqle); 176 | } 177 | } 178 | 179 | } 180 | --------------------------------------------------------------------------------