diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
index aa4da05e99..37c7aec4e1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
@@ -52,6 +52,12 @@ const routes: Routes = [
canMatch: [authenticationGuard],
loadChildren: () => import('./pages/users/feature/users.module').then((m) => m.UsersModule)
},
+ {
+ path: 'access-policies',
+ canMatch: [authenticationGuard],
+ loadChildren: () =>
+ import('./pages/access-policies/feature/access-policies.module').then((m) => m.AccessPoliciesModule)
+ },
{
path: 'summary',
canMatch: [authenticationGuard],
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
index da836bc227..f19e2ab92c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
@@ -40,6 +40,7 @@ import { StatusHistoryEffects } from './state/status-history/status-history.effe
import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServiceStateEffects } from './state/contoller-service-state/controller-service-state.effects';
import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diagnostics.effects';
+import { FlowConfigurationEffects } from './state/flow-configuration/flow-configuration.effects';
// @ts-ignore
@NgModule({
@@ -62,6 +63,7 @@ import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diag
CurrentUserEffects,
ExtensionTypesEffects,
AboutEffects,
+ FlowConfigurationEffects,
StatusHistoryEffects,
ControllerServiceStateEffects,
SystemDiagnosticsEffects
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies-routing.module.ts
new file mode 100644
index 0000000000..a611103f26
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies-routing.module.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { RouterModule, Routes } from '@angular/router';
+import { NgModule } from '@angular/core';
+import { AccessPolicies } from './access-policies.component';
+import { FlowConfiguration } from '../../../state/flow-configuration';
+import { checkFlowConfiguration } from '../../../service/guard/flow-configuration.guard';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: AccessPolicies,
+ canMatch: [
+ checkFlowConfiguration(
+ (flowConfiguration: FlowConfiguration) => flowConfiguration.supportsManagedAuthorizer
+ )
+ ],
+ children: [
+ {
+ path: 'global',
+ loadChildren: () =>
+ import('../ui/global-access-policies/global-access-policies.module').then(
+ (m) => m.GlobalAccessPoliciesModule
+ )
+ },
+ {
+ path: '',
+ loadChildren: () =>
+ import('../ui/component-access-policies/component-access-policies.module').then(
+ (m) => m.ComponentAccessPoliciesModule
+ )
+ }
+ ]
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AccessPoliciesRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.html
new file mode 100644
index 0000000000..42f3ca3285
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+
Access Policies
+
+
+
+
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.scss
new file mode 100644
index 0000000000..b19b15a73c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.scss
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.access-policies-header {
+ color: #728e9b;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.spec.ts
new file mode 100644
index 0000000000..21cbd7545d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.spec.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AccessPolicies } from './access-policies.component';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterModule } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../state/access-policy/access-policy.reducer';
+
+describe('AccessPolicies', () => {
+ let component: AccessPolicies;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [AccessPolicies],
+ imports: [RouterModule, RouterTestingModule],
+ providers: [
+ provideMockStore({
+ initialState
+ })
+ ]
+ });
+ fixture = TestBed.createComponent(AccessPolicies);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts
new file mode 100644
index 0000000000..9efae42064
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
+
+@Component({
+ selector: 'access-policies',
+ templateUrl: './access-policies.component.html',
+ styleUrls: ['./access-policies.component.scss']
+})
+export class AccessPolicies implements OnInit, OnDestroy {
+ constructor(private store: Store) {}
+
+ ngOnInit(): void {
+ this.store.dispatch(startCurrentUserPolling());
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch(stopCurrentUserPolling());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.module.ts
new file mode 100644
index 0000000000..9e7a66b10b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.module.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AccessPolicies } from './access-policies.component';
+import { AccessPoliciesRoutingModule } from './access-policies-routing.module';
+import { StoreModule } from '@ngrx/store';
+import { reducers, accessPoliciesFeatureKey } from '../state';
+import { EffectsModule } from '@ngrx/effects';
+import { MatDialogModule } from '@angular/material/dialog';
+import { AccessPolicyEffects } from '../state/access-policy/access-policy.effects';
+import { TenantsEffects } from '../state/tenants/tenants.effects';
+import { PolicyComponentEffects } from '../state/policy-component/policy-component.effects';
+
+@NgModule({
+ declarations: [AccessPolicies],
+ exports: [AccessPolicies],
+ imports: [
+ CommonModule,
+ AccessPoliciesRoutingModule,
+ StoreModule.forFeature(accessPoliciesFeatureKey, reducers),
+ EffectsModule.forFeature(AccessPolicyEffects, TenantsEffects, PolicyComponentEffects),
+ MatDialogModule
+ ]
+})
+export class AccessPoliciesModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/service/access-policy.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/service/access-policy.service.ts
new file mode 100644
index 0000000000..34ab4bf959
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/service/access-policy.service.ts
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Client } from '../../../service/client.service';
+import { Observable } from 'rxjs';
+import { AccessPolicyEntity, ComponentResourceAction, ResourceAction } from '../state/shared';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { TenantEntity } from '../../../state/shared';
+
+@Injectable({ providedIn: 'root' })
+export class AccessPolicyService {
+ private static readonly API: string = '../nifi-api';
+
+ constructor(
+ private httpClient: HttpClient,
+ private client: Client,
+ private nifiCommon: NiFiCommon
+ ) {}
+
+ /**
+ * The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
+ * does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
+ *
+ * https://stackoverflow.com/a/59586462
+ *
+ * @param url
+ * @private
+ */
+ private stripProtocol(url: string): string {
+ return this.nifiCommon.substringAfterFirst(url, ':');
+ }
+
+ createAccessPolicy(resourceAction: ResourceAction): Observable {
+ let resource: string = `/${resourceAction.resource}`;
+ if (resourceAction.resourceIdentifier) {
+ resource += `/${resourceAction.resourceIdentifier}`;
+ }
+
+ const payload: unknown = {
+ revision: {
+ version: 0,
+ clientId: this.client.getClientId()
+ },
+ component: {
+ action: resourceAction.action,
+ resource,
+ userGroups: [],
+ users: []
+ }
+ };
+
+ return this.httpClient.post(`${AccessPolicyService.API}/policies`, payload);
+ }
+
+ getAccessPolicy(resourceAction: ResourceAction): Observable {
+ const path: string[] = [resourceAction.action, resourceAction.resource];
+ if (resourceAction.resourceIdentifier) {
+ path.push(resourceAction.resourceIdentifier);
+ }
+ return this.httpClient.get(`${AccessPolicyService.API}/policies/${path.join('/')}`);
+ }
+
+ getPolicyComponent(resourceAction: ComponentResourceAction): Observable {
+ return this.httpClient.get(
+ `${AccessPolicyService.API}/${resourceAction.resource}/${resourceAction.resourceIdentifier}`
+ );
+ }
+
+ updateAccessPolicy(accessPolicy: AccessPolicyEntity, users: TenantEntity[], userGroups: TenantEntity[]) {
+ const payload: unknown = {
+ revision: this.client.getRevision(accessPolicy),
+ component: {
+ id: accessPolicy.id,
+ userGroups,
+ users
+ }
+ };
+
+ return this.httpClient.put(this.stripProtocol(accessPolicy.uri), payload);
+ }
+
+ deleteAccessPolicy(accessPolicy: AccessPolicyEntity): Observable {
+ const revision: any = this.client.getRevision(accessPolicy);
+ return this.httpClient.delete(this.stripProtocol(accessPolicy.uri), { params: revision });
+ }
+
+ getUsers(): Observable {
+ return this.httpClient.get(`${AccessPolicyService.API}/tenants/users`);
+ }
+
+ getUserGroups(): Observable {
+ return this.httpClient.get(`${AccessPolicyService.API}/tenants/user-groups`);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.actions.ts
new file mode 100644
index 0000000000..2a81093301
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.actions.ts
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createAction, props } from '@ngrx/store';
+import {
+ LoadAccessPolicyError,
+ LoadAccessPolicyRequest,
+ AccessPolicyResponse,
+ SelectGlobalAccessPolicyRequest,
+ SetAccessPolicyRequest,
+ ResetAccessPolicy,
+ RemoveTenantFromPolicyRequest,
+ AddTenantsToPolicyRequest,
+ SelectComponentAccessPolicyRequest
+} from './index';
+
+const ACCESS_POLICY_PREFIX: string = '[Access Policy]';
+
+export const selectGlobalAccessPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Select Global Access Policy`,
+ props<{ request: SelectGlobalAccessPolicyRequest }>()
+);
+
+export const selectComponentAccessPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Select Component Access Policy`,
+ props<{ request: SelectComponentAccessPolicyRequest }>()
+);
+
+export const setAccessPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Set Access Policy`,
+ props<{ request: SetAccessPolicyRequest }>()
+);
+
+export const reloadAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Reload Access Policy`);
+
+export const loadAccessPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Load Access Policy`,
+ props<{ request: LoadAccessPolicyRequest }>()
+);
+
+export const loadAccessPolicySuccess = createAction(
+ `${ACCESS_POLICY_PREFIX} Load Access Policy Success`,
+ props<{ response: AccessPolicyResponse }>()
+);
+
+export const createAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Create Access Policy`);
+
+export const createAccessPolicySuccess = createAction(
+ `${ACCESS_POLICY_PREFIX} Create Access Policy Success`,
+ props<{ response: AccessPolicyResponse }>()
+);
+
+export const openAddTenantToPolicyDialog = createAction(`${ACCESS_POLICY_PREFIX} Open Add Tenant To Policy Dialog`);
+
+export const addTenantsToPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Add Tenants To Policy`,
+ props<{ request: AddTenantsToPolicyRequest }>()
+);
+
+export const promptRemoveTenantFromPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Prompt Remove Tenant From Policy`,
+ props<{ request: RemoveTenantFromPolicyRequest }>()
+);
+
+export const removeTenantFromPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Remove Tenant From Policy`,
+ props<{ request: RemoveTenantFromPolicyRequest }>()
+);
+
+export const promptDeleteAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Prompt Delete Access Policy`);
+
+export const deleteAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Delete Access Policy`);
+
+export const resetAccessPolicy = createAction(
+ `${ACCESS_POLICY_PREFIX} Reset Access Policy`,
+ props<{ response: ResetAccessPolicy }>()
+);
+
+export const accessPolicyApiError = createAction(
+ `${ACCESS_POLICY_PREFIX} Access Policy Api Error`,
+ props<{ response: LoadAccessPolicyError }>()
+);
+
+export const resetAccessPolicyState = createAction(`${ACCESS_POLICY_PREFIX} Reset Access Policy State`);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.effects.ts
new file mode 100644
index 0000000000..3ccf7ec75c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.effects.ts
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { NiFiState } from '../../../../state';
+import { Store } from '@ngrx/store';
+import { Router } from '@angular/router';
+import * as AccessPolicyActions from './access-policy.actions';
+import { catchError, combineLatest, filter, from, map, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { AccessPolicyService } from '../../service/access-policy.service';
+import { AccessPolicyEntity, ComponentResourceAction, PolicyStatus, ResourceAction } from '../shared';
+import { selectAccessPolicy, selectResourceAction, selectSaving } from './access-policy.selectors';
+import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
+import { TenantEntity } from '../../../../state/shared';
+import { AddTenantToPolicyDialog } from '../../ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component';
+import { AddTenantsToPolicyRequest } from './index';
+import { ComponentAccessPolicies } from '../../ui/component-access-policies/component-access-policies.component';
+import { selectUserGroups, selectUsers } from '../tenants/tenants.selectors';
+
+@Injectable()
+export class AccessPolicyEffects {
+ constructor(
+ private actions$: Actions,
+ private store: Store,
+ private router: Router,
+ private accessPoliciesService: AccessPolicyService,
+ private dialog: MatDialog
+ ) {}
+
+ setAccessPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.setAccessPolicy),
+ map((action) => action.request),
+ switchMap((request) =>
+ of(
+ AccessPolicyActions.loadAccessPolicy({
+ request: {
+ resourceAction: request.resourceAction
+ }
+ })
+ )
+ )
+ )
+ );
+
+ reloadAccessPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.reloadAccessPolicy),
+ withLatestFrom(this.store.select(selectResourceAction)),
+ filter(([action, resourceAction]) => resourceAction != null),
+ switchMap(([action, resourceAction]) => {
+ return of(
+ AccessPolicyActions.loadAccessPolicy({
+ request: {
+ // @ts-ignore
+ resourceAction
+ }
+ })
+ );
+ })
+ )
+ );
+
+ loadAccessPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.loadAccessPolicy),
+ map((action) => action.request),
+ switchMap((request) =>
+ from(this.accessPoliciesService.getAccessPolicy(request.resourceAction)).pipe(
+ map((response) => {
+ const accessPolicy: AccessPolicyEntity = response;
+
+ let requestedResource: string = `/${request.resourceAction.resource}`;
+ if (request.resourceAction.resourceIdentifier) {
+ requestedResource += `/${request.resourceAction.resourceIdentifier}`;
+ }
+
+ let policyStatus: PolicyStatus | undefined;
+ if (accessPolicy.component.resource === requestedResource) {
+ policyStatus = PolicyStatus.Found;
+ } else {
+ policyStatus = PolicyStatus.Inherited;
+ }
+
+ return AccessPolicyActions.loadAccessPolicySuccess({
+ response: {
+ accessPolicy,
+ policyStatus
+ }
+ });
+ }),
+ catchError((error) => {
+ let policyStatus: PolicyStatus | undefined;
+ if (error.status === 404) {
+ policyStatus = PolicyStatus.NotFound;
+ } else if (error.status === 403) {
+ policyStatus = PolicyStatus.Forbidden;
+ }
+
+ if (policyStatus) {
+ return of(
+ AccessPolicyActions.resetAccessPolicy({
+ response: {
+ policyStatus
+ }
+ })
+ );
+ } else {
+ return of(
+ AccessPolicyActions.accessPolicyApiError({
+ response: {
+ error: error.error
+ }
+ })
+ );
+ }
+ })
+ )
+ )
+ )
+ );
+
+ createAccessPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.createAccessPolicy),
+ withLatestFrom(this.store.select(selectResourceAction)),
+ filter(([action, resourceAction]) => resourceAction != null),
+ switchMap(([action, resourceAction]) =>
+ // @ts-ignore
+ from(this.accessPoliciesService.createAccessPolicy(resourceAction)).pipe(
+ map((response) => {
+ const accessPolicy: AccessPolicyEntity = response;
+ const policyStatus: PolicyStatus = PolicyStatus.Found;
+
+ return AccessPolicyActions.createAccessPolicySuccess({
+ response: {
+ accessPolicy,
+ policyStatus
+ }
+ });
+ }),
+ catchError((error) =>
+ of(
+ AccessPolicyActions.accessPolicyApiError({
+ response: {
+ error: error.error
+ }
+ })
+ )
+ )
+ )
+ )
+ )
+ );
+
+ selectGlobalAccessPolicy$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.selectGlobalAccessPolicy),
+ map((action) => action.request),
+ tap((request) => {
+ const resourceAction: ResourceAction = request.resourceAction;
+ const commands: string[] = [
+ '/access-policies',
+ 'global',
+ resourceAction.action,
+ resourceAction.resource
+ ];
+ if (resourceAction.resourceIdentifier) {
+ commands.push(resourceAction.resourceIdentifier);
+ }
+
+ this.router.navigate(commands);
+ })
+ ),
+ { dispatch: false }
+ );
+
+ selectComponentAccessPolicy$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.selectComponentAccessPolicy),
+ map((action) => action.request),
+ tap((request) => {
+ const resourceAction: ComponentResourceAction = request.resourceAction;
+ const commands: string[] = [
+ '/access-policies',
+ resourceAction.action,
+ resourceAction.policy,
+ resourceAction.resource,
+ resourceAction.resourceIdentifier
+ ];
+ this.router.navigate(commands);
+ })
+ ),
+ { dispatch: false }
+ );
+
+ openAddTenantToPolicyDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.openAddTenantToPolicyDialog),
+ withLatestFrom(this.store.select(selectAccessPolicy)),
+ tap(([action, accessPolicy]) => {
+ const dialogReference = this.dialog.open(AddTenantToPolicyDialog, {
+ data: {
+ accessPolicy
+ },
+ panelClass: 'medium-dialog'
+ });
+
+ dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
+ dialogReference.componentInstance.users$ = this.store.select(selectUsers);
+ dialogReference.componentInstance.userGroups$ = this.store.select(selectUserGroups);
+
+ dialogReference.componentInstance.addTenants
+ .pipe(take(1))
+ .subscribe((request: AddTenantsToPolicyRequest) => {
+ this.store.dispatch(AccessPolicyActions.addTenantsToPolicy({ request }));
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ addTenantsToPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.addTenantsToPolicy),
+ map((action) => action.request),
+ withLatestFrom(this.store.select(selectAccessPolicy)),
+ filter(([request, accessPolicyEntity]) => accessPolicyEntity != null),
+ switchMap(([request, accessPolicyEntity]) => {
+ // @ts-ignore
+ const accessPolicy: AccessPolicyEntity = accessPolicyEntity;
+
+ const users: TenantEntity[] = [...accessPolicy.component.users, ...request.users];
+ const userGroups: TenantEntity[] = [...accessPolicy.component.userGroups, ...request.userGroups];
+
+ return from(this.accessPoliciesService.updateAccessPolicy(accessPolicy, users, userGroups)).pipe(
+ map((response: any) => {
+ this.dialog.closeAll();
+
+ return AccessPolicyActions.loadAccessPolicySuccess({
+ response: {
+ accessPolicy: response,
+ policyStatus: PolicyStatus.Found
+ }
+ });
+ }),
+ catchError((error) =>
+ of(
+ AccessPolicyActions.accessPolicyApiError({
+ response: {
+ error: error.error
+ }
+ })
+ )
+ )
+ );
+ })
+ )
+ );
+
+ promptRemoveTenantFromPolicy$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.promptRemoveTenantFromPolicy),
+ map((action) => action.request),
+ tap((request) => {
+ const dialogReference = this.dialog.open(YesNoDialog, {
+ data: {
+ title: 'Update Policy',
+ message: `Remove '${request.tenant.component.identity}' from this policy?`
+ },
+ panelClass: 'small-dialog'
+ });
+
+ dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
+ this.store.dispatch(AccessPolicyActions.removeTenantFromPolicy({ request }));
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ removeTenantFromPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.removeTenantFromPolicy),
+ map((action) => action.request),
+ withLatestFrom(this.store.select(selectAccessPolicy)),
+ filter(([request, accessPolicyEntity]) => accessPolicyEntity != null),
+ switchMap(([request, accessPolicyEntity]) => {
+ // @ts-ignore
+ const accessPolicy: AccessPolicyEntity = accessPolicyEntity;
+
+ const users: TenantEntity[] = [...accessPolicy.component.users];
+ const userGroups: TenantEntity[] = [...accessPolicy.component.userGroups];
+
+ let tenants: TenantEntity[];
+ if (request.tenantType === 'user') {
+ tenants = users;
+ } else {
+ tenants = userGroups;
+ }
+
+ if (tenants) {
+ const tenantIndex: number = tenants.findIndex(
+ (tenant: TenantEntity) => request.tenant.id === tenant.id
+ );
+ if (tenantIndex > -1) {
+ tenants.splice(tenantIndex, 1);
+ }
+ }
+
+ return from(this.accessPoliciesService.updateAccessPolicy(accessPolicy, users, userGroups)).pipe(
+ map((response: any) => {
+ return AccessPolicyActions.loadAccessPolicySuccess({
+ response: {
+ accessPolicy: response,
+ policyStatus: PolicyStatus.Found
+ }
+ });
+ }),
+ catchError((error) =>
+ of(
+ AccessPolicyActions.accessPolicyApiError({
+ response: {
+ error: error.error
+ }
+ })
+ )
+ )
+ );
+ })
+ )
+ );
+
+ promptDeleteAccessPolicy$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.promptDeleteAccessPolicy),
+ tap((request) => {
+ const dialogReference = this.dialog.open(YesNoDialog, {
+ data: {
+ title: 'Delete Policy',
+ message:
+ 'Are you sure you want to delete this policy? By doing so, the permissions for this component will revert to the inherited policy if applicable.'
+ },
+ panelClass: 'small-dialog'
+ });
+
+ dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
+ this.store.dispatch(AccessPolicyActions.deleteAccessPolicy());
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ deleteAccessPolicy$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessPolicyActions.deleteAccessPolicy),
+ withLatestFrom(this.store.select(selectResourceAction), this.store.select(selectAccessPolicy)),
+ filter(([action, resourceAction, accessPolicy]) => resourceAction != null && accessPolicy != null),
+ switchMap(([action, resourceAction, accessPolicy]) =>
+ // @ts-ignore
+ from(this.accessPoliciesService.deleteAccessPolicy(accessPolicy)).pipe(
+ map((response) => {
+ // the policy was removed, we need to reload the policy for this resource and action to fetch
+ // the inherited policy or correctly when it's not found
+ return AccessPolicyActions.loadAccessPolicy({
+ request: {
+ // @ts-ignore
+ resourceAction
+ }
+ });
+ }),
+ catchError((error) =>
+ of(
+ AccessPolicyActions.accessPolicyApiError({
+ response: {
+ error: error.error
+ }
+ })
+ )
+ )
+ )
+ )
+ )
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.reducer.ts
new file mode 100644
index 0000000000..0f2300b13d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.reducer.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AccessPolicyState } from './index';
+import { createReducer, on } from '@ngrx/store';
+import {
+ addTenantsToPolicy,
+ createAccessPolicySuccess,
+ accessPolicyApiError,
+ loadAccessPolicy,
+ loadAccessPolicySuccess,
+ removeTenantFromPolicy,
+ resetAccessPolicyState,
+ resetAccessPolicy,
+ setAccessPolicy
+} from './access-policy.actions';
+
+export const initialState: AccessPolicyState = {
+ saving: false,
+ loadedTimestamp: '',
+ error: null,
+ status: 'pending'
+};
+
+export const accessPolicyReducer = createReducer(
+ initialState,
+ on(setAccessPolicy, (state, { request }) => ({
+ ...state,
+ resourceAction: request.resourceAction
+ })),
+ on(loadAccessPolicy, (state, { request }) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(loadAccessPolicySuccess, createAccessPolicySuccess, (state, { response }) => ({
+ ...state,
+ accessPolicy: response.accessPolicy,
+ policyStatus: response.policyStatus,
+ loadedTimestamp: response.accessPolicy.generated,
+ saving: false,
+ status: 'success' as const
+ })),
+ on(addTenantsToPolicy, (state, { request }) => ({
+ ...state,
+ saving: true
+ })),
+ on(removeTenantFromPolicy, (state, { request }) => ({
+ ...state,
+ saving: true
+ })),
+ on(resetAccessPolicy, (state, { response }) => ({
+ ...state,
+ accessPolicy: undefined,
+ policyStatus: response.policyStatus,
+ loadedTimestamp: 'N/A',
+ status: 'success' as const
+ })),
+ on(accessPolicyApiError, (state, { response }) => ({
+ ...state,
+ error: response.error,
+ accessPolicy: undefined,
+ policyStatus: undefined,
+ status: 'error' as const
+ })),
+ on(resetAccessPolicyState, (state) => ({
+ ...initialState
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.selectors.ts
new file mode 100644
index 0000000000..37fa1d6b35
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/access-policy.selectors.ts
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createSelector } from '@ngrx/store';
+import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
+import { accessPolicyFeatureKey, AccessPolicyState } from './index';
+import { selectCurrentRoute } from '../../../../state/router/router.selectors';
+import { ComponentResourceAction, ResourceAction } from '../shared';
+
+export const selectAccessPolicyState = createSelector(
+ selectAccessPoliciesState,
+ (state: AccessPoliciesState) => state[accessPolicyFeatureKey]
+);
+
+export const selectResourceAction = createSelector(
+ selectAccessPolicyState,
+ (state: AccessPolicyState) => state.resourceAction
+);
+
+export const selectAccessPolicy = createSelector(
+ selectAccessPolicyState,
+ (state: AccessPolicyState) => state.accessPolicy
+);
+
+export const selectSaving = createSelector(selectAccessPolicyState, (state: AccessPolicyState) => state.saving);
+
+export const selectGlobalResourceActionFromRoute = createSelector(selectCurrentRoute, (route) => {
+ let selectedResourceAction: ResourceAction | null = null;
+ if (route?.params.action && route?.params.resource) {
+ // always select the action and resource from the route
+ selectedResourceAction = {
+ action: route.params.action,
+ resource: route.params.resource,
+ resourceIdentifier: route.params.resourceIdentifier
+ };
+ }
+ return selectedResourceAction;
+});
+
+export const selectComponentResourceActionFromRoute = createSelector(selectCurrentRoute, (route) => {
+ let selectedResourceAction: ComponentResourceAction | null = null;
+ if (route?.params.action && route?.params.policy && route?.params.resource && route?.params.resourceIdentifier) {
+ // always select the action and resource from the route
+ selectedResourceAction = {
+ action: route.params.action,
+ policy: route.params.policy,
+ resource: route.params.resource,
+ resourceIdentifier: route.params.resourceIdentifier
+ };
+ }
+ return selectedResourceAction;
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/index.ts
new file mode 100644
index 0000000000..d9b0a4fa53
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/access-policy/index.ts
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AccessPolicyEntity, ComponentResourceAction, PolicyStatus, ResourceAction } from '../shared';
+import { TenantEntity, UserEntity, UserGroupEntity } from '../../../../state/shared';
+
+export const accessPolicyFeatureKey = 'accessPolicy';
+
+export interface SetAccessPolicyRequest {
+ resourceAction: ResourceAction;
+}
+
+export interface SelectGlobalAccessPolicyRequest {
+ resourceAction: ResourceAction;
+}
+
+export interface SelectComponentAccessPolicyRequest {
+ resourceAction: ComponentResourceAction;
+}
+
+export interface LoadAccessPolicyRequest {
+ resourceAction: ResourceAction;
+}
+
+export interface AccessPolicyResponse {
+ accessPolicy: AccessPolicyEntity;
+ policyStatus?: PolicyStatus;
+}
+
+export interface ResetAccessPolicy {
+ policyStatus: PolicyStatus;
+}
+
+export interface LoadAccessPolicyError {
+ error: string;
+}
+
+export interface RemoveTenantFromPolicyRequest {
+ tenantType: 'user' | 'userGroup';
+ tenant: TenantEntity;
+}
+
+export interface AddTenantToPolicyDialogRequest {
+ accessPolicy: AccessPolicyEntity;
+}
+
+export interface AddTenantsToPolicyRequest {
+ users: TenantEntity[];
+ userGroups: TenantEntity[];
+}
+
+export interface AccessPolicyState {
+ resourceAction?: ResourceAction;
+ policyStatus?: PolicyStatus;
+ accessPolicy?: AccessPolicyEntity;
+ saving: boolean;
+ loadedTimestamp: string;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/index.ts
new file mode 100644
index 0000000000..746fe2c4ae
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
+import { accessPolicyFeatureKey, AccessPolicyState } from './access-policy';
+import { accessPolicyReducer } from './access-policy/access-policy.reducer';
+import { tenantsFeatureKey, TenantsState } from './tenants';
+import { tenantsReducer } from './tenants/tenants.reducer';
+import { policyComponentFeatureKey, PolicyComponentState } from './policy-component';
+import { policyComponentReducer } from './policy-component/policy-component.reducer';
+
+export const accessPoliciesFeatureKey = 'accessPolicies';
+
+export interface AccessPoliciesState {
+ [accessPolicyFeatureKey]: AccessPolicyState;
+ [tenantsFeatureKey]: TenantsState;
+ [policyComponentFeatureKey]: PolicyComponentState;
+}
+
+export function reducers(state: AccessPoliciesState | undefined, action: Action) {
+ return combineReducers({
+ [accessPolicyFeatureKey]: accessPolicyReducer,
+ [tenantsFeatureKey]: tenantsReducer,
+ [policyComponentFeatureKey]: policyComponentReducer
+ })(state, action);
+}
+
+export const selectAccessPoliciesState = createFeatureSelector(accessPoliciesFeatureKey);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/index.ts
new file mode 100644
index 0000000000..5579d16880
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/index.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentResourceAction } from '../shared';
+
+export const policyComponentFeatureKey = 'policyComponent';
+
+export interface LoadPolicyComponentRequest {
+ componentResourceAction: ComponentResourceAction;
+}
+
+export interface LoadPolicyComponentSuccess {
+ label: string;
+ resource: string;
+ allowRemoteAccess: boolean;
+}
+
+export interface PolicyComponentState {
+ label: string;
+ allowRemoteAccess: boolean;
+ resource: string;
+ loadedTimestamp: string;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.actions.ts
new file mode 100644
index 0000000000..e046d2cc9e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.actions.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createAction, props } from '@ngrx/store';
+import { LoadPolicyComponentRequest, LoadPolicyComponentSuccess } from './index';
+
+const POLICY_COMPONENT_PREFIX: string = '[Policy Component]';
+
+export const loadPolicyComponent = createAction(
+ `${POLICY_COMPONENT_PREFIX} Load Policy Component`,
+ props<{ request: LoadPolicyComponentRequest }>()
+);
+
+export const loadPolicyComponentSuccess = createAction(
+ `${POLICY_COMPONENT_PREFIX} Load Policy Component Success`,
+ props<{ response: LoadPolicyComponentSuccess }>()
+);
+
+export const policyComponentApiError = createAction(
+ `${POLICY_COMPONENT_PREFIX} Policy Component Api Error`,
+ props<{ error: string }>()
+);
+
+export const resetPolicyComponentState = createAction(`${POLICY_COMPONENT_PREFIX} Reset Policy Component State`);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.effects.ts
new file mode 100644
index 0000000000..baa50398bd
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.effects.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as PolicyComponentActions from './policy-component.actions';
+import { catchError, from, map, of, switchMap } from 'rxjs';
+import { AccessPolicyService } from '../../service/access-policy.service';
+
+@Injectable()
+export class PolicyComponentEffects {
+ constructor(
+ private actions$: Actions,
+ private accessPoliciesService: AccessPolicyService
+ ) {}
+
+ loadPolicyComponent$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(PolicyComponentActions.loadPolicyComponent),
+ map((action) => action.request),
+ switchMap((request) =>
+ from(this.accessPoliciesService.getPolicyComponent(request.componentResourceAction)).pipe(
+ map((response) =>
+ PolicyComponentActions.loadPolicyComponentSuccess({
+ response: {
+ label: response.component.name,
+ resource: request.componentResourceAction.resource,
+ allowRemoteAccess:
+ request.componentResourceAction.resource === 'input-ports' ||
+ request.componentResourceAction.resource === 'output-ports'
+ ? response.allowRemoteAccess
+ : false
+ }
+ })
+ ),
+ catchError((error) => {
+ if (error.status === 403) {
+ return of(
+ PolicyComponentActions.loadPolicyComponentSuccess({
+ response: {
+ label: request.componentResourceAction.resourceIdentifier,
+ resource: request.componentResourceAction.resource,
+ allowRemoteAccess: false
+ }
+ })
+ );
+ } else {
+ return of(
+ PolicyComponentActions.policyComponentApiError({
+ error: error.error
+ })
+ );
+ }
+ })
+ )
+ )
+ )
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.reducer.ts
new file mode 100644
index 0000000000..024a4ac0bd
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.reducer.ts
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { PolicyComponentState } from './index';
+import { createReducer, on } from '@ngrx/store';
+import {
+ loadPolicyComponent,
+ loadPolicyComponentSuccess,
+ resetPolicyComponentState,
+ policyComponentApiError
+} from './policy-component.actions';
+
+export const initialState: PolicyComponentState = {
+ label: '',
+ resource: '',
+ allowRemoteAccess: false,
+ loadedTimestamp: '',
+ error: null,
+ status: 'pending'
+};
+
+export const policyComponentReducer = createReducer(
+ initialState,
+ on(loadPolicyComponent, (state) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(loadPolicyComponentSuccess, (state, { response }) => ({
+ ...state,
+ label: response.label,
+ resource: response.resource,
+ allowRemoteAccess: response.allowRemoteAccess,
+ status: 'success' as const
+ })),
+ on(policyComponentApiError, (state, { error }) => ({
+ ...state,
+ error: error,
+ status: 'error' as const
+ })),
+ on(resetPolicyComponentState, (state) => ({
+ ...initialState
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.selectors.ts
new file mode 100644
index 0000000000..4a9d7d6bc9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/policy-component/policy-component.selectors.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createSelector } from '@ngrx/store';
+import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
+import { policyComponentFeatureKey } from './index';
+
+export const selectPolicyComponentState = createSelector(
+ selectAccessPoliciesState,
+ (state: AccessPoliciesState) => state[policyComponentFeatureKey]
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/shared/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/shared/index.ts
new file mode 100644
index 0000000000..3564ce195e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/shared/index.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AccessPolicySummary, Permissions, Revision, TenantEntity } from '../../../../state/shared';
+
+export enum PolicyStatus {
+ Found = 'Found',
+ Inherited = 'Inherited',
+ NotFound = 'NotFound',
+ Forbidden = 'Forbidden'
+}
+
+export enum Action {
+ Read = 'read',
+ Write = 'write'
+}
+
+export interface ResourceAction {
+ resource: string;
+ resourceIdentifier?: string;
+ action: Action;
+}
+
+export interface ComponentResourceAction extends ResourceAction {
+ resourceIdentifier: string;
+ policy: string;
+}
+
+export interface AccessPolicyEntity {
+ id: string;
+ component: AccessPolicy;
+ revision: Revision;
+ uri: string;
+ permissions: Permissions;
+ generated: string;
+}
+
+export interface AccessPolicy extends AccessPolicySummary {
+ users: TenantEntity[];
+ userGroups: TenantEntity[];
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/index.ts
new file mode 100644
index 0000000000..abfa1b8ff7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/index.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { UserEntity, UserGroupEntity } from '../../../../state/shared';
+
+export const tenantsFeatureKey = 'tenants';
+
+export interface LoadTenantsSuccess {
+ users: UserEntity[];
+ userGroups: UserGroupEntity[];
+}
+
+export interface TenantsState {
+ users: UserEntity[];
+ userGroups: UserGroupEntity[];
+ loadedTimestamp: string;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.actions.ts
new file mode 100644
index 0000000000..573c88ba75
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.actions.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createAction, props } from '@ngrx/store';
+import { LoadTenantsSuccess } from './index';
+
+const TENANTS_PREFIX: string = '[Tenants]';
+
+export const loadTenants = createAction(`${TENANTS_PREFIX} Load Tenants`);
+
+export const loadTenantsSuccess = createAction(
+ `${TENANTS_PREFIX} Load Tenants Success`,
+ props<{ response: LoadTenantsSuccess }>()
+);
+
+export const tenantsApiError = createAction(`${TENANTS_PREFIX} Tenants Api Error`, props<{ error: string }>());
+
+export const resetTenantsState = createAction(`${TENANTS_PREFIX} Reset Tenants State`);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.effects.ts
new file mode 100644
index 0000000000..775cfc892f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.effects.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as TenantsActions from './tenants.actions';
+import { catchError, combineLatest, filter, from, map, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
+import { AccessPolicyService } from '../../service/access-policy.service';
+
+@Injectable()
+export class TenantsEffects {
+ constructor(
+ private actions$: Actions,
+ private accessPoliciesService: AccessPolicyService
+ ) {}
+
+ loadTenants$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(TenantsActions.loadTenants),
+ switchMap(() =>
+ combineLatest([this.accessPoliciesService.getUsers(), this.accessPoliciesService.getUserGroups()]).pipe(
+ map(([usersResponse, userGroupsResponse]) =>
+ TenantsActions.loadTenantsSuccess({
+ response: {
+ users: usersResponse.users,
+ userGroups: userGroupsResponse.userGroups
+ }
+ })
+ ),
+ catchError((error) =>
+ of(
+ TenantsActions.tenantsApiError({
+ error: error.error
+ })
+ )
+ )
+ )
+ )
+ )
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.reducer.ts
new file mode 100644
index 0000000000..a971604af7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.reducer.ts
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TenantsState } from './index';
+import { createReducer, on } from '@ngrx/store';
+import { loadTenants, loadTenantsSuccess, resetTenantsState, tenantsApiError } from './tenants.actions';
+
+export const initialState: TenantsState = {
+ users: [],
+ userGroups: [],
+ loadedTimestamp: '',
+ error: null,
+ status: 'pending'
+};
+
+export const tenantsReducer = createReducer(
+ initialState,
+ on(loadTenants, (state) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(loadTenantsSuccess, (state, { response }) => ({
+ ...state,
+ users: response.users,
+ userGroups: response.userGroups,
+ status: 'success' as const
+ })),
+ on(tenantsApiError, (state, { error }) => ({
+ ...state,
+ error: error,
+ status: 'error' as const
+ })),
+ on(resetTenantsState, (state) => ({
+ ...initialState
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.selectors.ts
new file mode 100644
index 0000000000..97b75f0a02
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/state/tenants/tenants.selectors.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createSelector } from '@ngrx/store';
+import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
+import { tenantsFeatureKey, TenantsState } from './index';
+
+export const selectTenants = createSelector(
+ selectAccessPoliciesState,
+ (state: AccessPoliciesState) => state[tenantsFeatureKey]
+);
+
+export const selectUsers = createSelector(selectTenants, (state: TenantsState) => state.users);
+
+export const selectUserGroups = createSelector(selectTenants, (state: TenantsState) => state.userGroups);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.html
new file mode 100644
index 0000000000..ca63632e8d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.html
@@ -0,0 +1,54 @@
+
+
+
Add Users/Groups To Policy
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.scss
new file mode 100644
index 0000000000..d4bb96e3f2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.scss
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@use '@angular/material' as mat;
+
+.add-tenant-to-policy-form {
+ @include mat.button-density(-1);
+
+ .fa {
+ color: #004849;
+ }
+
+ mat-selection-list {
+ max-height: 250px;
+ overflow: auto;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.spec.ts
new file mode 100644
index 0000000000..e9ade3e655
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.spec.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AddTenantToPolicyDialog } from './add-tenant-to-policy-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { AddTenantToPolicyDialogRequest } from '../../../state/access-policy';
+
+describe('AddTenantToPolicyDialog', () => {
+ let component: AddTenantToPolicyDialog;
+ let fixture: ComponentFixture;
+
+ const data: AddTenantToPolicyDialogRequest = {
+ accessPolicy: {
+ revision: {
+ clientId: '311032c3-f210-42f9-8a31-862c88b5fbd4',
+ version: 4
+ },
+ id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
+ uri: 'https://localhost:4200/nifi-api/policies/f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
+ permissions: {
+ canRead: true,
+ canWrite: true
+ },
+ generated: '15:48:06 EST',
+ component: {
+ id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
+ resource: '/flow',
+ action: 'read',
+ configurable: true,
+ users: [
+ {
+ revision: {
+ version: 0
+ },
+ id: 'bc646be3-146f-3cf2-bfd6-3a9f687ee7ab',
+ permissions: {
+ canRead: true,
+ canWrite: true
+ },
+ component: {
+ id: 'bc646be3-146f-3cf2-bfd6-3a9f687ee7ab',
+ identity: 'identify',
+ configurable: false
+ }
+ }
+ ],
+ userGroups: []
+ }
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [AddTenantToPolicyDialog, BrowserAnimationsModule],
+ providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
+ });
+ fixture = TestBed.createComponent(AddTenantToPolicyDialog);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.ts
new file mode 100644
index 0000000000..0ee1c126b7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component.ts
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, DestroyRef, EventEmitter, inject, Inject, Input, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { AccessPolicy } from '../../../state/shared';
+import { MatButtonModule } from '@angular/material/button';
+import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatRadioModule } from '@angular/material/radio';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
+import { Observable } from 'rxjs';
+import { MatListModule } from '@angular/material/list';
+import { TenantEntity, UserEntity, UserGroupEntity } from '../../../../../state/shared';
+import { AddTenantsToPolicyRequest, AddTenantToPolicyDialogRequest } from '../../../state/access-policy';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
+
+@Component({
+ selector: 'add-tenant-to-policy-dialog',
+ standalone: true,
+ imports: [
+ MatDialogModule,
+ MatButtonModule,
+ FormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ ReactiveFormsModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ NgIf,
+ AsyncPipe,
+ MatListModule,
+ NgForOf,
+ NifiSpinnerDirective
+ ],
+ templateUrl: './add-tenant-to-policy-dialog.component.html',
+ styleUrls: ['./add-tenant-to-policy-dialog.component.scss']
+})
+export class AddTenantToPolicyDialog {
+ @Input() set users$(users$: Observable) {
+ users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users: UserEntity[]) => {
+ const policy: AccessPolicy = this.request.accessPolicy.component;
+
+ this.filteredUsers = users.filter((user: UserEntity) => {
+ return !policy.users.some((tenant: TenantEntity) => tenant.id === user.id);
+ });
+
+ this.userLookup.clear();
+ this.filteredUsers.forEach((user: UserEntity) => {
+ this.userLookup.set(user.id, user);
+ });
+ });
+ }
+
+ @Input() set userGroups$(userGroups$: Observable) {
+ userGroups$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((userGroups: UserGroupEntity[]) => {
+ const policy: AccessPolicy = this.request.accessPolicy.component;
+
+ this.filteredUserGroups = userGroups.filter((userGroup: UserGroupEntity) => {
+ return !policy.userGroups.some((tenant: TenantEntity) => tenant.id === userGroup.id);
+ });
+
+ this.userGroupLookup.clear();
+ this.filteredUserGroups.forEach((user: UserGroupEntity) => {
+ this.userGroupLookup.set(user.id, user);
+ });
+ });
+ }
+
+ @Input() saving$!: Observable;
+
+ @Output() addTenants: EventEmitter = new EventEmitter();
+
+ private destroyRef = inject(DestroyRef);
+
+ addTenantsForm: FormGroup;
+ filteredUsers: UserEntity[] = [];
+ filteredUserGroups: UserGroupEntity[] = [];
+
+ userLookup: Map = new Map();
+ userGroupLookup: Map = new Map();
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) private request: AddTenantToPolicyDialogRequest,
+ private formBuilder: FormBuilder
+ ) {
+ this.addTenantsForm = this.formBuilder.group({
+ users: new FormControl([]),
+ userGroups: new FormControl([])
+ });
+ }
+
+ addClicked(): void {
+ const users: TenantEntity[] = [];
+
+ const usersSelected: string[] = this.addTenantsForm.get('users')?.value;
+ usersSelected.forEach((userId: string) => {
+ const user: UserEntity | undefined = this.userLookup.get(userId);
+ if (user) {
+ users.push({
+ id: user.id,
+ revision: user.revision,
+ permissions: user.permissions,
+ component: {
+ id: user.component.id,
+ identity: user.component.identity,
+ configurable: user.component.configurable
+ }
+ });
+ }
+ });
+
+ const userGroups: TenantEntity[] = [];
+ const userGroupsSelected: string[] = this.addTenantsForm.get('userGroups')?.value;
+ userGroupsSelected.forEach((userGroupId: string) => {
+ const userGroup: UserGroupEntity | undefined = this.userGroupLookup.get(userGroupId);
+ if (userGroup) {
+ userGroups.push({
+ id: userGroup.id,
+ revision: userGroup.revision,
+ permissions: userGroup.permissions,
+ component: {
+ id: userGroup.component.id,
+ identity: userGroup.component.identity,
+ configurable: userGroup.component.configurable
+ }
+ });
+ }
+ });
+
+ this.addTenants.next({
+ users,
+ userGroups
+ });
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.html
new file mode 100644
index 0000000000..357defd7a1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
User
+
+ {{ item.user }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.scss
new file mode 100644
index 0000000000..4460252842
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.scss
@@ -0,0 +1,24 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.policy-table {
+ .listing-table {
+ .mat-column-actions {
+ width: 75px;
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.spec.ts
new file mode 100644
index 0000000000..1fd95e1a67
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.spec.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PolicyTable } from './policy-table.component';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { MatInputModule } from '@angular/material/input';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatSelectModule } from '@angular/material/select';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { CurrentUser } from '../../../../../state/current-user';
+
+describe('PolicyTable', () => {
+ let component: PolicyTable;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [],
+ imports: [
+ PolicyTable,
+ MatTableModule,
+ MatSortModule,
+ MatInputModule,
+ ReactiveFormsModule,
+ MatSelectModule,
+ NoopAnimationsModule
+ ]
+ });
+ fixture = TestBed.createComponent(PolicyTable);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.ts
new file mode 100644
index 0000000000..5b5880f4ef
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/common/policy-table/policy-table.component.ts
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { CurrentUser } from '../../../../../state/current-user';
+import { TenantEntity, UserEntity } from '../../../../../state/shared';
+import { NgIf } from '@angular/common';
+import { AccessPolicyEntity } from '../../../state/shared';
+import { RemoveTenantFromPolicyRequest } from '../../../state/access-policy';
+
+export interface TenantItem {
+ id: string;
+ user: string;
+ tenantType: 'user' | 'userGroup';
+ configurable: boolean;
+}
+
+@Component({
+ selector: 'policy-table',
+ standalone: true,
+ templateUrl: './policy-table.component.html',
+ imports: [MatTableModule, MatSortModule, NgIf],
+ styleUrls: ['./policy-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
+})
+export class PolicyTable {
+ displayedColumns: string[] = ['user', 'actions'];
+ dataSource: MatTableDataSource = new MatTableDataSource();
+
+ tenantLookup: Map = new Map();
+
+ @Input() set policy(policy: AccessPolicyEntity | undefined) {
+ const tenantItems: TenantItem[] = [];
+
+ if (policy) {
+ policy.component.users.forEach((user) => {
+ this.tenantLookup.set(user.id, user);
+ tenantItems.push({
+ id: user.id,
+ tenantType: 'user',
+ user: user.component.identity,
+ configurable: user.component.configurable
+ });
+ });
+ policy.component.userGroups.forEach((userGroup) => {
+ this.tenantLookup.set(userGroup.id, userGroup);
+ tenantItems.push({
+ id: userGroup.id,
+ tenantType: 'userGroup',
+ user: userGroup.component.identity,
+ configurable: userGroup.component.configurable
+ });
+ });
+ }
+
+ this.dataSource.data = this.sortUsers(tenantItems, this.sort);
+ this._policy = policy;
+ }
+
+ @Input() supportsPolicyModification!: boolean;
+
+ @Output() removeTenantFromPolicy: EventEmitter =
+ new EventEmitter();
+
+ private _policy: AccessPolicyEntity | undefined;
+ selectedTenantId: string | null = null;
+
+ sort: Sort = {
+ active: 'user',
+ direction: 'asc'
+ };
+
+ constructor(private nifiCommon: NiFiCommon) {}
+
+ updateSort(sort: Sort): void {
+ this.sort = sort;
+ this.dataSource.data = this.sortUsers(this.dataSource.data, sort);
+ }
+
+ sortUsers(items: TenantItem[], sort: Sort): TenantItem[] {
+ const data: TenantItem[] = items.slice();
+ return data.sort((a, b) => {
+ const isAsc = sort.direction === 'asc';
+
+ let retVal: number = 0;
+ switch (sort.active) {
+ case 'user':
+ retVal = this.nifiCommon.compareString(a.user, b.user);
+ break;
+ }
+
+ return retVal * (isAsc ? 1 : -1);
+ });
+ }
+
+ select(item: TenantItem): void {
+ this.selectedTenantId = item.id;
+ }
+
+ isSelected(item: TenantItem): boolean {
+ if (this.selectedTenantId) {
+ return item.id == this.selectedTenantId;
+ }
+ return false;
+ }
+
+ canRemove(): boolean {
+ if (this._policy) {
+ return (
+ this.supportsPolicyModification &&
+ this._policy.permissions.canWrite &&
+ this._policy.component.configurable
+ );
+ }
+ return false;
+ }
+
+ removeClicked(item: TenantItem): void {
+ const tenant: TenantEntity | undefined = this.tenantLookup.get(item.id);
+ if (tenant) {
+ this.removeTenantFromPolicy.next({
+ tenantType: item.tenantType,
+ tenant
+ });
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies-routing.module.ts
new file mode 100644
index 0000000000..ffe6845ba2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies-routing.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ComponentAccessPolicies } from './component-access-policies.component';
+import { authorizationGuard } from '../../../../service/guard/authorization.guard';
+import { CurrentUser } from '../../../../state/current-user';
+
+const routes: Routes = [
+ {
+ path: ':action/:policy/:resource/:resourceIdentifier',
+ canMatch: [authorizationGuard((user: CurrentUser) => user.tenantsPermissions.canRead)],
+ component: ComponentAccessPolicies
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ComponentAccessPoliciesRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.html
new file mode 100644
index 0000000000..f97374c1cb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.html
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No policy for the specified resource.
+
+ Create a new policy.
+
+
+
+
+
+
+
+
+ Not authorized to access the policy for the specified resource.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Last updated:
+
{{ accessPolicyState.loadedTimestamp }}
+
+
+
+
+
+
+
+ No component specific administrators.
+
+ Add policy for additional administrators.
+
+
+
+ Showing effective policy inherited from the controller.
+
+ Override this policy.
+
+
+
+ Showing effective policy inherited from global parameter context policy.
+
+ Override this policy.
+
+
+
+ Showing effective policy inherited from Process Group.
+
+ Override this policy.
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.scss
new file mode 100644
index 0000000000..7e9a22925f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.scss
@@ -0,0 +1,48 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.component-access-policies {
+ a {
+ font-size: 16px;
+ }
+
+ .policy-select {
+ .mat-mdc-form-field {
+ width: 300px;
+ }
+ }
+
+ .operation-context-logo {
+ .icon {
+ font-size: 36px;
+ color: #ad9897;
+ }
+ }
+
+ .operation-context-name {
+ font-size: 18px;
+ color: #262626;
+ width: 370px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .operation-context-type {
+ color: #728e9b;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.spec.ts
new file mode 100644
index 0000000000..3f6f5f5f84
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentAccessPolicies } from './component-access-policies.component';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../state/access-policy/access-policy.reducer';
+
+describe('ComponentAccessPolicies', () => {
+ let component: ComponentAccessPolicies;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ComponentAccessPolicies],
+ providers: [provideMockStore({ initialState })]
+ });
+ fixture = TestBed.createComponent(ComponentAccessPolicies);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.ts
new file mode 100644
index 0000000000..484d5b928e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.component.ts
@@ -0,0 +1,444 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
+import {
+ createAccessPolicy,
+ openAddTenantToPolicyDialog,
+ promptDeleteAccessPolicy,
+ promptRemoveTenantFromPolicy,
+ reloadAccessPolicy,
+ resetAccessPolicyState,
+ selectComponentAccessPolicy,
+ setAccessPolicy
+} from '../../state/access-policy/access-policy.actions';
+import { AccessPolicyState, RemoveTenantFromPolicyRequest } from '../../state/access-policy';
+import { initialState } from '../../state/access-policy/access-policy.reducer';
+import {
+ selectAccessPolicyState,
+ selectComponentResourceActionFromRoute
+} from '../../state/access-policy/access-policy.selectors';
+import { distinctUntilChanged, filter } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { NiFiCommon } from '../../../../service/nifi-common.service';
+import { ComponentType, SelectOption, TextTipInput } from '../../../../state/shared';
+import { TextTip } from '../../../../ui/common/tooltips/text-tip/text-tip.component';
+import { AccessPolicyEntity, Action, ComponentResourceAction, PolicyStatus, ResourceAction } from '../../state/shared';
+import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.actions';
+import { loadPolicyComponent, resetPolicyComponentState } from '../../state/policy-component/policy-component.actions';
+import { selectPolicyComponentState } from '../../state/policy-component/policy-component.selectors';
+import { PolicyComponentState } from '../../state/policy-component';
+
+@Component({
+ selector: 'global-access-policies',
+ templateUrl: './component-access-policies.component.html',
+ styleUrls: ['./component-access-policies.component.scss']
+})
+export class ComponentAccessPolicies implements OnInit, OnDestroy {
+ flowConfiguration$ = this.store.select(selectFlowConfiguration);
+ accessPolicyState$ = this.store.select(selectAccessPolicyState);
+ policyComponentState$ = this.store.select(selectPolicyComponentState);
+ currentUser$ = this.store.select(selectCurrentUser);
+
+ protected readonly TextTip = TextTip;
+ protected readonly Action = Action;
+ protected readonly PolicyStatus = PolicyStatus;
+ protected readonly ComponentType = ComponentType;
+
+ policyForm: FormGroup;
+ policyActionOptions: SelectOption[] = [
+ {
+ text: 'view the component',
+ value: 'read-component',
+ description: 'Allows users to view component configuration details'
+ },
+ {
+ text: 'modify the component',
+ value: 'write-component',
+ description: 'Allows users to modify component configuration details'
+ },
+ {
+ text: 'operate the component',
+ value: 'write-operation',
+ description:
+ 'Allows users to operate components by changing component run status (start/stop/enable/disable), remote port transmission status, or terminating processor threads'
+ },
+ {
+ text: 'view provenance',
+ value: 'read-provenance-data',
+ description: 'Allows users to view provenance events generated by this component'
+ },
+ {
+ text: 'view the data',
+ value: 'read-data',
+ description:
+ 'Allows users to view metadata and content for this component in flowfile queues in outbound connections and through provenance events'
+ },
+ {
+ text: 'modify the data',
+ value: 'write-data',
+ description:
+ 'Allows users to empty flowfile queues in outbound connections and submit replays through provenance events'
+ },
+ {
+ text: 'receive data via site-to-site',
+ value: 'write-receive-data',
+ description: 'Allows this port to receive data from these NiFi instances',
+ disabled: true
+ },
+ {
+ text: 'send data via site-to-site',
+ value: 'write-send-data',
+ description: 'Allows this port to send data to these NiFi instances',
+ disabled: true
+ },
+ {
+ text: 'view the policies',
+ value: 'read-policies',
+ description: 'Allows users to view the list of users who can view/modify this component'
+ },
+ {
+ text: 'modify the policies',
+ value: 'write-policies',
+ description: 'Allows users to modify the list of users who can view/modify this component'
+ }
+ ];
+
+ action!: Action;
+ resource!: string;
+ policy!: string;
+ resourceIdentifier!: string;
+
+ @ViewChild('inheritedFromPolicies') inheritedFromPolicies!: TemplateRef;
+ @ViewChild('inheritedFromController') inheritedFromController!: TemplateRef;
+ @ViewChild('inheritedFromGlobalParameterContexts') inheritedFromGlobalParameterContexts!: TemplateRef;
+ @ViewChild('inheritedFromProcessGroup') inheritedFromProcessGroup!: TemplateRef;
+
+ constructor(
+ private store: Store,
+ private formBuilder: FormBuilder,
+ private nifiCommon: NiFiCommon
+ ) {
+ this.policyForm = this.formBuilder.group({
+ policyAction: new FormControl(this.policyActionOptions[0].value, Validators.required)
+ });
+
+ this.store
+ .select(selectComponentResourceActionFromRoute)
+ .pipe(
+ filter((resourceAction) => resourceAction != null),
+ distinctUntilChanged((a, b) => {
+ // @ts-ignore
+ const aResourceAction: ComponentResourceAction = a;
+ // @ts-ignore
+ const bResourceAction: ComponentResourceAction = b;
+
+ return (
+ aResourceAction.action == bResourceAction.action &&
+ aResourceAction.policy == bResourceAction.policy &&
+ aResourceAction.resource == bResourceAction.resource &&
+ aResourceAction.resourceIdentifier == bResourceAction.resourceIdentifier
+ );
+ }),
+ takeUntilDestroyed()
+ )
+ .subscribe((componentResourceAction) => {
+ if (componentResourceAction) {
+ this.action = componentResourceAction.action;
+ this.policy = componentResourceAction.policy;
+ this.resource = componentResourceAction.resource;
+ this.resourceIdentifier = componentResourceAction.resourceIdentifier;
+
+ // data transfer policies for site to site are presented different in the form so
+ // we need to distinguish by type
+ let policyForResource: string = this.policy;
+ if (this.policy === 'data-transfer') {
+ if (this.resource === 'input-ports') {
+ policyForResource = 'receive-data';
+ } else {
+ policyForResource = 'send-data';
+ }
+ }
+
+ this.policyForm.get('policyAction')?.setValue(`${this.action}-${policyForResource}`);
+
+ // component policies are presented simply as '/processors/1234' while non-component policies
+ // like viewing provenance for a specific component is presented as `/provenance-data/processors/1234`
+ let resourceToLoad: string = this.resource;
+ if (componentResourceAction.policy !== 'component') {
+ resourceToLoad = `${this.policy}/${this.resource}`;
+ }
+
+ const resourceAction: ResourceAction = {
+ action: this.action,
+ resource: resourceToLoad,
+ resourceIdentifier: this.resourceIdentifier
+ };
+
+ this.store.dispatch(
+ loadPolicyComponent({
+ request: {
+ componentResourceAction
+ }
+ })
+ );
+ this.store.dispatch(
+ setAccessPolicy({
+ request: {
+ resourceAction
+ }
+ })
+ );
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ this.store.dispatch(loadFlowConfiguration());
+ this.store.dispatch(loadTenants());
+ }
+
+ isInitialLoading(state: AccessPolicyState): boolean {
+ return state.loadedTimestamp == initialState.loadedTimestamp;
+ }
+
+ isComponentPolicy(option: SelectOption, policyComponentState: PolicyComponentState): boolean {
+ // consider the type of component to override which policies shouldn't be supported
+
+ if (policyComponentState.resource === 'process-groups') {
+ switch (option.value) {
+ case 'write-send-data':
+ case 'write-receive-data':
+ return false;
+ }
+ } else if (
+ policyComponentState.resource === 'controller-services' ||
+ policyComponentState.resource === 'reporting-tasks'
+ ) {
+ switch (option.value) {
+ case 'read-data':
+ case 'write-data':
+ case 'write-send-data':
+ case 'write-receive-data':
+ case 'read-provenance-data':
+ return false;
+ }
+ } else if (
+ policyComponentState.resource === 'parameter-contexts' ||
+ policyComponentState.resource === 'parameter-providers'
+ ) {
+ switch (option.value) {
+ case 'read-data':
+ case 'write-data':
+ case 'write-send-data':
+ case 'write-receive-data':
+ case 'read-provenance-data':
+ case 'write-operation':
+ return false;
+ }
+ } else if (policyComponentState.resource === 'labels') {
+ switch (option.value) {
+ case 'write-operation':
+ case 'read-data':
+ case 'write-data':
+ case 'write-send-data':
+ case 'write-receive-data':
+ return false;
+ }
+ } else if (policyComponentState.resource === 'input-ports' && policyComponentState.allowRemoteAccess) {
+ // if input ports allow remote access, disable send data. if input ports do not allow remote
+ // access it will fall through to the else block where both send and receive data will be disabled
+ switch (option.value) {
+ case 'write-send-data':
+ return false;
+ }
+ } else if (policyComponentState.resource === 'output-ports' && policyComponentState.allowRemoteAccess) {
+ // if output ports allow remote access, disable receive data. if output ports do not allow remote
+ // access it will fall through to the else block where both send and receive data will be disabled
+ switch (option.value) {
+ case 'write-receive-data':
+ return false;
+ }
+ } else {
+ switch (option.value) {
+ case 'write-send-data':
+ case 'write-receive-data':
+ return false;
+ }
+ }
+
+ // enable all other options
+ return true;
+ }
+
+ getSelectOptionTipData(option: SelectOption): TextTipInput {
+ return {
+ // @ts-ignore
+ text: option.description
+ };
+ }
+
+ getContextIcon(): string {
+ switch (this.resource) {
+ case 'processors':
+ return 'icon-processor';
+ case 'input-ports':
+ return 'icon-port-in';
+ case 'output-ports':
+ return 'icon-port-out';
+ case 'funnels':
+ return 'icon-funnel';
+ case 'labels':
+ return 'icon-label';
+ case 'remote-process-groups':
+ return 'icon-group-remote';
+ case 'parameter-contexts':
+ return 'icon-drop';
+ }
+
+ return 'icon-group';
+ }
+
+ getContextType(): string {
+ switch (this.resource) {
+ case 'processors':
+ return 'Processor';
+ case 'input-ports':
+ return 'Input Ports';
+ case 'output-ports':
+ return 'Output Ports';
+ case 'funnels':
+ return 'Funnel';
+ case 'labels':
+ return 'Label';
+ case 'remote-process-groups':
+ return 'Remote Process Group';
+ case 'parameter-contexts':
+ return 'Parameter Contexts';
+ }
+
+ return 'Process Group';
+ }
+
+ policyActionChanged(value: string): void {
+ let action: Action;
+ let policy: string;
+
+ switch (value) {
+ case 'read-component':
+ action = Action.Read;
+ policy = 'component';
+ break;
+ case 'write-component':
+ action = Action.Write;
+ policy = 'component';
+ break;
+ case 'write-operation':
+ action = Action.Write;
+ policy = 'operation';
+ break;
+ case 'read-data':
+ action = Action.Read;
+ policy = 'data';
+ break;
+ case 'write-data':
+ action = Action.Write;
+ policy = 'data';
+ break;
+ case 'read-provenance-data':
+ action = Action.Read;
+ policy = 'provenance-data';
+ break;
+ case 'read-policies':
+ action = Action.Read;
+ policy = 'policies';
+ break;
+ case 'write-policies':
+ action = Action.Write;
+ policy = 'policies';
+ break;
+ default:
+ action = Action.Write;
+ policy = 'data-transfer';
+ break;
+ }
+
+ this.store.dispatch(
+ selectComponentAccessPolicy({
+ request: {
+ resourceAction: {
+ action,
+ policy,
+ resource: this.resource,
+ resourceIdentifier: this.resourceIdentifier
+ }
+ }
+ })
+ );
+ }
+
+ getTemplateForInheritedPolicy(policy: AccessPolicyEntity): TemplateRef {
+ if (policy.component.resource.startsWith('/policies')) {
+ return this.inheritedFromPolicies;
+ } else if (policy.component.resource === '/controller') {
+ return this.inheritedFromController;
+ } else if (policy.component.resource === '/parameter-contexts') {
+ return this.inheritedFromGlobalParameterContexts;
+ }
+
+ return this.inheritedFromProcessGroup;
+ }
+
+ getInheritedProcessGroupRoute(policy: AccessPolicyEntity): string[] {
+ return ['/process-groups', this.nifiCommon.substringAfterLast(policy.component.resource, '/')];
+ }
+
+ createNewPolicy(): void {
+ this.store.dispatch(createAccessPolicy());
+ }
+
+ removeTenantFromPolicy(request: RemoveTenantFromPolicyRequest): void {
+ this.store.dispatch(
+ promptRemoveTenantFromPolicy({
+ request
+ })
+ );
+ }
+
+ addTenantToPolicy(): void {
+ this.store.dispatch(openAddTenantToPolicyDialog());
+ }
+
+ deletePolicy(): void {
+ this.store.dispatch(promptDeleteAccessPolicy());
+ }
+
+ refreshGlobalAccessPolicy(): void {
+ this.store.dispatch(reloadAccessPolicy());
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch(resetAccessPolicyState());
+ this.store.dispatch(resetTenantsState());
+ this.store.dispatch(resetPolicyComponentState());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts
new file mode 100644
index 0000000000..548ac64904
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { ComponentAccessPolicies } from './component-access-policies.component';
+import { CommonModule } from '@angular/common';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { MatInputModule } from '@angular/material/input';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatSelectModule } from '@angular/material/select';
+import { ComponentAccessPoliciesRoutingModule } from './component-access-policies-routing.module';
+import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { PolicyTable } from '../common/policy-table/policy-table.component';
+
+@NgModule({
+ declarations: [ComponentAccessPolicies],
+ exports: [ComponentAccessPolicies],
+ imports: [
+ CommonModule,
+ ComponentAccessPoliciesRoutingModule,
+ NgxSkeletonLoaderModule,
+ MatTableModule,
+ MatSortModule,
+ MatInputModule,
+ ReactiveFormsModule,
+ MatSelectModule,
+ NifiTooltipDirective,
+ PolicyTable
+ ]
+})
+export class ComponentAccessPoliciesModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies-routing.module.ts
new file mode 100644
index 0000000000..ef4446978c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies-routing.module.ts
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { GlobalAccessPolicies } from './global-access-policies.component';
+import { authorizationGuard } from '../../../../service/guard/authorization.guard';
+import { CurrentUser } from '../../../../state/current-user';
+
+const routes: Routes = [
+ {
+ path: ':action/:resource',
+ canMatch: [
+ authorizationGuard(
+ (user: CurrentUser) =>
+ user.tenantsPermissions.canRead &&
+ user.policiesPermissions.canRead &&
+ user.policiesPermissions.canWrite
+ )
+ ],
+ component: GlobalAccessPolicies,
+ children: [
+ {
+ path: ':resourceIdentifier',
+ component: GlobalAccessPolicies
+ }
+ ]
+ },
+ { path: '', pathMatch: 'full', redirectTo: 'read/flow' }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class GlobalAccessPoliciesRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.html
new file mode 100644
index 0000000000..035b798202
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.html
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ No policy for the specified resource.
+
+ Create a new policy.
+
+
+
+
+
+
+
+
+ Not authorized to access the policy for the specified resource.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Last updated:
+
{{ accessPolicyState.loadedTimestamp }}
+
+
+
+
+
+
+ Showing effective policy inherited from all policies.
+
+ Override this policy.
+
+
+
+ Showing effective policy inherited from the controller.
+
+ Override this policy.
+
+
+
+ No restriction specific users.
+
+ Create a new policy.
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.scss
new file mode 100644
index 0000000000..59a3503416
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.scss
@@ -0,0 +1,40 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.global-access-policies {
+ a {
+ font-size: 16px;
+ }
+
+ .resource-select {
+ .mat-mdc-form-field {
+ width: 300px;
+ }
+ }
+
+ .resource-identifier-select {
+ .mat-mdc-form-field {
+ width: 375px;
+ }
+ }
+
+ .action-select {
+ .mat-mdc-form-field {
+ width: 150px;
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.spec.ts
new file mode 100644
index 0000000000..3171130730
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { GlobalAccessPolicies } from './global-access-policies.component';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../state/access-policy/access-policy.reducer';
+
+describe('GlobalAccessPolicies', () => {
+ let component: GlobalAccessPolicies;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [GlobalAccessPolicies],
+ providers: [provideMockStore({ initialState })]
+ });
+ fixture = TestBed.createComponent(GlobalAccessPolicies);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.ts
new file mode 100644
index 0000000000..9dc9a234c5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.component.ts
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
+import {
+ createAccessPolicy,
+ openAddTenantToPolicyDialog,
+ promptDeleteAccessPolicy,
+ promptRemoveTenantFromPolicy,
+ reloadAccessPolicy,
+ resetAccessPolicyState,
+ selectGlobalAccessPolicy,
+ setAccessPolicy
+} from '../../state/access-policy/access-policy.actions';
+import { AccessPolicyState, RemoveTenantFromPolicyRequest } from '../../state/access-policy';
+import { initialState } from '../../state/access-policy/access-policy.reducer';
+import {
+ selectAccessPolicyState,
+ selectGlobalResourceActionFromRoute
+} from '../../state/access-policy/access-policy.selectors';
+import { distinctUntilChanged, filter } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { NiFiCommon } from '../../../../service/nifi-common.service';
+import { ComponentType, RequiredPermission, SelectOption, TextTipInput } from '../../../../state/shared';
+import { TextTip } from '../../../../ui/common/tooltips/text-tip/text-tip.component';
+import { AccessPolicyEntity, Action, PolicyStatus, ResourceAction } from '../../state/shared';
+import { loadExtensionTypesForPolicies } from '../../../../state/extension-types/extension-types.actions';
+import { selectRequiredPermissions } from '../../../../state/extension-types/extension-types.selectors';
+import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import { AccessPoliciesState } from '../../state';
+import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.actions';
+import { loadCurrentUser } from '../../../../state/current-user/current-user.actions';
+
+@Component({
+ selector: 'global-access-policies',
+ templateUrl: './global-access-policies.component.html',
+ styleUrls: ['./global-access-policies.component.scss']
+})
+export class GlobalAccessPolicies implements OnInit, OnDestroy {
+ flowConfiguration$ = this.store.select(selectFlowConfiguration);
+ accessPolicyState$ = this.store.select(selectAccessPolicyState);
+ currentUser$ = this.store.select(selectCurrentUser);
+
+ protected readonly TextTip = TextTip;
+ protected readonly Action = Action;
+ protected readonly PolicyStatus = PolicyStatus;
+ protected readonly ComponentType = ComponentType;
+
+ policyForm: FormGroup;
+ resourceOptions: SelectOption[];
+ requiredPermissionOptions!: SelectOption[];
+ supportsReadWriteAction: boolean = false;
+ supportsResourceIdentifier: boolean = false;
+
+ @ViewChild('inheritedFromPolicies') inheritedFromPolicies!: TemplateRef;
+ @ViewChild('inheritedFromController') inheritedFromController!: TemplateRef;
+ @ViewChild('inheritedFromNoRestrictions') inheritedFromNoRestrictions!: TemplateRef;
+
+ constructor(
+ private store: Store,
+ private formBuilder: FormBuilder,
+ private nifiCommon: NiFiCommon
+ ) {
+ this.resourceOptions = this.nifiCommon.getAllPolicyTypeListing();
+
+ this.policyForm = this.formBuilder.group({
+ resource: new FormControl(null, Validators.required),
+ action: new FormControl(null, Validators.required)
+ });
+
+ this.store
+ .select(selectRequiredPermissions)
+ .pipe(takeUntilDestroyed())
+ .subscribe((requiredPermissions: RequiredPermission[]) => {
+ const regardlessOfRestrictions: string = 'regardless of restrictions';
+
+ const options: SelectOption[] = [
+ {
+ text: regardlessOfRestrictions,
+ value: '',
+ description:
+ 'Allows users to create/modify all restricted components regardless of restrictions.'
+ }
+ ];
+
+ options.push(
+ ...requiredPermissions.map((requiredPermission) => ({
+ text: "requiring '" + requiredPermission.label + "'",
+ value: requiredPermission.id,
+ description:
+ "Allows users to create/modify restricted components requiring '" +
+ requiredPermission.label +
+ "'"
+ }))
+ );
+
+ this.requiredPermissionOptions = options.sort((a: SelectOption, b: SelectOption): number => {
+ if (a.text === regardlessOfRestrictions) {
+ return -1;
+ } else if (b.text === regardlessOfRestrictions) {
+ return 1;
+ }
+
+ return this.nifiCommon.compareString(a.text, b.text);
+ });
+ });
+
+ this.store
+ .select(selectGlobalResourceActionFromRoute)
+ .pipe(
+ filter((resourceAction) => resourceAction != null),
+ distinctUntilChanged((aResourceAction, bResourceAction) => {
+ // @ts-ignore
+ const a: ResourceAction = aResourceAction;
+ // @ts-ignore
+ const b: ResourceAction = bResourceAction;
+
+ return (
+ a.action == b.action && a.resource == b.resource && a.resourceIdentifier == b.resourceIdentifier
+ );
+ }),
+ takeUntilDestroyed()
+ )
+ .subscribe((resourceAction) => {
+ if (resourceAction) {
+ this.supportsReadWriteAction = this.globalPolicySupportsReadWrite(resourceAction.resource);
+
+ this.policyForm.get('resource')?.setValue(resourceAction.resource);
+ this.policyForm.get('action')?.setValue(resourceAction.action);
+
+ this.updateResourceIdentifierVisibility(resourceAction.resource);
+
+ if (resourceAction.resource === 'restricted-components' && resourceAction.resourceIdentifier) {
+ this.policyForm.get('resourceIdentifier')?.setValue(resourceAction.resourceIdentifier);
+ }
+
+ this.store.dispatch(
+ setAccessPolicy({
+ request: {
+ resourceAction
+ }
+ })
+ );
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ this.store.dispatch(loadFlowConfiguration());
+ this.store.dispatch(loadTenants());
+ this.store.dispatch(loadExtensionTypesForPolicies());
+ }
+
+ isInitialLoading(state: AccessPolicyState): boolean {
+ return state.loadedTimestamp == initialState.loadedTimestamp;
+ }
+
+ getSelectOptionTipData(option: SelectOption): TextTipInput {
+ return {
+ // @ts-ignore
+ text: option.description
+ };
+ }
+
+ resourceChanged(value: string): void {
+ if (this.globalPolicySupportsReadWrite(value)) {
+ this.supportsReadWriteAction = true;
+ this.supportsResourceIdentifier = false;
+
+ // reset the action
+ this.policyForm.get('action')?.setValue(Action.Read);
+ } else {
+ this.supportsReadWriteAction = false;
+
+ // since this resource does not support read and write, update the form with the appropriate action this resource does support
+ this.policyForm.get('action')?.setValue(this.globalPolicySupportsWrite(value) ? Action.Write : Action.Read);
+
+ this.updateResourceIdentifierVisibility(value);
+ }
+
+ this.store.dispatch(
+ selectGlobalAccessPolicy({
+ request: {
+ resourceAction: {
+ resource: this.policyForm.get('resource')?.value,
+ action: this.policyForm.get('action')?.value,
+ resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
+ }
+ }
+ })
+ );
+ }
+
+ private globalPolicySupportsReadWrite(resource: string): boolean {
+ return (
+ resource === 'controller' ||
+ resource === 'parameter-contexts' ||
+ resource === 'counters' ||
+ resource === 'policies' ||
+ resource === 'tenants'
+ );
+ }
+
+ private globalPolicySupportsWrite(resource: string): boolean {
+ return resource === 'proxy' || resource === 'restricted-components';
+ }
+
+ private updateResourceIdentifierVisibility(resource: string): void {
+ if (resource === 'restricted-components') {
+ this.supportsResourceIdentifier = true;
+ this.policyForm.addControl('resourceIdentifier', new FormControl(''));
+ } else {
+ this.supportsResourceIdentifier = false;
+ this.policyForm.removeControl('resourceIdentifier');
+ }
+ }
+
+ actionChanged(): void {
+ this.store.dispatch(
+ selectGlobalAccessPolicy({
+ request: {
+ resourceAction: {
+ resource: this.policyForm.get('resource')?.value,
+ action: this.policyForm.get('action')?.value,
+ resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
+ }
+ }
+ })
+ );
+ }
+
+ resourceIdentifierChanged(): void {
+ this.store.dispatch(
+ selectGlobalAccessPolicy({
+ request: {
+ resourceAction: {
+ resource: this.policyForm.get('resource')?.value,
+ action: this.policyForm.get('action')?.value,
+ resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
+ }
+ }
+ })
+ );
+ }
+
+ getTemplateForInheritedPolicy(policy: AccessPolicyEntity): TemplateRef {
+ if (policy.component.resource === '/policies') {
+ return this.inheritedFromPolicies;
+ } else if (policy.component.resource === '/controller') {
+ return this.inheritedFromController;
+ }
+
+ return this.inheritedFromNoRestrictions;
+ }
+
+ createNewPolicy(): void {
+ this.store.dispatch(createAccessPolicy());
+ }
+
+ removeTenantFromPolicy(request: RemoveTenantFromPolicyRequest): void {
+ this.store.dispatch(
+ promptRemoveTenantFromPolicy({
+ request
+ })
+ );
+ }
+
+ addTenantToPolicy(): void {
+ this.store.dispatch(openAddTenantToPolicyDialog());
+ }
+
+ deletePolicy(): void {
+ this.store.dispatch(promptDeleteAccessPolicy());
+ }
+
+ refreshGlobalAccessPolicy(): void {
+ this.store.dispatch(reloadAccessPolicy());
+ }
+
+ ngOnDestroy(): void {
+ // reload the current user to ensure the latest global policies
+ this.store.dispatch(loadCurrentUser());
+
+ this.store.dispatch(resetAccessPolicyState());
+ this.store.dispatch(resetTenantsState());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts
new file mode 100644
index 0000000000..8657202f9d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { GlobalAccessPolicies } from './global-access-policies.component';
+import { CommonModule } from '@angular/common';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { MatInputModule } from '@angular/material/input';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatSelectModule } from '@angular/material/select';
+import { GlobalAccessPoliciesRoutingModule } from './global-access-policies-routing.module';
+import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { PolicyTable } from '../common/policy-table/policy-table.component';
+
+@NgModule({
+ declarations: [GlobalAccessPolicies],
+ exports: [GlobalAccessPolicies],
+ imports: [
+ CommonModule,
+ GlobalAccessPoliciesRoutingModule,
+ NgxSkeletonLoaderModule,
+ MatTableModule,
+ MatSortModule,
+ MatInputModule,
+ ReactiveFormsModule,
+ MatSelectModule,
+ NifiTooltipDirective,
+ PolicyTable
+ ]
+})
+export class GlobalAccessPoliciesModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
index 21a49215da..a85595ed17 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
@@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ConnectableBehavior', () => {
let service: ConnectableBehavior;
@@ -55,6 +57,10 @@ describe('ConnectableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
index abf24c1485..fcae86f79f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('DraggableBehavior', () => {
let service: DraggableBehavior;
@@ -60,6 +62,10 @@ describe('DraggableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
index bf1a38ea8a..46f59cb76d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('EditableBehaviorService', () => {
let service: EditableBehavior;
@@ -60,6 +62,10 @@ describe('EditableBehaviorService', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
index b9399b19cd..e4c2046f25 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
@@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('QuickSelectBehavior', () => {
let service: QuickSelectBehavior;
@@ -55,6 +57,10 @@ describe('QuickSelectBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
index 05a4f80321..75bcedf4a6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
@@ -30,6 +30,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('SelectableBehavior', () => {
let service: SelectableBehavior;
@@ -54,6 +56,10 @@ describe('SelectableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
index 235318deb7..e6e572bf2a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('BirdseyeView', () => {
let service: BirdseyeView;
@@ -60,6 +62,10 @@ describe('BirdseyeView', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index 80556134a1..4a6b03ea15 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -30,6 +30,7 @@ import {
navigateToControllerServicesForProcessGroup,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
+ navigateToManageComponentPolicies,
navigateToProvenanceForComponent,
navigateToViewStatusHistoryForComponent,
reloadFlow,
@@ -740,13 +741,57 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
},
{
condition: (selection: any) => {
- // TODO - canManagePolicies
- return false;
+ return (
+ this.canvasUtils.supportsManagedAuthorizer() && this.canvasUtils.canManagePolicies(selection)
+ );
},
clazz: 'fa fa-key',
text: 'Manage access policies',
- action: () => {
- // TODO - managePolicies
+ action: (selection: any) => {
+ if (selection.empty()) {
+ this.store.dispatch(
+ navigateToManageComponentPolicies({
+ request: {
+ resource: 'process-groups',
+ id: this.canvasUtils.getProcessGroupId()
+ }
+ })
+ );
+ } else {
+ const selectionData = selection.datum();
+ const componentType: ComponentType = selectionData.type;
+
+ let resource: string = 'process-groups';
+ switch (componentType) {
+ case ComponentType.Processor:
+ resource = 'processors';
+ break;
+ case ComponentType.InputPort:
+ resource = 'input-ports';
+ break;
+ case ComponentType.OutputPort:
+ resource = 'output-ports';
+ break;
+ case ComponentType.Funnel:
+ resource = 'funnels';
+ break;
+ case ComponentType.Label:
+ resource = 'labels';
+ break;
+ case ComponentType.RemoteProcessGroup:
+ resource = 'remote-process-groups';
+ break;
+ }
+
+ this.store.dispatch(
+ navigateToManageComponentPolicies({
+ request: {
+ resource,
+ id: selectionData.id
+ }
+ })
+ );
+ }
}
},
{
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
index 26bb27db85..8a41cb4a9e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
@@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('CanvasUtils', () => {
let service: CanvasUtils;
@@ -55,6 +57,10 @@ describe('CanvasUtils', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index 50002cee06..4e51520609 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -35,6 +35,9 @@ import { NiFiCommon } from '../../../service/nifi-common.service';
import { CurrentUser } from '../../../state/current-user';
import { initialState as initialUserState } from '../../../state/current-user/current-user.reducer';
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
+import { FlowConfiguration } from '../../../state/flow-configuration';
+import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
@Injectable({
providedIn: 'root'
@@ -49,6 +52,7 @@ export class CanvasUtils {
private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
private canvasPermissions: Permissions = initialFlowState.flow.permissions;
private currentUser: CurrentUser = initialUserState.user;
+ private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
private connections: any[] = [];
private readonly humanizeDuration: Humanizer;
@@ -94,6 +98,13 @@ export class CanvasUtils {
.subscribe((user) => {
this.currentUser = user;
});
+
+ this.store
+ .select(selectFlowConfiguration)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((flowConfiguration) => {
+ this.flowConfiguration = flowConfiguration;
+ });
}
public hasDownstream(selection: any): boolean {
@@ -453,6 +464,40 @@ export class CanvasUtils {
return selection.size() === 1 && selection.classed('funnel');
}
+ /**
+ * Determines whether the user can configure or open the policy management page.
+ */
+ public canManagePolicies(selection: any): boolean {
+ // ensure 0 or 1 components selected
+ if (selection.size() <= 1) {
+ // if something is selected, ensure it's not a connection
+ if (!selection.empty() && this.isConnection(selection)) {
+ return false;
+ }
+
+ // ensure access to read tenants
+ return this.canAccessTenants();
+ }
+
+ return false;
+ }
+
+ public supportsManagedAuthorizer(): boolean {
+ if (this.flowConfiguration) {
+ return this.flowConfiguration.supportsManagedAuthorizer;
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether the current user can access tenants.
+ *
+ * @returns {boolean}
+ */
+ public canAccessTenants(): boolean {
+ return this.currentUser.tenantsPermissions.canRead === true;
+ }
+
/**
* Determines whether the current user can access provenance for the specified component.
*
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
index 0f90774ceb..6d9f53f696 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('CanvasView', () => {
let service: CanvasView;
@@ -60,6 +62,10 @@ describe('CanvasView', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
index 991223e6d9..30ce5030c4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ConnectionManager', () => {
let service: ConnectionManager;
@@ -60,6 +62,10 @@ describe('ConnectionManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
index ae48fc3e68..62111a3495 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('FunnelManager', () => {
let service: FunnelManager;
@@ -60,6 +62,10 @@ describe('FunnelManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
index 2876e24c06..48aa777579 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('LabelManager', () => {
let service: LabelManager;
@@ -60,6 +62,10 @@ describe('LabelManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
index 91c78137d5..945b470556 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('PortManager', () => {
let service: PortManager;
@@ -60,6 +62,10 @@ describe('PortManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
index fd45d417d9..19b2024327 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ProcessGroupManager', () => {
let service: ProcessGroupManager;
@@ -60,6 +62,10 @@ describe('ProcessGroupManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
index 022eb13d46..10c97b1c86 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ProcessorManager', () => {
let service: ProcessorManager;
@@ -60,6 +62,10 @@ describe('ProcessorManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
index 5ba295003a..600eebfbd5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
@@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
+import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('RemoteProcessGroupManager', () => {
let service: RemoteProcessGroupManager;
@@ -60,6 +62,10 @@ describe('RemoteProcessGroupManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
+ },
+ {
+ selector: selectFlowConfiguration,
+ value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index ffd0b11dd8..e5bdc330ee 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -45,6 +45,7 @@ import {
MoveComponentsRequest,
NavigateToComponentRequest,
NavigateToControllerServicesRequest,
+ NavigateToManageComponentPoliciesRequest,
OpenComponentDialogRequest,
OpenGroupComponentsDialogRequest,
LoadChildProcessGroupRequest,
@@ -295,6 +296,11 @@ export const navigateToEditComponent = createAction(
props<{ request: OpenComponentDialogRequest }>()
);
+export const navigateToManageComponentPolicies = createAction(
+ `${CANVAS_PREFIX} Navigate To Manage Component Policies`,
+ props<{ request: NavigateToManageComponentPoliciesRequest }>()
+);
+
export const editComponent = createAction(
`${CANVAS_PREFIX} Edit Component`,
props<{ request: EditComponentDialogRequest }>()
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 325ac09228..67ba2913cb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -674,6 +674,18 @@ export class FlowEffects {
{ dispatch: false }
);
+ navigateToManageComponentPolicies$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(FlowActions.navigateToManageComponentPolicies),
+ map((action) => action.request),
+ tap((request) => {
+ this.router.navigate(['/access-policies', 'read', 'component', request.resource, request.id]);
+ })
+ ),
+ { dispatch: false }
+ );
+
navigateToViewStatusHistoryForComponent$ = createEffect(
() =>
this.actions$.pipe(
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 468d90ede8..bbc4aaa4da 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -215,6 +215,11 @@ export interface OpenComponentDialogRequest {
type: ComponentType;
}
+export interface NavigateToManageComponentPoliciesRequest {
+ resource: string;
+ id: string;
+}
+
export interface EditComponentDialogRequest {
type: ComponentType;
uri: string;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
index 46402f5df6..aead437ccd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
@@ -63,6 +63,7 @@ import { initialState } from '../../state/flow/flow.reducer';
import { ContextMenuDefinitionProvider } from '../../../../ui/common/context-menu/context-menu.component';
import { CanvasContextMenu } from '../../service/canvas-context-menu.service';
import { getStatusHistoryAndOpenDialog } from '../../../../state/status-history/status-history.actions';
+import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
@Component({
selector: 'fd-canvas',
@@ -270,6 +271,7 @@ export class Canvas implements OnInit, OnDestroy {
this.createSvg();
this.canvasView.init(this.viewContainerRef, this.svg, this.canvas);
+ this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(startProcessGroupPolling());
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/graph-controls.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/graph-controls.component.ts
index f1c794ad59..6247bcfb25 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/graph-controls.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/graph-controls.component.ts
@@ -26,6 +26,11 @@ import {
import { NavigationControl } from './navigation-control/navigation-control.component';
import { OperationControl } from './operation-control/operation-control.component';
import { AsyncPipe } from '@angular/common';
+import { NiFiState } from '../../../../../state';
+import {
+ selectFlowConfiguration,
+ selectSupportsManagedAuthorizer
+} from '../../../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'graph-controls',
@@ -38,6 +43,7 @@ export class GraphControls {
navigationCollapsed$ = this.store.select(selectNavigationCollapsed);
operationCollapsed$ = this.store.select(selectOperationCollapsed);
breadcrumbEntity$ = this.store.select(selectBreadcrumbs);
+ supportsManagedAuthorizer$ = this.store.select(selectSupportsManagedAuthorizer);
- constructor(private store: Store) {}
+ constructor(private store: Store) {}
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
index 5967f49213..a32c93bae2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
@@ -53,6 +53,7 @@
-
-
-
+
+
+
+
+
+
+