Skip to main content

Command Palette

Search for a command to run...

Couldn't export Issue Security Level Permission

Updated
•20 min read•View as Markdown
Couldn't export Issue Security Level Permission
M
I started LeanZero because I saw an industry full of process-heavy companies charging enterprise prices for work that could be done faster, leaner, and more honestly. The Atlassian ecosystem shouldn't require a small army and a six-figure budget to get things done right. I hold Atlassian Certified Administration Expert status, with ACP-120, ACP-220, ACP-420 and ACP-520 covering Jira, Confluence, Jira Service Management and cloud organization administration. Those are the platforms I work with on migrations and app projects. My other certifications include PSM I, SSM and AWS Cloud Practitioner. I specialize in Forge app development with an AI-powered agentic workflow I built myself. The rise of AI changed everything for me — building across multiple stacks became possible without needing a dozen specialists. I use these tools daily, not as a talking point, but as the backbone of how I deliver. Cloud migrations are my bread and butter — I've handled projects of all sizes, from small teams to organizations with tens of thousands of users, each with its own tangle of compliance requirements, legacy systems, and tight deadlines. Forge app development is my newfound love, my way of expressing the builder in me. I've worked on many Forge apps and plan to ship a lot more in the coming year. The MCP Doc Processor came from a simple frustration — when I couldn't find anything noteworthy on the market, I built my own. Privacy, security, and functionality are the holy triangle of any software I build. I'm not selling magic pixie dust. I'll tell you honestly what's possible, what's hard, and what you actually need. If you don't need to pay me, I'll tell you that too.

JCMA stops a project migration with "We couldn't export Issue Security Level Permission 10001. Reason: java.lang.NullPointerException. [JCMA 000]" when an issue still points at a security level that no longer exists. The full line in the migration log looks like this, where TEST is the project key:

ERROR TEST project-export We couldn't export Issue Security Level Permission 10001. Reason: java.lang.NullPointerException. [JCMA 000]

That is the example in Atlassian's own knowledge base article for this error. The fix it gives is three SQL statements with no walkthrough, and the one public Community thread about it, a question with this exact error, ends with the asker saying "I checked the database tables but couldn't find a table or so that looked like a "Issue Security Level Permission"". Atlassian has a ticket open about exactly that, MIG-1815, titled "The migration error for issue security level should be more clear".

By the end of this tutorial you will have found every issue that points at a missing security level, fixed them, checked the security grants that JCMA drops without failing, and proven on the Cloud side that a restricted issue is visible to the people it should be and hidden from everyone else. The last section covers the Cloud-to-Cloud route, where JCMA is not involved at all.

[!NOTE] Prerequisites — Jira Data Center or Server with read and write access to its database, and a backup taken before any change (Atlassian's own warning: "Always backup your instance database before any changes, or first try the changes in a staging environment"). For the Cloud checks, an API token for a Jira Cloud admin on the destination site, plus curl. We have no Data Center instance, so the SQL here was run on a local PostgreSQL 16 database with a stub of the relevant Jira tables, not on a live Jira. The Cloud steps were run on our own test site on 25 September 2026.

Why the issue security level migration error happens

Two tables matter, and the error's wording makes it easy to look in the wrong one.

schemeissuesecuritylevels holds the levels themselves, one row per level, with the scheme it belongs to. schemeissuesecurities holds the grants: who can see an issue at a given level. Each grant row has the level id in its security column, a sec_type, and a sec_parameter such as a group name. The issue itself carries its level in jiraissue.security.

Atlassian's article describes the cause in one line: "It indicates that there is a reference to an Issue Security Level Permission in a Jira Issue that no longer exists." Its first diagnostic step looks the id up in schemeissuesecuritylevels, so it treats the number as a level id. My guess is that a level removed directly in the database or by an old cleanup is the usual way in; Atlassian does not say. Either way, any issue that still carries that id in its security column has nothing to export.

Here is the part I could not settle. The error says "Permission", and a grant row is the thing that looks like a permission. JCMA also has a separate code, JCMA 133, "Group reference not found (issue security level permission)", which is clearly about grants. No Atlassian page, ticket or doc I could find says which table the number in the JCMA 000 line comes from, and ids in both tables start around 10000, so a number can exist in either. So the queries below check both. If your id turns out to be a grant row, you will know from step 4.

There is a second variant of the same problem that prints a different line. MIG-1815 quotes it:

ERROR PROJA project-export We couldn't export Issue PROJA-351. Reason: java.lang.IllegalStateException: Entity mri:mig:jira/classic:issueSecurityLevel:10010 is expected to be exported, but it was not. [JCMA 000]

Same family, but here JCMA names the issue and the level for you, which saves you steps 2 and 3.

project-export error: Reason: java.lang.NullPointerException [JCMA 000]

A NullPointerException in a JCMA log is not always this problem. In the Community thread, an Atlassian answer notes that "each instance could be encountering an NPE on many different root causes". There is also a knowledge base article called JCMA Project Migration: Handling NullPointerException, but it covers a different error on the project-import side, about a permission scheme, so do not follow its fix for this one.

The line you are fixing here has three things in it: project-export, the words "Issue Security Level Permission", and an id. Write down the project key and every id. The Community asker had six such lines for one project, with five distinct ids (10080 twice, then 10110, 11294, 11295 and 11596), so expect more than one. Run the steps below once per id.

JCMA's troubleshooting page does not cover this one. It has an entry for JCMA 133, where you fix the reference in the Pre-migration checks screen ("Select Fix remediable errors in the Data preparation section of the Pre-migration checks page"), choose a valid entity id and rerun. JCMA 000 has no entry and no such screen, so this one is fixed in the database.

Find and fix the orphaned security level on Data Center

These are the queries I ran, exactly as printed, on the stub database. The stub was seeded with one deleted level (10001), two issues still pointing at it, one grant row on the deleted level, and two custom-field grants. That is our seed, not real data, but it has the same shape as the case Atlassian describes. Swap TEST and 10001 for your project key and the id from your log.

[[steps]]

  1. Back up the database — take a full backup, or restore one to a staging instance and run everything there first. Every query up to step 6 is read-only; step 6 writes.
  2. Check whether the id exists as a level — this is Atlassian's step 1: SELECT * FROM schemeissuesecuritylevels WHERE id=10001; An empty result means the level is gone, which confirms the orphan.
  3. List the security column for every issue in the project — Atlassian's step 2, which returns every issue in the project so you can look for the id by eye. On a big project, use the orphan finder in the next step instead.
  4. Run the orphan finders — one query returns only the issues whose level no longer exists, a second returns grant rows on a missing level, and a third checks whether the id is a grant-row id rather than a level id.
  5. Check the custom-field grants — join the grants to customfield and read the customfieldtypekey of every field used in a grant. Anything that is not on JCMA's supported list will not migrate (next section).
  6. Update the orphaned issues — Atlassian's step 3: set jiraissue.security to null, or to the correct level, for each issue found in step 4. Then run the orphan finder again. It should return zero rows.
  7. Re-run the JCMA pre-migration checks — Atlassian's article ends at the UPDATE and says nothing about what to do next. Re-running the checks and the export is my advice, not theirs.

Here is the script for steps 2 to 6, in one file. The labels inside it keep Atlassian's numbering because this is the file exactly as I ran it: its "Step 1" is step 2 above, "Step 2" is step 3, "Step 2b", "Step 2c" and the grant-id check are step 4, the two custom-field queries are step 5, and its "Step 3" is step 6.

\echo '--- Step 1 (KB): does the ID from the error exist as a level?'
SELECT * FROM schemeissuesecuritylevels WHERE id=10001;
\echo '--- Step 2 (KB): security column of every issue in the project'
SELECT jiraissue.id
	,  jiraissue.issuenum
	,  jiraissue.security 
FROM jiraissue
JOIN project ON jiraissue.project = project.id
WHERE project.pkey = 'TEST';
\echo '--- Step 2b: only the issues pointing at a level that no longer exists'
SELECT p.pkey || '-' || i.issuenum AS issue_key, i.id, i.security
FROM jiraissue i
JOIN project p ON p.id = i.project
LEFT JOIN schemeissuesecuritylevels l ON l.id = i.security
WHERE i.security IS NOT NULL AND l.id IS NULL
ORDER BY i.id;
\echo '--- Step 2c: grant rows (schemeissuesecurities) pointing at a missing level'
SELECT s.id AS permission_id, s.scheme, s.security AS level_id, s.sec_type, s.sec_parameter
FROM schemeissuesecurities s
LEFT JOIN schemeissuesecuritylevels l ON l.id = s.security
WHERE l.id IS NULL;
\echo '--- Is 10001 a grant-row id? (checks the other reading of the error)'
SELECT * FROM schemeissuesecurities WHERE id=10001;
\echo '--- Custom-field grants and their field type keys'
SELECT s.id AS permission_id, l.name AS level, s.sec_type, s.sec_parameter, cf.cfname, cf.customfieldtypekey
FROM schemeissuesecurities s
JOIN schemeissuesecuritylevels l ON l.id = s.security
JOIN customfield cf ON s.sec_parameter = 'customfield_' || cf.id
ORDER BY s.id;
\echo '--- KB (unsupported custom fields) query'
select customfieldtypekey,cfname from customfield where cfname in ('SecureGroupPicker','SecureMultiUser');
\echo '--- Step 3 (KB): UPDATE then re-check'
BEGIN;
UPDATE jiraissue 
SET security = NULL
WHERE id = 20002;
UPDATE jiraissue 
SET security = NULL
WHERE id = 20004;
SELECT p.pkey || '-' || i.issuenum AS issue_key, i.id, i.security
FROM jiraissue i
JOIN project p ON p.id = i.project
LEFT JOIN schemeissuesecuritylevels l ON l.id = i.security
WHERE i.security IS NOT NULL AND l.id IS NULL
ORDER BY i.id;
COMMIT;

Saved as article.sql, it runs like this:

psql -h /tmp -p 55432 -U jira -d jirastub -f article.sql

And this is what it printed:

--- Step 1 (KB): does the ID from the error exist as a level?
 id | name | scheme 
----+------+--------
(0 rows)

--- Step 2 (KB): security column of every issue in the project
  id   | issuenum | security 
-------+----------+----------
 20001 |        1 |    10000
 20002 |        2 |    10001
 20003 |        3 |         
 20004 |        4 |    10001
(4 rows)

--- Step 2b: only the issues pointing at a level that no longer exists
 issue_key |  id   | security 
-----------+-------+----------
 TEST-2    | 20002 |    10001
 TEST-4    | 20004 |    10001
(2 rows)

--- Step 2c: grant rows (schemeissuesecurities) pointing at a missing level
 permission_id | scheme | level_id | sec_type |  sec_parameter  
---------------+--------+----------+----------+-----------------
         10101 |  10000 |    10001 | group    | old-contractors
(1 row)

--- Is 10001 a grant-row id? (checks the other reading of the error)
 id | scheme | security | sec_type | sec_parameter 
----+--------+----------+----------+---------------
(0 rows)

--- Custom-field grants and their field type keys
 permission_id |  level   | sec_type |   sec_parameter   |      cfname       |                        customfieldtypekey                         
---------------+----------+----------+-------------------+-------------------+-------------------------------------------------------------------
         10102 | Partners | userCF   | customfield_10200 | SecureMultiUser   | com.atlassian.jira.plugin.system.customfieldtypes:multiuserpicker
         10103 | Partners | groupCF  | customfield_10201 | SecureGroupPicker | com.example.vendor:secure-group-picker
(2 rows)

--- KB (unsupported custom fields) query
                        customfieldtypekey                         |      cfname       
-------------------------------------------------------------------+-------------------
 com.atlassian.jira.plugin.system.customfieldtypes:multiuserpicker | SecureMultiUser
 com.example.vendor:secure-group-picker                            | SecureGroupPicker
(2 rows)

--- Step 3 (KB): UPDATE then re-check
BEGIN
UPDATE 1
UPDATE 1
 issue_key | id | security 
-----------+----+----------
(0 rows)

COMMIT

To confirm the fix, read the last block: the orphan finder that returned TEST-2 and TEST-4 before the UPDATE returns zero rows after it. That empty result proves something because the same query, on the same database, found both issues a moment earlier.

A few things to know before you run this on a real instance.

The || concatenation is PostgreSQL. It also works on Oracle. On MySQL use CONCAT('customfield_', cf.id), and on SQL Server use 'customfield_' + CAST(cf.id AS varchar). I only ran the PostgreSQL version.

The custom-field query filters on sec_parameter = 'customfield_' || cf.id rather than on sec_type. Atlassian's own knowledge base shows custom-field grants storing customfield_<id> in sec_parameter. The userCF and groupCF values I seeded in sec_type are my inference, and I have not seen them in a live Data Center database, so do not filter on them.

If step 4 returns a grant row on a missing level, like 10101 here, that row points at nothing. Atlassian does not document a fix for it, so check it in the admin UI and your staging copy before you delete anything.

Choose between null and a real level carefully. Atlassian's Data Center article Issue cannot be viewed and Permission Violation error is thrown warns that setting security to null "will make the issue accessible by all users that has the Browse Project permission in the project", and tells you to set the level again through the UI afterwards. If TEST-2 was restricted for a reason, give it a level that still exists, not null. The UPDATE in step 6 widens access, not narrows it.

JCMA doesn't migrate issue security level permission with unsupported custom fields

This is the quieter half of the problem, and on the Cloud side it is the one that bites.

Atlassian's article JCMA doesn't migrate issue security level permission with unsupported custom fields (for Jira 7.6 and higher, JCMA 1.10.5 or higher) says: "JCMA doesn't migrate unsupported custom fields linked to issue security level permission for the Group custom field value and User customer field value." The article has a list of supported field type keys. The core user and group pickers are on it (userpicker, multiuserpicker, grouppicker, multigrouppicker), as are a set of Jira Service Management and Advanced Roadmaps fields. A third-party picker, whose key starts with the vendor's namespace, is not.

To be precise about what that article says: the grant is "not migrated". It does not say the migration fails, and I found nothing that ties an unsupported field to the JCMA 000 error. You get no error. The grant just does not arrive.

In the stub output above, SecureGroupPicker has the key com.example.vendor:secure-group-picker, which is not on the list, so that grant would not migrate. SecureMultiUser uses the core multiuserpicker and would.

Atlassian gives two fixes before migrating. Either delete the affected grant, migrate, and "Recreate the issue security level permission in cloud", or swap the unsupported field for a supported one on the level and then migrate. If you have already migrated, the article's instruction is: "re-create the issue security level permission in cloud."

Verify issue security levels on Cloud after the migration

Whichever route you took, the destination is where you confirm that the fix worked. I ran this on our test site with a throwaway project (LZSEC), a multi-user picker field, and a scheme with two levels: "Secure-field", granted to that field's users, and "Reporter-only". Three issues: LZSEC-1 at Secure-field with my account in the field, LZSEC-2 at Secure-field with the field empty, and LZSEC-3 with no level. Everything was deleted afterwards.

[[steps]]

  1. Set up credentials — export your site and an admin's API token so the commands below run as printed.
  2. List the issue security schemes — confirm the migrated scheme exists on the destination and note its id.
  3. List the levels and their members — read every grant on every level and compare against your Data Center list from the previous section.
  4. Fetch a restricted issue as a member — the positive control. A 200 with the level name proves the grant works.
  5. Fetch a restricted issue as a non-member — the negative control. Use a second account's API token, one you know is outside the level, or an issue whose grant provably excludes you. A 404 on an issue you know exists then proves the restriction works.
  6. Re-create any missing custom-field grant — add it back to the level, then repeat steps 4 and 5.

The commands below carry the ids from our test project, which no longer exists. Replace 10420 with your scheme id from the first call, 10379 and 10333 with your level and grant ids from the member call, customfield_11629 with your field id, and the LZSEC keys with a restricted issue of your own. On any other site the ids as printed will return 404, which is not a restriction working, just a wrong id.

export SITE=https://your-site.atlassian.net
export AUTH="you@example.com:your-api-token"

curl -s -u "\(AUTH" "\)SITE/rest/api/3/issuesecurityschemes"
curl -s -u "\(AUTH" "\)SITE/rest/api/3/issuesecurityschemes/level?schemeId=10420"
curl -s -u "\(AUTH" "\)SITE/rest/api/3/issuesecurityschemes/level/member?schemeId=10420&expand=all"
curl -s -u "\(AUTH" "\)SITE/rest/api/3/issue/LZSEC-1?fields=security"
curl -s -u "\(AUTH" "\)SITE/rest/api/3/issue/LZSEC-2?fields=security"

On our site the levels call returned 10380 "Reporter-only" and 10379 "Secure-field", and the member call returned two grants. Trimmed to the fields that matter:

{"id":"10333","issueSecurityLevelId":"10379",...,"holder":{"type":"userCustomField","parameter":"customfield_11629",...}}
{"id":"10334","issueSecurityLevelId":"10380",...,"holder":{"type":"reporter"}}

LZSEC-1 came back 200 with security "Secure-field". LZSEC-2, same level but with the field empty, came back 404 even though I am a site admin. In our test the admin was the non-member, because the only grant on that level was the field and my account was not in it. After a real migration your admin may be covered by a group grant, so use a second user's token for this call:

{"errorMessages":["Issue does not exist or you do not have permission to see it."],"errors":{}}

That pair is the whole check. The 200 shows a member can see the issue. The 404 on an issue you know exists shows the restriction is enforced. A 404 on its own proves nothing, because a typo in the key gives you the same response.

Then I recreated the failure the previous section describes. I deleted the custom-field grant (DELETE /rest/api/3/issuesecurityschemes/10420/level/10379/member/10333, 204), which is the state an unsupported field leaves you in after JCMA. LZSEC-1, the issue I could see a moment earlier, now returned the same 404. With its only grant gone, a level hides the issue from everyone not otherwise granted, admin included. That is what your users will report after the migration: issues that were there yesterday are gone.

To put it back, add the grant to the level:

curl -s -u "$AUTH" -X PUT -H 'Content-Type: application/json' \
  "$SITE/rest/api/3/issuesecurityschemes/10420/level/10379/member" \
  -d '{"members":[{"type":"userCF","parameter":"customfield_11629"}]}'

It returned 204. To confirm it, repeat the checks: LZSEC-1 returned 200 with "Secure-field" again, and LZSEC-2 still returned 404, so the negative control held.

Things the API taught me on the way:

The type name differs between write and read. When I created the grant with "type":"userCustomField", the name the read endpoint returns, I got {"errorMessages":["Type userCustomField isn't a valid type."]}. Creating it with "type":"userCF" worked, and reading it back returns userCustomField.

The levelId filter on the member endpoint looked like it was ignored in one of my calls. Filter by schemeId and sort the levels yourself.

JQL lags. project = LZSEC AND level = "Secure-field" came back empty about five seconds after I created the issues and correct about thirty-five seconds later. One observation, but enough to not trust an empty JQL result straight after a migration or a grant change.

Setting a level on an issue needs the Set Issue Security permission. On our site the admin did not have it under the shared default permission scheme, so the security field did not appear at all. I used a throwaway permission scheme for the test project rather than edit the shared one.

Assigning a security scheme to a project through PUT /rest/api/3/issuesecurityschemes/project takes {"schemeId":"10420","projectId":"11601"} and answers 303, because it runs asynchronously. {"issueSecuritySchemeId": ...} returns "Invalid request payload".

Issue security levels in Cloud to Cloud migration (Copy product data)

Cloud to Cloud has no JCMA, so you will not see this error, and you cannot fix anything with SQL, because there is no database you can reach. Atlassian's page What data is copied lists, under Jira project data, "✅ Work item security: Level, Scheme, and Permission", alongside "✅ Permission schemes" and "✅ Users, groups, and teams from active directories". It also notes that workflows and permission schemes not linked to any project won't be migrated.

It says nothing about grants based on custom fields, nothing about third-party pickers, and nothing about a level that exists in the scheme but has no grants. Neither does Atlassian's troubleshooting page for data transfers, Troubleshoot issues with transferring app data, though that page is about app data. I have not run a cross-site copy for this piece, so I cannot tell you what it does with those edge cases. Nothing I found says either way.

Two documented behaviours do change what you check. First, ids move. What happens when you copy data says custom field identifiers "such as customfield_10456" are "not guaranteed to be the same" between sites. That passage is written about sandboxes, but the rule matters for security grants: a user-custom-field grant on the destination must name the destination's field id, not the source's. Second, groups with the same name merge: "If a group with the same name already exists on your sandbox, we'll merge the two groups." A grant to a group can end up admitting more people than it did on the source. I wrote up that trap on its own in Migrate Jira users and groups without escalation.

So on Cloud to Cloud the procedure is the Cloud section above, run on the destination: list the schemes, list every level's members, compare against the source (the same REST calls work on the source site), and do the member and non-member fetch for at least one issue per level. Where a custom-field grant is missing or names a field that does not exist on the destination, re-create it with the destination's field id.

What this does not cover

The Data Center half was run on a stub schema with only the columns these queries use, on PostgreSQL, and not against a live Jira. The table and column names come from Atlassian's own knowledge base SQL. Whether the number in the error is a level id or a grant id is undocumented. The Cloud half was run live. The Cloud-to-Cloud half rests on one line in Atlassian's docs plus the REST check, not on a copy we ran.

Security grants are not the only thing that goes missing without an error in a JCMA run. Jira 32,767 characters limit: find what JCMA lost covers text that gets truncated, and Jira JQL counts a missing custom field as zero covers why a post-migration audit can read 0 when it should say "field not found". If your log has NullPointerExceptions that do not match anything here, the Community answer also pointed at memory, and Jira heap size for a Cloud migration is where I would look next.

[[takeaways]]

  • You can now find every issue that points at a missing security level with one orphan-finder query, fix it with Atlassian's UPDATE, and prove the fix by re-running the same query to zero.
  • You check both tables, because nothing documents whether the id in the error is a level or a grant row.
  • You know that null opens the issue to everyone with Browse Projects, and you pick a real level where the issue was meant to be restricted.
  • You find unsupported custom-field grants before JCMA drops them, and you re-create them on Cloud if it already has.
  • You verify on the destination with a member fetch that returns 200 and a non-member fetch that returns 404, on both JCMA and Copy product data migrations.
  • Not covered: MySQL, SQL Server and Oracle runs of the SQL, a live Data Center instance, and a real Copy product data run.