Make sure JAX-RS resource methods are advertizing the media type they support

Closes #15811
Closes #15810
This commit is contained in:
Pedro Igor 2022-12-02 10:33:24 -03:00
parent 2f0d8cd895
commit 022d2864a6
21 changed files with 210 additions and 55 deletions

View file

@ -0,0 +1,27 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.http;
import java.io.InputStream;
public interface FormPartValue {
String asString();
InputStream asInputStream();
}

View file

@ -47,6 +47,13 @@ public interface HttpRequest {
*/ */
MultivaluedMap<String, String> getDecodedFormParameters(); MultivaluedMap<String, String> getDecodedFormParameters();
/**
* Parses the parts from a multipart form request (e.g.: multipart/form-data media type).
*
* @return the parts from a multipart form request
*/
MultivaluedMap<String, FormPartValue> getMultiPartFormParameters();
/** /**
* Returns the HTTP headers. * Returns the HTTP headers.
* *

View file

@ -136,7 +136,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
@Override @Override
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) { public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
return Response.ok(identity.getToken()).build(); return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
} }
@Override @Override

View file

@ -254,7 +254,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
@Override @Override
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) { public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
return Response.ok(identity.getToken()).build(); return Response.ok(identity.getToken()).type(MediaType.TEXT_PLAIN_TYPE).build();
} }
@Override @Override

View file

@ -33,6 +33,8 @@ import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenIntrospectContext; import org.keycloak.services.clientpolicy.context.TokenIntrospectContext;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
@ -66,6 +68,7 @@ public class TokenIntrospectionEndpoint {
@POST @POST
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response introspect() { public Response introspect() {
event.event(EventType.INTROSPECT_TOKEN); event.event(EventType.INTROSPECT_TOKEN);

View file

@ -24,6 +24,7 @@ import java.util.stream.Collectors;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.OPTIONS; import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -83,6 +84,7 @@ public class TokenRevocationEndpoint {
} }
@POST @POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response revoke() { public Response revoke() {
event.event(EventType.REVOKE_GRANT); event.event(EventType.REVOKE_GRANT);

View file

@ -73,6 +73,7 @@ import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS; import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -119,6 +120,7 @@ public class UserInfoEndpoint {
@Path("/") @Path("/")
@GET @GET
@NoCache @NoCache
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
public Response issueUserInfoGet() { public Response issueUserInfoGet() {
setupCors(); setupCors();
String accessToken = this.appAuthManager.extractAuthorizationHeaderTokenOrReturnNull(session.getContext().getRequestHeaders()); String accessToken = this.appAuthManager.extractAuthorizationHeaderTokenOrReturnNull(session.getContext().getRequestHeaders());
@ -129,6 +131,7 @@ public class UserInfoEndpoint {
@Path("/") @Path("/")
@POST @POST
@NoCache @NoCache
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
public Response issueUserInfoPost() { public Response issueUserInfoPost() {
setupCors(); setupCors();

View file

@ -1425,7 +1425,7 @@ public class SamlService extends AuthorizationEndpointBase {
EntityUtils.consumeQuietly(result.getEntity()); EntityUtils.consumeQuietly(result.getEntity());
} }
} catch (IOException | ProcessingException | ParsingException e) { } catch (IOException | ProcessingException | ParsingException | IllegalArgumentException e) {
event.event(EventType.LOGIN); event.event(EventType.LOGIN);
event.detail(Details.REASON, e.getMessage()); event.detail(Details.REASON, e.getMessage());
event.error(Errors.IDENTITY_PROVIDER_ERROR); event.error(Errors.IDENTITY_PROVIDER_ERROR);

View file

@ -76,8 +76,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
uriInfo = new HashMap<>(); uriInfo = new HashMap<>();
} }
UriInfo originalUriInfo = getContextObject(UriInfo.class); uriInfo.put(type, new KeycloakUriInfo(session, type, getHttpRequest().getUri()));
uriInfo.put(type, new KeycloakUriInfo(session, type, originalUriInfo));
} }
return uriInfo.get(type); return uriInfo.get(type);
} }

View file

@ -0,0 +1,53 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.keycloak.http.FormPartValue;
public class FormPartValueImpl implements FormPartValue {
private String value;
private InputStream inputStream;
public FormPartValueImpl(String value) {
this.value = value;
}
public FormPartValueImpl(InputStream inputStream) {
this.inputStream = inputStream;
this.value = null;
}
@Override
public String asString() {
if (inputStream != null) {
throw new RuntimeException("Value is a input stream");
}
return value;
}
@Override
public InputStream asInputStream() {
if (inputStream == null) {
return new ByteArrayInputStream(value.getBytes());
}
return inputStream;
}
}

View file

@ -17,12 +17,26 @@
package org.keycloak.services; package org.keycloak.services;
import org.keycloak.http.HttpRequest; import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA_TYPE;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.security.cert.X509Certificate; import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Providers;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.keycloak.http.FormPartValue;
import org.keycloak.http.HttpRequest;
public class HttpRequestImpl implements HttpRequest { public class HttpRequestImpl implements HttpRequest {
@ -48,6 +62,41 @@ public class HttpRequestImpl implements HttpRequest {
return delegate.getDecodedFormParameters(); return delegate.getDecodedFormParameters();
} }
@Override
public MultivaluedMap<String, FormPartValue> getMultiPartFormParameters() {
try {
MediaType mediaType = getHttpHeaders().getMediaType();
if (!MULTIPART_FORM_DATA_TYPE.isCompatible(mediaType) || !mediaType.getParameters().containsKey("boundary")) {
return new MultivaluedHashMap<>();
}
Providers providers = ResteasyContext.getContextData(Providers.class);
MessageBodyReader<MultipartFormDataInput> multiPartProvider = providers.getMessageBodyReader(
MultipartFormDataInput.class, null, null, MULTIPART_FORM_DATA_TYPE);
MultipartFormDataInput inputs = multiPartProvider
.readFrom(null, null, null, mediaType, getHttpHeaders().getRequestHeaders(),
delegate.getInputStream());
MultivaluedHashMap<String, FormPartValue> parts = new MultivaluedHashMap<>();
for (Map.Entry<String, List<InputPart>> entry : inputs.getFormDataMap().entrySet()) {
for (InputPart value : entry.getValue()) {
MediaType valueMediaType = value.getMediaType();
if (TEXT_PLAIN_TYPE.isCompatible(valueMediaType)) {
parts.add(entry.getKey(), new FormPartValueImpl(value.getBodyAsString()));
} else {
parts.add(entry.getKey(), new FormPartValueImpl(value.getBody(InputStream.class, null)));
}
}
}
return parts;
} catch (IOException cause) {
throw new RuntimeException("Failed to parse multi part request", cause);
}
}
@Override @Override
public HttpHeaders getHttpHeaders() { public HttpHeaders getHttpHeaders() {
if (delegate == null) { if (delegate == null) {

View file

@ -40,6 +40,7 @@ import org.keycloak.urls.UrlType;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
@ -223,6 +224,18 @@ public class AdminRoot {
return new RealmsAdminResource(session, auth, tokenManager); return new RealmsAdminResource(session, auth, tokenManager);
} }
@Path("{any:.*}")
@OPTIONS
public Object preFlight() {
HttpRequest request = getHttpRequest();
if (!isAdminApiEnabled()) {
throw new NotFoundException();
}
return new AdminCorsPreflightService(request);
}
/** /**
* General information about the server * General information about the server
* *

View file

@ -18,8 +18,6 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import javax.ws.rs.NotAcceptableException; import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
@ -29,6 +27,7 @@ import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat; import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.http.FormPartValue;
import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser; import org.keycloak.jose.jwk.JWKParser;
@ -52,6 +51,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -61,8 +61,6 @@ import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -140,11 +138,11 @@ public class ClientAttributeCertificateResource {
@Path("upload") @Path("upload")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public CertificateRepresentation uploadJks(MultipartFormDataInput input) throws IOException { public CertificateRepresentation uploadJks() throws IOException {
auth.clients().requireConfigure(client); auth.clients().requireConfigure(client);
try { try {
CertificateRepresentation info = getCertFromRequest(input); CertificateRepresentation info = getCertFromRequest();
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
@ -165,11 +163,11 @@ public class ClientAttributeCertificateResource {
@Path("upload-certificate") @Path("upload-certificate")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public CertificateRepresentation uploadJksCertificate(MultipartFormDataInput input) throws IOException { public CertificateRepresentation uploadJksCertificate() throws IOException {
auth.clients().requireConfigure(client); auth.clients().requireConfigure(client);
try { try {
CertificateRepresentation info = getCertFromRequest(input); CertificateRepresentation info = getCertFromRequest();
info.setPrivateKey(null); info.setPrivateKey(null);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
@ -180,16 +178,16 @@ public class ClientAttributeCertificateResource {
} }
} }
private CertificateRepresentation getCertFromRequest(MultipartFormDataInput input) throws IOException { private CertificateRepresentation getCertFromRequest() throws IOException {
auth.clients().requireManage(client); auth.clients().requireManage(client);
CertificateRepresentation info = new CertificateRepresentation(); CertificateRepresentation info = new CertificateRepresentation();
Map<String, List<InputPart>> uploadForm = input.getFormDataMap(); MultivaluedMap<String, FormPartValue> uploadForm = session.getContext().getHttpRequest().getMultiPartFormParameters();
List<InputPart> keystoreFormatPart = uploadForm.get("keystoreFormat"); FormPartValue keystoreFormatPart = uploadForm.getFirst("keystoreFormat");
if (keystoreFormatPart == null) throw new BadRequestException(); if (keystoreFormatPart == null) throw new BadRequestException();
String keystoreFormat = keystoreFormatPart.get(0).getBodyAsString(); String keystoreFormat = keystoreFormatPart.asString();
List<InputPart> inputParts = uploadForm.get("file"); FormPartValue inputParts = uploadForm.getFirst("file");
if (keystoreFormat.equals(CERTIFICATE_PEM)) { if (keystoreFormat.equals(CERTIFICATE_PEM)) {
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null)); String pem = StreamUtil.readString(inputParts.asInputStream());
pem = PemUtils.removeBeginEnd(pem); pem = PemUtils.removeBeginEnd(pem);
@ -199,7 +197,7 @@ public class ClientAttributeCertificateResource {
info.setCertificate(pem); info.setCertificate(pem);
return info; return info;
} else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) { } else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) {
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null)); String pem = StreamUtil.readString(inputParts.asInputStream());
// Validate format // Validate format
KeycloakModelUtils.getPublicKey(pem); KeycloakModelUtils.getPublicKey(pem);
@ -207,7 +205,7 @@ public class ClientAttributeCertificateResource {
info.setPublicKey(pem); info.setPublicKey(pem);
return info; return info;
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
InputStream stream = inputParts.get(0).getBody(InputStream.class, null); InputStream stream = inputParts.asInputStream();
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class); JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
if (publicKeyJwk == null) { if (publicKeyJwk == null) {
@ -222,17 +220,17 @@ public class ClientAttributeCertificateResource {
} }
String keyAlias = uploadForm.get("keyAlias").get(0).getBodyAsString(); String keyAlias = uploadForm.getFirst("keyAlias").asString();
List<InputPart> keyPasswordPart = uploadForm.get("keyPassword"); FormPartValue keyPasswordPart = uploadForm.getFirst("keyPassword");
char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.get(0).getBodyAsString().toCharArray() : null; char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.asString().toCharArray() : null;
List<InputPart> storePasswordPart = uploadForm.get("storePassword"); FormPartValue storePasswordPart = uploadForm.getFirst("storePassword");
char[] storePassword = storePasswordPart != null ? storePasswordPart.get(0).getBodyAsString().toCharArray() : null; char[] storePassword = storePasswordPart != null ? storePasswordPart.asString().toCharArray() : null;
PrivateKey privateKey = null; PrivateKey privateKey = null;
X509Certificate certificate = null; X509Certificate certificate = null;
try { try {
KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(KeystoreFormat.valueOf(keystoreFormat)); KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(KeystoreFormat.valueOf(keystoreFormat));
keyStore.load(inputParts.get(0).getBody(InputStream.class, null), storePassword); keyStore.load(inputParts.asInputStream(), storePassword);
try { try {
privateKey = (PrivateKey)keyStore.getKey(keyAlias, keyPassword); privateKey = (PrivateKey)keyStore.getKey(keyAlias, keyPassword);
} catch (Exception e) { } catch (Exception e) {

View file

@ -31,6 +31,7 @@ import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
@ -169,8 +170,12 @@ public class GroupResource {
child = realm.createGroup(groupName, group); child = realm.createGroup(groupName, group);
updateGroup(rep, child, realm, session); updateGroup(rep, child, realm, session);
URI uri = session.getContext().getUri().getBaseUriBuilder() URI uri = session.getContext().getUri().getBaseUriBuilder()
.path(session.getContext().getUri().getMatchedURIs().get(2)) .path(AdminRoot.class)
.path(child.getId()).build(); .path(AdminRoot.class, "getRealmsAdmin")
.path(RealmsAdminResource.class, "getRealmAdmin")
.path(RealmAdminResource.class, "getGroups")
.path(GroupsResource.class, "getGroupById")
.build(realm.getName(), child.getId());
builder.status(201).location(uri); builder.status(201).location(uri);
rep.setId(child.getId()); rep.setId(child.getId());
adminEvent.operation(OperationType.CREATE); adminEvent.operation(OperationType.CREATE);

View file

@ -19,14 +19,13 @@ package org.keycloak.services.resources.admin;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.http.FormPartValue;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -47,10 +46,10 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -106,15 +105,14 @@ public class IdentityProvidersResource {
@Path("import-config") @Path("import-config")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Map<String, String> importFrom(MultipartFormDataInput input) throws IOException { public Map<String, String> importFrom() throws IOException {
this.auth.realm().requireManageIdentityProviders(); this.auth.realm().requireManageIdentityProviders();
Map<String, List<InputPart>> formDataMap = input.getFormDataMap(); MultivaluedMap<String, FormPartValue> formDataMap = session.getContext().getHttpRequest().getMultiPartFormParameters();
if (!(formDataMap.containsKey("providerId") && formDataMap.containsKey("file"))) { if (!(formDataMap.containsKey("providerId") && formDataMap.containsKey("file"))) {
throw new BadRequestException(); throw new BadRequestException();
} }
String providerId = formDataMap.get("providerId").get(0).getBodyAsString(); String providerId = formDataMap.getFirst("providerId").asString();
InputPart file = formDataMap.get("file").get(0); InputStream inputStream = formDataMap.getFirst("file").asInputStream();
InputStream inputStream = file.getBody(InputStream.class, null);
IdentityProviderFactory providerFactory = getProviderFactoryById(providerId); IdentityProviderFactory providerFactory = getProviderFactoryById(providerId);
Map<String, String> config = providerFactory.parseConfig(session, inputStream); Map<String, String> config = providerFactory.parseConfig(session, inputStream);
return config; return config;
@ -133,7 +131,7 @@ public class IdentityProvidersResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Map<String, String> importFrom(Map<String, Object> data) throws IOException { public Map<String, String> importFrom(Map<String, Object> data) throws IOException {
this.auth.realm().requireManageIdentityProviders(); this.auth.realm().requireManageIdentityProviders();
if (!(data.containsKey("providerId") && data.containsKey("fromUrl"))) { if (data == null || !(data.containsKey("providerId") && data.containsKey("fromUrl"))) {
throw new BadRequestException(); throw new BadRequestException();
} }

View file

@ -540,6 +540,7 @@ public class RealmAdminResource {
* *
*/ */
@Path("push-revocation") @Path("push-revocation")
@Produces(MediaType.APPLICATION_JSON)
@POST @POST
public GlobalRequestResult pushRevocation() { public GlobalRequestResult pushRevocation() {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
@ -1010,6 +1011,7 @@ public class RealmAdminResource {
*/ */
@Path("partialImport") @Path("partialImport")
@POST @POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response partialImport(InputStream requestBody) { public Response partialImport(InputStream requestBody) {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
@ -1065,6 +1067,7 @@ public class RealmAdminResource {
* @return * @return
*/ */
@Path("partial-export") @Path("partial-export")
@Produces(MediaType.APPLICATION_JSON)
@POST @POST
public Response partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles, public Response partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles,
@QueryParam("exportClients") Boolean exportClients) { @QueryParam("exportClients") Boolean exportClients) {

View file

@ -19,8 +19,7 @@ package org.keycloak.services.resources.admin;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.keycloak.http.FormPartValue;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -28,7 +27,6 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -45,6 +43,7 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
@ -82,16 +81,14 @@ public class RealmLocalizationResource {
@POST @POST
@Path("{locale}") @Path("{locale}")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
public void createOrUpdateRealmLocalizationTextsFromFile(@PathParam("locale") String locale, public void createOrUpdateRealmLocalizationTextsFromFile(@PathParam("locale") String locale) {
MultipartFormDataInput input) {
this.auth.realm().requireManageRealm(); this.auth.realm().requireManageRealm();
Map<String, List<InputPart>> formDataMap = input.getFormDataMap(); MultivaluedMap<String, FormPartValue> formDataMap = session.getContext().getHttpRequest().getMultiPartFormParameters();
if (!formDataMap.containsKey("file")) { if (!formDataMap.containsKey("file")) {
throw new BadRequestException(); throw new BadRequestException();
} }
InputPart file = formDataMap.get("file").get(0); try (InputStream inputStream = formDataMap.getFirst("file").asInputStream()) {
try (InputStream inputStream = file.getBody(InputStream.class, null)) {
TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() { TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {
}; };
Map<String, String> rep = JsonSerialization.readValue(inputStream, typeRef); Map<String, String> rep = JsonSerialization.readValue(inputStream, typeRef);

View file

@ -1893,7 +1893,7 @@ public class OAuthClient {
Header[] contentTypeHeaders = response.getHeaders("Content-Type"); Header[] contentTypeHeaders = response.getHeaders("Content-Type");
String contentType = (contentTypeHeaders != null && contentTypeHeaders.length > 0) ? contentTypeHeaders[0].getValue() : null; String contentType = (contentTypeHeaders != null && contentTypeHeaders.length > 0) ? contentTypeHeaders[0].getValue() : null;
if (!"application/json".equals(contentType)) { if (contentType == null || !contentType.startsWith("application/json")) {
Assert.fail("Invalid content type. Status: " + statusCode + ", contentType: " + contentType); Assert.fail("Invalid content type. Status: " + statusCode + ", contentType: " + contentType);
} }

View file

@ -18,10 +18,8 @@
package org.keycloak.testsuite.admin; package org.keycloak.testsuite.admin;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
@ -642,12 +640,12 @@ public class PermissionsTest extends AbstractKeycloakTest {
invoke(new Invocation() { invoke(new Invocation() {
public void invoke(RealmResource realm) { public void invoke(RealmResource realm) {
realm.clients().get(foo.getId()).getCertficateResource("nosuch").uploadJks(new MultipartFormDataOutput()); realm.clients().get(foo.getId()).getCertficateResource("nosuch").uploadJks(null);
} }
}, Resource.CLIENT, true); }, Resource.CLIENT, true);
invoke(new Invocation() { invoke(new Invocation() {
public void invoke(RealmResource realm) { public void invoke(RealmResource realm) {
realm.clients().get(foo.getId()).getCertficateResource("nosuch").uploadJksCertificate(new MultipartFormDataOutput()); realm.clients().get(foo.getId()).getCertficateResource("nosuch").uploadJksCertificate(null);
} }
}, Resource.CLIENT, true); }, Resource.CLIENT, true);
@ -1727,7 +1725,7 @@ public class PermissionsTest extends AbstractKeycloakTest {
}, Resource.IDENTITY_PROVIDER, true); }, Resource.IDENTITY_PROVIDER, true);
invoke(new Invocation() { invoke(new Invocation() {
public void invoke(RealmResource realm) { public void invoke(RealmResource realm) {
realm.identityProviders().importFrom(new MultipartFormDataOutput()); realm.identityProviders().importFrom(null);
} }
}, Resource.IDENTITY_PROVIDER, true); }, Resource.IDENTITY_PROVIDER, true);
} }

View file

@ -177,7 +177,7 @@ public class URLAssert {
public void assertResponse(CloseableHttpResponse response) throws IOException { public void assertResponse(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
Header contentType = entity.getContentType(); Header contentType = entity.getContentType();
Assert.assertEquals("application/json", contentType.getValue()); Assert.assertTrue(contentType.getValue().startsWith("application/json"));
char [] buf = new char[8192]; char [] buf = new char[8192];
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();