jaxrs-doclet support
This commit is contained in:
parent
38b52283a5
commit
199c786e22
24 changed files with 961 additions and 26 deletions
|
@ -18,6 +18,10 @@
|
|||
<directory>../../target/site/apidocs</directory>
|
||||
<outputDirectory>docs/javadocs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../../services/target/site/apidocs</directory>
|
||||
<outputDirectory>docs/rest-api</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../../docbook/target/docbook/publish/en-US</directory>
|
||||
<outputDirectory>docs/userguide</outputDirectory>
|
||||
|
|
|
@ -195,16 +195,34 @@
|
|||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<version>2.9.1</version>
|
||||
<executions>
|
||||
<!--
|
||||
<execution>
|
||||
<id>generate-service-docs</id>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<doclet>com.hypnoticocelot.jaxrs.doclet.ServiceDoclet</doclet>
|
||||
<docletArtifact>
|
||||
<groupId>com.hypnoticocelot</groupId>
|
||||
<artifactId>jaxrs-doclet</artifactId>
|
||||
<version>0.0.4-SNAPSHOT</version>
|
||||
</docletArtifact>
|
||||
<reportOutputDirectory>swagger</reportOutputDirectory>
|
||||
<useStandardDocletOptions>false</useStandardDocletOptions>
|
||||
<additionalparam>-apiVersion 1 -docBasePath /apidocs -apiBasePath /</additionalparam>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>javadoc</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
-->
|
||||
<execution>
|
||||
<id>generate-service-docs</id>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<doclet>com.lunatech.doclets.jax.jaxrs.JAXRSDoclet</doclet>
|
||||
<docletArtifacts>
|
||||
|
@ -214,13 +232,22 @@
|
|||
<version>0.10.0</version>
|
||||
</docletArtifact>
|
||||
</docletArtifacts>
|
||||
<detectOfflineLinks>false</detectOfflineLinks>
|
||||
<offlineLinks>
|
||||
<offlineLink>
|
||||
<url>../javadocs</url>
|
||||
<!-- <location>C:/Users/William/jboss/keycloak/p1b-repo/keycloak/target/site/apidocs</location> -->
|
||||
<location>${project.basedir}/../target/site/apidocs</location>
|
||||
</offlineLink>
|
||||
</offlineLinks>
|
||||
<additionalparam>-disablejavascriptexample</additionalparam>
|
||||
</configuration>
|
||||
<reports>
|
||||
<report>javadoc</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
<goals>
|
||||
<goal>javadoc</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -200,12 +200,22 @@ public class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS preflight
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("/")
|
||||
@OPTIONS
|
||||
public Response accountPreflight() {
|
||||
return Cors.add(request, Response.ok()).auth().preflight().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account information.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("/")
|
||||
@GET
|
||||
public Response accountPage() {
|
||||
|
@ -279,6 +289,18 @@ public class AccountService {
|
|||
return forwardToPage("sessions", AccountPages.SESSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update account information.
|
||||
*
|
||||
* Form params:
|
||||
*
|
||||
* firstName
|
||||
* lastName
|
||||
* email
|
||||
*
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("/")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -348,6 +370,17 @@ public class AccountService {
|
|||
return Response.seeOther(Urls.accountSessionsPage(uriInfo.getBaseUri(), realm.getName())).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TOTP for this account.
|
||||
*
|
||||
* form parameters:
|
||||
*
|
||||
* totp - otp generated by authenticator
|
||||
* totpSecret - totp secret to register
|
||||
*
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("totp")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -381,6 +414,18 @@ public class AccountService {
|
|||
return account.setSuccess("successTotp").createResponse(AccountPages.TOTP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update account password
|
||||
*
|
||||
* Form params:
|
||||
*
|
||||
* password - old password
|
||||
* password-new
|
||||
* pasword-confirm
|
||||
*
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("password")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
|
7
services/src/main/java/org/keycloak/services/resources/JsResource.java
Normal file → Executable file
7
services/src/main/java/org/keycloak/services/resources/JsResource.java
Normal file → Executable file
|
@ -7,11 +7,18 @@ import javax.ws.rs.core.Response;
|
|||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Get keycloak.js file for javascript clients
|
||||
*
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@Path("/js")
|
||||
public class JsResource {
|
||||
|
||||
/**
|
||||
* Get keycloak.js file for javascript clients
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("/keycloak.js")
|
||||
@Produces("text/javascript")
|
||||
|
|
|
@ -13,6 +13,8 @@ import javax.ws.rs.core.Context;
|
|||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* Resource class for public realm information
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -28,10 +30,15 @@ public class PublicRealmResource {
|
|||
this.realm = realm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public information about the realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces("application/json")
|
||||
public PublishedRealmRepresentation getRealm(@PathParam("realm") String id) {
|
||||
public PublishedRealmRepresentation getRealm() {
|
||||
return realmRep(realm, uriInfo);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,11 +18,23 @@ import java.io.IOException;
|
|||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Create a barcode image
|
||||
*
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@Path("/qrcode")
|
||||
public class QRCodeResource {
|
||||
|
||||
/**
|
||||
* Create a bar code image
|
||||
*
|
||||
* @param contents
|
||||
* @param size
|
||||
* @return
|
||||
* @throws ServletException
|
||||
* @throws IOException
|
||||
* @throws WriterException
|
||||
*/
|
||||
@GET
|
||||
@Produces("image/png")
|
||||
public Response createQrCode(@QueryParam("contents") String contents, @QueryParam("size") String size) throws ServletException, IOException, WriterException {
|
||||
|
|
|
@ -162,7 +162,7 @@ public class RealmsResource {
|
|||
protected RealmModel locateRealm(String name, RealmManager realmManager) {
|
||||
RealmModel realm = realmManager.getRealmByName(name);
|
||||
if (realm == null) {
|
||||
throw new NotFoundException("Realm " + name + " not found");
|
||||
throw new NotFoundException("Realm " + name + " does not exist");
|
||||
}
|
||||
return realm;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import javax.ws.rs.core.Response;
|
|||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Theme resource
|
||||
*
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@Path("/theme")
|
||||
|
@ -28,6 +30,14 @@ public class ThemeResource {
|
|||
@Context
|
||||
private ProviderSession providerSession;
|
||||
|
||||
/**
|
||||
* Get theme content
|
||||
*
|
||||
* @param themType
|
||||
* @param themeName
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("/{themType}/{themeName}/{path:.*}")
|
||||
public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) {
|
||||
|
|
|
@ -74,6 +74,8 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Resource class for the oauth/openid connect token service
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -198,7 +200,23 @@ public class TokenService {
|
|||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Direct grant REST invocation. One stop call to obtain an access token.
|
||||
*
|
||||
* If the client is a confidential client
|
||||
* you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header.
|
||||
*
|
||||
* If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name.
|
||||
*
|
||||
* The realm must be configured to allow these types of auth requests. (Direct Grant API in admin console Settings page)
|
||||
*
|
||||
*
|
||||
* @See <a href="http://tools.ietf.org/html/rfc6749#section-4.3">http://tools.ietf.org/html/rfc6749#section-4.3</a>
|
||||
*
|
||||
* @param authorizationHeader
|
||||
* @param form
|
||||
* @return @see org.keycloak.representations.AccessTokenResponse
|
||||
*/
|
||||
@Path("grants/access")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -284,6 +302,20 @@ public class TokenService {
|
|||
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL for making refresh token requests.
|
||||
*
|
||||
* @See <a href="http://tools.ietf.org/html/rfc6749#section-6">http://tools.ietf.org/html/rfc6749#section-6</a>
|
||||
*
|
||||
* If the client is a confidential client
|
||||
* you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header.
|
||||
*
|
||||
* If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name.
|
||||
*
|
||||
* @param authorizationHeader
|
||||
* @param form
|
||||
* @return
|
||||
*/
|
||||
@Path("refresh")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -321,6 +353,16 @@ public class TokenService {
|
|||
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY!
|
||||
*
|
||||
* @param clientId
|
||||
* @param scopeParam
|
||||
* @param state
|
||||
* @param redirect
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("auth/request/login")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -422,6 +464,16 @@ public class TokenService {
|
|||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration
|
||||
*
|
||||
* @param clientId
|
||||
* @param scopeParam
|
||||
* @param state
|
||||
* @param redirect
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("registrations")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
@ -530,6 +582,11 @@ public class TokenService {
|
|||
return processLogin(clientId, scopeParam, state, redirect, formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS preflight path for access code to token
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("access/codes")
|
||||
@OPTIONS
|
||||
@Produces("application/json")
|
||||
|
@ -538,6 +595,15 @@ public class TokenService {
|
|||
return Cors.add(request, Response.ok()).auth().preflight().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL invoked by adapter to turn an access code to access token
|
||||
*
|
||||
* @See <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
|
||||
*
|
||||
* @param authorizationHeader
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("access/codes")
|
||||
@POST
|
||||
@Produces("application/json")
|
||||
|
@ -720,6 +786,20 @@ public class TokenService {
|
|||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login page. Must be redirected to from the application or oauth client.
|
||||
*
|
||||
* @See <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
|
||||
*
|
||||
*
|
||||
* @param responseType
|
||||
* @param redirect
|
||||
* @param clientId
|
||||
* @param scopeParam
|
||||
* @param state
|
||||
* @param prompt
|
||||
* @return
|
||||
*/
|
||||
@Path("login")
|
||||
@GET
|
||||
public Response loginPage(final @QueryParam("response_type") String responseType,
|
||||
|
@ -784,6 +864,16 @@ public class TokenService {
|
|||
return Flows.forms(providerSession, realm, uriInfo).createLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration page. Must be redirected to from login page.
|
||||
*
|
||||
* @param responseType
|
||||
* @param redirect
|
||||
* @param clientId
|
||||
* @param scopeParam
|
||||
* @param state
|
||||
* @return
|
||||
*/
|
||||
@Path("registrations")
|
||||
@GET
|
||||
public Response registerPage(final @QueryParam("response_type") String responseType,
|
||||
|
@ -834,6 +924,13 @@ public class TokenService {
|
|||
return Flows.forms(providerSession, realm, uriInfo).createRegistration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user session.
|
||||
*
|
||||
* @param sessionState
|
||||
* @param redirectUri
|
||||
* @return
|
||||
*/
|
||||
@Path("logout")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -877,6 +974,12 @@ public class TokenService {
|
|||
audit.user(user).session(session).success();
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth grant page. You should not invoked this directly!
|
||||
*
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("oauth/grant")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
|
|
12
services/src/main/java/org/keycloak/services/resources/WelcomeResource.java
Normal file → Executable file
12
services/src/main/java/org/keycloak/services/resources/WelcomeResource.java
Normal file → Executable file
|
@ -20,6 +20,12 @@ public class WelcomeResource {
|
|||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
/**
|
||||
* Welcome page of Keycloak
|
||||
*
|
||||
* @return
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
@GET
|
||||
@Produces("text/html")
|
||||
public Response getWelcomePage() throws URISyntaxException {
|
||||
|
@ -31,6 +37,12 @@ public class WelcomeResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resources for welcome page
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("/welcome-content/{name}")
|
||||
@Produces("text/html")
|
||||
|
|
|
@ -147,6 +147,11 @@ public class AdminConsole {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter configuration for the admin console for this realm
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("config")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -160,6 +165,12 @@ public class AdminConsole {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission information
|
||||
*
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
@Path("whoami")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -231,6 +242,11 @@ public class AdminConsole {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from the admin console
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("logout")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -248,6 +264,12 @@ public class AdminConsole {
|
|||
|
||||
private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap();
|
||||
|
||||
/**
|
||||
* Main page of this realm's admin console
|
||||
*
|
||||
* @return
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
@GET
|
||||
public Response getMainPage() throws URISyntaxException {
|
||||
if (!uriInfo.getRequestUri().getPath().endsWith("/")) {
|
||||
|
@ -257,6 +279,11 @@ public class AdminConsole {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Javascript used by admin console
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("js/keycloak.js")
|
||||
@Produces("text/javascript")
|
||||
|
@ -270,7 +297,12 @@ public class AdminConsole {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Theme resources for this realm's admin console. (images, html files, etc..)
|
||||
*
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("{path:.+}")
|
||||
public Response getResource(@PathParam("path") String path) {
|
||||
|
|
|
@ -35,6 +35,8 @@ import javax.ws.rs.core.UriInfo;
|
|||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Root resource for admin console and admin REST API
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -72,6 +74,12 @@ public class AdminRoot {
|
|||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Convenience path to master realm admin console
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
public Response masterRealmAdminConsoleRedirect() {
|
||||
RealmModel master = new RealmManager(session).getKeycloakAdminstrationRealm();
|
||||
|
@ -80,7 +88,12 @@ public class AdminRoot {
|
|||
).build();
|
||||
}
|
||||
|
||||
@Path("index.{hack:html}")
|
||||
/**
|
||||
* Convenience path to master realm admin console
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("index.{html:html}") // expression is actually "index.html" but this is a hack to get around jax-doclet bug
|
||||
@GET
|
||||
public Response masterRealmAdminConsoleRedirectHtml() {
|
||||
return masterRealmAdminConsoleRedirect();
|
||||
|
@ -103,7 +116,12 @@ public class AdminRoot {
|
|||
return adminBaseUrl(base).path(AdminRoot.class, "getAdminConsole");
|
||||
}
|
||||
|
||||
@Path("{realm}/console")
|
||||
/**
|
||||
* path to realm admin console ui
|
||||
*
|
||||
* @param name Realm name (not id!)
|
||||
* @return
|
||||
*/
|
||||
public AdminConsole getAdminConsole(final @PathParam("realm") String name) {
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = locateRealm(name, realmManager);
|
||||
|
@ -152,6 +170,13 @@ public class AdminRoot {
|
|||
return adminBaseUrl(base).path(AdminRoot.class, "getRealmsAdmin");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base Path to realm admin REST interface
|
||||
*
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
@Path("realms")
|
||||
public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) {
|
||||
if (request.getHttpMethod().equalsIgnoreCase("OPTIONS")) {
|
||||
|
@ -173,6 +198,12 @@ public class AdminRoot {
|
|||
return adminResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* General information about the server
|
||||
*
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
@Path("serverinfo")
|
||||
public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) {
|
||||
ServerInfoAdminResource adminResource = new ServerInfoAdminResource();
|
||||
|
|
|
@ -45,6 +45,8 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base resource class for managing one particular application of a realm.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -73,11 +75,21 @@ public class ApplicationResource {
|
|||
auth.init(RealmAuth.Resource.APPLICATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* base path for managing allowed application claims
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("claims")
|
||||
public ClaimResource getClaimResource() {
|
||||
return new ClaimResource(application, auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the application.
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response update(final ApplicationRepresentation rep) {
|
||||
|
@ -93,6 +105,11 @@ public class ApplicationResource {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get representation of the application.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
@ -104,6 +121,12 @@ public class ApplicationResource {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return keycloak.json file for this application to be used to configure the adapter of that application.
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("installation/json")
|
||||
|
@ -118,6 +141,12 @@ public class ApplicationResource {
|
|||
return JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return XML that can be included in the JBoss/Wildfly Keycloak subsystem to configure the adapter of that application.
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("installation/jboss")
|
||||
|
@ -129,6 +158,10 @@ public class ApplicationResource {
|
|||
return applicationManager.toJBossSubsystemConfig(realm, application, getKeycloakApplication().getBaseUri(uriInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this application.
|
||||
*
|
||||
*/
|
||||
@DELETE
|
||||
@NoCache
|
||||
public void deleteApplication() {
|
||||
|
@ -137,6 +170,12 @@ public class ApplicationResource {
|
|||
realm.removeApplication(application.getId());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a new secret for this application
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("client-secret")
|
||||
@POST
|
||||
@Produces("application/json")
|
||||
|
@ -150,6 +189,11 @@ public class ApplicationResource {
|
|||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the secret of this application
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("client-secret")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -162,7 +206,11 @@ public class ApplicationResource {
|
|||
return ModelToRepresentation.toRepresentation(model);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base path for managing the scope mappings for this application
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("scope-mappings")
|
||||
public ScopeMappedResource getScopeMappedResource() {
|
||||
return new ScopeMappedResource(realm, auth, application, session);
|
||||
|
@ -173,6 +221,12 @@ public class ApplicationResource {
|
|||
return new RoleContainerResource(realm, auth, application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set of allowed origin. This is used for CORS requests. Access tokens will have
|
||||
* their allowedOrigins claim set to this value for tokens created for this application.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("allowed-origins")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -183,6 +237,12 @@ public class ApplicationResource {
|
|||
return application.getWebOrigins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the set of allowed origins. This is used for CORS requests. Access tokens will have
|
||||
* their allowedOrigins claim set to this value for tokens created for this application.
|
||||
*
|
||||
* @param allowedOrigins
|
||||
*/
|
||||
@Path("allowed-origins")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -193,6 +253,12 @@ public class ApplicationResource {
|
|||
application.setWebOrigins(allowedOrigins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove set of allowed origins from current allowed origins list. This is used for CORS requests. Access tokens will have
|
||||
* their allowedOrigins claim set to this value for tokens created for this application.
|
||||
*
|
||||
* @param allowedOrigins
|
||||
*/
|
||||
@Path("allowed-origins")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
@ -205,6 +271,10 @@ public class ApplicationResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the application has an admin URL, push the application's revocation policy to it.
|
||||
*
|
||||
*/
|
||||
@Path("push-revocation")
|
||||
@POST
|
||||
public void pushRevocation() {
|
||||
|
@ -212,6 +282,12 @@ public class ApplicationResource {
|
|||
new ResourceAdminManager().pushApplicationRevocationPolicy(uriInfo.getRequestUri(), realm, application);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the application has an admin URL, query it directly for session stats.
|
||||
*
|
||||
* @param users whether to include users logged in.
|
||||
* @return
|
||||
*/
|
||||
@Path("session-stats")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -235,6 +311,15 @@ public class ApplicationResource {
|
|||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of user sessions associated with this application
|
||||
*
|
||||
* {
|
||||
* "count": number
|
||||
* }
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("session-count")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -246,6 +331,11 @@ public class ApplicationResource {
|
|||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of user sessions associated with this application
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("user-sessions")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -260,6 +350,10 @@ public class ApplicationResource {
|
|||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the application has an admin URL, invalidate all sessions associated with that application directly.
|
||||
*
|
||||
*/
|
||||
@Path("logout-all")
|
||||
@POST
|
||||
public void logoutAll() {
|
||||
|
@ -267,6 +361,10 @@ public class ApplicationResource {
|
|||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the application has an admin URL, invalidate the sessions for a particular user directly.
|
||||
*
|
||||
*/
|
||||
@Path("logout-user/{username}")
|
||||
@POST
|
||||
public void logout(final @PathParam("username") String username) {
|
||||
|
|
|
@ -27,6 +27,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base resource class for managing a realm's applications.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -45,6 +47,11 @@ public class ApplicationsResource {
|
|||
auth.init(RealmAuth.Resource.APPLICATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of applications belonging to this realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
|
@ -68,6 +75,13 @@ public class ApplicationsResource {
|
|||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new application. Application name must be unique!
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response createApplication(final @Context UriInfo uriInfo, final ApplicationRepresentation rep) {
|
||||
|
@ -82,6 +96,12 @@ public class ApplicationsResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing a specific application.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
@Path("{app-name}")
|
||||
public ApplicationResource getApplication(final @PathParam("app-name") String name) {
|
||||
ApplicationModel applicationModel = realm.getApplicationByName(name);
|
||||
|
|
|
@ -12,6 +12,8 @@ import javax.ws.rs.Produces;
|
|||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
/**
|
||||
* Base resource class for managing allowed claims for an application or oauth client
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -24,6 +26,11 @@ public class ClaimResource {
|
|||
this.auth = auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the claims a client is allowed to ask for
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ClaimRepresentation getClaims() {
|
||||
|
@ -31,6 +38,11 @@ public class ClaimResource {
|
|||
return ModelToRepresentation.toRepresentation(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cliams a client is allowed to ask for.
|
||||
*
|
||||
* @param rep
|
||||
*/
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void updateClaims(ClaimRepresentation rep) {
|
||||
|
|
|
@ -30,6 +30,8 @@ import javax.ws.rs.core.UriInfo;
|
|||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Base resource class for managing oauth clients
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -58,12 +60,22 @@ public class OAuthClientResource {
|
|||
auth.init(RealmAuth.Resource.CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing allowed oauth client claims
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("claims")
|
||||
public ClaimResource getClaimResource() {
|
||||
return new ClaimResource(oauthClient, auth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the oauth client
|
||||
*
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response update(final OAuthClientRepresentation rep) {
|
||||
|
@ -78,7 +90,11 @@ public class OAuthClientResource {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a representation of the oauth client
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
@ -88,6 +104,12 @@ public class OAuthClientResource {
|
|||
return OAuthClientManager.toRepresentation(oauthClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an example keycloak.json file to use to configure the oauth client
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("installation")
|
||||
|
@ -102,6 +124,10 @@ public class OAuthClientResource {
|
|||
return JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the OAuth Client
|
||||
*
|
||||
*/
|
||||
@DELETE
|
||||
@NoCache
|
||||
public void deleteOAuthClient() {
|
||||
|
@ -110,6 +136,12 @@ public class OAuthClientResource {
|
|||
realm.removeOAuthClient(oauthClient.getId());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new client secret for the oauth client
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("client-secret")
|
||||
@POST
|
||||
@Produces("application/json")
|
||||
|
@ -124,6 +156,11 @@ public class OAuthClientResource {
|
|||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the secret of the oauth client
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("client-secret")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -136,6 +173,11 @@ public class OAuthClientResource {
|
|||
return ModelToRepresentation.toRepresentation(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing the oauth client's scope
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("scope-mappings")
|
||||
public ScopeMappedResource getScopeMappedResource() {
|
||||
return new ScopeMappedResource(realm, auth, oauthClient, session);
|
||||
|
|
|
@ -50,6 +50,11 @@ public class OAuthClientsResource {
|
|||
auth.init(RealmAuth.Resource.CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of oauth clients in this realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
|
@ -70,6 +75,13 @@ public class OAuthClientsResource {
|
|||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an oauth client
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response createOAuthClient(final @Context UriInfo uriInfo, final OAuthClientRepresentation rep) {
|
||||
|
@ -84,6 +96,12 @@ public class OAuthClientsResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path to manage one specific oauth client
|
||||
*
|
||||
* @param id oauth client's id (not clientId!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{id}")
|
||||
public OAuthClientResource getOAuthClient(final @PathParam("id") String id) {
|
||||
auth.requireView();
|
||||
|
|
|
@ -41,6 +41,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base resource class for the admin REST api of one realm
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -72,6 +74,11 @@ public class RealmAdminResource {
|
|||
auth.init(RealmAuth.Resource.REALM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing applications under this realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("applications")
|
||||
public ApplicationsResource getApplications() {
|
||||
ApplicationsResource applicationsResource = new ApplicationsResource(realm, auth);
|
||||
|
@ -80,6 +87,11 @@ public class RealmAdminResource {
|
|||
return applicationsResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* base path for managing oauth clients in this realm
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("oauth-clients")
|
||||
public OAuthClientsResource getOAuthClients() {
|
||||
OAuthClientsResource oauth = new OAuthClientsResource(realm, auth, session);
|
||||
|
@ -88,11 +100,22 @@ public class RealmAdminResource {
|
|||
return oauth;
|
||||
}
|
||||
|
||||
/**
|
||||
* base path for managing realm-level roles of this realm
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("roles")
|
||||
public RoleContainerResource getRoleContainerResource() {
|
||||
return new RoleContainerResource(realm, auth, realm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top-level representation of the realm. It will not include nested information like User, Application, or OAuth
|
||||
* Client representations.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces("application/json")
|
||||
|
@ -109,6 +132,13 @@ public class RealmAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the top-level information of this realm. Any user, roles, application, or oauth client information in the representation
|
||||
* will be ignored. This will only update top-level attributes of the realm.
|
||||
*
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
public Response updateRealm(final RealmRepresentation rep) {
|
||||
|
@ -123,6 +153,10 @@ public class RealmAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this realm.
|
||||
*
|
||||
*/
|
||||
@DELETE
|
||||
public void deleteRealm() {
|
||||
auth.requireManage();
|
||||
|
@ -132,6 +166,11 @@ public class RealmAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing users in this realm.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("users")
|
||||
public UsersResource users() {
|
||||
UsersResource users = new UsersResource(providers, realm, auth, tokenManager);
|
||||
|
@ -140,6 +179,11 @@ public class RealmAdminResource {
|
|||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path for managing all realm-level or application-level roles defined in this realm by it's id.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("roles-by-id")
|
||||
public RoleByIdResource rolesById() {
|
||||
RoleByIdResource resource = new RoleByIdResource(realm, auth);
|
||||
|
@ -148,6 +192,10 @@ public class RealmAdminResource {
|
|||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the realm's revocation policy to any application that has an admin url associated with it.
|
||||
*
|
||||
*/
|
||||
@Path("push-revocation")
|
||||
@POST
|
||||
public void pushRevocation() {
|
||||
|
@ -155,6 +203,11 @@ public class RealmAdminResource {
|
|||
new ResourceAdminManager().pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all user sessions. Any application that has an admin url will also be told to invalidate any sessions
|
||||
* they have.
|
||||
*
|
||||
*/
|
||||
@Path("logout-all")
|
||||
@POST
|
||||
public void logoutAll() {
|
||||
|
@ -163,6 +216,12 @@ public class RealmAdminResource {
|
|||
new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific user session. Any application that has an admin url will also be told to invalidate this
|
||||
* particular session.
|
||||
*
|
||||
* @param sessionId
|
||||
*/
|
||||
@Path("sessions/{session}")
|
||||
@DELETE
|
||||
public void deleteSession(@PathParam("session") String sessionId) {
|
||||
|
@ -172,6 +231,12 @@ public class RealmAdminResource {
|
|||
new ResourceAdminManager().logoutSession(uriInfo.getRequestUri(), realm, session.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON map. The key is the application name, the value is the number of sessions that currently are active
|
||||
* with that application. Only application's that actually have a session associated with them will be in this map.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("application-session-stats")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -187,6 +252,12 @@ public class RealmAdminResource {
|
|||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any application that has an admin URL will be asked directly how many sessions they have active and what users
|
||||
* are involved with those sessions.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("session-stats")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -203,6 +274,11 @@ public class RealmAdminResource {
|
|||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* View the audit provider and how it is configured.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("audit")
|
||||
@Produces("application/json")
|
||||
|
@ -212,6 +288,11 @@ public class RealmAdminResource {
|
|||
return ModelToRepresentation.toAuditReprensetation(realm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the audit provider and/or it's configuration
|
||||
*
|
||||
* @param rep
|
||||
*/
|
||||
@PUT
|
||||
@Path("audit")
|
||||
@Consumes("application/json")
|
||||
|
@ -222,6 +303,17 @@ public class RealmAdminResource {
|
|||
new RealmManager(session).updateRealmAudit(rep, realm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query audit events. Returns all events, or will query based on URL query parameters listed here
|
||||
*
|
||||
* @param client app or oauth client name
|
||||
* @param event event type
|
||||
* @param user user id
|
||||
* @param ipAddress
|
||||
* @param firstResult
|
||||
* @param maxResults
|
||||
* @return
|
||||
*/
|
||||
@Path("audit/events")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -255,6 +347,10 @@ public class RealmAdminResource {
|
|||
return query.getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all audit events.
|
||||
*
|
||||
*/
|
||||
@Path("audit/events")
|
||||
@DELETE
|
||||
public void clearAudit() {
|
||||
|
|
|
@ -41,6 +41,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Top level resource for Admin REST API
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -71,6 +73,12 @@ public class RealmsAdminResource {
|
|||
@Context
|
||||
protected KeycloakApplication keycloak;
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of realms. This list is filtered based on what realms the caller is allowed to view.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces("application/json")
|
||||
|
@ -101,6 +109,13 @@ public class RealmsAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a realm from a full representation of that realm. Realm name must be unique.
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param rep JSON representation
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
public Response importRealm(@Context final UriInfo uriInfo, final RealmRepresentation rep) {
|
||||
|
@ -127,6 +142,15 @@ public class RealmsAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a realm from a uploaded JSON file. The posted represenation is expected to be a multipart/form-data encapsulation
|
||||
* of a JSON file. The same format a browser would use when uploading a file.
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param input multipart/form data
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadRealm(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||
|
@ -178,7 +202,13 @@ public class RealmsAdminResource {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base path for the admin REST API for one particular realm.
|
||||
*
|
||||
* @param headers
|
||||
* @param name realm name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{realm}")
|
||||
public RealmAdminResource getRealmAdmin(@Context final HttpHeaders headers,
|
||||
@PathParam("realm") final String name) {
|
||||
|
|
|
@ -39,6 +39,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
this.auth = auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific role's representation
|
||||
*
|
||||
* @param id id of role
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-id}")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -70,6 +76,11 @@ public class RoleByIdResource extends RoleResource {
|
|||
return roleModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this role
|
||||
*
|
||||
* @param id id of role
|
||||
*/
|
||||
@Path("{role-id}")
|
||||
@DELETE
|
||||
@NoCache
|
||||
|
@ -79,6 +90,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
deleteRole(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this role
|
||||
*
|
||||
* @param id id of role
|
||||
* @param rep
|
||||
*/
|
||||
@Path("{role-id}")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -88,6 +105,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
updateRole(rep, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this role a composite role by associating some child roles to it.
|
||||
*
|
||||
* @param id
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{role-id}/composites")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -97,6 +120,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
addComposites(roles, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this role is a composite, return a set of its children
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-id}/composites")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -109,6 +138,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
return getRoleComposites(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a set of realm-level roles that are in the role's composite
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-id}/composites/realm")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -119,6 +154,13 @@ public class RoleByIdResource extends RoleResource {
|
|||
return getRealmRoleComposites(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a set of application-level roles for a specific app that are in the role's composite
|
||||
*
|
||||
* @param id
|
||||
* @param appName
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-id}/composites/applications/{app}")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -130,7 +172,12 @@ public class RoleByIdResource extends RoleResource {
|
|||
return getApplicationRoleComposites(appName, role);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove the listed set of roles from this role's composite
|
||||
*
|
||||
* @param id
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{role-id}/composites")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
|
|
@ -42,6 +42,11 @@ public class RoleContainerResource extends RoleResource {
|
|||
this.roleContainer = roleContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all roles for this realm or application
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces("application/json")
|
||||
|
@ -56,6 +61,13 @@ public class RoleContainerResource extends RoleResource {
|
|||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new role for this realm or application
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
public Response createRole(final @Context UriInfo uriInfo, final RoleRepresentation rep) {
|
||||
|
@ -70,6 +82,12 @@ public class RoleContainerResource extends RoleResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role by name
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-name}")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -84,6 +102,11 @@ public class RoleContainerResource extends RoleResource {
|
|||
return getRole(roleModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role by name
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
*/
|
||||
@Path("{role-name}")
|
||||
@DELETE
|
||||
@NoCache
|
||||
|
@ -97,6 +120,13 @@ public class RoleContainerResource extends RoleResource {
|
|||
deleteRole(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a role by name
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-name}")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -115,6 +145,12 @@ public class RoleContainerResource extends RoleResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a composite to this role
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{role-name}/composites")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -128,6 +164,12 @@ public class RoleContainerResource extends RoleResource {
|
|||
addComposites(roles, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* List composites of this role
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-name}/composites")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -142,6 +184,12 @@ public class RoleContainerResource extends RoleResource {
|
|||
return getRoleComposites(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get realm-level roles of this role's composite
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-name}/composites/realm")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -156,6 +204,13 @@ public class RoleContainerResource extends RoleResource {
|
|||
return getRealmRoleComposites(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* An app-level roles for a specific app for this role's composite
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @param appName
|
||||
* @return
|
||||
*/
|
||||
@Path("{role-name}/composites/application/{app}")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -172,6 +227,12 @@ public class RoleContainerResource extends RoleResource {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove roles from this role's composite
|
||||
*
|
||||
* @param roleName role's name (not id!)
|
||||
* @param roles roles to remove
|
||||
*/
|
||||
@Path("{role-name}/composites")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
|
|
@ -28,6 +28,8 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base class for managing the scope mappings of a specific client (application or oauth).
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -44,6 +46,11 @@ public class ScopeMappedResource {
|
|||
this.session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scope mappings for this client
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
@NoCache
|
||||
|
@ -82,6 +89,11 @@ public class ScopeMappedResource {
|
|||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of realm-level roles associated with this client's scope.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("realm")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -97,6 +109,11 @@ public class ScopeMappedResource {
|
|||
return realmMappingsRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of realm-level roles that are available to attach to this client's scope.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("realm/available")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -117,6 +134,13 @@ public class ScopeMappedResource {
|
|||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all effective realm-level roles that are associated with this client's scope. What this does is recurse
|
||||
* any composite roles associated with the client's scope and adds the roles to this lists. The method is really
|
||||
* to show a comprehensive total view of realm-level roles associated with the client.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("realm/composite")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -136,6 +160,11 @@ public class ScopeMappedResource {
|
|||
return composite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of realm-level roles to the client's scope
|
||||
*
|
||||
* @param roles
|
||||
*/
|
||||
@Path("realm")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -153,6 +182,11 @@ public class ScopeMappedResource {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a set of realm-level roles from the client's scope
|
||||
*
|
||||
* @param roles
|
||||
*/
|
||||
@Path("realm")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
@ -176,6 +210,12 @@ public class ScopeMappedResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles associated with a client's scope for a specific application.
|
||||
*
|
||||
* @param appName roles associated with client's scope for a specific application
|
||||
* @return
|
||||
*/
|
||||
@Path("applications/{app}")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -197,6 +237,12 @@ public class ScopeMappedResource {
|
|||
return mapRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* The available application-level roles that can be associated with the client's scope
|
||||
*
|
||||
* @param appName available roles for a specific application
|
||||
* @return
|
||||
*/
|
||||
@Path("applications/{app}/available")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -214,6 +260,12 @@ public class ScopeMappedResource {
|
|||
return getAvailable(roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective application roles that are associated with the client's scope for a specific application.
|
||||
*
|
||||
* @param appName effective roles for a specific app
|
||||
* @return
|
||||
*/
|
||||
@Path("applications/{app}/composite")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -231,6 +283,12 @@ public class ScopeMappedResource {
|
|||
return getComposite(roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add application-level roles to the client's scope
|
||||
*
|
||||
* @param appName
|
||||
* @param roles
|
||||
*/
|
||||
@Path("applications/{app}")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -253,6 +311,12 @@ public class ScopeMappedResource {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove application-level roles from the client's scope.
|
||||
*
|
||||
* @param appName
|
||||
* @param roles
|
||||
*/
|
||||
@Path("applications/{app}")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
|
5
services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
Normal file → Executable file
5
services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
Normal file → Executable file
|
@ -26,6 +26,11 @@ public class ServerInfoAdminResource {
|
|||
@Context
|
||||
private ProviderSession providers;
|
||||
|
||||
/**
|
||||
* Returns a list of themes, social providers, auth providers, and audit listeners available on this server
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
public ServerInfoRepresentation getInfo() {
|
||||
ServerInfoRepresentation info = new ServerInfoRepresentation();
|
||||
|
|
|
@ -58,6 +58,8 @@ import java.util.Set;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Base resource for managing users
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -91,6 +93,13 @@ public class UsersResource {
|
|||
protected KeycloakSession session;
|
||||
|
||||
|
||||
/**
|
||||
* Update the user
|
||||
*
|
||||
* @param username user name (not id!)
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -110,6 +119,13 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user. Must be a unique username!
|
||||
*
|
||||
* @param uriInfo
|
||||
* @param rep
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
public Response createUser(final @Context UriInfo uriInfo, final UserRepresentation rep) {
|
||||
|
@ -153,6 +169,12 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get represenation of the user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -167,6 +189,15 @@ public class UsersResource {
|
|||
return ModelToRepresentation.toRepresentation(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each application with an admin URL, query them for the set of users logged in. This not as reliable
|
||||
* as getSessions().
|
||||
*
|
||||
* @See getSessions
|
||||
*
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/session-stats")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -188,6 +219,12 @@ public class UsersResource {
|
|||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* List set of sessions associated with this user.
|
||||
*
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/sessions")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -208,6 +245,12 @@ public class UsersResource {
|
|||
return reps;
|
||||
}
|
||||
|
||||
/**
|
||||
* List set of social logins associated with this user.
|
||||
*
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/social-links")
|
||||
@GET
|
||||
@NoCache
|
||||
|
@ -227,6 +270,12 @@ public class UsersResource {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all user sessions associated with this user. And, for all applications that have an admin URL, tell
|
||||
* them to invalidate the sessions for this particular user.
|
||||
*
|
||||
* @param username username (not id!)
|
||||
*/
|
||||
@Path("{username}/logout")
|
||||
@POST
|
||||
public void logout(final @PathParam("username") String username) {
|
||||
|
@ -241,7 +290,11 @@ public class UsersResource {
|
|||
new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* delete this user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
*/
|
||||
@Path("{username}")
|
||||
@DELETE
|
||||
@NoCache
|
||||
|
@ -251,6 +304,16 @@ public class UsersResource {
|
|||
realm.removeUser(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query list of users. May pass in query criteria
|
||||
*
|
||||
* @param search string contained in username, first or last name, or email
|
||||
* @param last
|
||||
* @param first
|
||||
* @param email
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces("application/json")
|
||||
|
@ -294,6 +357,12 @@ public class UsersResource {
|
|||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role mappings for this user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -339,6 +408,12 @@ public class UsersResource {
|
|||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get realm-level role mappings for this user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/realm")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -359,6 +434,12 @@ public class UsersResource {
|
|||
return realmMappingsRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective realm-level role mappings for this user. Will recurse all composite roles to get this list.
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/realm/composite")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -381,6 +462,12 @@ public class UsersResource {
|
|||
return realmMappingsRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Realm-level roles that can be mapped to this user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/realm/available")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -397,6 +484,12 @@ public class UsersResource {
|
|||
return getAvailableRoles(user, available);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add realm-level role mappings
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{username}/role-mappings/realm")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -420,6 +513,12 @@ public class UsersResource {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete realm-level role mappings
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{username}/role-mappings/realm")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
@ -449,6 +548,13 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application-level role mappings for this user for a specific app
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param appName app name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/applications/{app}")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -478,6 +584,13 @@ public class UsersResource {
|
|||
return mapRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective application-level role mappings. This recurses any composite roles
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param appName app name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/applications/{app}/composite")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -507,6 +620,13 @@ public class UsersResource {
|
|||
return mapRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available application-level roles that can be mapped to the user
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param appName app name (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/role-mappings/applications/{app}/available")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
|
@ -544,6 +664,13 @@ public class UsersResource {
|
|||
return mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add applicaiton-level roles to the user role mapping.
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param appName app name (not id!)
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{username}/role-mappings/applications/{app}")
|
||||
@POST
|
||||
@Consumes("application/json")
|
||||
|
@ -572,6 +699,13 @@ public class UsersResource {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete application-level roles from user role mapping.
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param appName app name (not id!)
|
||||
* @param roles
|
||||
*/
|
||||
@Path("{username}/role-mappings/applications/{app}")
|
||||
@DELETE
|
||||
@Consumes("application/json")
|
||||
|
@ -610,6 +744,13 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a temporary password for this user. User will have to reset this temporary password when they log
|
||||
* in next.
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @param pass temporary password
|
||||
*/
|
||||
@Path("{username}/reset-password")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -629,6 +770,11 @@ public class UsersResource {
|
|||
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param username username (not id!)
|
||||
*/
|
||||
@Path("{username}/remove-totp")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
@ -643,6 +789,12 @@ public class UsersResource {
|
|||
user.setTotp(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email to the user with a link they can click to reset their password
|
||||
*
|
||||
* @param username username (not id!)
|
||||
* @return
|
||||
*/
|
||||
@Path("{username}/reset-password-email")
|
||||
@PUT
|
||||
@Consumes("application/json")
|
||||
|
|
Loading…
Reference in a new issue