Skip to content

Commit

Permalink
Merged in SHIBUI-1029 (pull request #275)
Browse files Browse the repository at this point in the history
SHIBUI-1029
  • Loading branch information
Bill Smith authored and Jonathan Johnson committed Jan 23, 2019
2 parents 0c735ac + 850690f commit c4043ea
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 52 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ configurations {

processResources.dependsOn(':ui:npm_run_buildProd')

jar {
enabled = true
}

//Integration of the frontend and backend into the build to have all of the UI resources available in the app's executable war
bootWar.dependsOn(':ui:npm_run_buildProd')
bootWar.baseName = 'shibui'
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ shibui.nameid-filter-ui-schema-location=classpath:nameid-filter.schema.json
# shibui.metadataProviders.taskRunRate=30000

# Email configuration (local mailhog)
spring.mail.host=localhost
spring.mail.host=mailhog
spring.mail.port=1025
spring.mail.username=username
spring.mail.password=password
Expand Down
3 changes: 3 additions & 0 deletions backend/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ message.required-for-regex=Required for Regex
message.file-doesnt-exist=The requested file to be processed does not exist on the server.
message.database-constraint=There was a database constraint problem processing the request. Check the request to ensure that fields that must be unique are truly unique.

message.user-request-received-title=User request received
message.user-request-received-body=Your request has been received and is being reviewed. You will be notified with access status.

tooltip.entity-id=Entity ID
tooltip.service-provider-name=Service Provider Name (Dashboard Display Only)
tooltip.force-authn=Disallows use (or reuse) of authentication results and login flows that don\u0027t provide a real-time proof of user presence in the login process
Expand Down
13 changes: 6 additions & 7 deletions pac4j-module/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
plugins {
id 'java'
id 'groovy'
id 'com.palantir.docker' version '0.20.1'
id 'jacoco'
id 'io.franzbecker.gradle-lombok' version '1.13'
id 'org.springframework.boot' version '2.0.0.RELEASE' apply false
id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}
Expand All @@ -18,11 +17,6 @@ repositories {
}
}

lombok {
version = "1.16.20"
sha256 = "c5178b18caaa1a15e17b99ba5e4023d2de2ebc18b58cde0f5a04ca4b31c10e6d"
}

dependencyManagement {
imports {
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
Expand All @@ -39,6 +33,11 @@ dependencies {
exclude group: 'org.opensaml'
}

testCompile project(':backend')
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
testCompile "org.spockframework:spock-spring:1.1-groovy-2.4"

annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

docker project(':backend')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package net.unicon.shibui.pac4j;

import com.fasterxml.jackson.databind.ObjectMapper;
import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse;
import edu.internet2.tier.shibboleth.admin.ui.security.model.Role;
import edu.internet2.tier.shibboleth.admin.ui.security.model.User;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import edu.internet2.tier.shibboleth.admin.ui.service.EmailService;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCrypt;

import javax.mail.MessagingException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
public class AddNewUserFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(AddNewUserFilter.class);

private static final String ROLE_NONE = "ROLE_NONE";

private UserRepository userRepository;
private RoleRepository roleRepository;
private EmailService emailService;

public AddNewUserFilter(UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.emailService = emailService;
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
String username = authentication.getName();
if (username != null) {
Optional<User> persistedUser = userRepository.findByUsername(username);
User user;
if (!persistedUser.isPresent()) {
user = new User();
user.setUsername(username);
user.setPassword(BCrypt.hashpw(RandomStringUtils.randomAlphanumeric(20), BCrypt.gensalt()));
Role noRole = roleRepository.findByName(ROLE_NONE).orElse(new Role(ROLE_NONE));
roleRepository.save(noRole);
user.getRoles().add(noRole);
userRepository.save(user);
try {
emailService.sendNewUserMail(username);
} catch (MessagingException e) {
logger.warn(String.format("Unable to send new user email for user [%s]", username), e);
}
} else {
user = persistedUser.get();
}
if (user.getRole().equals(ROLE_NONE)) {
((HttpServletResponse) response).sendRedirect("/static.html");
} else {
chain.doFilter(request, response); // else, user is in the system already, carry on
}
}
}
}

@Override
public void destroy() {
}

private byte[] getJsonResponseBytes(ErrorResponse eErrorResponse) throws IOException {
String errorResponseJson = new ObjectMapper().writeValueAsString(eErrorResponse);
return errorResponseJson.getBytes();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package net.unicon.shibui.pac4j;

import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import edu.internet2.tier.shibboleth.admin.ui.service.EmailService;
import org.pac4j.core.config.Config;
import org.pac4j.springframework.security.web.CallbackFilter;
import org.pac4j.springframework.security.web.SecurityFilter;
Expand All @@ -17,8 +20,8 @@
@AutoConfigureOrder(-1)
public class WebSecurity {
@Bean("webSecurityConfig")
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config) {
return new Pac4jWebSecurityConfigurerAdapter(config);
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) {
return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService);
}

@Configuration
Expand All @@ -30,28 +33,51 @@ protected void configure(HttpSecurity http) throws Exception {
}
}

@Configuration
@Order(1)
public static class StaticSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/static.html").authorizeRequests().antMatchers("/static.html").permitAll();
}
}

@Configuration
@Order(2)
public static class ErrorSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/error").authorizeRequests().antMatchers("/error").permitAll();
}
}

@Order(100)
public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
private final Config config;
private UserRepository userRepository;
private RoleRepository roleRepository;
private EmailService emailService;

public Pac4jWebSecurityConfigurerAdapter(final Config config) {
public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) {
this.config = config;
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.emailService = emailService;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
final SecurityFilter securityFilter = new SecurityFilter(this.config, "Saml2Client");

final CallbackFilter callbackFilter = new CallbackFilter(this.config);
// http.regexMatcher("/callback").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class);
http.antMatcher("/**").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class);
http.antMatcher("/**").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class)
.addFilterBefore(securityFilter, BasicAuthenticationFilter.class)
.addFilterAfter(new AddNewUserFilter(userRepository, roleRepository, emailService), SecurityFilter.class);

http.authorizeRequests().anyRequest().fullyAuthenticated();

http.addFilterBefore(securityFilter, BasicAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

// http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

http.csrf().disable();
http.headers().frameOptions().disable();
}
Expand Down
4 changes: 2 additions & 2 deletions pac4j-module/src/test/docker/conf/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ shibui:
keystorePath: "/conf/samlKeystore.jks"
keystorePassword: "changeit"
privateKeyPassword: "changeit"
serviceProviderEntityId: "https://unicon.net/shibui"
serviceProviderEntityId: "https://unicon.net/test/shibui"
serviceProviderMetadataPath: "/conf/sp-metadata.xml"
identityProviderMetadataPath: "/conf/idp-metadata.xml"
forceServiceProviderMetadataGeneration: true
Expand All @@ -19,4 +19,4 @@ shibui:
logging:
level:
org.pac4j: "TRACE"
org.opensaml: "INFO"
org.opensaml: "INFO"
112 changes: 82 additions & 30 deletions pac4j-module/src/test/docker/conf/idp-metadata.xml
Original file line number Diff line number Diff line change
@@ -1,30 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://accounts.google.com/o/saml2?idpid=C04alwt8m" validUntil="2021-06-30T14:58:44.000Z">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDdDCCAlygAwIBAgIGAVWm+BpSMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ
bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv
b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzAx
MTQ1ODQ0WhcNMjEwNjMwMTQ1ODQ0WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN
TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAirwyeCS6SZpnYxprfXhpTNXwVfQC+J9OvBlJp8/7ngA627yER1bvfUkBMQxo0CXe
H6HX6Vw1DgalZJeEGDZSErlAY7lWkXkHdsejlMoYayQSZz2b/EfeRetwxh3Ek0hMDScOgDlsdfAn
AiZ4//n3IlypCi4ZMnLPs308FYunvp+R0Wd8Yqj8ctKhiYs6fCSHksDd+JKPe2FC1Zqw9GCGhi32
DBNRTHfE3tX3rTRs1pT0qbrQmpPfeBYfX00astGa3Dq/XWVO62IlqM7nVjglIPdi0tCIx+5RVZrY
uvULMipA+131TMxTpcGjUFxNwzPdogdpNhtL8+erfhG26C6b8wIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQCIOe/bW+mdE9PuarSz60HPGe9ROibyEOTyAWGxvSFfqoNFzaH3oOiEHMNG+ZkHHGtGEeWc
KYQ72V1OKO4aNqy2XaT3onOkd2oh4N8Q5pWrgMRkAB2HvBhBcQeO6yojVamTd43Kbtc+Hly3o+Or
XXOR9cgfxX/0Dbb+xwzTcwcMoJ1CPd3T4zxByKMHNflWrgrmZ9DmDOya4Aqs+xvrvPJB2VHaXoJ6
r/N+xtG8zO8wNRuxQxNUvtcFKKX2sZAqQRASGi1z8Y1FhU6rWBdBRtaiASAIgkNwOmS603Mm08Yr
0Yq7x6h3XlG8HO0bAOto6pr6q85pLqqv7v7/x7mfdjV3</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://accounts.google.com/o/saml2/idp?idpid=C04alwt8m"/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://accounts.google.com/o/saml2/idp?idpid=C04alwt8m"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
<?xml version="1.0" encoding="UTF-8"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://idp.unicon.net/idp/shibboleth">

<IDPSSODescriptor protocolSupportEnumeration="urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">

<Extensions>
<shibmd:Scope regexp="false">unicon.net</shibmd:Scope>
<mdui:UIInfo>
<mdui:DisplayName xml:lang="en">Unicon, Inc.</mdui:DisplayName>
<mdui:Description xml:lang="en">Login service for Unicon Employees</mdui:Description>
<mdui:Logo height="40" width="135" xml:lang="en">https://idp.unicon.net/logo_135_0.png</mdui:Logo>
</mdui:UIInfo>
</Extensions>

<KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIDIzCCAgugAwIBAgIUIEHTfbStY0ckKZzxIgqd5p1O2K0wDQYJKoZIhvcNAQEF
BQAwGTEXMBUGA1UEAxMOaWRwLnVuaWNvbi5uZXQwHhcNMTEwOTEzMDMyMzE2WhcN
MzEwOTEzMDMyMzE2WjAZMRcwFQYDVQQDEw5pZHAudW5pY29uLm5ldDCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBANtUsFXxlhvD3bWT5Y7TqKkf5rxa+dPA
z7vpbJ6bWhDPSMXb/9MiJe/ciY5ZKKrB1rdRC04s7blrzem3YtjGihfGd4ld+NRt
Pi0xoAT2YIp83CvEe5BHAKwqD7KTonN1unbN84mVo65itbme9d8lZKc0PfLM+BQp
fhXKUBfYeBCkYU4YWxmgL4Vs7XBaKjEjpTN4ncar4YSrarWTTPyO5RzmVPLAcv88
1OBqewTyN41+JRXt0Jopi4ZQ8JjKkm73vhoYDBPHr/VMqk1lFfrDcDwJa2ygyWCm
qTlq6zyLE9Fr6sYz6CbgA2lAqu/b1rYCqVCnRpoHZKahAQ9uGQSfHD8CAwEAAaNj
MGEwQAYDVR0RBDkwN4IOaWRwLnVuaWNvbi5uZXSGJWh0dHBzOi8vaWRwLnVuaWNv
bi5uZXQvaWRwL3NoaWJib2xldGgwHQYDVR0OBBYEFK6yUrpGjvY3B09ke0kVl4wA
CMAnMA0GCSqGSIb3DQEBBQUAA4IBAQDG/gMpr3N+nAMuo7RhtDBsckiJV2+BwT/r
JmpxlHAV1Zgc3eeuOdyxm5/jA78tspLldL0+6W/LzZWov/je36IqVT1wSGy1n0Sc
Pjw8DHgyEJLCij2vVScV+j/Y4Eg0bVy6pZTeQW+e3ygb6WgiVT/ARM8QBp6GjAUC
qIlJCads9Rcx3vAih72I4exUUD4qMuBMeLIdY5XReHy5YHqxbkPjQhDIEORAFlzJ
jLqO/Ldzn4waEa5snDZyeYjsl6pi+8CVGfXLSDVsDuk5s47B9OD+gOSJ1wEc7O/N
nU9d/WCcM1V4IGZGL8TXUdfJoVXYZUFF08jUGSL2mj30WS1orIWo

</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>

<KeyDescriptor use="encryption">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIDIzCCAgugAwIBAgIUIEHTfbStY0ckKZzxIgqd5p1O2K0wDQYJKoZIhvcNAQEF
BQAwGTEXMBUGA1UEAxMOaWRwLnVuaWNvbi5uZXQwHhcNMTEwOTEzMDMyMzE2WhcN
MzEwOTEzMDMyMzE2WjAZMRcwFQYDVQQDEw5pZHAudW5pY29uLm5ldDCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBANtUsFXxlhvD3bWT5Y7TqKkf5rxa+dPA
z7vpbJ6bWhDPSMXb/9MiJe/ciY5ZKKrB1rdRC04s7blrzem3YtjGihfGd4ld+NRt
Pi0xoAT2YIp83CvEe5BHAKwqD7KTonN1unbN84mVo65itbme9d8lZKc0PfLM+BQp
fhXKUBfYeBCkYU4YWxmgL4Vs7XBaKjEjpTN4ncar4YSrarWTTPyO5RzmVPLAcv88
1OBqewTyN41+JRXt0Jopi4ZQ8JjKkm73vhoYDBPHr/VMqk1lFfrDcDwJa2ygyWCm
qTlq6zyLE9Fr6sYz6CbgA2lAqu/b1rYCqVCnRpoHZKahAQ9uGQSfHD8CAwEAAaNj
MGEwQAYDVR0RBDkwN4IOaWRwLnVuaWNvbi5uZXSGJWh0dHBzOi8vaWRwLnVuaWNv
bi5uZXQvaWRwL3NoaWJib2xldGgwHQYDVR0OBBYEFK6yUrpGjvY3B09ke0kVl4wA
CMAnMA0GCSqGSIb3DQEBBQUAA4IBAQDG/gMpr3N+nAMuo7RhtDBsckiJV2+BwT/r
JmpxlHAV1Zgc3eeuOdyxm5/jA78tspLldL0+6W/LzZWov/je36IqVT1wSGy1n0Sc
Pjw8DHgyEJLCij2vVScV+j/Y4Eg0bVy6pZTeQW+e3ygb6WgiVT/ARM8QBp6GjAUC
qIlJCads9Rcx3vAih72I4exUUD4qMuBMeLIdY5XReHy5YHqxbkPjQhDIEORAFlzJ
jLqO/Ldzn4waEa5snDZyeYjsl6pi+8CVGfXLSDVsDuk5s47B9OD+gOSJ1wEc7O/N
nU9d/WCcM1V4IGZGL8TXUdfJoVXYZUFF08jUGSL2mj30WS1orIWo

</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<!--
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.unicon.net/idp/profile/SAML2/Redirect/SLO"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.unicon.net/idp/profile/SAML2/POST/SLO"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://idp.unicon.net/idp/profile/SAML2/POST-SimpleSign/SLO"/>
-->
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>

<SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://idp.unicon.net/idp/profile/Shibboleth/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.unicon.net/idp/profile/SAML2/POST/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://idp.unicon.net/idp/profile/SAML2/POST-SimpleSign/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.unicon.net/idp/profile/SAML2/Redirect/SSO"/>
</IDPSSODescriptor>

</EntityDescriptor>
14 changes: 12 additions & 2 deletions pac4j-module/src/test/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: "3.7"
services:
shibui:
image: unicon/shibui-pac4j
entrypoint: ["/usr/bin/java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", "-jar", "app.jar"]
entrypoint: ["/usr/bin/java", "-Dspring.profiles.active=dev", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", "-jar", "app.jar"]
ports:
- 8080:8080
- 5005:5005
Expand All @@ -13,6 +13,16 @@ services:
- ./conf/application.yml:/application.yml
networks:
- front

mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
container_name: mailhog
networks:
- front

networks:
front:
driver: bridge
driver: bridge
Loading

0 comments on commit c4043ea

Please sign in to comment.