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

+
+ +
+ Users + + {{ user.component.identity }} + + +
+
+ User Groups + + + {{ userGroup.component.identity }} + + +
+
+ All users and groups are assigned to 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/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. + +
+
+
+
+
+
+ +
+
{{ policyComponentState.label }}
+
{{ getContextType() }}
+
+
+
+ + Policy + + + {{ option.text }} + + + + +
+
+
+
+ + +
+
+
+ +
+
+
+ +
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. + +
+
+
+
+
+
+ + Policy + + {{ option.text }} + + + +
+
+ + Option + + {{ option.text }} + + + +
+
+ + Action + + view + modify + + +
+
+
+
+ + +
+
+
+ +
+
+
+ +
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 @@ - - - + + + + + + +