secure actions execution context
This commit is contained in:
+29
-20
@@ -48,7 +48,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
|
|
||||||
it('throws an error when the ref is not provided', async () => {
|
it('throws an error when the ref is not provided', async () => {
|
||||||
getInputMock.mockReturnValueOnce('token')
|
getInputMock.mockReturnValueOnce('token')
|
||||||
process.env.GITHUB_REF = ''
|
github.context.ref = ''
|
||||||
|
|
||||||
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
||||||
'Could not find GITHUB_REF.'
|
'Could not find GITHUB_REF.'
|
||||||
@@ -66,7 +66,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
|
|
||||||
it('throws an error when the repository is not provided', async () => {
|
it('throws an error when the repository is not provided', async () => {
|
||||||
getInputMock.mockReturnValueOnce('token')
|
getInputMock.mockReturnValueOnce('token')
|
||||||
process.env.GITHUB_REPOSITORY = ''
|
github.context.payload.repository = undefined
|
||||||
|
|
||||||
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
||||||
'Could not find Repository.'
|
'Could not find Repository.'
|
||||||
@@ -75,7 +75,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
|
|
||||||
it('throws an error when the apiBaseUrl is not provided', async () => {
|
it('throws an error when the apiBaseUrl is not provided', async () => {
|
||||||
getInputMock.mockReturnValueOnce('token')
|
getInputMock.mockReturnValueOnce('token')
|
||||||
process.env.GITHUB_API_URL = ''
|
github.context.apiUrl = ''
|
||||||
|
|
||||||
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
||||||
'Could not find GITHUB_API_URL.'
|
'Could not find GITHUB_API_URL.'
|
||||||
@@ -93,7 +93,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
|
|
||||||
it('throws an error when the sha is not provided', async () => {
|
it('throws an error when the sha is not provided', async () => {
|
||||||
getInputMock.mockReturnValueOnce('token')
|
getInputMock.mockReturnValueOnce('token')
|
||||||
process.env.GITHUB_SHA = ''
|
github.context.sha = ''
|
||||||
|
|
||||||
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
||||||
'Could not find GITHUB_SHA.'
|
'Could not find GITHUB_SHA.'
|
||||||
@@ -102,7 +102,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
|
|
||||||
it('throws an error when the githubServerUrl is not provided', async () => {
|
it('throws an error when the githubServerUrl is not provided', async () => {
|
||||||
getInputMock.mockReturnValueOnce('token')
|
getInputMock.mockReturnValueOnce('token')
|
||||||
process.env.GITHUB_SERVER_URL = ''
|
github.context.serverUrl = ''
|
||||||
|
|
||||||
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
|
||||||
'Could not find GITHUB_SERVER_URL.'
|
'Could not find GITHUB_SERVER_URL.'
|
||||||
@@ -235,7 +235,7 @@ describe('config.resolvePublishActionOptions', () => {
|
|||||||
ownerId: 'repositoryOwnerId'
|
ownerId: 'repositoryOwnerId'
|
||||||
})
|
})
|
||||||
|
|
||||||
process.env.GITHUB_SERVER_URL = 'https://github-enterprise.com'
|
github.context.serverUrl = 'https://github-enterprise.com'
|
||||||
|
|
||||||
const options = await cfg.resolvePublishActionOptions()
|
const options = await cfg.resolvePublishActionOptions()
|
||||||
|
|
||||||
@@ -296,27 +296,36 @@ describe('config.serializeOptions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function configureEventContext(): void {
|
function configureEventContext(): void {
|
||||||
process.env.GITHUB_REF = 'ref'
|
github.context.ref = 'ref'
|
||||||
process.env.GITHUB_WORKSPACE = 'workspaceDir'
|
github.context.eventName = 'release'
|
||||||
process.env.GITHUB_REPOSITORY = 'nameWithOwner'
|
github.context.apiUrl = 'apiBaseUrl'
|
||||||
process.env.GITHUB_API_URL = 'apiBaseUrl'
|
github.context.sha = 'sha'
|
||||||
|
github.context.serverUrl = 'https://github.com/'
|
||||||
|
github.context.payload = {
|
||||||
|
repository: {
|
||||||
|
full_name: 'nameWithOwner',
|
||||||
|
name: 'name',
|
||||||
|
owner: {
|
||||||
|
login: 'owner'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
process.env.RUNNER_TEMP = 'runnerTempDir'
|
process.env.RUNNER_TEMP = 'runnerTempDir'
|
||||||
process.env.GITHUB_SHA = 'sha'
|
process.env.GITHUB_WORKSPACE = 'workspaceDir'
|
||||||
process.env.GITHUB_SERVER_URL = 'https://github.com/'
|
|
||||||
process.env.GITHUB_REPOSITORY_ID = 'repositoryId'
|
process.env.GITHUB_REPOSITORY_ID = 'repositoryId'
|
||||||
process.env.GITHUB_REPOSITORY_OWNER_ID = 'repositoryOwnerId'
|
process.env.GITHUB_REPOSITORY_OWNER_ID = 'repositoryOwnerId'
|
||||||
github.context.eventName = 'release'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearEventContext(): void {
|
function clearEventContext(): void {
|
||||||
process.env.GITHUB_REF = ''
|
github.context.ref = ''
|
||||||
process.env.GITHUB_WORKSPACE = ''
|
github.context.eventName = ''
|
||||||
process.env.GITHUB_REPOSITORY = ''
|
github.context.apiUrl = ''
|
||||||
process.env.GITHUB_API_URL = ''
|
github.context.sha = ''
|
||||||
|
github.context.serverUrl = ''
|
||||||
|
github.context.payload = {}
|
||||||
process.env.RUNNER_TEMP = ''
|
process.env.RUNNER_TEMP = ''
|
||||||
process.env.GITHUB_SHA = ''
|
process.env.GITHUB_WORKSPACE = ''
|
||||||
process.env.GITHUB_SERVER_URL = ''
|
|
||||||
process.env.GITHUB_REPOSITORY_ID = ''
|
process.env.GITHUB_REPOSITORY_ID = ''
|
||||||
process.env.GITHUB_REPOSITORY_OWNER_ID = ''
|
process.env.GITHUB_REPOSITORY_OWNER_ID = ''
|
||||||
github.context.eventName = ''
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,18 +256,32 @@ describe('ensureCorrectShaCheckedOut', () => {
|
|||||||
it('throws an error if the correct SHA is not checked out', async () => {
|
it('throws an error if the correct SHA is not checked out', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit1, dir)
|
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit1, dir)
|
||||||
).rejects.toThrow()
|
).rejects.toThrow(
|
||||||
|
'The expected commit associated with the tag refs/tags/tag1 is not checked out.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws if there is an issue getting sha for tag', async () => {
|
||||||
|
await expect(async () =>
|
||||||
|
fsHelper.ensureTagAndRefCheckedOut(
|
||||||
|
`refs/tags/some-unknown-tag`,
|
||||||
|
commit2,
|
||||||
|
dir
|
||||||
|
)
|
||||||
|
).rejects.toThrow('Error retrieving commit associated with tag')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if the sha of the tag does not match expected sha', async () => {
|
it('throws an error if the sha of the tag does not match expected sha', async () => {
|
||||||
await expect(async () =>
|
await expect(async () =>
|
||||||
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit2, dir)
|
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit2, dir)
|
||||||
).rejects.toThrow()
|
).rejects.toThrow(
|
||||||
|
'The commit associated with the tag refs/tags/tag1 does not match the SHA of the commit provided by the actions context.'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws if the provided ref is not a tag ref', async () => {
|
it('throws if the provided ref is not a tag ref', async () => {
|
||||||
await expect(async () =>
|
await expect(async () =>
|
||||||
fsHelper.ensureTagAndRefCheckedOut(`refs/heads/main`, commit2, dir)
|
fsHelper.ensureTagAndRefCheckedOut(`refs/heads/main`, commit2, dir)
|
||||||
).rejects.toThrow()
|
).rejects.toThrow('Tag ref provided is not in expected format.')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.56%"><title>Coverage: 97.56%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.56%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.56%</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.31%"><title>Coverage: 97.31%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.31%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.31%</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+32
-20
@@ -106410,35 +106410,35 @@ async function resolvePublishActionOptions() {
|
|||||||
if (event === '') {
|
if (event === '') {
|
||||||
throw new Error(`Could not find event name.`);
|
throw new Error(`Could not find event name.`);
|
||||||
}
|
}
|
||||||
// Environment Variables
|
const ref = github.context.ref || '';
|
||||||
const ref = process.env.GITHUB_REF || '';
|
|
||||||
if (ref === '') {
|
if (ref === '') {
|
||||||
throw new Error(`Could not find GITHUB_REF.`);
|
throw new Error(`Could not find GITHUB_REF.`);
|
||||||
}
|
}
|
||||||
|
const nameWithOwner = github.context.payload.repository?.full_name || '';
|
||||||
|
if (nameWithOwner === '') {
|
||||||
|
throw new Error(`Could not find Repository.`);
|
||||||
|
}
|
||||||
|
const sha = github.context.sha || '';
|
||||||
|
if (sha === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_SHA.`);
|
||||||
|
}
|
||||||
|
const apiBaseUrl = github.context.apiUrl || '';
|
||||||
|
if (apiBaseUrl === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_API_URL.`);
|
||||||
|
}
|
||||||
|
const githubServerUrl = github.context.serverUrl || '';
|
||||||
|
if (githubServerUrl === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_SERVER_URL.`);
|
||||||
|
}
|
||||||
|
// Environment Variables
|
||||||
const workspaceDir = process.env.GITHUB_WORKSPACE || '';
|
const workspaceDir = process.env.GITHUB_WORKSPACE || '';
|
||||||
if (workspaceDir === '') {
|
if (workspaceDir === '') {
|
||||||
throw new Error(`Could not find GITHUB_WORKSPACE.`);
|
throw new Error(`Could not find GITHUB_WORKSPACE.`);
|
||||||
}
|
}
|
||||||
const nameWithOwner = process.env.GITHUB_REPOSITORY || '';
|
|
||||||
if (nameWithOwner === '') {
|
|
||||||
throw new Error(`Could not find Repository.`);
|
|
||||||
}
|
|
||||||
const apiBaseUrl = process.env.GITHUB_API_URL || '';
|
|
||||||
if (apiBaseUrl === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_API_URL.`);
|
|
||||||
}
|
|
||||||
const runnerTempDir = process.env.RUNNER_TEMP || '';
|
const runnerTempDir = process.env.RUNNER_TEMP || '';
|
||||||
if (runnerTempDir === '') {
|
if (runnerTempDir === '') {
|
||||||
throw new Error(`Could not find RUNNER_TEMP.`);
|
throw new Error(`Could not find RUNNER_TEMP.`);
|
||||||
}
|
}
|
||||||
const sha = process.env.GITHUB_SHA || '';
|
|
||||||
if (sha === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_SHA.`);
|
|
||||||
}
|
|
||||||
const githubServerUrl = process.env.GITHUB_SERVER_URL || '';
|
|
||||||
if (githubServerUrl === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_SERVER_URL.`);
|
|
||||||
}
|
|
||||||
const repositoryId = process.env.GITHUB_REPOSITORY_ID || '';
|
const repositoryId = process.env.GITHUB_REPOSITORY_ID || '';
|
||||||
if (repositoryId === '') {
|
if (repositoryId === '') {
|
||||||
throw new Error(`Could not find GITHUB_REPOSITORY_ID.`);
|
throw new Error(`Could not find GITHUB_REPOSITORY_ID.`);
|
||||||
@@ -106622,11 +106622,23 @@ async function ensureTagAndRefCheckedOut(tagRef, expectedSha, gitDir) {
|
|||||||
throw new Error(`Tag ref provided is not in expected format.`);
|
throw new Error(`Tag ref provided is not in expected format.`);
|
||||||
}
|
}
|
||||||
const git = simpleGit.simpleGit(gitDir);
|
const git = simpleGit.simpleGit(gitDir);
|
||||||
const tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef]);
|
let tagCommitSha;
|
||||||
|
try {
|
||||||
|
tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef]);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
throw new Error(`Error retrieving commit associated with tag: ${err}`);
|
||||||
|
}
|
||||||
if (tagCommitSha.trim() !== expectedSha) {
|
if (tagCommitSha.trim() !== expectedSha) {
|
||||||
throw new Error(`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`);
|
throw new Error(`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`);
|
||||||
}
|
}
|
||||||
const currentlyCheckedOutSha = await git.revparse(['HEAD']);
|
let currentlyCheckedOutSha;
|
||||||
|
try {
|
||||||
|
currentlyCheckedOutSha = await git.revparse(['HEAD']);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
throw new Error(`Error validating checked out tag and ref: ${err}`);
|
||||||
|
}
|
||||||
if (currentlyCheckedOutSha.trim() !== expectedSha) {
|
if (currentlyCheckedOutSha.trim() !== expectedSha) {
|
||||||
throw new Error(`The expected commit associated with the tag ${tagRef} is not checked out.`);
|
throw new Error(`The expected commit associated with the tag ${tagRef} is not checked out.`);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-22
@@ -45,42 +45,43 @@ export async function resolvePublishActionOptions(): Promise<PublishActionOption
|
|||||||
throw new Error(`Could not find event name.`)
|
throw new Error(`Could not find event name.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment Variables
|
const ref: string = github.context.ref || ''
|
||||||
const ref: string = process.env.GITHUB_REF || ''
|
|
||||||
if (ref === '') {
|
if (ref === '') {
|
||||||
throw new Error(`Could not find GITHUB_REF.`)
|
throw new Error(`Could not find GITHUB_REF.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameWithOwner: string =
|
||||||
|
github.context.payload.repository?.full_name || ''
|
||||||
|
if (nameWithOwner === '') {
|
||||||
|
throw new Error(`Could not find Repository.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha: string = github.context.sha || ''
|
||||||
|
if (sha === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_SHA.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiBaseUrl: string = github.context.apiUrl || ''
|
||||||
|
if (apiBaseUrl === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_API_URL.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubServerUrl = github.context.serverUrl || ''
|
||||||
|
if (githubServerUrl === '') {
|
||||||
|
throw new Error(`Could not find GITHUB_SERVER_URL.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment Variables
|
||||||
const workspaceDir: string = process.env.GITHUB_WORKSPACE || ''
|
const workspaceDir: string = process.env.GITHUB_WORKSPACE || ''
|
||||||
if (workspaceDir === '') {
|
if (workspaceDir === '') {
|
||||||
throw new Error(`Could not find GITHUB_WORKSPACE.`)
|
throw new Error(`Could not find GITHUB_WORKSPACE.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameWithOwner: string = process.env.GITHUB_REPOSITORY || ''
|
|
||||||
if (nameWithOwner === '') {
|
|
||||||
throw new Error(`Could not find Repository.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiBaseUrl: string = process.env.GITHUB_API_URL || ''
|
|
||||||
if (apiBaseUrl === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_API_URL.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const runnerTempDir: string = process.env.RUNNER_TEMP || ''
|
const runnerTempDir: string = process.env.RUNNER_TEMP || ''
|
||||||
if (runnerTempDir === '') {
|
if (runnerTempDir === '') {
|
||||||
throw new Error(`Could not find RUNNER_TEMP.`)
|
throw new Error(`Could not find RUNNER_TEMP.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sha: string = process.env.GITHUB_SHA || ''
|
|
||||||
if (sha === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_SHA.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubServerUrl = process.env.GITHUB_SERVER_URL || ''
|
|
||||||
if (githubServerUrl === '') {
|
|
||||||
throw new Error(`Could not find GITHUB_SERVER_URL.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const repositoryId = process.env.GITHUB_REPOSITORY_ID || ''
|
const repositoryId = process.env.GITHUB_REPOSITORY_ID || ''
|
||||||
if (repositoryId === '') {
|
if (repositoryId === '') {
|
||||||
throw new Error(`Could not find GITHUB_REPOSITORY_ID.`)
|
throw new Error(`Could not find GITHUB_REPOSITORY_ID.`)
|
||||||
|
|||||||
+13
-2
@@ -129,14 +129,25 @@ export async function ensureTagAndRefCheckedOut(
|
|||||||
|
|
||||||
const git: simpleGit.SimpleGit = simpleGit.simpleGit(gitDir)
|
const git: simpleGit.SimpleGit = simpleGit.simpleGit(gitDir)
|
||||||
|
|
||||||
const tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef])
|
let tagCommitSha: string
|
||||||
|
|
||||||
|
try {
|
||||||
|
tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef])
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error retrieving commit associated with tag: ${err}`)
|
||||||
|
}
|
||||||
if (tagCommitSha.trim() !== expectedSha) {
|
if (tagCommitSha.trim() !== expectedSha) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`
|
`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentlyCheckedOutSha = await git.revparse(['HEAD'])
|
let currentlyCheckedOutSha: string
|
||||||
|
try {
|
||||||
|
currentlyCheckedOutSha = await git.revparse(['HEAD'])
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error validating checked out tag and ref: ${err}`)
|
||||||
|
}
|
||||||
if (currentlyCheckedOutSha.trim() !== expectedSha) {
|
if (currentlyCheckedOutSha.trim() !== expectedSha) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The expected commit associated with the tag ${tagRef} is not checked out.`
|
`The expected commit associated with the tag ${tagRef} is not checked out.`
|
||||||
|
|||||||
Reference in New Issue
Block a user