mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-17 19:37:38 +00:00
feat: added universal auth endpoints for revoke and client secret endpoint to fetch details
This commit is contained in:
@ -65,6 +65,7 @@ export enum EventType {
|
||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth",
|
||||
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
|
||||
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
|
||||
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
|
||||
@ -72,6 +73,7 @@ export enum EventType {
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
|
||||
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
|
||||
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
|
||||
@ -434,6 +436,13 @@ interface GetIdentityUniversalAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityUniversalAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityKubernetesAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
|
||||
metadata: {
|
||||
@ -493,6 +502,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityUniversalAuthClientSecretByIdEvent {
|
||||
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
clientSecretId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RevokeIdentityUniversalAuthClientSecretEvent {
|
||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
|
||||
metadata: {
|
||||
@ -1003,6 +1020,7 @@ export type Event =
|
||||
| LoginIdentityUniversalAuthEvent
|
||||
| AddIdentityUniversalAuthEvent
|
||||
| UpdateIdentityUniversalAuthEvent
|
||||
| DeleteIdentityUniversalAuthEvent
|
||||
| GetIdentityUniversalAuthEvent
|
||||
| LoginIdentityKubernetesAuthEvent
|
||||
| AddIdentityKubernetesAuthEvent
|
||||
@ -1010,6 +1028,7 @@ export type Event =
|
||||
| GetIdentityKubernetesAuthEvent
|
||||
| CreateIdentityUniversalAuthClientSecretEvent
|
||||
| GetIdentityUniversalAuthClientSecretsEvent
|
||||
| GetIdentityUniversalAuthClientSecretByIdEvent
|
||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||
| LoginIdentityGcpAuthEvent
|
||||
| AddIdentityGcpAuthEvent
|
||||
|
@ -72,6 +72,9 @@ export const UNIVERSAL_AUTH = {
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update.",
|
||||
clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
|
||||
@ -90,6 +93,10 @@ export const UNIVERSAL_AUTH = {
|
||||
LIST_CLIENT_SECRETS: {
|
||||
identityId: "The ID of the identity to list client secrets for."
|
||||
},
|
||||
GET_CLIENT_SECRET: {
|
||||
identityId: "The ID of the identity to get the client secret from.",
|
||||
clientSecretId: "The ID of the client secret to get details."
|
||||
},
|
||||
REVOKE_CLIENT_SECRET: {
|
||||
identityId: "The ID of the identity to revoke the client secret from.",
|
||||
clientSecretId: "The ID of the client secret to revoke."
|
||||
|
@ -134,7 +134,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityUniversalAuth = await server.services.identityUa.attachUa({
|
||||
const identityUniversalAuth = await server.services.identityUa.attachUniversalAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
@ -219,7 +219,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityUniversalAuth = await server.services.identityUa.updateUa({
|
||||
const identityUniversalAuth = await server.services.identityUa.updateUniversalAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
@ -272,7 +272,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityUniversalAuth = await server.services.identityUa.getIdentityUa({
|
||||
const identityUniversalAuth = await server.services.identityUa.getIdentityUniversalAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -295,6 +295,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/universal-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Delete Universal Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityUniversalAuth = await server.services.identityUa.revokeIdentityUniversalAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityUniversalAuth.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH,
|
||||
metadata: {
|
||||
identityId: identityUniversalAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityUniversalAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/universal-auth/identities/:identityId/client-secrets",
|
||||
@ -325,14 +372,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId,
|
||||
...req.body
|
||||
});
|
||||
const { clientSecret, clientSecretData, orgId } =
|
||||
await server.services.identityUa.createUniversalAuthClientSecret({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@ -374,13 +422,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUniversalAuthClientSecrets(
|
||||
{
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
}
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@ -396,6 +446,56 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get Universal Auth Client Secret for identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.identityId),
|
||||
clientSecretId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.clientSecretId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
clientSecretData: sanitizedClientSecretSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const clientSecretData = await server.services.identityUa.getUniversalAuthClientSecretById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId,
|
||||
clientSecretId: req.params.clientSecretId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: clientSecretData.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
|
||||
metadata: {
|
||||
identityId: clientSecretData.identityId,
|
||||
clientSecretId: clientSecretData.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { clientSecretData };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
|
||||
@ -421,7 +521,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const clientSecretData = await server.services.identityUa.revokeUaClientSecret({
|
||||
const clientSecretData = await server.services.identityUa.revokeUniversalAuthClientSecret({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
@ -25,7 +25,9 @@ import {
|
||||
TCreateUaClientSecretDTO,
|
||||
TGetUaClientSecretsDTO,
|
||||
TGetUaDTO,
|
||||
TGetUniversalAuthClientSecretByIdDTO,
|
||||
TRevokeUaClientSecretDTO,
|
||||
TRevokeUaDTO,
|
||||
TUpdateUaDTO
|
||||
} from "./identity-ua-types";
|
||||
|
||||
@ -136,7 +138,7 @@ export const identityUaServiceFactory = ({
|
||||
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachUa = async ({
|
||||
const attachUniversalAuth = async ({
|
||||
accessTokenMaxTTL,
|
||||
identityId,
|
||||
accessTokenNumUsesLimit,
|
||||
@ -227,7 +229,7 @@ export const identityUaServiceFactory = ({
|
||||
return { ...identityUa, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateUa = async ({
|
||||
const updateUniversalAuth = async ({
|
||||
accessTokenMaxTTL,
|
||||
identityId,
|
||||
accessTokenNumUsesLimit,
|
||||
@ -312,7 +314,7 @@ export const identityUaServiceFactory = ({
|
||||
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
|
||||
const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
@ -333,7 +335,50 @@ export const identityUaServiceFactory = ({
|
||||
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const createUaClientSecret = async ({
|
||||
const revokeIdentityUniversalAuth = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRevokeUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke universal auth of identity with more privileged role"
|
||||
});
|
||||
|
||||
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
||||
const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityUniversalAuth;
|
||||
};
|
||||
|
||||
const createUniversalAuthClientSecret = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
@ -396,7 +441,7 @@ export const identityUaServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getUaClientSecrets = async ({
|
||||
const getUniversalAuthClientSecrets = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
@ -442,7 +487,47 @@ export const identityUaServiceFactory = ({
|
||||
return { clientSecrets, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const revokeUaClientSecret = async ({
|
||||
const getUniversalAuthClientSecretById = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
clientSecretId
|
||||
}: TGetUniversalAuthClientSecretByIdDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to read identity client secret of project with more privileged role"
|
||||
});
|
||||
|
||||
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
|
||||
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const revokeUniversalAuthClientSecret = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
@ -475,7 +560,7 @@ export const identityUaServiceFactory = ({
|
||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPriviledge)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to add identity to project with more privileged role"
|
||||
message: "Failed to revoke identity client secret with more privileged role"
|
||||
});
|
||||
|
||||
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
||||
@ -486,11 +571,13 @@ export const identityUaServiceFactory = ({
|
||||
|
||||
return {
|
||||
login,
|
||||
attachUa,
|
||||
updateUa,
|
||||
getIdentityUa,
|
||||
createUaClientSecret,
|
||||
getUaClientSecrets,
|
||||
revokeUaClientSecret
|
||||
attachUniversalAuth,
|
||||
updateUniversalAuth,
|
||||
getIdentityUniversalAuth,
|
||||
revokeIdentityUniversalAuth,
|
||||
createUniversalAuthClientSecret,
|
||||
getUniversalAuthClientSecrets,
|
||||
revokeUniversalAuthClientSecret,
|
||||
getUniversalAuthClientSecretById
|
||||
};
|
||||
};
|
||||
|
@ -22,6 +22,10 @@ export type TGetUaDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeUaDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateUaClientSecretDTO = {
|
||||
identityId: string;
|
||||
description: string;
|
||||
@ -37,3 +41,8 @@ export type TRevokeUaClientSecretDTO = {
|
||||
identityId: string;
|
||||
clientSecretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetUniversalAuthClientSecretByIdDTO = {
|
||||
identityId: string;
|
||||
clientSecretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Client Secret By ID"
|
||||
openapi: "GET /api/v1/auth/universal-auth/identities/{identityId}/client-secrets/{clientSecretId}"
|
||||
---
|
4
docs/api-reference/endpoints/universal-auth/revoke.mdx
Normal file
4
docs/api-reference/endpoints/universal-auth/revoke.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Revoke"
|
||||
openapi: "DELETE /api/v1/auth/universal-auth/identities/{identityId}"
|
||||
---
|
@ -428,9 +428,11 @@
|
||||
"api-reference/endpoints/universal-auth/attach",
|
||||
"api-reference/endpoints/universal-auth/retrieve",
|
||||
"api-reference/endpoints/universal-auth/update",
|
||||
"api-reference/endpoints/universal-auth/revoke",
|
||||
"api-reference/endpoints/universal-auth/create-client-secret",
|
||||
"api-reference/endpoints/universal-auth/list-client-secrets",
|
||||
"api-reference/endpoints/universal-auth/revoke-client-secret",
|
||||
"api-reference/endpoints/universal-auth/get-client-secret-by-id",
|
||||
"api-reference/endpoints/universal-auth/renew-access-token",
|
||||
"api-reference/endpoints/universal-auth/revoke-access-token"
|
||||
]
|
||||
|
Reference in New Issue
Block a user