Mar 12, 2015

[JIRA] 作成できる課題タイプをグループによって制限したい

JIRA 6.2で、同じプロジェクトで2種類のバグ票を運用したいと思った。
通常、JIRAのバグ票は、そのプロジェクトの関係者(のRole)をAdministrators, Developers, Usersと分ける時、Usersが起票し、Developersが処理する。それに加えて、今回は、Developers+α("group1"に所属するメンバーとする)だけが起票可能、閲覧可能な、開発者側の内部バグ票("IT Bug"とする)を、通常のバグ票とは別の課題タイプで運用したかった。
※結合テスト=Integration Testで発見された不具合、またはInternal Bugの略

特定の課題タイプを、特定のグループのメンバーだけが閲覧可能にするのも、直接的な設定方法が無く、容易ではないが、例えば、
  • Issue Security Schemeのセキュリティレベル定義を、default=Private(group1のみ閲覧可能)にする
  • Set Issue Security権限を課題作成可能者全員に与える
  • 画面にSecurity Levelフィールドを設ける
とすると、group1が開いた課題作成画面ではセキュリティレベルがPrivateになり、それ以外のメンバーが開いた課題作成画面ではセキュリティレベルがNoneになる(Noneは制限無し、設定権限があるのにNoneしか選べないからそうなる)ので、IT Bug以外は手操作でセキュリティレベルをNoneにすれば、何とか近いことが実現できる。
かなり強引で多少面倒だが、通常のバグ票を開発者が起票することがあまり無く、あっても非公開にしたいことがあり得るとすれば、現実的には妥当な解と言えなくはない。
(ちなみに、画面にSecurity Levelフィールドを設けなければ、全ての課題のセキュリティレベルがPrivateになる。これがNoneになるなら、IT Bugの作成画面だけにSecurity Levelフィールドを設ければ、IT Bug以外は自動的に全員閲覧可能にできるのだが、そうはならない。また、IT Bugのワークフローだけ、Create IssueトランジションのPost FunctionsにてSecurity Level=Noneにできれば良いが、JIRA 6.2にはその関数が無く、有料のプラグインをインストールしないとできない。)

しかし、特定の課題タイプは、特定のグループのメンバーだけが課題を作成可能にすることは、次の理由で、さらに難しい。

  • Create Issue権限を課題タイプ毎に別にできれば良いだけなのだが、Permission Schemeは、課題タイプ毎には設定できない。
  • 課題タイプ毎にworkflowは別にできるので、Create IssueトランジションのConditionsで制限すれば良いだけなのだが、なぜかCreate IssueトランジションにはConditionsが無い。
  • Create IssueトランジションにValidatorはあるが、グループではなく、何らかのPermissionしか指定できない(そのPermissionが無いというエラーになるので、Create Issue権限以外を指定すると意味不明なエラーになってしまう)し、作成ボタンを押した後にしかエラーにならない。
  • Workflow propertiesで制限できれば良いが、"(you can use in) Step"と書いてあり、実際に試してみたが、jira.permission.*はtransitionに対しては無効だった。Create Issueをする前には状態(Step)が無いので、jira.permission.*を設定する術が無い。
  • 同じプロジェクトで、課題タイプ毎に変えられる設定というのは、ワークフロー、画面、フィールド設定と、カスタムフィールドの初期値くらいしか見当たらない。

JIRA関連のドキュメントでもGoogleでも、"Permission per Issue Type"で検索して辿って行くと、大体、次の課題票に行き着く。
[JRA-5865] Allow permission schemes to be configured per issue type - Atlassian JIRA
約10年前に出され、多くの人に必要性を語られ、継続的に議論されてきた要望だが、JIRAを開発するAtlassian社によって、"Won't Fix"として昨年7月にcloseされている。
1つのプロジェクトで、課題タイプ毎に権限を変えるようなことはするな、という意味にしか取れない。
しかし、同じプロジェクトで、どの帳票も起票するメンバー、処理するメンバーが同じ、という制限では不便である。
例えば、発注者がバグ報告をする場合、開発者でない発注者が担当者になることは無いので、バグ票のAssigneeの権限は開発者に限定したいが、開発者が発注者に質問する場合、発注者が担当者になるので、Q&A票のAssigneeの権限は発注者に限定したい。
しかし、それだけのことすら、JIRAでは可能にす対応する予定が無いという。
プロジェクトを別にすれば良い、ということかも知れないが、課題タイプ毎にプロジェクトを別にするのでは課題タイプの意味が無いし、JIRAでプロジェクトを別に立ち上げるのは、バージョンやロールなど、共有できないので二重管理になるものもあるし、カスタムフィールドなど、プロジェクトに依存する設定もあり、結構面倒である。

帳票毎に権限を制約できないが為に、例えばAssignee権限を広くすると、その帳票の担当者になり得ないメンバーに間違ってassignしてしまう可能性があるし、一部の帳票は閲覧を制限するように課題にセキュリティレベルを設けていると、間違ってassignされた人がその課題を参照できず、課題が行方不明になってしまう。
今回やりたいことは、課題タイプが"IT Bug"の課題は、開発受注者のグループしか作成できないようにすることである。これも、帳票毎に権限を制約できないが為に、発注者も"IT Bug"を作成可能にすると、発注者にとっては、起票時に知らない課題タイプが選択肢に出て来るのがいまいちだし、間違って"IT Bug"で起票されてしまうと、いちいち開発者側で課題タイプを修正するのが面倒である。
それだけの為に、"IT Bug"を別プロジェクトにするのも、管理が面倒だし、見苦しい。

どうしても諦められなくて、上記の課題票JRA-5865のコメント欄を読むと、初期の頃から、JavaScriptでCreateボタンを消すことによる暫定対策が検討されており、貼られているJavaScriptを試してみると、JIRA 6.2でも部分的には動いたので、JavaScriptで何とかできないかを追究してみることにした。
なお、筆者にはJavaScriptの知識はほとんど無い。documentクラスでHTMLを操作する方法も、windowクラスでWebブラウザを制御する方法も知らない。AJS(Atlassian JavaScriptライブラリ) 頼みであり、AJSのドキュメントが無いのでGoogle頼みである。

■JIRAでのJavaScript使用の基本

フィールドのDescriptionにJavaScriptを書くと、そのDescriptionが表示される画面で実行される。
例えば、次のテキストをSummaryフィールド等のDescriptionに貼り付けると、課題作成画面を開く時に"JavaScript ran!"という警告ダイアログが出るようになる。

This description has some JavaScript.
<script language="JavaScript">
<!--
alert("JavaScript ran!");
//-->
</script>
Announcement Bannerに含めても実行されるが、scriptタグ以外に何も無くても、細いAnnoucement Bannerが表示されるようになってしまう。
参考:Fields Allowing Custom HTML or JavaScript - Atlassian Documentation
なお、 JIRA 6.2.x以降、これらのJavaScriptを埋め込む方法の一部は使えなくなる、と書いてあるが、JIRA 6.2.7では上の方法でJavaScriptが実行されることを確認した。

AJSを使用する場合は、実行されるタイミングが問題になるので、次の例の(function($) { ... })(AJS.$);の部分のように書くのが定跡のようである。

<script language="JavaScript">
(function($) {
  AJS.toInit(function(){
    //(A)
    alert("init on load: current user = " + AJS.params.loggedInUser);
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    //(B)
    alert("init on refresh: current user = " + AJS.params.loggedInUser);
  });
})(AJS.$);

//(C)
alert("outside function: current user = " + AJS.params.loggedInUser);
</script>
これをSummaryフィールドのDescriptionに埋め込むと、次のことがわかる。
  • 通常の(ポップアップでない)課題作成画面が開く時に(A)が実行される。
  • 通常の課題作成画面が開く時は(C)の位置ではAJSが働かない。
  • ポッブアッブ形式の課題作成画面が開く時に(A)が実行される。
    その前に(B)が呼ばれることもある。(元のページによっては、例えばClosedでない課題画面は、開いた途端に(A)が呼ばれ、(B)のコールバックも登録されるので、Create Issueボタンを押してポップアップ課題画面が開いた途端に(B)が呼ばれる。)
  • ポッブアッブ形式の課題作成画面でプロジェクトや課題タイプを変えると(B)が呼ばれる。
なお、ポップアップ課題作成画面を開く度にコールバックが追加されるので、元の画面をそのままに、何度もポップアップ課題作成画面を開くと、開く度に(B)が何度も呼ばれるようになってしまうが、これは仕方が無さそうだ。

ポップアップ課題作成画面におけるJavaScript実行について

ポップアップ形式の課題作成画面は、開いた状態でプロジェクトや課題タイプが切り替えることが可能で、それらの切り替えに連動してフィールドが出現したりDescriptionが変化することがあるが、切り替えによって表示される新たなDescriptionのJavaScriptは実行されない。従って、ポップアップ課題作成画面で実行されるべきJavaScriptは、ポップアップ課題作成画面が開く時にどのプロジェクトのどの課題タイプの画面から始まっても、必ず実行されるようにする必要がある。
例えば、全てのフィールド設定において、いずれかのDescriptionにJavaScriptを含める方法や、Announcement Bannerに含める方法が考えられる。
今回は、あらゆるプロジェクトのあらゆる課題タイプでSummaryフィールドが表示されるものとして、全てのフィールド設定のSummaryフィールドのDescriptionにJavaScriptを埋め込んでテストした。

■作戦1

該当プロジェクトの、作成が制限された課題タイプなら、Createボタンをdisabledにする。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
    AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = value;
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var project = AJS.$("#issue-create-project-name").text();
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (isUserInGroup('group1') == false){
    var project = AJS.$("#project-field").val();
    var issueType = AJS.$("#issuetype-field").val();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    } else {
      disableCreate(false);
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

isUserInGroup()は、Webのどこかから拾ったものである。
disableCreate()は、JRA-5865のコメント欄にあるものを、display='none'(非表示)でなくdisabled=true(グレーアウトして無効化)にするよう変えたものである。Createボタン(type="submit"のINPUTタグ)を非表示にしても、いくつかのブラウザではSubmitのショートカットキー(ChromeやIEではAlt+S、FirefoxではAlt+Shift+S、SafariではCtrl+Shift+Sなど)は効いてしまうため、disabledにした。(ChromeとSafariは効く、FirefoxとIE8は効かない)
initCreateIssueScreen()は、通常の(ポップアップでない)課題作成画面のCreateボタンの処理である。ユーザーが'group1'に属さず、プロジェクトが'Test Project 1'で、課題タイプが'IT Bug'なら、Createボタンをdisabledにしている。
initPopUpCreateIssueScreen()は、ポップアップ形式の課題作成画面のCreateボタンの処理である。ユーザーが'group1'に属さず、プロジェクトが'Test Project 1'で、課題タイプが'IT Bug'なら、Createボタンをdisabledに、そうでなければ、enabledにしている。
AJS.toInit()の部分は、このスクリプトの中で最初に実行される処理である。課題作成画面が開く時にも実行される。通常の課題作成画面ならinitCreateIssueScreen()を、ポップアップ課題作成画面ならinitPopUpCreateIssueScreen()を呼び出している。
JIRA.bind()の部分は、同じページで画面が変化すると実行される処理である。現在のページがポップアップ課題作成画面なら、initPopUpCreateIssueScreen()を呼び出している。
なお、プロジェクトIDや課題タイプIDを取り出す方法がわからなかったので、プロジェクトや課題タイプは、IDではなく実際に表示される文字列を比較に使っている。これらの文字列は変更される可能性があるので、このやり方はあまり好ましくない。この方法だと、課題タイプは多言語訳の登録が可能なので、登録した全言語の文字列と比較するようなことも必要になるが、ここでは省略している。

結果

通常の課題作成画面では、作成が制限される条件であれば、うまくCreateボタンが無効化される。
ポップアップ形式の課題作成画面でも、開いた時はCreateボタンが無効化されるが、開いたままプロジェクトや課題タイプを切り替えると、disableCreate(true)が呼び出されても、Createボタンが有効になってしまう。(Safari, Firefox, Chrome, IE全て同様)
元々、ポップアップの課題作成画面では、プロジェクトや課題タイプを切り替えると、Createボタンが一時的に無効になって有効に戻るので、この有効にする処理が後から走ってしまうのだと推測される。
色々試したが(後述)、最終的にCreateボタンをdisabledにする適当な方法は見つからなかった。
但し、Createボタンを非表示にすると、その状態は維持されることがわかった。

■作戦2

通常の課題作成画面では、該当プロジェクトの、作成が制限された課題タイプなら、Createボタンをdisabledにする。
該当プロジェクトの、作成が制限された課題タイプでは、Createボタンをdisabledかつ非表示にする。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
    AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = value;
    }
  }
}

function hideCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      if (value == true) {
        inps[i].style.display = 'none';
      } else {
        inps[i].style.display = '';
      }
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var project = AJS.$("#issue-create-project-name").text();
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (AJS.$("#create-issue-dialog").length) {
    if (isUserInGroup('group1') == false){
      var project = AJS.$("#project-field").val();
      var issueType = AJS.$("#issuetype-field").val();
      if (project == 'Test Project 1' && issueType == 'IT Bug') {
        disableCreate(true);
        hideCreate(true);
      } else {
        disableCreate(false);
        hideCreate(false);
      }
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

作戦1のスクリプトのinitPopUpCreateIssueScreen()に、hideCreate()の呼び出しを加えた。。
hideCreate()は、Createボタンをdisplay='none'にする処理である。

結果

作戦1同様、通常の課題作成画面では、作成が制限される条件であれば、Createボタンが無効化される。
ポップアップ形式の課題作成画面では、意図通りに、作成が制限される条件ならCreateボタンが消え、そうでなければCreateボタンが出現する。画面を開いたままプロジェクトや課題タイプを切り替えると、条件に従ってCreateボタンが出たり消えたりする。
但し、開いた直後にCreateボタンが消えていればSubmitのショートカットキーも効かないが、開いた後にプロジェクトや課題タイプを切り替えると、Createボタンが消えても、作戦1と同様、ショートカットキーは効いてしまう。

■作戦3

通常の課題作成画面では、作成が制限された課題タイプなら、Createボタンをdisabledにする。
ポップアップ課題作成画面では、課題タイプの選択肢から、制限された課題タイプを削除する。
※プロジェクトを問わず、その課題タイプの使用を制限する。その理由は後述。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
  AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate() {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = true;
    }
  }
}

function hideITBug(){
  var ops = AJS.$("#issuetype option");
  for (i = 0; i < ops.length; i++) {
    if(ops[i].text == 'IT Bug'){
      AJS.$("#" + ops[i].id).remove();
      return;
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (issueType == 'IT Bug') {
      disableCreate();
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (AJS.$("#create-issue-dialog").length) {
    if (isUserInGroup('group1') == false){
      hideITBug();
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

作戦1のスクリプトのinitPopUpCreateIssueScreen()は、hideITBug()を呼び出すように変えた。
hideITBug()は、課題タイプのプルダウンメニューから'IT Bug'のオプションを削除する処理である。
また、全体的にプロジェクト名の比較は無くした。
その結果、disableCreate()の引数は不要になったので、無くした。

補足説明

このようにして課題タイプの選択肢から'IT Bug'を削除しても、ポップアップ画面が開く時の初期値が'IT Bug'だったり、ポップアップ画面で別プロジェクトにして'IT Bug'を選択してからプロジェクトを切り替えると、課題タイプが'IT Bug'になってしまう。その為、課題タイプの選択肢から削除する方針なら、全プロジェクトにおいてその課題タイプを制限する必要がある。
通常の課題作成画面に入る前の、プロジェクトと課題タイプを選択する画面では、フィールドのDescriptionが表示されないので、JavaScriptによる課題タイプの制限が難しい。その為、通常の課題作成画面では、Createボタンをdisabledにしている。

結果

作戦1同様、通常の課題作成画面では、作成が制限される条件であれば、Createボタンが無効化される。
ポップアップ形式の課題作成画面では、意図通りに、課題タイプの選択肢から'IT Bug'が無くなる。
しかし、デフォルトの課題タイプを'IT Bug'にしていると、ポップアップ画面で'Test Project 1'に存在しない課題タイプを選択した状態から'Test Project 1'に切り替えると、課題タイプが'IT Bug'になってしまう。従って、例えば結合テストのフェーズではデフォルトの課題タイプを'IT Bug'にしたくても、それができないことになる。デフォルトの課題タイプを'IT Bug'にし得るなら、作戦2のように、その時にCreateボタンを非表示かつdisabledにする必要がありそうだ(それでも、作戦2と同じく、ショートカットキーによるSubmitまでは止められない)。
また、この方針だと、通常の課題作成画面の手前の課題タイプ選択画面では相変わらず'IT Bug'が選択できてしまうのが、統一感が無くて不満である。

結論

いずれの作戦もデメリットがあるし、全プロジェクトに影響してしまうので、特定の課題タイプの課題の作成をJavaScriptで制限するのは諦める。

面倒でもプロジェクトを分けるか、どうしてもプロジェクトを分けたくなければ、エラーメッセージがわかりにくくなるが、Create IssueトランジションのValidatorsで作成者を制限するのが最善だと思われる。
なお、Create IssueトランジションのValidatorsは、ワークフロー編集画面のDiagram版から開くことができる。Validatorとしてはいずれかの権限しか選択できないので、いずれの課題タイプについても内部バグ票の作成者に制限しても良さそうな権限、例えばSchedule Issues権限をを選んでそれを内部バグ票の作成可能者に制限し、内部バグ票のCreate IssueトランジションのValidatorにも使用する。

See more ...

Posted at 18:25 in PC一般 | WriteBacks (0)
WriteBacks