diff --git a/index.adoc b/index.adoc index afc4dad..0b8b735 100644 --- a/index.adoc +++ b/index.adoc @@ -686,6 +686,332 @@ ifdef::internal-generation[] endif::internal-generation[] +[.Authentication] +=== Authentication + + +[.getAuthenticationIdps] +==== getAuthenticationIdps + +`GET /authentication/idps` + +Get all authentication identity providers + +===== Description + + + + +// markup not found, no include::{specDir}authentication/idps/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns all authentication identity providers. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authentication/idps/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authentication/idps/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authentication/idps/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authentication/idps/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getAuthenticationSso] +==== getAuthenticationSso + +`GET /authentication/sso` + +Get authentication SSO configuration + +===== Description + + + + +// markup not found, no include::{specDir}authentication/sso/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns the authentication SSO configuration. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authentication/sso/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authentication/sso/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authentication/sso/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authentication/sso/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.setAuthenticationIdps] +==== setAuthenticationIdps + +`PATCH /authentication/idps` + +Set all authentication identity providers + +===== Description + + + + +// markup not found, no include::{specDir}authentication/idps/PATCH/spec.adoc[opts=optional] + + + +===== Parameters + + +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| AuthenticationIdpsBean +| <> +| - +| +| + +|=== + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns the set authentication identity providers. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authentication/idps/PATCH/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authentication/idps/PATCH/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authentication/idps/PATCH/PATCH.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authentication/idps/PATCH/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.setAuthenticationSso] +==== setAuthenticationSso + +`PATCH /authentication/sso` + +Set authentication SSO configuration + +===== Description + + + + +// markup not found, no include::{specDir}authentication/sso/PATCH/spec.adoc[opts=optional] + + + +===== Parameters + + +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| AuthenticationSsoBean +| <> +| - +| +| + +|=== + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Returns the set authentication SSO configuration. +| <> + + +| 0 +| Returns a list of error messages. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authentication/sso/PATCH/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authentication/sso/PATCH/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authentication/sso/PATCH/PATCH.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authentication/sso/PATCH/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.Cache] === Cache @@ -4126,6 +4452,141 @@ endif::internal-generation[] |=== +[#AuthenticationIdpOidcBean] +=== _AuthenticationIdpOidcBean_ + + + +[.fields-AuthenticationIdpOidcBean] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| id +| +| Long +| +| int64 + +| name +| +| String +| +| + +| enabled +| +| Boolean +| +| + +| url +| +| String +| +| + +| enableRememberMe +| +| Boolean +| +| + +| buttonText +| +| String +| +| + +| clientId +| +| String +| +| + +| clientSecret +| +| String +| +| + +| usernameClaim +| +| String +| +| + +| additionalScopes +| +| List of <> +| +| + +| discoveryEnabled +| +| Boolean +| +| + +| authorizationEndpoint +| +| String +| +| + +| tokenEndpoint +| +| String +| +| + +| userInfoEndpoint +| +| String +| +| + +|=== + + +[#AuthenticationIdpsBean] +=== _AuthenticationIdpsBean_ + + + +[.fields-AuthenticationIdpsBean] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| authenticationIdpBeans +| +| List of <> +| +| + +|=== + + +[#AuthenticationSsoBean] +=== _AuthenticationSsoBean_ + + + +[.fields-AuthenticationSsoBean] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| showOnLogin +| +| Boolean +| +| + +|=== + + [#CacheBean] === _CacheBean_ diff --git a/pom.xml b/pom.xml index c1a43fd..c58ee1c 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 8.1.4 ${project.groupId}.${project.artifactId} 2.2.4 - 0.4.0-SNAPSHOT + 0.5.0 2.0.2 2.3.1 2.1.1 @@ -245,6 +245,13 @@ + + com.atlassian.plugins.authentication + atlassian-authentication-plugin + 4.3.8 + provided + + io.swagger.core.v3 swagger-annotations diff --git a/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationIdpBeanUtil.java b/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationIdpBeanUtil.java new file mode 100644 index 0000000..96bf365 --- /dev/null +++ b/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationIdpBeanUtil.java @@ -0,0 +1,135 @@ +package de.aservo.confapi.confluence.model.util; + +import com.atlassian.plugins.authentication.api.config.IdpConfig; +import com.atlassian.plugins.authentication.api.config.SsoType; +import com.atlassian.plugins.authentication.api.config.oidc.OidcConfig; +import de.aservo.confapi.commons.exception.BadRequestException; +import de.aservo.confapi.commons.exception.InternalServerErrorException; +import de.aservo.confapi.commons.model.AbstractAuthenticationIdpBean; +import de.aservo.confapi.commons.model.AuthenticationIdpOidcBean; + +public class AuthenticationIdpBeanUtil { + + public static IdpConfig toIdpConfig( + final AbstractAuthenticationIdpBean authenticationIdpBean) { + + return toIdpConfig(authenticationIdpBean, null); + }; + + public static IdpConfig toIdpConfig( + final AbstractAuthenticationIdpBean authenticationIdpBean, + final IdpConfig existingIdpConfig) { + + if (authenticationIdpBean.getClass().isAssignableFrom(AuthenticationIdpOidcBean.class)) { + return toOidcConfig((AuthenticationIdpOidcBean) authenticationIdpBean, existingIdpConfig); + } + + throw new UnsupportedIdpTypeException(); + } + + private static OidcConfig toOidcConfig( + final AuthenticationIdpOidcBean authenticationIdpOidcBean, + final IdpConfig existingIdpConfig) { + + final OidcConfig.Builder oidcConfigBuilder; + + if (existingIdpConfig != null) { + if (!existingIdpConfig.getClass().isAssignableFrom(OidcConfig.class)) { + throw new BadRequestException("The existing IDP config with the same name is not of type OIDC"); + } + + oidcConfigBuilder = OidcConfig.builder((OidcConfig) existingIdpConfig); + } else { + oidcConfigBuilder = OidcConfig.builder(); + } + + if (authenticationIdpOidcBean.getName() != null) { + oidcConfigBuilder.setName(authenticationIdpOidcBean.getName()); + } + if (authenticationIdpOidcBean.getEnabled() != null) { + oidcConfigBuilder.setEnabled(authenticationIdpOidcBean.getEnabled()); + } + if (authenticationIdpOidcBean.getUrl() != null) { + oidcConfigBuilder.setIssuer(authenticationIdpOidcBean.getUrl()); + } + if (authenticationIdpOidcBean.getEnableRememberMe() != null) { + oidcConfigBuilder.setEnableRememberMe(authenticationIdpOidcBean.getEnableRememberMe()); + } + if (authenticationIdpOidcBean.getButtonText() != null) { + oidcConfigBuilder.setButtonText(authenticationIdpOidcBean.getButtonText()); + } + if (authenticationIdpOidcBean.getClientId() != null) { + oidcConfigBuilder.setClientId(authenticationIdpOidcBean.getClientId()); + } + if (authenticationIdpOidcBean.getClientSecret() != null) { + oidcConfigBuilder.setClientSecret(authenticationIdpOidcBean.getClientSecret()); + } + if (authenticationIdpOidcBean.getUsernameClaim() != null) { + oidcConfigBuilder.setUsernameClaim(authenticationIdpOidcBean.getUsernameClaim()); + } + if (authenticationIdpOidcBean.getAdditionalScopes() != null) { + oidcConfigBuilder.setAdditionalScopes(authenticationIdpOidcBean.getAdditionalScopes()); + } + if (authenticationIdpOidcBean.getDiscoveryEnabled() != null) { + oidcConfigBuilder.setDiscoveryEnabled(authenticationIdpOidcBean.getDiscoveryEnabled()); + } + if (authenticationIdpOidcBean.getAuthorizationEndpoint() != null) { + oidcConfigBuilder.setAuthorizationEndpoint(authenticationIdpOidcBean.getAuthorizationEndpoint()); + } + if (authenticationIdpOidcBean.getTokenEndpoint() != null) { + oidcConfigBuilder.setTokenEndpoint(authenticationIdpOidcBean.getTokenEndpoint()); + } + if (authenticationIdpOidcBean.getUserInfoEndpoint() != null) { + oidcConfigBuilder.setUserInfoEndpoint(authenticationIdpOidcBean.getUserInfoEndpoint()); + } + + return oidcConfigBuilder.build(); + } + + public static AbstractAuthenticationIdpBean toAuthenticationIdpBean( + final IdpConfig idpConfig) { + + if (idpConfig.getSsoType().equals(SsoType.OIDC)) { + return toAuthenticationIdpOidcBean(idpConfig); + } + + throw new UnsupportedIdpTypeException(); + } + + public static AuthenticationIdpOidcBean toAuthenticationIdpOidcBean( + final IdpConfig idpConfig) { + + if (!idpConfig.getClass().isAssignableFrom(OidcConfig.class)) { + throw new InternalServerErrorException("The class of the IDP config is not OIDC"); + } + + final OidcConfig oidcConfig = (OidcConfig) idpConfig; + + final AuthenticationIdpOidcBean authenticationIdpOidcBean = new AuthenticationIdpOidcBean(); + authenticationIdpOidcBean.setId(oidcConfig.getId()); + authenticationIdpOidcBean.setName(oidcConfig.getName()); + authenticationIdpOidcBean.setEnabled(oidcConfig.isEnabled()); + authenticationIdpOidcBean.setUrl(oidcConfig.getIssuer()); + authenticationIdpOidcBean.setEnableRememberMe(oidcConfig.isEnableRememberMe()); + authenticationIdpOidcBean.setButtonText(oidcConfig.getButtonText()); + authenticationIdpOidcBean.setClientId(oidcConfig.getClientId()); + authenticationIdpOidcBean.setUsernameClaim(oidcConfig.getUsernameClaim()); + authenticationIdpOidcBean.setAdditionalScopes(oidcConfig.getAdditionalScopes()); + authenticationIdpOidcBean.setDiscoveryEnabled(oidcConfig.isDiscoveryEnabled()); + authenticationIdpOidcBean.setAuthorizationEndpoint(oidcConfig.getAuthorizationEndpoint()); + authenticationIdpOidcBean.setTokenEndpoint(oidcConfig.getTokenEndpoint()); + authenticationIdpOidcBean.setUserInfoEndpoint(oidcConfig.getUserInfoEndpoint()); + + return authenticationIdpOidcBean; + } + + private AuthenticationIdpBeanUtil() { + } + + static class UnsupportedIdpTypeException extends UnsupportedOperationException { + public UnsupportedIdpTypeException() { + super("IDP types other than OIDC are not (yet) supported"); + } + } + +} diff --git a/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationSsoBeanUtil.java b/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationSsoBeanUtil.java new file mode 100644 index 0000000..317a5e9 --- /dev/null +++ b/src/main/java/de/aservo/confapi/confluence/model/util/AuthenticationSsoBeanUtil.java @@ -0,0 +1,46 @@ +package de.aservo.confapi.confluence.model.util; + +import com.atlassian.plugins.authentication.api.config.ImmutableSsoConfig; +import com.atlassian.plugins.authentication.api.config.SsoConfig; +import de.aservo.confapi.commons.model.AuthenticationSsoBean; + +public class AuthenticationSsoBeanUtil { + + public static SsoConfig toSsoConfig( + final AuthenticationSsoBean authenticationSsoBean) { + + return toSsoConfig(authenticationSsoBean, null); + }; + + public static SsoConfig toSsoConfig( + final AuthenticationSsoBean authenticationSsoBean, + final SsoConfig existingSsoConfig) { + + final ImmutableSsoConfig.Builder ssoConfigBuilder; + + if (existingSsoConfig != null) { + ssoConfigBuilder = ImmutableSsoConfig.toBuilder(existingSsoConfig); + } else { + ssoConfigBuilder = ImmutableSsoConfig.builder(); + } + + if (authenticationSsoBean.getShowOnLogin() != null) { + ssoConfigBuilder.setShowLoginForm(authenticationSsoBean.getShowOnLogin()); + } + + return ssoConfigBuilder.build(); + } + + public static AuthenticationSsoBean toAuthenticationSsoBean( + final SsoConfig ssoConfig) { + + final AuthenticationSsoBean authenticationSsoBean = new AuthenticationSsoBean(); + authenticationSsoBean.setShowOnLogin(ssoConfig.getShowLoginForm()); + + return authenticationSsoBean; + } + + private AuthenticationSsoBeanUtil() { + } + +} diff --git a/src/main/java/de/aservo/confapi/confluence/rest/AuthenticationResourceImpl.java b/src/main/java/de/aservo/confapi/confluence/rest/AuthenticationResourceImpl.java new file mode 100644 index 0000000..5ddc6ac --- /dev/null +++ b/src/main/java/de/aservo/confapi/confluence/rest/AuthenticationResourceImpl.java @@ -0,0 +1,23 @@ +package de.aservo.confapi.confluence.rest; + +import com.sun.jersey.spi.container.ResourceFilters; +import de.aservo.confapi.commons.constants.ConfAPI; +import de.aservo.confapi.commons.rest.AbstractAuthenticationResourceImpl; +import de.aservo.confapi.commons.service.api.AuthenticationService; +import de.aservo.confapi.confluence.filter.SysAdminOnlyResourceFilter; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.ws.rs.Path; + +@Path(ConfAPI.AUTHENTICATION) +@ResourceFilters(SysAdminOnlyResourceFilter.class) +@Component +public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { + + @Inject + public AuthenticationResourceImpl(AuthenticationService authenticationService) { + super(authenticationService); + } + +} diff --git a/src/main/java/de/aservo/confapi/confluence/service/AuthenticationServiceImpl.java b/src/main/java/de/aservo/confapi/confluence/service/AuthenticationServiceImpl.java new file mode 100644 index 0000000..5478f10 --- /dev/null +++ b/src/main/java/de/aservo/confapi/confluence/service/AuthenticationServiceImpl.java @@ -0,0 +1,119 @@ +package de.aservo.confapi.confluence.service; + +import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.plugins.authentication.api.config.IdpConfig; +import com.atlassian.plugins.authentication.api.config.IdpConfigService; +import com.atlassian.plugins.authentication.api.config.SsoConfig; +import com.atlassian.plugins.authentication.api.config.SsoConfigService; +import com.atlassian.plugins.authentication.api.config.oidc.OidcConfig; +import de.aservo.confapi.commons.exception.BadRequestException; +import de.aservo.confapi.commons.model.AbstractAuthenticationIdpBean; +import de.aservo.confapi.commons.model.AuthenticationIdpsBean; +import de.aservo.confapi.commons.model.AuthenticationSsoBean; +import de.aservo.confapi.commons.service.api.AuthenticationService; +import de.aservo.confapi.confluence.model.util.AuthenticationIdpBeanUtil; +import de.aservo.confapi.confluence.model.util.AuthenticationSsoBeanUtil; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +@ExportAsService(AuthenticationService.class) +public class AuthenticationServiceImpl implements AuthenticationService { + + @ComponentImport + private final IdpConfigService idpConfigService; + + @ComponentImport + private final SsoConfigService ssoConfigService; + + public AuthenticationServiceImpl( + final IdpConfigService idpConfigService, + final SsoConfigService ssoConfigService) { + + this.idpConfigService = idpConfigService; + this.ssoConfigService = ssoConfigService; + } + + @Override + public AuthenticationIdpsBean getAuthenticationIdps() { + return new AuthenticationIdpsBean(idpConfigService.getIdpConfigs().stream() + .map(AuthenticationIdpBeanUtil::toAuthenticationIdpBean) + .sorted(authenticationIdpBeanComparator) + .collect(Collectors.toList())); + } + + @Override + public AuthenticationIdpsBean setAuthenticationIdps( + final AuthenticationIdpsBean authenticationIdpsBean) { + + return new AuthenticationIdpsBean(authenticationIdpsBean.getAuthenticationIdpBeans().stream() + .map(this::setAuthenticationIdp) + .sorted(authenticationIdpBeanComparator) + .collect(Collectors.toList())); + } + + public AbstractAuthenticationIdpBean setAuthenticationIdp( + final AbstractAuthenticationIdpBean authenticationIdpBean) { + + if (authenticationIdpBean.getName() == null || authenticationIdpBean.getName().trim().isEmpty()) { + throw new BadRequestException("The name cannot be empty"); + } + + final IdpConfig existingIdpConfig = findIdpConfigByName(authenticationIdpBean.getName()); + + if (existingIdpConfig == null) { + final IdpConfig idpConfig = AuthenticationIdpBeanUtil.toIdpConfig(authenticationIdpBean); + final IdpConfig addedIdpConfig = idpConfigService.addIdpConfig(idpConfig); + return AuthenticationIdpBeanUtil.toAuthenticationIdpBean(addedIdpConfig); + } + + if (authenticationIdpBean.getId() != null && !authenticationIdpBean.getId().equals(existingIdpConfig.getId())) { + throw new BadRequestException("An ID has been passed but it does not match the ID of the existing IDP with the same name"); + } + + if (!OidcConfig.class.isAssignableFrom(existingIdpConfig.getClass())) { + throw new BadRequestException("IDP types other than OIDC are not (yet) supported"); + } + + final IdpConfig idpConfig = AuthenticationIdpBeanUtil.toIdpConfig(authenticationIdpBean, existingIdpConfig); + final IdpConfig updatedIdpConfig = idpConfigService.updateIdpConfig(idpConfig); + return AuthenticationIdpBeanUtil.toAuthenticationIdpBean(updatedIdpConfig); + } + + @Override + public AuthenticationSsoBean getAuthenticationSso() { + return AuthenticationSsoBeanUtil.toAuthenticationSsoBean(ssoConfigService.getSsoConfig()); + } + + @Override + public AuthenticationSsoBean setAuthenticationSso(AuthenticationSsoBean authenticationSsoBean) { + final SsoConfig existingSsoConfig = ssoConfigService.getSsoConfig(); + final SsoConfig ssoConfig = AuthenticationSsoBeanUtil.toSsoConfig(authenticationSsoBean, existingSsoConfig); + return AuthenticationSsoBeanUtil.toAuthenticationSsoBean(ssoConfigService.updateSsoConfig(ssoConfig)); + } + + IdpConfig findIdpConfigByName( + final String name) { + + final Map idpConfigsByName = idpConfigService.getIdpConfigs().stream().collect(Collectors.toMap( + IdpConfig::getName, Function.identity(), (existing, replacement) -> { + throw new IllegalStateException("Duplicate name key found: " + existing.getName()); + } + )); + + return idpConfigsByName.get(name); + } + + static Comparator authenticationIdpBeanComparator = new Comparator() { + @Override + public int compare(AbstractAuthenticationIdpBean a1, AbstractAuthenticationIdpBean a2) { + return a1.getName().compareToIgnoreCase(a2.getName()); + } + }; + +}