rfellows commented on code in PR #8225: URL: https://github.com/apache/nifi/pull/8225#discussion_r1449063397
########## 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,427 @@ +/* + * 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 { 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, 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<any>; + @ViewChild('inheritedFromController') inheritedFromController!: TemplateRef<any>; + @ViewChild('inheritedFromGlobalParameterContexts') inheritedFromGlobalParameterContexts!: TemplateRef<any>; + @ViewChild('inheritedFromProcessGroup') inheritedFromProcessGroup!: TemplateRef<any>; + + constructor( + private store: Store<AccessPolicyState>, + 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), + 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'; + } + + 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'; + } + + return 'Process Group'; Review Comment: No case for `parameter-contexts`? ########## 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,155 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <div + class="global-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="resource-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="resource" + (selectionChange)="resourceChanged($event.value)"> + <mat-option + *ngFor="let option of resourceOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="resource-identifier-select" *ngIf="supportsResourceIdentifier"> + <mat-form-field> + <mat-label>Option</mat-label> + <mat-select + formControlName="resourceIdentifier" + (selectionChange)="resourceIdentifierChanged()"> + <mat-option + *ngFor="let option of requiredPermissionOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="action-select" [class.hidden]="!supportsReadWriteAction"> + <mat-form-field> + <mat-label>Action</mat-label> + <mat-select formControlName="action" (selectionChange)="actionChanged()"> + <mat-option [value]="Action.Read">view</mat-option> + <mat-option [value]="Action.Write">modify</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2 items-center"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> Review Comment: only a single implicit parameter is allowed. ```suggestion <ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer=supportsConfigurableAuthorizer> ``` ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-container> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> + No component specific administrators. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Add</a> policy for additional administrators. + </ng-container> +</ng-template> +<ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from the controller. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromGlobalParameterContexts let-policy let-supportsConfigurableAuthorizer> Review Comment: only 1 implicit parameter supported ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" Review Comment: add a title. it is unclear initially that this trashcan will delete the entire policy and not the selected user/group in the list. ########## 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 + }; + } Review Comment: This is getting triggered after (or during) the component policy screen was requested to be closed. and after the state was reset. this results in the `accessPolicies/policyComponent` section of the store to get re-populated. Doesn't seem that should be happening. ########## 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<NiFiState>) {} + + ngOnInit(): void { + this.store.dispatch(startCurrentUserPolling()); + } + + ngOnDestroy(): void { + this.store.dispatch(stopCurrentUserPolling()); Review Comment: We should consider forcing a refresh of the currentUser on destroy. The way it works now, if the user makes a change to their own user policies and closes the policies page, they must wait for the flow canvas to refresh the current user which will take 30 seconds or so. This can lead the user to think their policy change didn't take effect immediately. ########## 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,155 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <div + class="global-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="resource-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="resource" + (selectionChange)="resourceChanged($event.value)"> + <mat-option + *ngFor="let option of resourceOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="resource-identifier-select" *ngIf="supportsResourceIdentifier"> + <mat-form-field> + <mat-label>Option</mat-label> + <mat-select + formControlName="resourceIdentifier" + (selectionChange)="resourceIdentifierChanged()"> + <mat-option + *ngFor="let option of requiredPermissionOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="action-select" [class.hidden]="!supportsReadWriteAction"> + <mat-form-field> + <mat-label>Action</mat-label> + <mat-select formControlName="action" (selectionChange)="actionChanged()"> + <mat-option [value]="Action.Read">view</mat-option> + <mat-option [value]="Action.Write">modify</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2 items-center"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> Review Comment: add a title to this button to clarify that the action is deleting the policy and not just deleting the selected item from the table below. ```suggestion <button class="nifi-button" title="Delete this policy" [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" (click)="deletePolicy()"> <i class="fa fa-trash"></i> </button> ``` ########## 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,155 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <div + class="global-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="resource-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="resource" + (selectionChange)="resourceChanged($event.value)"> + <mat-option + *ngFor="let option of resourceOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="resource-identifier-select" *ngIf="supportsResourceIdentifier"> + <mat-form-field> + <mat-label>Option</mat-label> + <mat-select + formControlName="resourceIdentifier" + (selectionChange)="resourceIdentifierChanged()"> + <mat-option + *ngFor="let option of requiredPermissionOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="action-select" [class.hidden]="!supportsReadWriteAction"> + <mat-form-field> + <mat-label>Action</mat-label> + <mat-select formControlName="action" (selectionChange)="actionChanged()"> + <mat-option [value]="Action.Read">view</mat-option> + <mat-option [value]="Action.Write">modify</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2 items-center"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> Review Comment: add a title to this button to clarify what it does. ```suggestion <button class="nifi-button" title="Add users/groups to this policy" [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" (click)="addTenantToPolicy()"> <i class="fa fa-user-plus"></i> </button> ``` ########## 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,427 @@ +/* + * 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 { 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, 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<any>; + @ViewChild('inheritedFromController') inheritedFromController!: TemplateRef<any>; + @ViewChild('inheritedFromGlobalParameterContexts') inheritedFromGlobalParameterContexts!: TemplateRef<any>; + @ViewChild('inheritedFromProcessGroup') inheritedFromProcessGroup!: TemplateRef<any>; + + constructor( + private store: Store<AccessPolicyState>, + 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), + 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'; + } + + return 'icon-group'; Review Comment: No case for `parameter-contexts`? ########## 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,155 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <div + class="global-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="resource-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="resource" + (selectionChange)="resourceChanged($event.value)"> + <mat-option + *ngFor="let option of resourceOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="resource-identifier-select" *ngIf="supportsResourceIdentifier"> + <mat-form-field> + <mat-label>Option</mat-label> + <mat-select + formControlName="resourceIdentifier" + (selectionChange)="resourceIdentifierChanged()"> + <mat-option + *ngFor="let option of requiredPermissionOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="action-select" [class.hidden]="!supportsReadWriteAction"> + <mat-form-field> + <mat-label>Action</mat-label> + <mat-select formControlName="action" (selectionChange)="actionChanged()"> + <mat-option [value]="Action.Read">view</mat-option> + <mat-option [value]="Action.Write">modify</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2 items-center"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from all policies. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer> Review Comment: only a single implicit parameter is allowed. ```suggestion <ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer=supportsConfigurableAuthorizer> ``` ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-container> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> Review Comment: only 1 implicit parameter supported ########## 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,155 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <div + class="global-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="resource-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="resource" + (selectionChange)="resourceChanged($event.value)"> + <mat-option + *ngFor="let option of resourceOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="resource-identifier-select" *ngIf="supportsResourceIdentifier"> + <mat-form-field> + <mat-label>Option</mat-label> + <mat-select + formControlName="resourceIdentifier" + (selectionChange)="resourceIdentifierChanged()"> + <mat-option + *ngFor="let option of requiredPermissionOptions" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <div class="action-select" [class.hidden]="!supportsReadWriteAction"> + <mat-form-field> + <mat-label>Action</mat-label> + <mat-select formControlName="action" (selectionChange)="actionChanged()"> + <mat-option [value]="Action.Read">view</mat-option> + <mat-option [value]="Action.Write">modify</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2 items-center"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from all policies. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from the controller. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromNoRestrictions let-policy let-supportsConfigurableAuthorizer> Review Comment: Only a single implicit parameter is allowed. Currently, this results in `supportsConfigurableAuthorizer` evaluating to true. Thus, allowing creation of a policy even it `supportsConfigurableAuthorizer` is actually false. ```suggestion <ng-template #inheritedFromNoRestrictions let-policy let-supportsConfigurableAuthorizer=supportsConfigurableAuthorizer> ``` ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html: ########## @@ -131,7 +131,15 @@ <i class="fa fa-fw fa-users mr-2"></i> Users </button> - <button mat-menu-item class="global-menu-item"> + <button + mat-menu-item + class="global-menu-item" + [routerLink]="['/access-policies', 'global']" + [disabled]=" + !user.tenantsPermissions.canRead || + !user.policiesPermissions.canRead || + !user.policiesPermissions.canWrite + "> Review Comment: Question because I'm unclear TBH... If the FlowConfiguration indicates that `supportsManagedAuthorizer` is false, should we allow the user to navigate to the Policies screen? Seems like there will be nothing to show or configure in this case. We don't evaluate that condition here. Or is it not an issue as the permissions in question in such a scenario would evaluate to false? Same question regarding Users above. ########## 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,145 @@ +/* + * 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 implements AfterViewInit { + displayedColumns: string[] = ['user', 'actions']; + dataSource: MatTableDataSource<TenantItem> = new MatTableDataSource<TenantItem>(); + + tenantLookup: Map<string, TenantEntity> = new Map<string, TenantEntity>(); + + @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<RemoveTenantFromPolicyRequest> = + new EventEmitter<RemoveTenantFromPolicyRequest>(); + + _policy: AccessPolicyEntity | undefined; + selectedTenantId: string | null = null; + + sort: Sort = { + active: 'user', + direction: 'asc' + }; + + constructor(private nifiCommon: NiFiCommon) {} + + ngAfterViewInit(): void {} Review Comment: can we just remove this? ########## 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 + }; Review Comment: This is getting triggered after (or during) the global policy screen was requested to be closed. and after the state was reset. this results in the `accessPolicies/accessPolicy` section of the store to get re-populated. Doesn't seem that should be happening. ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-container> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> + No component specific administrators. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Add</a> policy for additional administrators. + </ng-container> +</ng-template> +<ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer> Review Comment: only 1 implicit parameter supported ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="addTenantToPolicy()"> + <i class="fa fa-user-plus"></i> + </button> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" + (click)="deletePolicy()"> + <i class="fa fa-trash"></i> + </button> + </div> + </div> + <div class="flex-1 -mt-2" *ngIf="currentUser$ | async as user"> + <policy-table + [policy]="accessPolicyState.accessPolicy" + [supportsPolicyModification]=" + flowConfiguration.supportsConfigurableAuthorizer && + accessPolicyState.policyStatus === PolicyStatus.Found + " + (removeTenantFromPolicy)="removeTenantFromPolicy($event)"></policy-table> + </div> + <div class="flex justify-between"> + <div class="refresh-container flex items-center gap-x-2"> + <button class="nifi-button" (click)="refreshGlobalAccessPolicy()"> + <i class="fa fa-refresh" [class.fa-spin]="accessPolicyState.status === 'loading'"></i> + </button> + <div>Last updated:</div> + <div class="refresh-timestamp">{{ accessPolicyState.loadedTimestamp }}</div> + </div> + </div> + </div> + </ng-container> + </ng-template> +</ng-container> +<ng-template #inheritedFromPolicies let-policy let-supportsConfigurableAuthorizer> + No component specific administrators. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Add</a> policy for additional administrators. + </ng-container> +</ng-template> +<ng-template #inheritedFromController let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from the controller. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromGlobalParameterContexts let-policy let-supportsConfigurableAuthorizer> + Showing effective policy inherited from global parameter context policy. + <ng-container *ngIf="supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Override</a> this policy. + </ng-container> +</ng-template> +<ng-template #inheritedFromProcessGroup let-policy let-supportsConfigurableAuthorizer> Review Comment: only 1 implicit parameter supported ########## 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,145 @@ +/* + * 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 implements AfterViewInit { + displayedColumns: string[] = ['user', 'actions']; + dataSource: MatTableDataSource<TenantItem> = new MatTableDataSource<TenantItem>(); + + tenantLookup: Map<string, TenantEntity> = new Map<string, TenantEntity>(); + + @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<RemoveTenantFromPolicyRequest> = + new EventEmitter<RemoveTenantFromPolicyRequest>(); + + _policy: AccessPolicyEntity | undefined; Review Comment: The appears to be intended to be private based on naming convention. I don't see any usage in the html referencing it, can it be private? ########## 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,147 @@ +<!-- + ~ 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. + --> + +<ng-container *ngIf="accessPolicyState$ | async; let accessPolicyState"> + <div *ngIf="isInitialLoading(accessPolicyState); else loaded"> + <ngx-skeleton-loader count="3"></ngx-skeleton-loader> + </div> + + <ng-template #loaded> + <ng-container *ngIf="policyComponentState$ | async; let policyComponentState"> + <div + class="component-access-policies flex flex-col h-full gap-y-2" + *ngIf="flowConfiguration$ | async; let flowConfiguration"> + <div class="value"> + <div class="mb-2" [ngSwitch]="accessPolicyState.policyStatus"> + <ng-container *ngSwitchCase="PolicyStatus.NotFound"> + No policy for the specified resource. + <ng-container *ngIf="flowConfiguration.supportsConfigurableAuthorizer"> + <a (click)="createNewPolicy()">Create</a> a new policy. + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Inherited"> + <ng-container *ngIf="accessPolicyState.accessPolicy"> + <ng-container + *ngTemplateOutlet=" + getTemplateForInheritedPolicy(accessPolicyState.accessPolicy); + context: { + $implicit: accessPolicyState.accessPolicy, + supportsConfigurableAuthorizer: + flowConfiguration.supportsConfigurableAuthorizer + } + "></ng-container> + </ng-container> + </ng-container> + <ng-container *ngSwitchCase="PolicyStatus.Forbidden"> + Not authorized to access the policy for the specified resource. + </ng-container> + </div> + </div> + <div class="flex justify-between items-center"> + <form [formGroup]="policyForm"> + <div class="flex gap-x-2"> + <div class="flex gap-x-1 -mt-2"> + <div class="operation-context-logo flex flex-col"> + <i class="icon" [class]="getContextIcon()"></i> + </div> + <div class="flex flex-col"> + <div class="operation-context-name">{{ policyComponentState.label }}</div> + <div class="operation-context-type">{{ getContextType() }}</div> + </div> + </div> + <div class="policy-select"> + <mat-form-field> + <mat-label>Policy</mat-label> + <mat-select + formControlName="policyAction" + (selectionChange)="policyActionChanged($event.value)"> + <mat-option + *ngFor="let option of policyActionOptions" + [disabled]="!isComponentPolicy(option, policyComponentState)" + [value]="option.value" + nifiTooltip + [tooltipComponentType]="TextTip" + [tooltipInputData]="getSelectOptionTipData(option)" + [delayClose]="false" + >{{ option.text }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + </form> + <div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2"> + <button + class="nifi-button" + [disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found" Review Comment: add a title -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
