Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@ on:
pull_request:
branches: [ main ]

env:
NODE_VERSION: 22.x

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x] # Build on Node.js 18

steps:
- uses: actions/checkout@v2

- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ env.NODE_VERSION }}

- name: Setup pnpm
uses: pnpm/action-setup@v2.0.1
Expand All @@ -45,10 +43,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Setup Node.js 18.x
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: ${{ env.NODE_VERSION }}

- name: Setup pnpm
uses: pnpm/action-setup@v2.0.1
Expand All @@ -74,10 +72,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Setup Node.js 18.x
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: ${{ env.NODE_VERSION }}

- name: Setup pnpm
uses: pnpm/action-setup@v2.0.1
Expand Down
13 changes: 13 additions & 0 deletions addon/components/widget/access-coverage.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Widget::IamDashboardWidget @endpoint="access-coverage" @title="Access Coverage" @subtitle="Assignment coverage by user" @icon="shield-user" as |data|>
<div class="iam-coverage-hero">
<span>{{data.coverage}}%</span>
<small>Users with access assignments</small>
</div>
<div class="iam-row-list mt-3">
<div class="iam-row"><span>With roles</span><strong>{{data.with_roles}}</strong></div>
<div class="iam-row"><span>With groups</span><strong>{{data.with_groups}}</strong></div>
<div class="iam-row"><span>With policies</span><strong>{{data.with_policies}}</strong></div>
<div class="iam-row"><span>Direct permissions</span><strong>{{data.with_direct_permissions}}</strong></div>
<div class="iam-row iam-row-warning"><span>No assignments</span><strong>{{data.without_assignments}}</strong></div>
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/access-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetAccessCoverageComponent extends Component {}
20 changes: 20 additions & 0 deletions addon/components/widget/access-risk.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Widget::IamDashboardWidget @endpoint="privileged-access" @title="Access Risk" @subtitle="Privileged exposure requiring review" @icon="user-lock" as |data|>
<div class="iam-stat-grid iam-stat-grid-3 iam-risk-summary">
<div class="iam-stat-card"><span>{{data.privileged_roles_count}}</span><small>Admin roles</small></div>
<div class="iam-stat-card"><span>{{data.wildcard_policies_count}}</span><small>Wildcard policies</small></div>
<div class="iam-stat-card"><span>{{data.direct_privileged_grants}}</span><small>Direct grants</small></div>
</div>
<div class="iam-scroll-list mt-3">
{{#each data.roles as |role|}}
<div class="iam-list-row iam-risk-row">
<div class="min-w-0">
<div class="iam-list-title">{{role.name}}</div>
<div class="iam-list-subtitle">{{role.type}}</div>
</div>
<strong>{{role.permissions_count}}</strong>
</div>
{{else}}
<div class="iam-widget-empty">No privileged access risks detected</div>
{{/each}}
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/access-risk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetAccessRiskComponent extends Component {}
12 changes: 12 additions & 0 deletions addon/components/widget/group-coverage.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Widget::IamDashboardWidget @endpoint="group-coverage" @title="Group Coverage" @subtitle="Membership distribution" @icon="people-group" as |data|>
<div class="iam-stat-grid iam-stat-grid-3">
<div class="iam-stat-card"><span>{{data.total_groups}}</span><small>Groups</small></div>
<div class="iam-stat-card"><span>{{data.total_memberships}}</span><small>Memberships</small></div>
<div class="iam-stat-card"><span>{{data.empty_groups}}</span><small>Empty</small></div>
</div>
<div class="iam-row-list mt-3">
{{#each data.buckets as |bucket|}}
<div class="iam-row"><span>{{bucket.label}}</span><strong>{{bucket.value}}</strong></div>
{{/each}}
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/group-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetGroupCoverageComponent extends Component {}
27 changes: 27 additions & 0 deletions addon/components/widget/iam-dashboard-widget.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="iam-dashboard-widget flex h-full min-h-0 flex-col overflow-hidden rounded-md border shadow-sm {{@class}}" ...attributes>
<div class="iam-widget-header">
<div class="min-w-0">
<div class="iam-widget-title">{{@title}}</div>
{{#if @subtitle}}
<div class="iam-widget-subtitle">{{@subtitle}}</div>
{{/if}}
</div>
{{#if @icon}}
<div class="iam-widget-icon">
<FaIcon @icon={{@icon}} @prefix={{or @iconPrefix "fas"}} />
</div>
{{/if}}
</div>

<div class="iam-widget-body">
{{#if this.load.isRunning}}
<div class="iam-widget-empty">
<Spinner />
</div>
{{else if this.error}}
<div class="iam-widget-empty text-rose-600 dark:text-rose-300">{{this.error}}</div>
{{else}}
{{yield this.data}}
{{/if}}
</div>
</div>
29 changes: 29 additions & 0 deletions addon/components/widget/iam-dashboard-widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';

export default class WidgetIamDashboardWidgetComponent extends Component {
@service fetch;

@tracked data = null;
@tracked error = null;

constructor() {
super(...arguments);
this.load.perform();
}

get params() {
return this.args.params ?? {};
}

@task *load() {
try {
this.data = yield this.fetch.get(`metrics/iam/${this.args.endpoint}`, this.params);
this.error = null;
} catch (error) {
this.error = error?.message ?? 'Unable to load widget';
}
}
}
21 changes: 21 additions & 0 deletions addon/components/widget/iam-kpi-tile.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="iam-dashboard-widget iam-kpi-tile {{this.accentClass}} flex h-full min-h-0 flex-col justify-between overflow-hidden rounded-md border shadow-sm p-3" ...attributes>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="iam-kpi-title">{{@title}}</div>
{{#if this.error}}
<div class="mt-2 truncate text-xs font-semibold text-rose-600 dark:text-rose-300">{{this.error}}</div>
{{else if this.load.isRunning}}
<div class="mt-3"><Spinner /></div>
{{else}}
<div class="iam-kpi-value mt-2 truncate">{{this.value}}</div>
{{/if}}
</div>
<div class="iam-kpi-icon">
<FaIcon @icon={{@icon}} @prefix={{or @iconPrefix "fas"}} />
</div>
</div>

<div class="iam-kpi-footer">
<span class="truncate">{{this.footnote}}</span>
</div>
</div>
53 changes: 53 additions & 0 deletions addon/components/widget/iam-kpi-tile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';

export default class WidgetIamKpiTileComponent extends Component {
@service fetch;

@tracked data = null;
@tracked error = null;

constructor() {
super(...arguments);
this.load.perform();
}

get metric() {
return this.data?.[this.args.metric] ?? {};
}

get value() {
if (this.metric.format === 'unavailable' || this.metric.available === false) {
return 'N/A';
}

if (this.metric.format === 'percent') {
return `${this.metric.value ?? 0}%`;
}

return Number(this.metric.value ?? 0).toLocaleString();
}

get footnote() {
if (this.metric.format === 'unavailable' || this.metric.available === false) {
return this.args.unavailableText ?? 'Coverage unavailable';
}

return this.args.footnote ?? this.metric.format ?? 'Current';
}

get accentClass() {
return `iam-kpi-accent-${this.args.accent ?? 'blue'}`;
}

@task *load() {
try {
this.data = yield this.fetch.get('metrics/iam/kpis');
this.error = null;
} catch (error) {
this.error = error?.message ?? 'Unable to load KPI';
}
}
}
1 change: 1 addition & 0 deletions addon/components/widget/iam-metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class WidgetIamMetricsComponent extends Component {
* @memberof WidgetKeyMetricsComponent
*/
@service fetch;
@service notifications;

/**
* Property for loading metrics to.
Expand Down
34 changes: 34 additions & 0 deletions addon/components/widget/identity-health.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Widget::IamDashboardWidget @endpoint="identity-health" @title="Identity Health" @subtitle="Access posture snapshot" @icon="heart-pulse" as |data|>
<div class="iam-stat-grid">
<div class="iam-stat-card">
<span>{{data.status.active}}</span>
<small>Active</small>
</div>
<div class="iam-stat-card">
<span>{{data.status.pending}}</span>
<small>Pending</small>
</div>
<div class="iam-stat-card">
<span>{{data.verification.unverified}}</span>
<small>Unverified</small>
</div>
<div class="iam-stat-card">
<span>{{data.dormant.count}}</span>
<small>Dormant</small>
</div>
</div>
<div class="iam-row-list mt-3">
<div class="iam-row">
<span>MFA coverage</span>
<strong>{{if data.mfa.available (concat data.mfa.value "%") "Unavailable"}}</strong>
</div>
<div class="iam-row">
<span>Organization 2FA</span>
<strong>{{if data.mfa.company_enforced "Enforced" (if data.mfa.company_enabled "Enabled" "Off")}}</strong>
</div>
<div class="iam-row">
<span>Dormant threshold</span>
<strong>{{data.dormant.threshold_days}} days</strong>
</div>
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/identity-health.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetIdentityHealthComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-active-users.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="active_users" @title="Active Users" @icon="user-check" @accent="green" @footnote="Current access" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-active-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiActiveUsersComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-dormant-users.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="dormant_users" @title="Dormant Users" @icon="bed" @accent="rose" @footnote="No recent login" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-dormant-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiDormantUsersComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-inactive-users.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="inactive_users" @title="Inactive Users" @icon="user-slash" @accent="slate" @footnote="Disabled access" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-inactive-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiInactiveUsersComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-mfa-coverage.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="mfa_coverage" @title="MFA Coverage" @icon="lock" @accent="violet" @unavailableText="Org enforcement shown" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-mfa-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiMfaCoverageComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-pending-invites.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="pending_invites" @title="Pending Invites" @icon="envelope-open-text" @accent="amber" @footnote="Awaiting access" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-pending-invites.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiPendingInvitesComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-policies.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="policies" @title="Policies" @icon="file-shield" @accent="green" @footnote="Organization managed" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-policies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiPoliciesComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-roles.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="roles" @title="Roles" @icon="user-tag" @accent="blue" @footnote="Organization managed" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-roles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiRolesComponent extends Component {}
1 change: 1 addition & 0 deletions addon/components/widget/kpi-verified-users.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Widget::IamKpiTile @metric="verified_users" @title="Verified Users" @icon="circle-check" @accent="blue" @footnote="Identity verified" ...attributes />
3 changes: 3 additions & 0 deletions addon/components/widget/kpi-verified-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetKpiVerifiedUsersComponent extends Component {}
17 changes: 17 additions & 0 deletions addon/components/widget/policy-surface.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Widget::IamDashboardWidget @endpoint="policy-surface" @title="Policy Surface" @subtitle="Policies by service" @icon="diagram-project" as |data|>
<div class="iam-stat-grid iam-stat-grid-3">
<div class="iam-stat-card"><span>{{data.total}}</span><small>Total</small></div>
<div class="iam-stat-card"><span>{{data.organization_managed}}</span><small>Org managed</small></div>
<div class="iam-stat-card"><span>{{data.fleetbase_managed}}</span><small>Fleetbase</small></div>
</div>
<div class="iam-scroll-list mt-3">
{{#each data.by_service as |service|}}
<div class="iam-list-row">
<span class="iam-list-title">{{service.label}}</span>
<strong>{{service.value}}</strong>
</div>
{{else}}
<div class="iam-widget-empty">No policies available</div>
{{/each}}
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/policy-surface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetPolicySurfaceComponent extends Component {}
20 changes: 20 additions & 0 deletions addon/components/widget/privileged-access.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Widget::IamDashboardWidget @endpoint="privileged-access" @title="Privileged Access" @subtitle="High access exposure" @icon="user-lock" as |data|>
<div class="iam-stat-grid iam-stat-grid-3">
<div class="iam-stat-card"><span>{{data.privileged_roles_count}}</span><small>Admin roles</small></div>
<div class="iam-stat-card"><span>{{data.wildcard_policies_count}}</span><small>Wildcard policies</small></div>
<div class="iam-stat-card"><span>{{data.direct_privileged_grants}}</span><small>Direct grants</small></div>
</div>
<div class="iam-scroll-list mt-3">
{{#each data.roles as |role|}}
<div class="iam-list-row">
<div class="min-w-0">
<div class="iam-list-title">{{role.name}}</div>
<div class="iam-list-subtitle">{{role.type}}</div>
</div>
<strong>{{role.permissions_count}}</strong>
</div>
{{else}}
<div class="iam-widget-empty">No privileged roles detected</div>
{{/each}}
</div>
</Widget::IamDashboardWidget>
3 changes: 3 additions & 0 deletions addon/components/widget/privileged-access.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class WidgetPrivilegedAccessComponent extends Component {}
Loading
Loading