diff --git a/docs/openapi.json b/docs/openapi.json
index 42d59f5..598598b 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -49,6 +49,9 @@
}
}
}
+ },
+ "401" : {
+ "description" : "Unauthorized"
}
}
},
@@ -68,6 +71,12 @@
"responses" : {
"201" : {
"description" : "Created"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
}
}
}
@@ -82,10 +91,33 @@
"content" : {
"application/json" : {
"schema" : {
- "$ref" : "#/components/schemas/TenantRepresentation"
+ "required" : [ "name" ],
+ "type" : "object",
+ "properties" : {
+ "id" : {
+ "type" : "string",
+ "readOnly" : true
+ },
+ "name" : {
+ "type" : "string"
+ },
+ "realm" : {
+ "type" : "string",
+ "readOnly" : true
+ }
+ }
}
}
}
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
+ },
+ "404" : {
+ "description" : "Not Found"
}
}
},
@@ -102,8 +134,17 @@
}
},
"responses" : {
- "200" : {
- "description" : "OK"
+ "204" : {
+ "description" : "No Content"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
+ },
+ "404" : {
+ "description" : "Not Found"
}
}
},
@@ -113,6 +154,12 @@
"responses" : {
"204" : {
"description" : "No Content"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
}
}
},
@@ -166,6 +213,12 @@
}
}
}
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
}
}
},
@@ -185,6 +238,12 @@
"responses" : {
"201" : {
"description" : "Created"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
}
}
},
@@ -210,8 +269,17 @@
}
} ],
"responses" : {
- "200" : {
- "description" : "OK"
+ "204" : {
+ "description" : "No Content"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
+ },
+ "404" : {
+ "description" : "Not Found"
}
}
},
@@ -265,6 +333,12 @@
}
}
}
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
}
}
},
@@ -290,8 +364,17 @@
}
} ],
"responses" : {
- "200" : {
- "description" : "OK"
+ "204" : {
+ "description" : "No Content"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
+ },
+ "404" : {
+ "description" : "Not Found"
}
}
},
@@ -317,8 +400,17 @@
"required" : true
},
"responses" : {
- "200" : {
- "description" : "OK"
+ "204" : {
+ "description" : "No Content"
+ },
+ "401" : {
+ "description" : "Unauthorized"
+ },
+ "403" : {
+ "description" : "Forbidden"
+ },
+ "404" : {
+ "description" : "Not Found"
}
}
},
@@ -334,6 +426,111 @@
},
"components" : {
"schemas" : {
+ "CredentialRepresentation" : {
+ "type" : "object",
+ "properties" : {
+ "id" : {
+ "type" : "string"
+ },
+ "type" : {
+ "type" : "string"
+ },
+ "userLabel" : {
+ "type" : "string"
+ },
+ "createdDate" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "secretData" : {
+ "type" : "string"
+ },
+ "credentialData" : {
+ "type" : "string"
+ },
+ "priority" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "value" : {
+ "type" : "string"
+ },
+ "temporary" : {
+ "type" : "boolean"
+ },
+ "device" : {
+ "type" : "string",
+ "deprecated" : true
+ },
+ "hashedSaltedValue" : {
+ "type" : "string",
+ "deprecated" : true
+ },
+ "salt" : {
+ "type" : "string",
+ "deprecated" : true
+ },
+ "hashIterations" : {
+ "format" : "int32",
+ "type" : "integer",
+ "deprecated" : true
+ },
+ "counter" : {
+ "format" : "int32",
+ "type" : "integer",
+ "deprecated" : true
+ },
+ "algorithm" : {
+ "type" : "string",
+ "deprecated" : true
+ },
+ "digits" : {
+ "format" : "int32",
+ "type" : "integer",
+ "deprecated" : true
+ },
+ "period" : {
+ "format" : "int32",
+ "type" : "integer",
+ "deprecated" : true
+ },
+ "config" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ },
+ "deprecated" : true
+ }
+ }
+ },
+ "FederatedIdentityRepresentation" : {
+ "type" : "object",
+ "properties" : {
+ "identityProvider" : {
+ "type" : "string"
+ },
+ "userId" : {
+ "type" : "string"
+ },
+ "userName" : {
+ "type" : "string"
+ }
+ }
+ },
+ "SocialLinkRepresentation" : {
+ "type" : "object",
+ "properties" : {
+ "socialProvider" : {
+ "type" : "string"
+ },
+ "socialUserId" : {
+ "type" : "string"
+ },
+ "socialUsername" : {
+ "type" : "string"
+ }
+ }
+ },
"TenantInvitationRepresentation" : {
"required" : [ "email", "roles" ],
"type" : "object",
@@ -375,6 +572,9 @@
},
"user" : {
"type" : "object",
+ "allOf" : [ {
+ "$ref" : "#/components/schemas/UserRepresentation"
+ } ],
"readOnly" : true
},
"roles" : {
@@ -402,6 +602,237 @@
"readOnly" : true
}
}
+ },
+ "UserConsentRepresentation" : {
+ "type" : "object",
+ "properties" : {
+ "clientId" : {
+ "type" : "string"
+ },
+ "grantedClientScopes" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "createdDate" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "lastUpdatedDate" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "grantedRealmRoles" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ },
+ "deprecated" : true
+ }
+ }
+ },
+ "UserProfileAttributeGroupMetadata" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "displayHeader" : {
+ "type" : "string"
+ },
+ "displayDescription" : {
+ "type" : "string"
+ },
+ "annotations" : {
+ "type" : "object",
+ "additionalProperties" : { }
+ }
+ }
+ },
+ "UserProfileAttributeMetadata" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "displayName" : {
+ "type" : "string"
+ },
+ "required" : {
+ "type" : "boolean"
+ },
+ "readOnly" : {
+ "type" : "boolean"
+ },
+ "annotations" : {
+ "type" : "object",
+ "additionalProperties" : { }
+ },
+ "validators" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "object",
+ "additionalProperties" : { }
+ }
+ },
+ "group" : {
+ "type" : "string"
+ }
+ }
+ },
+ "UserProfileMetadata" : {
+ "type" : "object",
+ "properties" : {
+ "attributes" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/UserProfileAttributeMetadata"
+ }
+ },
+ "groups" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/UserProfileAttributeGroupMetadata"
+ }
+ }
+ }
+ },
+ "UserRepresentation" : {
+ "type" : "object",
+ "properties" : {
+ "self" : {
+ "type" : "string"
+ },
+ "id" : {
+ "type" : "string"
+ },
+ "origin" : {
+ "type" : "string"
+ },
+ "createdTimestamp" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "username" : {
+ "type" : "string"
+ },
+ "enabled" : {
+ "type" : "boolean"
+ },
+ "totp" : {
+ "type" : "boolean"
+ },
+ "emailVerified" : {
+ "type" : "boolean"
+ },
+ "firstName" : {
+ "type" : "string"
+ },
+ "lastName" : {
+ "type" : "string"
+ },
+ "email" : {
+ "type" : "string"
+ },
+ "federationLink" : {
+ "type" : "string"
+ },
+ "serviceAccountClientId" : {
+ "type" : "string"
+ },
+ "attributes" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ },
+ "credentials" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/CredentialRepresentation"
+ }
+ },
+ "disableableCredentialTypes" : {
+ "uniqueItems" : true,
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "requiredActions" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "federatedIdentities" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/FederatedIdentityRepresentation"
+ }
+ },
+ "realmRoles" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "clientRoles" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ },
+ "clientConsents" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/UserConsentRepresentation"
+ }
+ },
+ "notBefore" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "applicationRoles" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "deprecated" : true
+ },
+ "socialLinks" : {
+ "type" : "array",
+ "items" : {
+ "$ref" : "#/components/schemas/SocialLinkRepresentation"
+ },
+ "deprecated" : true
+ },
+ "groups" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "access" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "boolean"
+ }
+ },
+ "userProfileMetadata" : {
+ "$ref" : "#/components/schemas/UserProfileMetadata"
+ }
+ }
}
}
}
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 7651b70..5c1f926 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -37,6 +37,8 @@ paths:
type: array
items:
$ref: '#/components/schemas/TenantRepresentation'
+ "401":
+ description: Unauthorized
post:
summary: Create a tenant
operationId: createTenant
@@ -49,6 +51,10 @@ paths:
responses:
"201":
description: Created
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
/tenants/{tenantId}:
get:
summary: Get tenant
@@ -59,7 +65,24 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/TenantRepresentation'
+ required:
+ - name
+ type: object
+ properties:
+ id:
+ type: string
+ readOnly: true
+ name:
+ type: string
+ realm:
+ type: string
+ readOnly: true
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: Not Found
put:
summary: Update tenant
operationId: updateTenant
@@ -69,14 +92,24 @@ paths:
schema:
$ref: '#/components/schemas/TenantRepresentation'
responses:
- "200":
- description: OK
+ "204":
+ description: No Content
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: Not Found
delete:
summary: Delete tenant
operationId: deleteTenant
responses:
"204":
description: No Content
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
parameters:
- name: tenantId
in: path
@@ -114,6 +147,10 @@ paths:
type: array
items:
$ref: '#/components/schemas/TenantInvitationRepresentation'
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
post:
summary: Create invitation
operationId: createInvitation
@@ -126,6 +163,10 @@ paths:
responses:
"201":
description: Created
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
parameters:
- name: tenantId
in: path
@@ -143,8 +184,14 @@ paths:
schema:
type: string
responses:
- "200":
- description: OK
+ "204":
+ description: No Content
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: Not Found
parameters:
- name: tenantId
in: path
@@ -182,6 +229,10 @@ paths:
type: array
items:
$ref: '#/components/schemas/TenantMembershipRepresentation'
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
parameters:
- name: tenantId
in: path
@@ -199,8 +250,14 @@ paths:
schema:
type: string
responses:
- "200":
- description: OK
+ "204":
+ description: No Content
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: Not Found
patch:
summary: Update tenant membership
operationId: updateMembership
@@ -217,8 +274,14 @@ paths:
$ref: '#/components/schemas/TenantMembershipRepresentation'
required: true
responses:
- "200":
- description: OK
+ "204":
+ description: No Content
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: Not Found
parameters:
- name: tenantId
in: path
@@ -227,6 +290,80 @@ paths:
type: string
components:
schemas:
+ CredentialRepresentation:
+ type: object
+ properties:
+ id:
+ type: string
+ type:
+ type: string
+ userLabel:
+ type: string
+ createdDate:
+ format: int64
+ type: integer
+ secretData:
+ type: string
+ credentialData:
+ type: string
+ priority:
+ format: int32
+ type: integer
+ value:
+ type: string
+ temporary:
+ type: boolean
+ device:
+ type: string
+ deprecated: true
+ hashedSaltedValue:
+ type: string
+ deprecated: true
+ salt:
+ type: string
+ deprecated: true
+ hashIterations:
+ format: int32
+ type: integer
+ deprecated: true
+ counter:
+ format: int32
+ type: integer
+ deprecated: true
+ algorithm:
+ type: string
+ deprecated: true
+ digits:
+ format: int32
+ type: integer
+ deprecated: true
+ period:
+ format: int32
+ type: integer
+ deprecated: true
+ config:
+ type: object
+ additionalProperties:
+ type: string
+ deprecated: true
+ FederatedIdentityRepresentation:
+ type: object
+ properties:
+ identityProvider:
+ type: string
+ userId:
+ type: string
+ userName:
+ type: string
+ SocialLinkRepresentation:
+ type: object
+ properties:
+ socialProvider:
+ type: string
+ socialUserId:
+ type: string
+ socialUsername:
+ type: string
TenantInvitationRepresentation:
required:
- email
@@ -261,6 +398,8 @@ components:
readOnly: true
user:
type: object
+ allOf:
+ - $ref: '#/components/schemas/UserRepresentation'
readOnly: true
roles:
uniqueItems: true
@@ -280,3 +419,159 @@ components:
realm:
type: string
readOnly: true
+ UserConsentRepresentation:
+ type: object
+ properties:
+ clientId:
+ type: string
+ grantedClientScopes:
+ type: array
+ items:
+ type: string
+ createdDate:
+ format: int64
+ type: integer
+ lastUpdatedDate:
+ format: int64
+ type: integer
+ grantedRealmRoles:
+ type: array
+ items:
+ type: string
+ deprecated: true
+ UserProfileAttributeGroupMetadata:
+ type: object
+ properties:
+ name:
+ type: string
+ displayHeader:
+ type: string
+ displayDescription:
+ type: string
+ annotations:
+ type: object
+ additionalProperties: {}
+ UserProfileAttributeMetadata:
+ type: object
+ properties:
+ name:
+ type: string
+ displayName:
+ type: string
+ required:
+ type: boolean
+ readOnly:
+ type: boolean
+ annotations:
+ type: object
+ additionalProperties: {}
+ validators:
+ type: object
+ additionalProperties:
+ type: object
+ additionalProperties: {}
+ group:
+ type: string
+ UserProfileMetadata:
+ type: object
+ properties:
+ attributes:
+ type: array
+ items:
+ $ref: '#/components/schemas/UserProfileAttributeMetadata'
+ groups:
+ type: array
+ items:
+ $ref: '#/components/schemas/UserProfileAttributeGroupMetadata'
+ UserRepresentation:
+ type: object
+ properties:
+ self:
+ type: string
+ id:
+ type: string
+ origin:
+ type: string
+ createdTimestamp:
+ format: int64
+ type: integer
+ username:
+ type: string
+ enabled:
+ type: boolean
+ totp:
+ type: boolean
+ emailVerified:
+ type: boolean
+ firstName:
+ type: string
+ lastName:
+ type: string
+ email:
+ type: string
+ federationLink:
+ type: string
+ serviceAccountClientId:
+ type: string
+ attributes:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ credentials:
+ type: array
+ items:
+ $ref: '#/components/schemas/CredentialRepresentation'
+ disableableCredentialTypes:
+ uniqueItems: true
+ type: array
+ items:
+ type: string
+ requiredActions:
+ type: array
+ items:
+ type: string
+ federatedIdentities:
+ type: array
+ items:
+ $ref: '#/components/schemas/FederatedIdentityRepresentation'
+ realmRoles:
+ type: array
+ items:
+ type: string
+ clientRoles:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ clientConsents:
+ type: array
+ items:
+ $ref: '#/components/schemas/UserConsentRepresentation'
+ notBefore:
+ format: int32
+ type: integer
+ applicationRoles:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ deprecated: true
+ socialLinks:
+ type: array
+ items:
+ $ref: '#/components/schemas/SocialLinkRepresentation'
+ deprecated: true
+ groups:
+ type: array
+ items:
+ type: string
+ access:
+ type: object
+ additionalProperties:
+ type: boolean
+ userProfileMetadata:
+ $ref: '#/components/schemas/UserProfileMetadata'
diff --git a/pom.xml b/pom.xml
index 2dc961e..57e6c34 100644
--- a/pom.xml
+++ b/pom.xml
@@ -181,6 +181,8 @@
${project.name}
${project.version}
{{keycloakUrl}}/auth/realms/{{realmName}}
+ provided
+ dev.sultanov.keycloak.multitenancy.resource,org.keycloak.representations.idm
/tenants
diff --git a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantInvitationsResource.java b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantInvitationsResource.java
index a1fa4cb..7a9d390 100644
--- a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantInvitationsResource.java
+++ b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantInvitationsResource.java
@@ -33,9 +33,9 @@
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -52,7 +52,11 @@ public TenantInvitationsResource(AbstractAdminResource parent,
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "createInvitation", summary = "Create invitation")
- @APIResponse(responseCode = "201", description = "Created")
+ @APIResponses({
+ @APIResponse(responseCode = "201", description = "Created"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden")
+ })
public Response createInvitation(@RequestBody(required = true) TenantInvitationRepresentation request) {
String email = request.getEmail();
if (!isValidEmail(email)) {
@@ -93,7 +97,11 @@ public Response createInvitation(@RequestBody(required = true) TenantInvitationR
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "listInvitations", summary = "List invitations")
- @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantInvitationRepresentation.class)))
+ @APIResponses({
+ @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantInvitationRepresentation.class))),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden")
+ })
public Stream listInvitations(
@Parameter(description = "Invitee email") @QueryParam("search") String searchQuery,
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
@@ -112,13 +120,19 @@ public Stream listInvitations(
@DELETE
@Operation(operationId = "revokeInvitation", summary = "Revoke invitation")
@Path("{invitationId}")
+ @APIResponses({
+ @APIResponse(responseCode = "204", description = "No Content"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden"),
+ @APIResponse(responseCode = "404", description = "Not Found")
+ })
public Response revokeInvitation(@PathParam("invitationId") String invitationId) {
var revoked = tenant.revokeInvitation(invitationId);
if (revoked) {
adminEvent.operation(OperationType.DELETE)
.resourcePath(session.getContext().getUri())
.success();
- return Response.status(204).build();
+ return Response.noContent().build();
} else {
throw new NotFoundException(String.format("No invitation with id %s", invitationId));
}
diff --git a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantMembershipsResource.java b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantMembershipsResource.java
index cf6043b..4b9f34a 100644
--- a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantMembershipsResource.java
+++ b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantMembershipsResource.java
@@ -23,6 +23,7 @@
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.Constants;
import org.keycloak.utils.StringUtil;
@@ -39,7 +40,11 @@ public TenantMembershipsResource(AbstractAdminResource parent,
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "listMemberships", summary = "List tenant memberships")
- @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantMembershipRepresentation.class)))
+ @APIResponses({
+ @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantMembershipRepresentation.class))),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden")
+ })
public Stream listMemberships(
@Parameter(description = "Member email") @QueryParam("search") String search,
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
@@ -63,6 +68,12 @@ public Stream listMemberships(
@Produces(MediaType.APPLICATION_JSON)
@Consumes(value = MediaType.APPLICATION_JSON)
@Operation(operationId = "updateMembership", summary = "Update tenant membership")
+ @APIResponses({
+ @APIResponse(responseCode = "204", description = "No Content"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden"),
+ @APIResponse(responseCode = "404", description = "Not Found")
+ })
public Response update(@PathParam("membershipId") String membershipId, @RequestBody(required = true) TenantMembershipRepresentation request) {
var optionalMembership = tenant.getMembershipById(membershipId);
if (optionalMembership.isEmpty()) {
@@ -81,13 +92,19 @@ public Response update(@PathParam("membershipId") String membershipId, @RequestB
@DELETE
@Path("{membershipId}")
@Operation(operationId = "revokeMembership", summary = "Revoke tenant membership")
+ @APIResponses({
+ @APIResponse(responseCode = "204", description = "No Content"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden"),
+ @APIResponse(responseCode = "404", description = "Not Found")
+ })
public Response revokeMembership(@PathParam("membershipId") String membershipId) {
var revoked = tenant.revokeMembership(membershipId);
if (revoked) {
adminEvent.operation(OperationType.DELETE)
.resourcePath(session.getContext().getUri())
.success();
- return Response.status(204).build();
+ return Response.noContent().build();
} else {
throw new NotFoundException(String.format("No membership with id %s", membershipId));
}
diff --git a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantResource.java b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantResource.java
index 0a0b0a4..ea155c0 100644
--- a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantResource.java
+++ b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantResource.java
@@ -11,6 +11,11 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
+import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
+import org.eclipse.microprofile.openapi.annotations.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.keycloak.events.admin.OperationType;
public class TenantResource extends AbstractAdminResource {
@@ -25,6 +30,12 @@ public TenantResource(AbstractAdminResource parent, TenantModel
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getTenant", summary = "Get tenant")
+ @APIResponses({
+ @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.OBJECT, implementation = TenantRepresentation.class))),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden"),
+ @APIResponse(responseCode = "404", description = "Not Found")
+ })
public TenantRepresentation getTenant() {
return ModelMapper.toRepresentation(tenant);
}
@@ -33,6 +44,12 @@ public TenantRepresentation getTenant() {
@Produces(MediaType.APPLICATION_JSON)
@Consumes(value = MediaType.APPLICATION_JSON)
@Operation(operationId = "updateTenant", summary = "Update tenant")
+ @APIResponses({
+ @APIResponse(responseCode = "204", description = "No Content"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden"),
+ @APIResponse(responseCode = "404", description = "Not Found")
+ })
public Response updateTenant(TenantRepresentation request) {
tenant.setName(request.getName());
@@ -47,6 +64,11 @@ public Response updateTenant(TenantRepresentation request) {
@DELETE
@Operation(operationId = "deleteTenant", summary = "Delete tenant")
+ @APIResponses({
+ @APIResponse(responseCode = "204", description = "No Content"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden")
+ })
public void deleteTenant() {
if (tenantProvider.deleteTenant(realm, tenant.getId())) {
adminEvent.operation(OperationType.DELETE)
diff --git a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantsResource.java b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantsResource.java
index b0a7c33..ffb2e41 100644
--- a/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantsResource.java
+++ b/src/main/java/dev/sultanov/keycloak/multitenancy/resource/TenantsResource.java
@@ -3,8 +3,8 @@
import dev.sultanov.keycloak.multitenancy.model.TenantModel;
import dev.sultanov.keycloak.multitenancy.resource.representation.TenantRepresentation;
import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
-import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -22,10 +22,12 @@
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
+
public class TenantsResource extends AbstractAdminResource {
public TenantsResource(KeycloakSession session) {
@@ -36,12 +38,16 @@ public TenantsResource(KeycloakSession session) {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "createTenant", summary = "Create a tenant")
- @APIResponse(responseCode = "201", description = "Created")
+ @APIResponses({
+ @APIResponse(responseCode = "201", description = "Created"),
+ @APIResponse(responseCode = "401", description = "Unauthorized"),
+ @APIResponse(responseCode = "403", description = "Forbidden")
+ })
public Response createTenant(@RequestBody(required = true) TenantRepresentation request) {
var requiredRole = realm.getAttribute("requiredRoleForTenantCreation");
if (requiredRole != null && !auth.hasAppRole(auth.getClient(), requiredRole)) {
- throw new NotAuthorizedException(String.format("Missing required role for tenant creation: %s", requiredRole));
+ throw new ForbiddenException(String.format("Missing required role for tenant creation: %s", requiredRole));
}
TenantModel model = tenantProvider.createTenant(realm, request.getName(), auth.getUser());
@@ -58,7 +64,10 @@ public Response createTenant(@RequestBody(required = true) TenantRepresentation
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "listTenants", summary = "List tenants")
- @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantRepresentation.class)))
+ @APIResponses({
+ @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TenantRepresentation.class))),
+ @APIResponse(responseCode = "401", description = "Unauthorized")
+ })
public Stream listTenants(
@Parameter(description = "Tenant name") @QueryParam("search") String searchQuery,
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
@@ -79,7 +88,7 @@ public TenantResource getTenantResource(@PathParam("tenantId") String tenantId)
TenantModel model = tenantProvider.getTenantById(realm, tenantId)
.orElseThrow(() -> new NotFoundException(String.format("%s not found", tenantId)));
if (!auth.isTenantAdmin(model)) {
- throw new NotAuthorizedException(String.format("Insufficient permission to access %s", tenantId));
+ throw new ForbiddenException(String.format("Insufficient permission to access %s", tenantId));
} else {
return new TenantResource(this, model);
}