diff --git a/pom.xml b/pom.xml index 63705b7..1d64c5f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ confapi-commons - 0.4.1-SNAPSHOT + 0.5.0-SNAPSHOT jar ConfAPI Commons diff --git a/src/main/java/de/aservo/confapi/commons/constants/ConfAPI.java b/src/main/java/de/aservo/confapi/commons/constants/ConfAPI.java index 9da103e..3a3d3d4 100644 --- a/src/main/java/de/aservo/confapi/commons/constants/ConfAPI.java +++ b/src/main/java/de/aservo/confapi/commons/constants/ConfAPI.java @@ -7,6 +7,12 @@ public class ConfAPI { public static final String APPLICATIONS = "applications"; public static final String APPLICATION_LINK = "application-link"; public static final String APPLICATION_LINKS = "application-links"; + public static final String AUTHENTICATION = "authentication"; + public static final String AUTHENTICATION_IDP = "idp"; + public static final String AUTHENTICATION_IDPS = "idps"; + public static final String AUTHENTICATION_IDP_OIDC = "oidc"; + public static final String AUTHENTICATION_IDP_SAML = "saml"; + public static final String AUTHENTICATION_SSO = "sso"; public static final String BACKUP = "backup"; public static final String BACKUP_EXPORT = "export"; public static final String BACKUP_IMPORT = "import"; diff --git a/src/main/java/de/aservo/confapi/commons/model/AbstractAuthenticationIdpBean.java b/src/main/java/de/aservo/confapi/commons/model/AbstractAuthenticationIdpBean.java new file mode 100644 index 0000000..a679f84 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/model/AbstractAuthenticationIdpBean.java @@ -0,0 +1,45 @@ +package de.aservo.confapi.commons.model; + +import de.aservo.confapi.commons.constants.ConfAPI; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.codehaus.jackson.annotate.JsonSubTypes; +import org.codehaus.jackson.annotate.JsonTypeInfo; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@XmlRootElement +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +// Note: New subtypes must also be registered in AuthenticationIdpsBean.java to be considered in the REST API documentation +@JsonSubTypes({ + @JsonSubTypes.Type(value = AuthenticationIdpOidcBean.class, name = ConfAPI.AUTHENTICATION_IDP_OIDC), + @JsonSubTypes.Type(value = AuthenticationIdpSamlBean.class, name = ConfAPI.AUTHENTICATION_IDP_SAML), +}) +public abstract class AbstractAuthenticationIdpBean { + + @XmlElement + private Long id; + + @XmlElement + private String name; + + @XmlElement + private Boolean enabled; + + @XmlElement + private String url; + + @XmlElement + private Boolean enableRememberMe; + + @XmlElement + private String buttonText; + +} diff --git a/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpOidcBean.java b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpOidcBean.java new file mode 100644 index 0000000..e905b63 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpOidcBean.java @@ -0,0 +1,67 @@ +package de.aservo.confapi.commons.model; + +import de.aservo.confapi.commons.constants.ConfAPI; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collections; +import java.util.List; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = ConfAPI.AUTHENTICATION + "-" + ConfAPI.AUTHENTICATION_IDP + "-" + ConfAPI.AUTHENTICATION_IDP_OIDC) +public class AuthenticationIdpOidcBean extends AbstractAuthenticationIdpBean { + + @XmlElement + private String clientId; + + @XmlElement + private String clientSecret; + + @XmlElement + private String usernameClaim; + + @XmlElement + private List additionalScopes; + + @XmlElement + private Boolean discoveryEnabled; + + @XmlElement + private String authorizationEndpoint; + + @XmlElement + private String tokenEndpoint; + + @XmlElement + private String userInfoEndpoint; + + // Just-in-time user provisioning is not implemented yet + + // Example instances for documentation and tests + + public static final AuthenticationIdpOidcBean EXAMPLE_1; + + static { + EXAMPLE_1 = new AuthenticationIdpOidcBean(); + EXAMPLE_1.setId(1L); + EXAMPLE_1.setName("OIDC"); + EXAMPLE_1.setEnabled(true); + EXAMPLE_1.setUrl("https://oidc.example.com"); + EXAMPLE_1.setEnableRememberMe(true); + EXAMPLE_1.setButtonText("Login with OIDC"); + EXAMPLE_1.setClientId("oidc"); + EXAMPLE_1.setClientSecret("s3cr3t"); + EXAMPLE_1.setUsernameClaim("userName"); + EXAMPLE_1.setAdditionalScopes(Collections.emptyList()); + EXAMPLE_1.setDiscoveryEnabled(false); + EXAMPLE_1.setAuthorizationEndpoint("https://oidc.example.com/authorization"); + EXAMPLE_1.setTokenEndpoint("https://oidc.example.com/token"); + EXAMPLE_1.setUserInfoEndpoint("https://oidc.example.com/userinfo"); + } + +} diff --git a/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpSamlBean.java b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpSamlBean.java new file mode 100644 index 0000000..e428fdf --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpSamlBean.java @@ -0,0 +1,41 @@ +package de.aservo.confapi.commons.model; + +import de.aservo.confapi.commons.constants.ConfAPI; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = ConfAPI.AUTHENTICATION + "-" + ConfAPI.AUTHENTICATION_IDP + "-" + ConfAPI.AUTHENTICATION_IDP_SAML) +public class AuthenticationIdpSamlBean extends AbstractAuthenticationIdpBean { + + @XmlElement + private String certificate; + + @XmlElement + private String usernameAttribute; + + // Just-in-time user provisioning is not implemented yet + + // Example instances for documentation and tests + + public static final AuthenticationIdpSamlBean EXAMPLE_1; + + static { + EXAMPLE_1 = new AuthenticationIdpSamlBean(); + EXAMPLE_1.setId(1L); + EXAMPLE_1.setName("SAML"); + EXAMPLE_1.setEnabled(true); + EXAMPLE_1.setUrl("https://saml.example.com"); + EXAMPLE_1.setEnableRememberMe(true); + EXAMPLE_1.setButtonText("Login with SAML"); + EXAMPLE_1.setCertificate("certificate"); + EXAMPLE_1.setUsernameAttribute("username"); + } + +} diff --git a/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpsBean.java b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpsBean.java new file mode 100644 index 0000000..6386361 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/model/AuthenticationIdpsBean.java @@ -0,0 +1,25 @@ +package de.aservo.confapi.commons.model; + +import de.aservo.confapi.commons.constants.ConfAPI; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = ConfAPI.AUTHENTICATION + "-" + ConfAPI.AUTHENTICATION_IDPS) +public class AuthenticationIdpsBean { + + @XmlElement + @Schema(anyOf = { + AuthenticationIdpOidcBean.class, + }) + private Collection authenticationIdpBeans; + +} diff --git a/src/main/java/de/aservo/confapi/commons/model/AuthenticationSsoBean.java b/src/main/java/de/aservo/confapi/commons/model/AuthenticationSsoBean.java new file mode 100644 index 0000000..385f5e9 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/model/AuthenticationSsoBean.java @@ -0,0 +1,27 @@ +package de.aservo.confapi.commons.model; + +import de.aservo.confapi.commons.constants.ConfAPI; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@XmlRootElement(name = ConfAPI.AUTHENTICATION + "-" + ConfAPI.AUTHENTICATION_SSO) +public class AuthenticationSsoBean { + + @XmlElement + private Boolean showOnLogin; + + // Example instances for documentation and tests + + public static final AuthenticationSsoBean EXAMPLE_1; + + static { + EXAMPLE_1 = new AuthenticationSsoBean(); + EXAMPLE_1.setShowOnLogin(true); + } + +} diff --git a/src/main/java/de/aservo/confapi/commons/rest/AbstractAuthenticationResourceImpl.java b/src/main/java/de/aservo/confapi/commons/rest/AbstractAuthenticationResourceImpl.java new file mode 100644 index 0000000..b294046 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/rest/AbstractAuthenticationResourceImpl.java @@ -0,0 +1,48 @@ +package de.aservo.confapi.commons.rest; + +import de.aservo.confapi.commons.model.AuthenticationIdpsBean; +import de.aservo.confapi.commons.model.AuthenticationSsoBean; +import de.aservo.confapi.commons.rest.api.AuthenticationResource; +import de.aservo.confapi.commons.service.api.AuthenticationService; + +import javax.ws.rs.core.Response; + +public abstract class AbstractAuthenticationResourceImpl implements AuthenticationResource { + + private final AuthenticationService authenticationService; + + protected AbstractAuthenticationResourceImpl( + final AuthenticationService authenticationService) { + + this.authenticationService = authenticationService; + } + + @Override + public Response getAuthenticationIdps() { + final AuthenticationIdpsBean resultAuthenticationIdpsBean = authenticationService.getAuthenticationIdps(); + return Response.ok(resultAuthenticationIdpsBean).build(); + } + + @Override + public Response setAuthenticationIdps( + final AuthenticationIdpsBean authenticationIdpsBean) { + + final AuthenticationIdpsBean resultAuthenticationIdpsBean = authenticationService.setAuthenticationIdps(authenticationIdpsBean); + return Response.ok(resultAuthenticationIdpsBean).build(); + } + + @Override + public Response getAuthenticationSso() { + final AuthenticationSsoBean resultAuthenticationSsoBean = authenticationService.getAuthenticationSso(); + return Response.ok(resultAuthenticationSsoBean).build(); + } + + @Override + public Response setAuthenticationSso( + final AuthenticationSsoBean authenticationSsoBean) { + + final AuthenticationSsoBean resultAuthenticationSsoBean = authenticationService.setAuthenticationSso(authenticationSsoBean); + return Response.ok(resultAuthenticationSsoBean).build(); + } + +} diff --git a/src/main/java/de/aservo/confapi/commons/rest/api/AuthenticationResource.java b/src/main/java/de/aservo/confapi/commons/rest/api/AuthenticationResource.java new file mode 100644 index 0000000..af33e10 --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/rest/api/AuthenticationResource.java @@ -0,0 +1,98 @@ +package de.aservo.confapi.commons.rest.api; + +import de.aservo.confapi.commons.constants.ConfAPI; +import de.aservo.confapi.commons.http.PATCH; +import de.aservo.confapi.commons.model.AuthenticationIdpsBean; +import de.aservo.confapi.commons.model.AuthenticationSsoBean; +import de.aservo.confapi.commons.model.ErrorCollection; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +public interface AuthenticationResource { + + @GET + @Path(ConfAPI.AUTHENTICATION_IDPS) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + tags = { ConfAPI.AUTHENTICATION }, + summary = "Get all authentication identity providers", + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = AuthenticationIdpsBean.class)), + description = "Returns all authentication identity providers."), + @ApiResponse( + content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = "Returns a list of error messages." + ) + } + ) + Response getAuthenticationIdps(); + + @PATCH + @Path(ConfAPI.AUTHENTICATION_IDPS) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + tags = { ConfAPI.AUTHENTICATION }, + summary = "Set all authentication identity providers", + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = AuthenticationIdpsBean.class)), + description = "Returns the set authentication identity providers."), + @ApiResponse( + content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = "Returns a list of error messages." + ) + } + ) + Response setAuthenticationIdps( + final AuthenticationIdpsBean authenticationIdpsBean); + + @GET + @Path(ConfAPI.AUTHENTICATION_SSO) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + tags = { ConfAPI.AUTHENTICATION }, + summary = "Get authentication SSO configuration", + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = AuthenticationSsoBean.class)), + description = "Returns the authentication SSO configuration."), + @ApiResponse( + content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = "Returns a list of error messages." + ) + } + ) + Response getAuthenticationSso(); + + @PATCH + @Path(ConfAPI.AUTHENTICATION_SSO) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + tags = { ConfAPI.AUTHENTICATION }, + summary = "Set authentication SSO configuration", + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = AuthenticationSsoBean.class)), + description = "Returns the set authentication SSO configuration."), + @ApiResponse( + content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = "Returns a list of error messages." + ) + } + ) + Response setAuthenticationSso( + final AuthenticationSsoBean authenticationSsoBean); + +} diff --git a/src/main/java/de/aservo/confapi/commons/service/api/AuthenticationService.java b/src/main/java/de/aservo/confapi/commons/service/api/AuthenticationService.java new file mode 100644 index 0000000..a099c9c --- /dev/null +++ b/src/main/java/de/aservo/confapi/commons/service/api/AuthenticationService.java @@ -0,0 +1,22 @@ +package de.aservo.confapi.commons.service.api; + +import de.aservo.confapi.commons.model.AbstractAuthenticationIdpBean; +import de.aservo.confapi.commons.model.AuthenticationIdpsBean; +import de.aservo.confapi.commons.model.AuthenticationSsoBean; + +public interface AuthenticationService { + + AuthenticationIdpsBean getAuthenticationIdps(); + + AuthenticationIdpsBean setAuthenticationIdps( + AuthenticationIdpsBean authenticationIdpsBean); + + AbstractAuthenticationIdpBean setAuthenticationIdp( + AbstractAuthenticationIdpBean authenticationIdpBean); + + AuthenticationSsoBean getAuthenticationSso(); + + AuthenticationSsoBean setAuthenticationSso( + AuthenticationSsoBean authenticationSsoBean); + +} diff --git a/src/test/java/de/aservo/confapi/commons/rest/AuthenticationResourceTest.java b/src/test/java/de/aservo/confapi/commons/rest/AuthenticationResourceTest.java new file mode 100644 index 0000000..440c04f --- /dev/null +++ b/src/test/java/de/aservo/confapi/commons/rest/AuthenticationResourceTest.java @@ -0,0 +1,88 @@ +package de.aservo.confapi.commons.rest; + +import de.aservo.confapi.commons.model.*; +import de.aservo.confapi.commons.rest.impl.TestAuthenticationResourceImpl; +import de.aservo.confapi.commons.service.api.AuthenticationService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(MockitoExtension.class) +class AuthenticationResourceTest { + + @Mock + private AuthenticationService authenticationService; + + private TestAuthenticationResourceImpl resource; + + @BeforeEach + public void setup() { + resource = new TestAuthenticationResourceImpl(authenticationService); + } + + @Test + void testGetAuthenticationIdps() { + final Collection authenticationIdpBeans = Arrays.asList( + AuthenticationIdpOidcBean.EXAMPLE_1, + AuthenticationIdpSamlBean.EXAMPLE_1 + ); + final AuthenticationIdpsBean authenticationIdpsBean = new AuthenticationIdpsBean(authenticationIdpBeans); + doReturn(authenticationIdpsBean).when(authenticationService).getAuthenticationIdps(); + + final Response response = resource.getAuthenticationIdps(); + assertEquals(200, response.getStatus()); + + final AuthenticationIdpsBean authenticationIdpsBeanResponse = (AuthenticationIdpsBean) response.getEntity(); + assertEquals(authenticationIdpsBean, authenticationIdpsBeanResponse); + } + + @Test + void testSetAuthenticationIdps() { + final Collection authenticationIdpBeans = Arrays.asList( + AuthenticationIdpOidcBean.EXAMPLE_1, + AuthenticationIdpSamlBean.EXAMPLE_1 + ); + final AuthenticationIdpsBean authenticationIdpsBean = new AuthenticationIdpsBean(authenticationIdpBeans); + doReturn(authenticationIdpsBean).when(authenticationService).setAuthenticationIdps(authenticationIdpsBean); + + final Response response = resource.setAuthenticationIdps(authenticationIdpsBean); + assertEquals(200, response.getStatus()); + + final AuthenticationIdpsBean authenticationIdpsBeanResponse = (AuthenticationIdpsBean) response.getEntity(); + assertEquals(authenticationIdpsBean, authenticationIdpsBeanResponse); + } + + @Test + void testGetAuthenticationSso() { + final AuthenticationSsoBean authenticationSsoBean = AuthenticationSsoBean.EXAMPLE_1; + doReturn(authenticationSsoBean).when(authenticationService).getAuthenticationSso(); + + final Response response = resource.getAuthenticationSso(); + assertEquals(200, response.getStatus()); + + final AuthenticationSsoBean authenticationSsoBeanResponse = (AuthenticationSsoBean) response.getEntity(); + assertEquals(authenticationSsoBean, authenticationSsoBeanResponse); + } + + @Test + void testSetAuthenticationSso() { + final AuthenticationSsoBean authenticationSsoBean = AuthenticationSsoBean.EXAMPLE_1; + doReturn(authenticationSsoBean).when(authenticationService).setAuthenticationSso(authenticationSsoBean); + + final Response response = resource.setAuthenticationSso(authenticationSsoBean); + assertEquals(200, response.getStatus()); + + final AuthenticationSsoBean authenticationSsoBeanResponse = (AuthenticationSsoBean) response.getEntity(); + assertEquals(authenticationSsoBean, authenticationSsoBeanResponse); + } + +} diff --git a/src/test/java/de/aservo/confapi/commons/rest/impl/TestAuthenticationResourceImpl.java b/src/test/java/de/aservo/confapi/commons/rest/impl/TestAuthenticationResourceImpl.java new file mode 100644 index 0000000..53869ea --- /dev/null +++ b/src/test/java/de/aservo/confapi/commons/rest/impl/TestAuthenticationResourceImpl.java @@ -0,0 +1,12 @@ +package de.aservo.confapi.commons.rest.impl; + +import de.aservo.confapi.commons.rest.AbstractAuthenticationResourceImpl; +import de.aservo.confapi.commons.service.api.AuthenticationService; + +public class TestAuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { + + public TestAuthenticationResourceImpl(AuthenticationService authenticationService) { + super(authenticationService); + } + +}