├── 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 extends UserProfile> profiles;
28 | private final boolean byName;
29 | private String userPrefix = "";
30 | private String principalNameAttribute;
31 |
32 | public Pac4jPrincipalName(final List extends UserProfile> profiles) {
33 | this(profiles, null, false);
34 | }
35 |
36 | public Pac4jPrincipalName(final List extends UserProfile> 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 extends UserProfile> 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 extends UserProfile> 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