Splunkのロールの色々な権限をSPL (| rest)で調べたい

## はじめに

roleがどのindexにアクセスできて、どのAppとかダッシュボードの読み取り権限があるか、調べようと思ったら大変じゃないですか？

ということで、その辺りを`| rest` でざーっと取る方法を調べました。
ついでにダッシュボードも作りました。

## 使用するAPI
この辺りを使えばOK。Splunk Cloudでも動作します。
adminかsc_adminで見てください。

restコマンドの権限などについてはこちらの記事がとても参考になります。

https://qiita.com/bashi100kmrun/items/676aba59d6d8b86d6108

### Role
```text
| rest /services/authorization/roles splunk_server=local
| table title, srchIndexesAllowed, srchIndexesDefault, capabilities, imported_roles, imported_capabilities
| search title="<対象role>"
```
roleのsearch可能indexやcapabilityなどが取れます。import_*は継承したrole、capabilityです。
https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTaccess#authorization.2Froles


### User
```text
| rest /services/authentication/users splunk_server=local
| table title, roles
| search roles="<対象role>"
```
userにアサインされたroleが取れます。
https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTaccess#authentication.2Fusers

### App
```text
| rest /services/apps/local splunk_server=local
| table title, eai:acl.perms.read, eai:acl.perms.write
| rename eai:acl.perms.read as read_role, eai:acl.perms.write as write_role
| where read_role="<対象role>" OR read_role="*"
``` | where write_role="<対象role>" OR write_role="*" ```
```
appのread/write権限のroleが取れます。
https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTapps#apps.2Flocal

※eai:acl.perms.read/writeが\*の場合はどのロールでも権限があることを示しています。そのために\*が含まれている場合も拾わなければならないのですが、`| search`で\*を指定するとワイルドカード検索になってしまいます。一方、`| where`で"\*"とすることで文字としての\*（アスタリスク）を検索できます。

### Dashboard

```text
| rest /services/data/ui/views splunk_server=local
| search isDashboard=1
| table title, eai:acl.perms.read, eai:acl.perms.write
| rename eai:acl.perms.read as read_role, eai:acl.perms.write as write_role
| where read_role="<対象role>" OR read_role="*"
``` | where write_role="<対象role>" OR write_role="*" ```
```
dashboardのread/write権限のroleが取れます。
https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTknowledge#data.2Fui.2Fviews


## ダッシュボードサンプル

こんな感じで作りました。

指定したRoleを持っているユーザー、Read/Write権限のあるApp、Read/Write権限のあるDashboardが分かるようになっています。
一点、Roleを継承していた場合がちょっと面倒で、上記のRESTでは直接検証しているRoleは拾えますが、そのRoleが更に別のRoleを継承していた場合は拾えません。
そのために ~~joinを重ねたり~~ Tokenを増やしたり力技で解決してます。
（どなたかスマートな方法ご存じでしたら教えてください！）
※改修：何度もrestしてjoinせずとも、最初に取得した親子関係だけを何度も使いまわせるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1218403/ec90cc95-18c8-9281-2067-6a741ae19b78.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1218403/a1f85642-2da3-2bfa-8e51-0c94a3110b10.png)


<details><summary>サンプルダッシュボードのソース</summary>

```xml
<form version="1.1" theme="dark">
  <search id="roles">
    <query>
| rest /services/authorization/roles splunk_server=local 
    </query>
  </search>
  <search id="apps">
    <query>
| rest /services/apps/local splunk_server=local 
    </query>
  </search>
  <search id="users">
    <query>
| rest /services/authentication/users splunk_server=local
    </query>
  </search>
  <search id="dashboards">
    <query>
| rest /services/data/ui/views splunk_server=local
| search isDashboard=1
    </query>
  </search>
  <label>Role Investigator</label>
  <fieldset submitButton="false">
    <input type="dropdown" token="tk_role" searchWhenChanged="true">
      <label>Role</label>
      <fieldForLabel>title</fieldForLabel>
      <fieldForValue>title</fieldForValue>
      <search base="roles">
        <query>| stats count by title</query>
      </search>
      <change>
        <condition>
          <unset token="form.tk_imported_roles"></unset>
        </condition>
      </change>
    </input>
    <input type="multiselect" token="tk_imported_roles" searchWhenChanged="true">
      <label>Imported Roles</label>
      <fieldForLabel>title</fieldForLabel>
      <fieldForValue>title</fieldForValue>
      <search base="roles">
        <query>| stats count by title</query>
      </search>
      <delimiter> ,</delimiter>
      <valuePrefix>"</valuePrefix>
      <valueSuffix>"</valueSuffix>
      <choice value="None">None</choice>
      <default>None</default>
    </input>
  </fieldset>
  <row>
    <panel>
      <title>Imported Roles</title>
      <table>
        <title>継承したロールも調査対象にする場合はクリックしてください</title>
        <search base="roles">
          <query>| table title, imported_roles 
| rename title as parent, imported_roles as child 
| fillnull child value="null" 
| stats count by parent, child 
| eval line=parent."-&gt;".child ```keep all rels``` 
| table parent, child, line 
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), -2), child=mvindex(split(line, "-&gt;"), -1) | appendpipe [| eval child1=child, child=parent, parent=null()] | eventstats values(child1) as child1 by child | where isnotnull(parent) | mvexpand child1 | eval line=if(child1="", line."-&gt;null", line."-&gt;".child1) | fields - child1
| eval parent=mvindex(split(line, "-&gt;"), 0)
| table parent, line 
| rex mode=sed field=line "s/\-\&gt;null//g"
| where line!="" 
| stats values(line) as lines by parent 
| rename parent as role 
| search role="$tk_role$"
    ``` extract all roles ``` 
| stats count by role, lines 
| eval all_roles=split(lines,"-&gt;") 
| stats count by role, lines, all_roles 
| eval all_roles=if(role=all_roles ,null(),all_roles)
| stats values(lines) as lines, values(all_roles) as all_imported_roles by role 
| eval all_imported_roles=mvjoin(all_imported_roles,",")
| eval all_imported_roles=if(all_imported_roles!="",all_imported_roles,"")</query>
        </search>
        <option name="count">5</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">cell</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
        <drilldown>
          <eval token="form.tk_imported_roles">split($row.all_imported_roles$,",")</eval>
        </drilldown>
      </table>
    </panel>
  </row>
  <row>
    <panel>
      <title>Assigned Users</title>
      <table>
        <search base="users">
          <query>| table title, roles
| search roles=$tk_role$
| rename title as user</query>
        </search>
        <option name="count">5</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">none</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
      </table>
    </panel>
    <panel>
      <title>Searchable Indexes</title>
      <table>
        <search base="roles">
          <query>| search title=$tk_role$ OR title IN($tk_imported_roles$)
| dedup title
| table title, srchIndexesAllowed, srchIndexesDefault
| rename title as role</query>
        </search>
        <option name="count">10</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">none</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
      </table>
    </panel>
    <panel>
      <title>Capabilities</title>
      <table>
        <search base="roles">
          <query>| search title=$tk_role$ OR title IN($tk_imported_roles$)
| table title, capabilities
| mvexpand capabilities
| rename title as role</query>
        </search>
        <option name="count">10</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">none</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
      </table>
    </panel>
  </row>
  <row>
    <panel>
      <title>Apps</title>
      <input type="dropdown" token="tk_app_visible">
        <label></label>
        <choice value="*">Show All Apps</choice>
        <choice value="1">Show Only Apps Shown in Nav</choice>
        <default>1</default>
      </input>
      <table>
        <search base="apps">
          <query>| search visible=$tk_app_visible$ disabled=0 
| rename title as app, eai:acl.perms.read as read_roles, eai:acl.perms.write as write_roles, visible as shown_in_nav
| eval is_visible=if(read_roles="*" OR read_roles="$tk_role$" OR read_roles IN($tk_imported_roles$), "YES", "NO") 
| eval is_editable=if(write_roles="*" OR write_roles="$tk_role$" OR write_roles IN($tk_imported_roles$), "YES", "NO") 
| join app type=left 
    [| rest /services/data/ui/views splunk_server=local 
    | search isDashboard=1 
    | rename eai:acl.app as app, eai:acl.perms.read as read_roles, eai:acl.perms.write as write_roles 
    | eval is_visible=if(read_roles="*" OR read_roles="$tk_role$" OR read_roles IN($tk_imported_roles$), 1, 0) 
    | eval is_editable=if(write_roles="*" OR write_roles="$tk_role$" OR write_roles IN($tk_imported_roles$), 1, 0)
    | stats sum(is_visible) as visible_dashboards, sum(is_editable) as editable_dashboards by app] 
| eval visible_dashboards=if(isnull(visible_dashboards),0,visible_dashboards)
| eval editable_dashboards=if(isnull(editable_dashboards),0,editable_dashboards) 
| table app, label, read_roles, is_visible, write_roles, is_editable, visible_dashboards, editable_dashboards, shown_in_nav</query>
        </search>
        <option name="count">10</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">cell</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
        <format type="color" field="is_visible">
          <colorPalette type="map">{"YES":#55C169,"NO":#F98C83}</colorPalette>
        </format>
        <format type="color" field="is_editable">
          <colorPalette type="map">{"YES":#55C169,"NO":#F98C83}</colorPalette>
        </format>
        <format type="color" field="visible_dashboards">
          <colorPalette type="list">[#3C444D,#0099E0]</colorPalette>
          <scale type="threshold">1</scale>
        </format>
        <format type="color" field="editable_dashboards">
          <colorPalette type="list">[#3C444D,#0099E0]</colorPalette>
          <scale type="threshold">1</scale>
        </format>
        <drilldown>
          <set token="form.tk_app">$row.app$</set>
        </drilldown>
      </table>
    </panel>
  </row>
  <row>
    <panel>
      <title>Dashboards</title>
      <input type="dropdown" token="tk_app">
        <label>App</label>
        <choice value="*">All</choice>
        <default>*</default>
        <fieldForLabel>app</fieldForLabel>
        <fieldForValue>title</fieldForValue>
        <search base="apps">
          <query>| stats count by title, label
| eval app=label." (".title.")"</query>
        </search>
      </input>
      <table>
        <search base="dashboards">
          <query>| rename eai:acl.app as app, eai:acl.perms.read as read_roles, eai:acl.perms.write as write_roles, title as dashboard
| search app=$tk_app$
| eval is_visible=if(read_roles="*" OR read_roles="$tk_role$" OR read_roles IN($tk_imported_roles$), "YES", "NO") 
| eval is_editable=if(write_roles="*" OR write_roles="$tk_role$" OR write_roles IN($tk_imported_roles$), "YES", "NO") 
| sort - is_visible
| table app, dashboard, label, read_roles, is_visible, write_roles, is_editable</query>
        </search>
        <option name="count">10</option>
        <option name="dataOverlayMode">none</option>
        <option name="drilldown">cell</option>
        <option name="percentagesRow">false</option>
        <option name="refresh.display">progressbar</option>
        <option name="rowNumbers">false</option>
        <option name="totalsRow">false</option>
        <option name="wrap">true</option>
        <format type="color" field="is_visible">
          <colorPalette type="map">{"YES":#55C169,"NO":#F98C83}</colorPalette>
        </format>
        <format type="color" field="is_editable">
          <colorPalette type="map">{"YES":#55C169,"NO":#F98C83}</colorPalette>
        </format>
        <drilldown>
          <link target="_blank">/app/$row.app$/$row.dashboard$</link>
        </drilldown>
      </table>
    </panel>
  </row>
</form>

```

</details>