NIFI-12548: Policy Management (#8225)

* NIFI-12548:
- Global Access Policies.

* NIFI-12548:
- Component Access Policies.

* NIFI-12548:
- Addressing review feedback.

* NIFI-12548:
- Addressing review feedback.

This closes #8225
This commit is contained in:
Matt Gilman 2024-01-12 14:32:22 -05:00 committed by GitHub
parent 9dd832e150
commit 0a3393b091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 4479 additions and 85 deletions

View File

@ -52,6 +52,12 @@ const routes: Routes = [
canMatch: [authenticationGuard],
loadChildren: () => import('./pages/users/feature/users.module').then((m) => m.UsersModule)
},
{
path: 'access-policies',
canMatch: [authenticationGuard],
loadChildren: () =>
import('./pages/access-policies/feature/access-policies.module').then((m) => m.AccessPoliciesModule)
},
{
path: 'summary',
canMatch: [authenticationGuard],

View File

@ -40,6 +40,7 @@ import { StatusHistoryEffects } from './state/status-history/status-history.effe
import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServiceStateEffects } from './state/contoller-service-state/controller-service-state.effects';
import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diagnostics.effects';
import { FlowConfigurationEffects } from './state/flow-configuration/flow-configuration.effects';
// @ts-ignore
@NgModule({
@ -62,6 +63,7 @@ import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diag
CurrentUserEffects,
ExtensionTypesEffects,
AboutEffects,
FlowConfigurationEffects,
StatusHistoryEffects,
ControllerServiceStateEffects,
SystemDiagnosticsEffects

View File

@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { AccessPolicies } from './access-policies.component';
import { FlowConfiguration } from '../../../state/flow-configuration';
import { checkFlowConfiguration } from '../../../service/guard/flow-configuration.guard';
const routes: Routes = [
{
path: '',
component: AccessPolicies,
canMatch: [
checkFlowConfiguration(
(flowConfiguration: FlowConfiguration) => flowConfiguration.supportsManagedAuthorizer
)
],
children: [
{
path: 'global',
loadChildren: () =>
import('../ui/global-access-policies/global-access-policies.module').then(
(m) => m.GlobalAccessPoliciesModule
)
},
{
path: '',
loadChildren: () =>
import('../ui/component-access-policies/component-access-policies.module').then(
(m) => m.ComponentAccessPoliciesModule
)
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccessPoliciesRoutingModule {}

View File

@ -0,0 +1,28 @@
<!--
~ 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.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<h3 class="text-xl bold access-policies-header">Access Policies</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,20 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.access-policies-header {
color: #728e9b;
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AccessPolicies } from './access-policies.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../state/access-policy/access-policy.reducer';
describe('AccessPolicies', () => {
let component: AccessPolicies;
let fixture: ComponentFixture<AccessPolicies>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AccessPolicies],
imports: [RouterModule, RouterTestingModule],
providers: [
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(AccessPolicies);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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());
}
}

View File

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AccessPolicies } from './access-policies.component';
import { AccessPoliciesRoutingModule } from './access-policies-routing.module';
import { StoreModule } from '@ngrx/store';
import { reducers, accessPoliciesFeatureKey } from '../state';
import { EffectsModule } from '@ngrx/effects';
import { MatDialogModule } from '@angular/material/dialog';
import { AccessPolicyEffects } from '../state/access-policy/access-policy.effects';
import { TenantsEffects } from '../state/tenants/tenants.effects';
import { PolicyComponentEffects } from '../state/policy-component/policy-component.effects';
@NgModule({
declarations: [AccessPolicies],
exports: [AccessPolicies],
imports: [
CommonModule,
AccessPoliciesRoutingModule,
StoreModule.forFeature(accessPoliciesFeatureKey, reducers),
EffectsModule.forFeature(AccessPolicyEffects, TenantsEffects, PolicyComponentEffects),
MatDialogModule
]
})
export class AccessPoliciesModule {}

View File

@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { Observable } from 'rxjs';
import { AccessPolicyEntity, ComponentResourceAction, ResourceAction } from '../state/shared';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { TenantEntity } from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class AccessPolicyService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client,
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
createAccessPolicy(resourceAction: ResourceAction): Observable<any> {
let resource: string = `/${resourceAction.resource}`;
if (resourceAction.resourceIdentifier) {
resource += `/${resourceAction.resourceIdentifier}`;
}
const payload: unknown = {
revision: {
version: 0,
clientId: this.client.getClientId()
},
component: {
action: resourceAction.action,
resource,
userGroups: [],
users: []
}
};
return this.httpClient.post(`${AccessPolicyService.API}/policies`, payload);
}
getAccessPolicy(resourceAction: ResourceAction): Observable<any> {
const path: string[] = [resourceAction.action, resourceAction.resource];
if (resourceAction.resourceIdentifier) {
path.push(resourceAction.resourceIdentifier);
}
return this.httpClient.get(`${AccessPolicyService.API}/policies/${path.join('/')}`);
}
getPolicyComponent(resourceAction: ComponentResourceAction): Observable<any> {
return this.httpClient.get(
`${AccessPolicyService.API}/${resourceAction.resource}/${resourceAction.resourceIdentifier}`
);
}
updateAccessPolicy(accessPolicy: AccessPolicyEntity, users: TenantEntity[], userGroups: TenantEntity[]) {
const payload: unknown = {
revision: this.client.getRevision(accessPolicy),
component: {
id: accessPolicy.id,
userGroups,
users
}
};
return this.httpClient.put(this.stripProtocol(accessPolicy.uri), payload);
}
deleteAccessPolicy(accessPolicy: AccessPolicyEntity): Observable<any> {
const revision: any = this.client.getRevision(accessPolicy);
return this.httpClient.delete(this.stripProtocol(accessPolicy.uri), { params: revision });
}
getUsers(): Observable<any> {
return this.httpClient.get(`${AccessPolicyService.API}/tenants/users`);
}
getUserGroups(): Observable<any> {
return this.httpClient.get(`${AccessPolicyService.API}/tenants/user-groups`);
}
}

View File

@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createAction, props } from '@ngrx/store';
import {
LoadAccessPolicyError,
LoadAccessPolicyRequest,
AccessPolicyResponse,
SelectGlobalAccessPolicyRequest,
SetAccessPolicyRequest,
ResetAccessPolicy,
RemoveTenantFromPolicyRequest,
AddTenantsToPolicyRequest,
SelectComponentAccessPolicyRequest
} from './index';
const ACCESS_POLICY_PREFIX: string = '[Access Policy]';
export const selectGlobalAccessPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Select Global Access Policy`,
props<{ request: SelectGlobalAccessPolicyRequest }>()
);
export const selectComponentAccessPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Select Component Access Policy`,
props<{ request: SelectComponentAccessPolicyRequest }>()
);
export const setAccessPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Set Access Policy`,
props<{ request: SetAccessPolicyRequest }>()
);
export const reloadAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Reload Access Policy`);
export const loadAccessPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Load Access Policy`,
props<{ request: LoadAccessPolicyRequest }>()
);
export const loadAccessPolicySuccess = createAction(
`${ACCESS_POLICY_PREFIX} Load Access Policy Success`,
props<{ response: AccessPolicyResponse }>()
);
export const createAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Create Access Policy`);
export const createAccessPolicySuccess = createAction(
`${ACCESS_POLICY_PREFIX} Create Access Policy Success`,
props<{ response: AccessPolicyResponse }>()
);
export const openAddTenantToPolicyDialog = createAction(`${ACCESS_POLICY_PREFIX} Open Add Tenant To Policy Dialog`);
export const addTenantsToPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Add Tenants To Policy`,
props<{ request: AddTenantsToPolicyRequest }>()
);
export const promptRemoveTenantFromPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Prompt Remove Tenant From Policy`,
props<{ request: RemoveTenantFromPolicyRequest }>()
);
export const removeTenantFromPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Remove Tenant From Policy`,
props<{ request: RemoveTenantFromPolicyRequest }>()
);
export const promptDeleteAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Prompt Delete Access Policy`);
export const deleteAccessPolicy = createAction(`${ACCESS_POLICY_PREFIX} Delete Access Policy`);
export const resetAccessPolicy = createAction(
`${ACCESS_POLICY_PREFIX} Reset Access Policy`,
props<{ response: ResetAccessPolicy }>()
);
export const accessPolicyApiError = createAction(
`${ACCESS_POLICY_PREFIX} Access Policy Api Error`,
props<{ response: LoadAccessPolicyError }>()
);
export const resetAccessPolicyState = createAction(`${ACCESS_POLICY_PREFIX} Reset Access Policy State`);

View File

@ -0,0 +1,407 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { NiFiState } from '../../../../state';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import * as AccessPolicyActions from './access-policy.actions';
import { catchError, combineLatest, filter, from, map, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { AccessPolicyService } from '../../service/access-policy.service';
import { AccessPolicyEntity, ComponentResourceAction, PolicyStatus, ResourceAction } from '../shared';
import { selectAccessPolicy, selectResourceAction, selectSaving } from './access-policy.selectors';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { TenantEntity } from '../../../../state/shared';
import { AddTenantToPolicyDialog } from '../../ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component';
import { AddTenantsToPolicyRequest } from './index';
import { ComponentAccessPolicies } from '../../ui/component-access-policies/component-access-policies.component';
import { selectUserGroups, selectUsers } from '../tenants/tenants.selectors';
@Injectable()
export class AccessPolicyEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private router: Router,
private accessPoliciesService: AccessPolicyService,
private dialog: MatDialog
) {}
setAccessPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.setAccessPolicy),
map((action) => action.request),
switchMap((request) =>
of(
AccessPolicyActions.loadAccessPolicy({
request: {
resourceAction: request.resourceAction
}
})
)
)
)
);
reloadAccessPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.reloadAccessPolicy),
withLatestFrom(this.store.select(selectResourceAction)),
filter(([action, resourceAction]) => resourceAction != null),
switchMap(([action, resourceAction]) => {
return of(
AccessPolicyActions.loadAccessPolicy({
request: {
// @ts-ignore
resourceAction
}
})
);
})
)
);
loadAccessPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.loadAccessPolicy),
map((action) => action.request),
switchMap((request) =>
from(this.accessPoliciesService.getAccessPolicy(request.resourceAction)).pipe(
map((response) => {
const accessPolicy: AccessPolicyEntity = response;
let requestedResource: string = `/${request.resourceAction.resource}`;
if (request.resourceAction.resourceIdentifier) {
requestedResource += `/${request.resourceAction.resourceIdentifier}`;
}
let policyStatus: PolicyStatus | undefined;
if (accessPolicy.component.resource === requestedResource) {
policyStatus = PolicyStatus.Found;
} else {
policyStatus = PolicyStatus.Inherited;
}
return AccessPolicyActions.loadAccessPolicySuccess({
response: {
accessPolicy,
policyStatus
}
});
}),
catchError((error) => {
let policyStatus: PolicyStatus | undefined;
if (error.status === 404) {
policyStatus = PolicyStatus.NotFound;
} else if (error.status === 403) {
policyStatus = PolicyStatus.Forbidden;
}
if (policyStatus) {
return of(
AccessPolicyActions.resetAccessPolicy({
response: {
policyStatus
}
})
);
} else {
return of(
AccessPolicyActions.accessPolicyApiError({
response: {
error: error.error
}
})
);
}
})
)
)
)
);
createAccessPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.createAccessPolicy),
withLatestFrom(this.store.select(selectResourceAction)),
filter(([action, resourceAction]) => resourceAction != null),
switchMap(([action, resourceAction]) =>
// @ts-ignore
from(this.accessPoliciesService.createAccessPolicy(resourceAction)).pipe(
map((response) => {
const accessPolicy: AccessPolicyEntity = response;
const policyStatus: PolicyStatus = PolicyStatus.Found;
return AccessPolicyActions.createAccessPolicySuccess({
response: {
accessPolicy,
policyStatus
}
});
}),
catchError((error) =>
of(
AccessPolicyActions.accessPolicyApiError({
response: {
error: error.error
}
})
)
)
)
)
)
);
selectGlobalAccessPolicy$ = createEffect(
() =>
this.actions$.pipe(
ofType(AccessPolicyActions.selectGlobalAccessPolicy),
map((action) => action.request),
tap((request) => {
const resourceAction: ResourceAction = request.resourceAction;
const commands: string[] = [
'/access-policies',
'global',
resourceAction.action,
resourceAction.resource
];
if (resourceAction.resourceIdentifier) {
commands.push(resourceAction.resourceIdentifier);
}
this.router.navigate(commands);
})
),
{ dispatch: false }
);
selectComponentAccessPolicy$ = createEffect(
() =>
this.actions$.pipe(
ofType(AccessPolicyActions.selectComponentAccessPolicy),
map((action) => action.request),
tap((request) => {
const resourceAction: ComponentResourceAction = request.resourceAction;
const commands: string[] = [
'/access-policies',
resourceAction.action,
resourceAction.policy,
resourceAction.resource,
resourceAction.resourceIdentifier
];
this.router.navigate(commands);
})
),
{ dispatch: false }
);
openAddTenantToPolicyDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(AccessPolicyActions.openAddTenantToPolicyDialog),
withLatestFrom(this.store.select(selectAccessPolicy)),
tap(([action, accessPolicy]) => {
const dialogReference = this.dialog.open(AddTenantToPolicyDialog, {
data: {
accessPolicy
},
panelClass: 'medium-dialog'
});
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
dialogReference.componentInstance.users$ = this.store.select(selectUsers);
dialogReference.componentInstance.userGroups$ = this.store.select(selectUserGroups);
dialogReference.componentInstance.addTenants
.pipe(take(1))
.subscribe((request: AddTenantsToPolicyRequest) => {
this.store.dispatch(AccessPolicyActions.addTenantsToPolicy({ request }));
});
})
),
{ dispatch: false }
);
addTenantsToPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.addTenantsToPolicy),
map((action) => action.request),
withLatestFrom(this.store.select(selectAccessPolicy)),
filter(([request, accessPolicyEntity]) => accessPolicyEntity != null),
switchMap(([request, accessPolicyEntity]) => {
// @ts-ignore
const accessPolicy: AccessPolicyEntity = accessPolicyEntity;
const users: TenantEntity[] = [...accessPolicy.component.users, ...request.users];
const userGroups: TenantEntity[] = [...accessPolicy.component.userGroups, ...request.userGroups];
return from(this.accessPoliciesService.updateAccessPolicy(accessPolicy, users, userGroups)).pipe(
map((response: any) => {
this.dialog.closeAll();
return AccessPolicyActions.loadAccessPolicySuccess({
response: {
accessPolicy: response,
policyStatus: PolicyStatus.Found
}
});
}),
catchError((error) =>
of(
AccessPolicyActions.accessPolicyApiError({
response: {
error: error.error
}
})
)
)
);
})
)
);
promptRemoveTenantFromPolicy$ = createEffect(
() =>
this.actions$.pipe(
ofType(AccessPolicyActions.promptRemoveTenantFromPolicy),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Update Policy',
message: `Remove '${request.tenant.component.identity}' from this policy?`
},
panelClass: 'small-dialog'
});
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(AccessPolicyActions.removeTenantFromPolicy({ request }));
});
})
),
{ dispatch: false }
);
removeTenantFromPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.removeTenantFromPolicy),
map((action) => action.request),
withLatestFrom(this.store.select(selectAccessPolicy)),
filter(([request, accessPolicyEntity]) => accessPolicyEntity != null),
switchMap(([request, accessPolicyEntity]) => {
// @ts-ignore
const accessPolicy: AccessPolicyEntity = accessPolicyEntity;
const users: TenantEntity[] = [...accessPolicy.component.users];
const userGroups: TenantEntity[] = [...accessPolicy.component.userGroups];
let tenants: TenantEntity[];
if (request.tenantType === 'user') {
tenants = users;
} else {
tenants = userGroups;
}
if (tenants) {
const tenantIndex: number = tenants.findIndex(
(tenant: TenantEntity) => request.tenant.id === tenant.id
);
if (tenantIndex > -1) {
tenants.splice(tenantIndex, 1);
}
}
return from(this.accessPoliciesService.updateAccessPolicy(accessPolicy, users, userGroups)).pipe(
map((response: any) => {
return AccessPolicyActions.loadAccessPolicySuccess({
response: {
accessPolicy: response,
policyStatus: PolicyStatus.Found
}
});
}),
catchError((error) =>
of(
AccessPolicyActions.accessPolicyApiError({
response: {
error: error.error
}
})
)
)
);
})
)
);
promptDeleteAccessPolicy$ = createEffect(
() =>
this.actions$.pipe(
ofType(AccessPolicyActions.promptDeleteAccessPolicy),
tap((request) => {
const dialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Delete Policy',
message:
'Are you sure you want to delete this policy? By doing so, the permissions for this component will revert to the inherited policy if applicable.'
},
panelClass: 'small-dialog'
});
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(AccessPolicyActions.deleteAccessPolicy());
});
})
),
{ dispatch: false }
);
deleteAccessPolicy$ = createEffect(() =>
this.actions$.pipe(
ofType(AccessPolicyActions.deleteAccessPolicy),
withLatestFrom(this.store.select(selectResourceAction), this.store.select(selectAccessPolicy)),
filter(([action, resourceAction, accessPolicy]) => resourceAction != null && accessPolicy != null),
switchMap(([action, resourceAction, accessPolicy]) =>
// @ts-ignore
from(this.accessPoliciesService.deleteAccessPolicy(accessPolicy)).pipe(
map((response) => {
// the policy was removed, we need to reload the policy for this resource and action to fetch
// the inherited policy or correctly when it's not found
return AccessPolicyActions.loadAccessPolicy({
request: {
// @ts-ignore
resourceAction
}
});
}),
catchError((error) =>
of(
AccessPolicyActions.accessPolicyApiError({
response: {
error: error.error
}
})
)
)
)
)
)
);
}

View File

@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AccessPolicyState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
addTenantsToPolicy,
createAccessPolicySuccess,
accessPolicyApiError,
loadAccessPolicy,
loadAccessPolicySuccess,
removeTenantFromPolicy,
resetAccessPolicyState,
resetAccessPolicy,
setAccessPolicy
} from './access-policy.actions';
export const initialState: AccessPolicyState = {
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const accessPolicyReducer = createReducer(
initialState,
on(setAccessPolicy, (state, { request }) => ({
...state,
resourceAction: request.resourceAction
})),
on(loadAccessPolicy, (state, { request }) => ({
...state,
status: 'loading' as const
})),
on(loadAccessPolicySuccess, createAccessPolicySuccess, (state, { response }) => ({
...state,
accessPolicy: response.accessPolicy,
policyStatus: response.policyStatus,
loadedTimestamp: response.accessPolicy.generated,
saving: false,
status: 'success' as const
})),
on(addTenantsToPolicy, (state, { request }) => ({
...state,
saving: true
})),
on(removeTenantFromPolicy, (state, { request }) => ({
...state,
saving: true
})),
on(resetAccessPolicy, (state, { response }) => ({
...state,
accessPolicy: undefined,
policyStatus: response.policyStatus,
loadedTimestamp: 'N/A',
status: 'success' as const
})),
on(accessPolicyApiError, (state, { response }) => ({
...state,
error: response.error,
accessPolicy: undefined,
policyStatus: undefined,
status: 'error' as const
})),
on(resetAccessPolicyState, (state) => ({
...initialState
}))
);

View File

@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from '@ngrx/store';
import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
import { accessPolicyFeatureKey, AccessPolicyState } from './index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
import { ComponentResourceAction, ResourceAction } from '../shared';
export const selectAccessPolicyState = createSelector(
selectAccessPoliciesState,
(state: AccessPoliciesState) => state[accessPolicyFeatureKey]
);
export const selectResourceAction = createSelector(
selectAccessPolicyState,
(state: AccessPolicyState) => state.resourceAction
);
export const selectAccessPolicy = createSelector(
selectAccessPolicyState,
(state: AccessPolicyState) => state.accessPolicy
);
export const selectSaving = createSelector(selectAccessPolicyState, (state: AccessPolicyState) => state.saving);
export const selectGlobalResourceActionFromRoute = createSelector(selectCurrentRoute, (route) => {
let selectedResourceAction: ResourceAction | null = null;
if (route?.params.action && route?.params.resource) {
// always select the action and resource from the route
selectedResourceAction = {
action: route.params.action,
resource: route.params.resource,
resourceIdentifier: route.params.resourceIdentifier
};
}
return selectedResourceAction;
});
export const selectComponentResourceActionFromRoute = createSelector(selectCurrentRoute, (route) => {
let selectedResourceAction: ComponentResourceAction | null = null;
if (route?.params.action && route?.params.policy && route?.params.resource && route?.params.resourceIdentifier) {
// always select the action and resource from the route
selectedResourceAction = {
action: route.params.action,
policy: route.params.policy,
resource: route.params.resource,
resourceIdentifier: route.params.resourceIdentifier
};
}
return selectedResourceAction;
});

View File

@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AccessPolicyEntity, ComponentResourceAction, PolicyStatus, ResourceAction } from '../shared';
import { TenantEntity, UserEntity, UserGroupEntity } from '../../../../state/shared';
export const accessPolicyFeatureKey = 'accessPolicy';
export interface SetAccessPolicyRequest {
resourceAction: ResourceAction;
}
export interface SelectGlobalAccessPolicyRequest {
resourceAction: ResourceAction;
}
export interface SelectComponentAccessPolicyRequest {
resourceAction: ComponentResourceAction;
}
export interface LoadAccessPolicyRequest {
resourceAction: ResourceAction;
}
export interface AccessPolicyResponse {
accessPolicy: AccessPolicyEntity;
policyStatus?: PolicyStatus;
}
export interface ResetAccessPolicy {
policyStatus: PolicyStatus;
}
export interface LoadAccessPolicyError {
error: string;
}
export interface RemoveTenantFromPolicyRequest {
tenantType: 'user' | 'userGroup';
tenant: TenantEntity;
}
export interface AddTenantToPolicyDialogRequest {
accessPolicy: AccessPolicyEntity;
}
export interface AddTenantsToPolicyRequest {
users: TenantEntity[];
userGroups: TenantEntity[];
}
export interface AccessPolicyState {
resourceAction?: ResourceAction;
policyStatus?: PolicyStatus;
accessPolicy?: AccessPolicyEntity;
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
import { accessPolicyFeatureKey, AccessPolicyState } from './access-policy';
import { accessPolicyReducer } from './access-policy/access-policy.reducer';
import { tenantsFeatureKey, TenantsState } from './tenants';
import { tenantsReducer } from './tenants/tenants.reducer';
import { policyComponentFeatureKey, PolicyComponentState } from './policy-component';
import { policyComponentReducer } from './policy-component/policy-component.reducer';
export const accessPoliciesFeatureKey = 'accessPolicies';
export interface AccessPoliciesState {
[accessPolicyFeatureKey]: AccessPolicyState;
[tenantsFeatureKey]: TenantsState;
[policyComponentFeatureKey]: PolicyComponentState;
}
export function reducers(state: AccessPoliciesState | undefined, action: Action) {
return combineReducers({
[accessPolicyFeatureKey]: accessPolicyReducer,
[tenantsFeatureKey]: tenantsReducer,
[policyComponentFeatureKey]: policyComponentReducer
})(state, action);
}
export const selectAccessPoliciesState = createFeatureSelector<AccessPoliciesState>(accessPoliciesFeatureKey);

View File

@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentResourceAction } from '../shared';
export const policyComponentFeatureKey = 'policyComponent';
export interface LoadPolicyComponentRequest {
componentResourceAction: ComponentResourceAction;
}
export interface LoadPolicyComponentSuccess {
label: string;
resource: string;
allowRemoteAccess: boolean;
}
export interface PolicyComponentState {
label: string;
allowRemoteAccess: boolean;
resource: string;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createAction, props } from '@ngrx/store';
import { LoadPolicyComponentRequest, LoadPolicyComponentSuccess } from './index';
const POLICY_COMPONENT_PREFIX: string = '[Policy Component]';
export const loadPolicyComponent = createAction(
`${POLICY_COMPONENT_PREFIX} Load Policy Component`,
props<{ request: LoadPolicyComponentRequest }>()
);
export const loadPolicyComponentSuccess = createAction(
`${POLICY_COMPONENT_PREFIX} Load Policy Component Success`,
props<{ response: LoadPolicyComponentSuccess }>()
);
export const policyComponentApiError = createAction(
`${POLICY_COMPONENT_PREFIX} Policy Component Api Error`,
props<{ error: string }>()
);
export const resetPolicyComponentState = createAction(`${POLICY_COMPONENT_PREFIX} Reset Policy Component State`);

View File

@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as PolicyComponentActions from './policy-component.actions';
import { catchError, from, map, of, switchMap } from 'rxjs';
import { AccessPolicyService } from '../../service/access-policy.service';
@Injectable()
export class PolicyComponentEffects {
constructor(
private actions$: Actions,
private accessPoliciesService: AccessPolicyService
) {}
loadPolicyComponent$ = createEffect(() =>
this.actions$.pipe(
ofType(PolicyComponentActions.loadPolicyComponent),
map((action) => action.request),
switchMap((request) =>
from(this.accessPoliciesService.getPolicyComponent(request.componentResourceAction)).pipe(
map((response) =>
PolicyComponentActions.loadPolicyComponentSuccess({
response: {
label: response.component.name,
resource: request.componentResourceAction.resource,
allowRemoteAccess:
request.componentResourceAction.resource === 'input-ports' ||
request.componentResourceAction.resource === 'output-ports'
? response.allowRemoteAccess
: false
}
})
),
catchError((error) => {
if (error.status === 403) {
return of(
PolicyComponentActions.loadPolicyComponentSuccess({
response: {
label: request.componentResourceAction.resourceIdentifier,
resource: request.componentResourceAction.resource,
allowRemoteAccess: false
}
})
);
} else {
return of(
PolicyComponentActions.policyComponentApiError({
error: error.error
})
);
}
})
)
)
)
);
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PolicyComponentState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
loadPolicyComponent,
loadPolicyComponentSuccess,
resetPolicyComponentState,
policyComponentApiError
} from './policy-component.actions';
export const initialState: PolicyComponentState = {
label: '',
resource: '',
allowRemoteAccess: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const policyComponentReducer = createReducer(
initialState,
on(loadPolicyComponent, (state) => ({
...state,
status: 'loading' as const
})),
on(loadPolicyComponentSuccess, (state, { response }) => ({
...state,
label: response.label,
resource: response.resource,
allowRemoteAccess: response.allowRemoteAccess,
status: 'success' as const
})),
on(policyComponentApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(resetPolicyComponentState, (state) => ({
...initialState
}))
);

View File

@ -0,0 +1,25 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from '@ngrx/store';
import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
import { policyComponentFeatureKey } from './index';
export const selectPolicyComponentState = createSelector(
selectAccessPoliciesState,
(state: AccessPoliciesState) => state[policyComponentFeatureKey]
);

View File

@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AccessPolicySummary, Permissions, Revision, TenantEntity } from '../../../../state/shared';
export enum PolicyStatus {
Found = 'Found',
Inherited = 'Inherited',
NotFound = 'NotFound',
Forbidden = 'Forbidden'
}
export enum Action {
Read = 'read',
Write = 'write'
}
export interface ResourceAction {
resource: string;
resourceIdentifier?: string;
action: Action;
}
export interface ComponentResourceAction extends ResourceAction {
resourceIdentifier: string;
policy: string;
}
export interface AccessPolicyEntity {
id: string;
component: AccessPolicy;
revision: Revision;
uri: string;
permissions: Permissions;
generated: string;
}
export interface AccessPolicy extends AccessPolicySummary {
users: TenantEntity[];
userGroups: TenantEntity[];
}

View File

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UserEntity, UserGroupEntity } from '../../../../state/shared';
export const tenantsFeatureKey = 'tenants';
export interface LoadTenantsSuccess {
users: UserEntity[];
userGroups: UserGroupEntity[];
}
export interface TenantsState {
users: UserEntity[];
userGroups: UserGroupEntity[];
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createAction, props } from '@ngrx/store';
import { LoadTenantsSuccess } from './index';
const TENANTS_PREFIX: string = '[Tenants]';
export const loadTenants = createAction(`${TENANTS_PREFIX} Load Tenants`);
export const loadTenantsSuccess = createAction(
`${TENANTS_PREFIX} Load Tenants Success`,
props<{ response: LoadTenantsSuccess }>()
);
export const tenantsApiError = createAction(`${TENANTS_PREFIX} Tenants Api Error`, props<{ error: string }>());
export const resetTenantsState = createAction(`${TENANTS_PREFIX} Reset Tenants State`);

View File

@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as TenantsActions from './tenants.actions';
import { catchError, combineLatest, filter, from, map, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
import { AccessPolicyService } from '../../service/access-policy.service';
@Injectable()
export class TenantsEffects {
constructor(
private actions$: Actions,
private accessPoliciesService: AccessPolicyService
) {}
loadTenants$ = createEffect(() =>
this.actions$.pipe(
ofType(TenantsActions.loadTenants),
switchMap(() =>
combineLatest([this.accessPoliciesService.getUsers(), this.accessPoliciesService.getUserGroups()]).pipe(
map(([usersResponse, userGroupsResponse]) =>
TenantsActions.loadTenantsSuccess({
response: {
users: usersResponse.users,
userGroups: userGroupsResponse.userGroups
}
})
),
catchError((error) =>
of(
TenantsActions.tenantsApiError({
error: error.error
})
)
)
)
)
)
);
}

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TenantsState } from './index';
import { createReducer, on } from '@ngrx/store';
import { loadTenants, loadTenantsSuccess, resetTenantsState, tenantsApiError } from './tenants.actions';
export const initialState: TenantsState = {
users: [],
userGroups: [],
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const tenantsReducer = createReducer(
initialState,
on(loadTenants, (state) => ({
...state,
status: 'loading' as const
})),
on(loadTenantsSuccess, (state, { response }) => ({
...state,
users: response.users,
userGroups: response.userGroups,
status: 'success' as const
})),
on(tenantsApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(resetTenantsState, (state) => ({
...initialState
}))
);

View File

@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from '@ngrx/store';
import { AccessPoliciesState, selectAccessPoliciesState } from '../index';
import { tenantsFeatureKey, TenantsState } from './index';
export const selectTenants = createSelector(
selectAccessPoliciesState,
(state: AccessPoliciesState) => state[tenantsFeatureKey]
);
export const selectUsers = createSelector(selectTenants, (state: TenantsState) => state.users);
export const selectUserGroups = createSelector(selectTenants, (state: TenantsState) => state.userGroups);

View File

@ -0,0 +1,54 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<h2 mat-dialog-title>Add Users/Groups To Policy</h2>
<form class="add-tenant-to-policy-form" [formGroup]="addTenantsForm">
<mat-dialog-content>
<div *ngIf="filteredUsers.length > 0">
<mat-label>Users</mat-label>
<mat-selection-list formControlName="users" class="border">
<mat-list-option *ngFor="let user of filteredUsers" togglePosition="before" [value]="user.id"
>{{ user.component.identity }}
</mat-list-option>
</mat-selection-list>
</div>
<div *ngIf="filteredUserGroups.length > 0" [class.mt-4]="filteredUsers.length > 0">
<mat-label>User Groups</mat-label>
<mat-selection-list formControlName="userGroups" class="border">
<mat-list-option
*ngFor="let userGroup of filteredUserGroups"
togglePosition="before"
[value]="userGroup.id">
<i class="fa fa-users mr-3"></i>{{ userGroup.component.identity }}
</mat-list-option>
</mat-selection-list>
</div>
<div *ngIf="filteredUsers.length === 0 && filteredUserGroups.length === 0" class="value">
All users and groups are assigned to this policy.
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button mat-raised-button mat-dialog-close color="accent">Cancel</button>
<button
mat-raised-button
[disabled]="addTenantsForm.invalid || saving.value"
(click)="addClicked()"
color="primary">
<span *nifiSpinner="saving.value">Add</span>
</button>
</mat-dialog-actions>
</form>

View File

@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '@angular/material' as mat;
.add-tenant-to-policy-form {
@include mat.button-density(-1);
.fa {
color: #004849;
}
mat-selection-list {
max-height: 250px;
overflow: auto;
}
}

View File

@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddTenantToPolicyDialog } from './add-tenant-to-policy-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AddTenantToPolicyDialogRequest } from '../../../state/access-policy';
describe('AddTenantToPolicyDialog', () => {
let component: AddTenantToPolicyDialog;
let fixture: ComponentFixture<AddTenantToPolicyDialog>;
const data: AddTenantToPolicyDialogRequest = {
accessPolicy: {
revision: {
clientId: '311032c3-f210-42f9-8a31-862c88b5fbd4',
version: 4
},
id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
uri: 'https://localhost:4200/nifi-api/policies/f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
permissions: {
canRead: true,
canWrite: true
},
generated: '15:48:06 EST',
component: {
id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
resource: '/flow',
action: 'read',
configurable: true,
users: [
{
revision: {
version: 0
},
id: 'bc646be3-146f-3cf2-bfd6-3a9f687ee7ab',
permissions: {
canRead: true,
canWrite: true
},
component: {
id: 'bc646be3-146f-3cf2-bfd6-3a9f687ee7ab',
identity: 'identify',
configurable: false
}
}
],
userGroups: []
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AddTenantToPolicyDialog, BrowserAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(AddTenantToPolicyDialog);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,153 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, DestroyRef, EventEmitter, inject, Inject, Input, Output } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { AccessPolicy } from '../../../state/shared';
import { MatButtonModule } from '@angular/material/button';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { Observable } from 'rxjs';
import { MatListModule } from '@angular/material/list';
import { TenantEntity, UserEntity, UserGroupEntity } from '../../../../../state/shared';
import { AddTenantsToPolicyRequest, AddTenantToPolicyDialogRequest } from '../../../state/access-policy';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
@Component({
selector: 'add-tenant-to-policy-dialog',
standalone: true,
imports: [
MatDialogModule,
MatButtonModule,
FormsModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
MatRadioModule,
MatCheckboxModule,
NgIf,
AsyncPipe,
MatListModule,
NgForOf,
NifiSpinnerDirective
],
templateUrl: './add-tenant-to-policy-dialog.component.html',
styleUrls: ['./add-tenant-to-policy-dialog.component.scss']
})
export class AddTenantToPolicyDialog {
@Input() set users$(users$: Observable<UserEntity[]>) {
users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users: UserEntity[]) => {
const policy: AccessPolicy = this.request.accessPolicy.component;
this.filteredUsers = users.filter((user: UserEntity) => {
return !policy.users.some((tenant: TenantEntity) => tenant.id === user.id);
});
this.userLookup.clear();
this.filteredUsers.forEach((user: UserEntity) => {
this.userLookup.set(user.id, user);
});
});
}
@Input() set userGroups$(userGroups$: Observable<UserGroupEntity[]>) {
userGroups$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((userGroups: UserGroupEntity[]) => {
const policy: AccessPolicy = this.request.accessPolicy.component;
this.filteredUserGroups = userGroups.filter((userGroup: UserGroupEntity) => {
return !policy.userGroups.some((tenant: TenantEntity) => tenant.id === userGroup.id);
});
this.userGroupLookup.clear();
this.filteredUserGroups.forEach((user: UserGroupEntity) => {
this.userGroupLookup.set(user.id, user);
});
});
}
@Input() saving$!: Observable<boolean>;
@Output() addTenants: EventEmitter<AddTenantsToPolicyRequest> = new EventEmitter<AddTenantsToPolicyRequest>();
private destroyRef = inject(DestroyRef);
addTenantsForm: FormGroup;
filteredUsers: UserEntity[] = [];
filteredUserGroups: UserGroupEntity[] = [];
userLookup: Map<string, UserEntity> = new Map<string, UserEntity>();
userGroupLookup: Map<string, UserGroupEntity> = new Map<string, UserGroupEntity>();
constructor(
@Inject(MAT_DIALOG_DATA) private request: AddTenantToPolicyDialogRequest,
private formBuilder: FormBuilder
) {
this.addTenantsForm = this.formBuilder.group({
users: new FormControl([]),
userGroups: new FormControl([])
});
}
addClicked(): void {
const users: TenantEntity[] = [];
const usersSelected: string[] = this.addTenantsForm.get('users')?.value;
usersSelected.forEach((userId: string) => {
const user: UserEntity | undefined = this.userLookup.get(userId);
if (user) {
users.push({
id: user.id,
revision: user.revision,
permissions: user.permissions,
component: {
id: user.component.id,
identity: user.component.identity,
configurable: user.component.configurable
}
});
}
});
const userGroups: TenantEntity[] = [];
const userGroupsSelected: string[] = this.addTenantsForm.get('userGroups')?.value;
userGroupsSelected.forEach((userGroupId: string) => {
const userGroup: UserGroupEntity | undefined = this.userGroupLookup.get(userGroupId);
if (userGroup) {
userGroups.push({
id: userGroup.id,
revision: userGroup.revision,
permissions: userGroup.permissions,
component: {
id: userGroup.component.id,
identity: userGroup.component.identity,
configurable: userGroup.component.configurable
}
});
}
});
this.addTenants.next({
users,
userGroups
});
}
}

View File

@ -0,0 +1,57 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<div class="policy-table h-full relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<table
mat-table
[dataSource]="dataSource"
matSort
matSortDisableClear
(matSortChange)="updateSort($event)"
[matSortActive]="sort.active"
[matSortDirection]="sort.direction">
<!-- User column -->
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef mat-sort-header>User</th>
<td mat-cell *matCellDef="let item" class="items-center">
<i *ngIf="item.tenantType === 'userGroup'" class="fa fa-users mr-3"></i>{{ item.user }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
<div
class="pointer fa fa-trash"
title="Remove"
*ngIf="canRemove()"
(click)="removeClicked(item)"></div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
(click)="select(row)"
[class.selected]="isSelected(row)"
[class.even]="even"></tr>
</table>
</div>
</div>

View File

@ -0,0 +1,24 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.policy-table {
.listing-table {
.mat-column-actions {
width: 75px;
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PolicyTable } from './policy-table.component';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CurrentUser } from '../../../../../state/current-user';
describe('PolicyTable', () => {
let component: PolicyTable;
let fixture: ComponentFixture<PolicyTable>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [],
imports: [
PolicyTable,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
NoopAnimationsModule
]
});
fixture = TestBed.createComponent(PolicyTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,143 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { CurrentUser } from '../../../../../state/current-user';
import { TenantEntity, UserEntity } from '../../../../../state/shared';
import { NgIf } from '@angular/common';
import { AccessPolicyEntity } from '../../../state/shared';
import { RemoveTenantFromPolicyRequest } from '../../../state/access-policy';
export interface TenantItem {
id: string;
user: string;
tenantType: 'user' | 'userGroup';
configurable: boolean;
}
@Component({
selector: 'policy-table',
standalone: true,
templateUrl: './policy-table.component.html',
imports: [MatTableModule, MatSortModule, NgIf],
styleUrls: ['./policy-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class PolicyTable {
displayedColumns: string[] = ['user', 'actions'];
dataSource: MatTableDataSource<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>();
private _policy: AccessPolicyEntity | undefined;
selectedTenantId: string | null = null;
sort: Sort = {
active: 'user',
direction: 'asc'
};
constructor(private nifiCommon: NiFiCommon) {}
updateSort(sort: Sort): void {
this.sort = sort;
this.dataSource.data = this.sortUsers(this.dataSource.data, sort);
}
sortUsers(items: TenantItem[], sort: Sort): TenantItem[] {
const data: TenantItem[] = items.slice();
return data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
let retVal: number = 0;
switch (sort.active) {
case 'user':
retVal = this.nifiCommon.compareString(a.user, b.user);
break;
}
return retVal * (isAsc ? 1 : -1);
});
}
select(item: TenantItem): void {
this.selectedTenantId = item.id;
}
isSelected(item: TenantItem): boolean {
if (this.selectedTenantId) {
return item.id == this.selectedTenantId;
}
return false;
}
canRemove(): boolean {
if (this._policy) {
return (
this.supportsPolicyModification &&
this._policy.permissions.canWrite &&
this._policy.component.configurable
);
}
return false;
}
removeClicked(item: TenantItem): void {
const tenant: TenantEntity | undefined = this.tenantLookup.get(item.id);
if (tenant) {
this.removeTenantFromPolicy.next({
tenantType: item.tenantType,
tenant
});
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ComponentAccessPolicies } from './component-access-policies.component';
import { authorizationGuard } from '../../../../service/guard/authorization.guard';
import { CurrentUser } from '../../../../state/current-user';
const routes: Routes = [
{
path: ':action/:policy/:resource/:resourceIdentifier',
canMatch: [authorizationGuard((user: CurrentUser) => user.tenantsPermissions.canRead)],
component: ComponentAccessPolicies
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ComponentAccessPoliciesRoutingModule {}

View File

@ -0,0 +1,153 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<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)">
<ng-container *ngFor="let option of policyActionOptions">
<mat-option
*ngIf="isComponentPolicy(option, policyComponentState)"
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
[delayClose]="false"
>{{ option.text }}
</mat-option>
</ng-container>
</mat-select>
</mat-form-field>
</div>
</div>
</form>
<div *ngIf="flowConfiguration.supportsConfigurableAuthorizer" class="flex gap-x-2">
<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>
<button
class="nifi-button"
title="Delete this policy"
[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="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="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="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="supportsConfigurableAuthorizer">
Showing effective policy inherited from <a [routerLink]="getInheritedProcessGroupRoute(policy)">Process Group</a>.
<ng-container *ngIf="supportsConfigurableAuthorizer">
<a (click)="createNewPolicy()">Override</a> this policy.
</ng-container>
</ng-template>

View File

@ -0,0 +1,48 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.component-access-policies {
a {
font-size: 16px;
}
.policy-select {
.mat-mdc-form-field {
width: 300px;
}
}
.operation-context-logo {
.icon {
font-size: 36px;
color: #ad9897;
}
}
.operation-context-name {
font-size: 18px;
color: #262626;
width: 370px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.operation-context-type {
color: #728e9b;
}
}

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentAccessPolicies } from './component-access-policies.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/access-policy/access-policy.reducer';
describe('ComponentAccessPolicies', () => {
let component: ComponentAccessPolicies;
let fixture: ComponentFixture<ComponentAccessPolicies>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ComponentAccessPolicies],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(ComponentAccessPolicies);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,444 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import {
createAccessPolicy,
openAddTenantToPolicyDialog,
promptDeleteAccessPolicy,
promptRemoveTenantFromPolicy,
reloadAccessPolicy,
resetAccessPolicyState,
selectComponentAccessPolicy,
setAccessPolicy
} from '../../state/access-policy/access-policy.actions';
import { AccessPolicyState, RemoveTenantFromPolicyRequest } from '../../state/access-policy';
import { initialState } from '../../state/access-policy/access-policy.reducer';
import {
selectAccessPolicyState,
selectComponentResourceActionFromRoute
} from '../../state/access-policy/access-policy.selectors';
import { distinctUntilChanged, filter } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { ComponentType, SelectOption, TextTipInput } from '../../../../state/shared';
import { TextTip } from '../../../../ui/common/tooltips/text-tip/text-tip.component';
import { AccessPolicyEntity, Action, ComponentResourceAction, PolicyStatus, ResourceAction } from '../../state/shared';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.actions';
import { loadPolicyComponent, resetPolicyComponentState } from '../../state/policy-component/policy-component.actions';
import { selectPolicyComponentState } from '../../state/policy-component/policy-component.selectors';
import { PolicyComponentState } from '../../state/policy-component';
@Component({
selector: 'global-access-policies',
templateUrl: './component-access-policies.component.html',
styleUrls: ['./component-access-policies.component.scss']
})
export class ComponentAccessPolicies implements OnInit, OnDestroy {
flowConfiguration$ = this.store.select(selectFlowConfiguration);
accessPolicyState$ = this.store.select(selectAccessPolicyState);
policyComponentState$ = this.store.select(selectPolicyComponentState);
currentUser$ = this.store.select(selectCurrentUser);
protected readonly TextTip = TextTip;
protected readonly Action = Action;
protected readonly PolicyStatus = PolicyStatus;
protected readonly ComponentType = ComponentType;
policyForm: FormGroup;
policyActionOptions: SelectOption[] = [
{
text: 'view the component',
value: 'read-component',
description: 'Allows users to view component configuration details'
},
{
text: 'modify the component',
value: 'write-component',
description: 'Allows users to modify component configuration details'
},
{
text: 'operate the component',
value: 'write-operation',
description:
'Allows users to operate components by changing component run status (start/stop/enable/disable), remote port transmission status, or terminating processor threads'
},
{
text: 'view provenance',
value: 'read-provenance-data',
description: 'Allows users to view provenance events generated by this component'
},
{
text: 'view the data',
value: 'read-data',
description:
'Allows users to view metadata and content for this component in flowfile queues in outbound connections and through provenance events'
},
{
text: 'modify the data',
value: 'write-data',
description:
'Allows users to empty flowfile queues in outbound connections and submit replays through provenance events'
},
{
text: 'receive data via site-to-site',
value: 'write-receive-data',
description: 'Allows this port to receive data from these NiFi instances',
disabled: true
},
{
text: 'send data via site-to-site',
value: 'write-send-data',
description: 'Allows this port to send data to these NiFi instances',
disabled: true
},
{
text: 'view the policies',
value: 'read-policies',
description: 'Allows users to view the list of users who can view/modify this component'
},
{
text: 'modify the policies',
value: 'write-policies',
description: 'Allows users to modify the list of users who can view/modify this component'
}
];
action!: Action;
resource!: string;
policy!: string;
resourceIdentifier!: string;
@ViewChild('inheritedFromPolicies') inheritedFromPolicies!: TemplateRef<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),
distinctUntilChanged((a, b) => {
// @ts-ignore
const aResourceAction: ComponentResourceAction = a;
// @ts-ignore
const bResourceAction: ComponentResourceAction = b;
return (
aResourceAction.action == bResourceAction.action &&
aResourceAction.policy == bResourceAction.policy &&
aResourceAction.resource == bResourceAction.resource &&
aResourceAction.resourceIdentifier == bResourceAction.resourceIdentifier
);
}),
takeUntilDestroyed()
)
.subscribe((componentResourceAction) => {
if (componentResourceAction) {
this.action = componentResourceAction.action;
this.policy = componentResourceAction.policy;
this.resource = componentResourceAction.resource;
this.resourceIdentifier = componentResourceAction.resourceIdentifier;
// data transfer policies for site to site are presented different in the form so
// we need to distinguish by type
let policyForResource: string = this.policy;
if (this.policy === 'data-transfer') {
if (this.resource === 'input-ports') {
policyForResource = 'receive-data';
} else {
policyForResource = 'send-data';
}
}
this.policyForm.get('policyAction')?.setValue(`${this.action}-${policyForResource}`);
// component policies are presented simply as '/processors/1234' while non-component policies
// like viewing provenance for a specific component is presented as `/provenance-data/processors/1234`
let resourceToLoad: string = this.resource;
if (componentResourceAction.policy !== 'component') {
resourceToLoad = `${this.policy}/${this.resource}`;
}
const resourceAction: ResourceAction = {
action: this.action,
resource: resourceToLoad,
resourceIdentifier: this.resourceIdentifier
};
this.store.dispatch(
loadPolicyComponent({
request: {
componentResourceAction
}
})
);
this.store.dispatch(
setAccessPolicy({
request: {
resourceAction
}
})
);
}
});
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadTenants());
}
isInitialLoading(state: AccessPolicyState): boolean {
return state.loadedTimestamp == initialState.loadedTimestamp;
}
isComponentPolicy(option: SelectOption, policyComponentState: PolicyComponentState): boolean {
// consider the type of component to override which policies shouldn't be supported
if (policyComponentState.resource === 'process-groups') {
switch (option.value) {
case 'write-send-data':
case 'write-receive-data':
return false;
}
} else if (
policyComponentState.resource === 'controller-services' ||
policyComponentState.resource === 'reporting-tasks'
) {
switch (option.value) {
case 'read-data':
case 'write-data':
case 'write-send-data':
case 'write-receive-data':
case 'read-provenance-data':
return false;
}
} else if (
policyComponentState.resource === 'parameter-contexts' ||
policyComponentState.resource === 'parameter-providers'
) {
switch (option.value) {
case 'read-data':
case 'write-data':
case 'write-send-data':
case 'write-receive-data':
case 'read-provenance-data':
case 'write-operation':
return false;
}
} else if (policyComponentState.resource === 'labels') {
switch (option.value) {
case 'write-operation':
case 'read-data':
case 'write-data':
case 'write-send-data':
case 'write-receive-data':
return false;
}
} else if (policyComponentState.resource === 'input-ports' && policyComponentState.allowRemoteAccess) {
// if input ports allow remote access, disable send data. if input ports do not allow remote
// access it will fall through to the else block where both send and receive data will be disabled
switch (option.value) {
case 'write-send-data':
return false;
}
} else if (policyComponentState.resource === 'output-ports' && policyComponentState.allowRemoteAccess) {
// if output ports allow remote access, disable receive data. if output ports do not allow remote
// access it will fall through to the else block where both send and receive data will be disabled
switch (option.value) {
case 'write-receive-data':
return false;
}
} else {
switch (option.value) {
case 'write-send-data':
case 'write-receive-data':
return false;
}
}
// enable all other options
return true;
}
getSelectOptionTipData(option: SelectOption): TextTipInput {
return {
// @ts-ignore
text: option.description
};
}
getContextIcon(): string {
switch (this.resource) {
case 'processors':
return 'icon-processor';
case 'input-ports':
return 'icon-port-in';
case 'output-ports':
return 'icon-port-out';
case 'funnels':
return 'icon-funnel';
case 'labels':
return 'icon-label';
case 'remote-process-groups':
return 'icon-group-remote';
case 'parameter-contexts':
return 'icon-drop';
}
return 'icon-group';
}
getContextType(): string {
switch (this.resource) {
case 'processors':
return 'Processor';
case 'input-ports':
return 'Input Ports';
case 'output-ports':
return 'Output Ports';
case 'funnels':
return 'Funnel';
case 'labels':
return 'Label';
case 'remote-process-groups':
return 'Remote Process Group';
case 'parameter-contexts':
return 'Parameter Contexts';
}
return 'Process Group';
}
policyActionChanged(value: string): void {
let action: Action;
let policy: string;
switch (value) {
case 'read-component':
action = Action.Read;
policy = 'component';
break;
case 'write-component':
action = Action.Write;
policy = 'component';
break;
case 'write-operation':
action = Action.Write;
policy = 'operation';
break;
case 'read-data':
action = Action.Read;
policy = 'data';
break;
case 'write-data':
action = Action.Write;
policy = 'data';
break;
case 'read-provenance-data':
action = Action.Read;
policy = 'provenance-data';
break;
case 'read-policies':
action = Action.Read;
policy = 'policies';
break;
case 'write-policies':
action = Action.Write;
policy = 'policies';
break;
default:
action = Action.Write;
policy = 'data-transfer';
break;
}
this.store.dispatch(
selectComponentAccessPolicy({
request: {
resourceAction: {
action,
policy,
resource: this.resource,
resourceIdentifier: this.resourceIdentifier
}
}
})
);
}
getTemplateForInheritedPolicy(policy: AccessPolicyEntity): TemplateRef<any> {
if (policy.component.resource.startsWith('/policies')) {
return this.inheritedFromPolicies;
} else if (policy.component.resource === '/controller') {
return this.inheritedFromController;
} else if (policy.component.resource === '/parameter-contexts') {
return this.inheritedFromGlobalParameterContexts;
}
return this.inheritedFromProcessGroup;
}
getInheritedProcessGroupRoute(policy: AccessPolicyEntity): string[] {
return ['/process-groups', this.nifiCommon.substringAfterLast(policy.component.resource, '/')];
}
createNewPolicy(): void {
this.store.dispatch(createAccessPolicy());
}
removeTenantFromPolicy(request: RemoveTenantFromPolicyRequest): void {
this.store.dispatch(
promptRemoveTenantFromPolicy({
request
})
);
}
addTenantToPolicy(): void {
this.store.dispatch(openAddTenantToPolicyDialog());
}
deletePolicy(): void {
this.store.dispatch(promptDeleteAccessPolicy());
}
refreshGlobalAccessPolicy(): void {
this.store.dispatch(reloadAccessPolicy());
}
ngOnDestroy(): void {
this.store.dispatch(resetAccessPolicyState());
this.store.dispatch(resetTenantsState());
this.store.dispatch(resetPolicyComponentState());
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { ComponentAccessPolicies } from './component-access-policies.component';
import { CommonModule } from '@angular/common';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { ComponentAccessPoliciesRoutingModule } from './component-access-policies-routing.module';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { PolicyTable } from '../common/policy-table/policy-table.component';
@NgModule({
declarations: [ComponentAccessPolicies],
exports: [ComponentAccessPolicies],
imports: [
CommonModule,
ComponentAccessPoliciesRoutingModule,
NgxSkeletonLoaderModule,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
NifiTooltipDirective,
PolicyTable
]
})
export class ComponentAccessPoliciesModule {}

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { GlobalAccessPolicies } from './global-access-policies.component';
import { authorizationGuard } from '../../../../service/guard/authorization.guard';
import { CurrentUser } from '../../../../state/current-user';
const routes: Routes = [
{
path: ':action/:resource',
canMatch: [
authorizationGuard(
(user: CurrentUser) =>
user.tenantsPermissions.canRead &&
user.policiesPermissions.canRead &&
user.policiesPermissions.canWrite
)
],
component: GlobalAccessPolicies,
children: [
{
path: ':resourceIdentifier',
component: GlobalAccessPolicies
}
]
},
{ path: '', pathMatch: 'full', redirectTo: 'read/flow' }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GlobalAccessPoliciesRoutingModule {}

View File

@ -0,0 +1,160 @@
<!--
~ 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"
title="Add users/groups to this policy"
[disabled]="accessPolicyState.policyStatus !== PolicyStatus.Found"
(click)="addTenantToPolicy()">
<i class="fa fa-user-plus"></i>
</button>
<button
class="nifi-button"
title="Delete this policy"
[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="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="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="supportsConfigurableAuthorizer">
No restriction specific users.
<ng-container *ngIf="supportsConfigurableAuthorizer">
<a (click)="createNewPolicy()">Create</a> a new policy.
</ng-container>
</ng-template>

View File

@ -0,0 +1,40 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.global-access-policies {
a {
font-size: 16px;
}
.resource-select {
.mat-mdc-form-field {
width: 300px;
}
}
.resource-identifier-select {
.mat-mdc-form-field {
width: 375px;
}
}
.action-select {
.mat-mdc-form-field {
width: 150px;
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GlobalAccessPolicies } from './global-access-policies.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/access-policy/access-policy.reducer';
describe('GlobalAccessPolicies', () => {
let component: GlobalAccessPolicies;
let fixture: ComponentFixture<GlobalAccessPolicies>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [GlobalAccessPolicies],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(GlobalAccessPolicies);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,305 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import {
createAccessPolicy,
openAddTenantToPolicyDialog,
promptDeleteAccessPolicy,
promptRemoveTenantFromPolicy,
reloadAccessPolicy,
resetAccessPolicyState,
selectGlobalAccessPolicy,
setAccessPolicy
} from '../../state/access-policy/access-policy.actions';
import { AccessPolicyState, RemoveTenantFromPolicyRequest } from '../../state/access-policy';
import { initialState } from '../../state/access-policy/access-policy.reducer';
import {
selectAccessPolicyState,
selectGlobalResourceActionFromRoute
} from '../../state/access-policy/access-policy.selectors';
import { distinctUntilChanged, filter } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { ComponentType, RequiredPermission, SelectOption, TextTipInput } from '../../../../state/shared';
import { TextTip } from '../../../../ui/common/tooltips/text-tip/text-tip.component';
import { AccessPolicyEntity, Action, PolicyStatus, ResourceAction } from '../../state/shared';
import { loadExtensionTypesForPolicies } from '../../../../state/extension-types/extension-types.actions';
import { selectRequiredPermissions } from '../../../../state/extension-types/extension-types.selectors';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { AccessPoliciesState } from '../../state';
import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.actions';
import { loadCurrentUser } from '../../../../state/current-user/current-user.actions';
@Component({
selector: 'global-access-policies',
templateUrl: './global-access-policies.component.html',
styleUrls: ['./global-access-policies.component.scss']
})
export class GlobalAccessPolicies implements OnInit, OnDestroy {
flowConfiguration$ = this.store.select(selectFlowConfiguration);
accessPolicyState$ = this.store.select(selectAccessPolicyState);
currentUser$ = this.store.select(selectCurrentUser);
protected readonly TextTip = TextTip;
protected readonly Action = Action;
protected readonly PolicyStatus = PolicyStatus;
protected readonly ComponentType = ComponentType;
policyForm: FormGroup;
resourceOptions: SelectOption[];
requiredPermissionOptions!: SelectOption[];
supportsReadWriteAction: boolean = false;
supportsResourceIdentifier: boolean = false;
@ViewChild('inheritedFromPolicies') inheritedFromPolicies!: TemplateRef<any>;
@ViewChild('inheritedFromController') inheritedFromController!: TemplateRef<any>;
@ViewChild('inheritedFromNoRestrictions') inheritedFromNoRestrictions!: TemplateRef<any>;
constructor(
private store: Store<AccessPoliciesState>,
private formBuilder: FormBuilder,
private nifiCommon: NiFiCommon
) {
this.resourceOptions = this.nifiCommon.getAllPolicyTypeListing();
this.policyForm = this.formBuilder.group({
resource: new FormControl(null, Validators.required),
action: new FormControl(null, Validators.required)
});
this.store
.select(selectRequiredPermissions)
.pipe(takeUntilDestroyed())
.subscribe((requiredPermissions: RequiredPermission[]) => {
const regardlessOfRestrictions: string = 'regardless of restrictions';
const options: SelectOption[] = [
{
text: regardlessOfRestrictions,
value: '',
description:
'Allows users to create/modify all restricted components regardless of restrictions.'
}
];
options.push(
...requiredPermissions.map((requiredPermission) => ({
text: "requiring '" + requiredPermission.label + "'",
value: requiredPermission.id,
description:
"Allows users to create/modify restricted components requiring '" +
requiredPermission.label +
"'"
}))
);
this.requiredPermissionOptions = options.sort((a: SelectOption, b: SelectOption): number => {
if (a.text === regardlessOfRestrictions) {
return -1;
} else if (b.text === regardlessOfRestrictions) {
return 1;
}
return this.nifiCommon.compareString(a.text, b.text);
});
});
this.store
.select(selectGlobalResourceActionFromRoute)
.pipe(
filter((resourceAction) => resourceAction != null),
distinctUntilChanged((aResourceAction, bResourceAction) => {
// @ts-ignore
const a: ResourceAction = aResourceAction;
// @ts-ignore
const b: ResourceAction = bResourceAction;
return (
a.action == b.action && a.resource == b.resource && a.resourceIdentifier == b.resourceIdentifier
);
}),
takeUntilDestroyed()
)
.subscribe((resourceAction) => {
if (resourceAction) {
this.supportsReadWriteAction = this.globalPolicySupportsReadWrite(resourceAction.resource);
this.policyForm.get('resource')?.setValue(resourceAction.resource);
this.policyForm.get('action')?.setValue(resourceAction.action);
this.updateResourceIdentifierVisibility(resourceAction.resource);
if (resourceAction.resource === 'restricted-components' && resourceAction.resourceIdentifier) {
this.policyForm.get('resourceIdentifier')?.setValue(resourceAction.resourceIdentifier);
}
this.store.dispatch(
setAccessPolicy({
request: {
resourceAction
}
})
);
}
});
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadTenants());
this.store.dispatch(loadExtensionTypesForPolicies());
}
isInitialLoading(state: AccessPolicyState): boolean {
return state.loadedTimestamp == initialState.loadedTimestamp;
}
getSelectOptionTipData(option: SelectOption): TextTipInput {
return {
// @ts-ignore
text: option.description
};
}
resourceChanged(value: string): void {
if (this.globalPolicySupportsReadWrite(value)) {
this.supportsReadWriteAction = true;
this.supportsResourceIdentifier = false;
// reset the action
this.policyForm.get('action')?.setValue(Action.Read);
} else {
this.supportsReadWriteAction = false;
// since this resource does not support read and write, update the form with the appropriate action this resource does support
this.policyForm.get('action')?.setValue(this.globalPolicySupportsWrite(value) ? Action.Write : Action.Read);
this.updateResourceIdentifierVisibility(value);
}
this.store.dispatch(
selectGlobalAccessPolicy({
request: {
resourceAction: {
resource: this.policyForm.get('resource')?.value,
action: this.policyForm.get('action')?.value,
resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
}
}
})
);
}
private globalPolicySupportsReadWrite(resource: string): boolean {
return (
resource === 'controller' ||
resource === 'parameter-contexts' ||
resource === 'counters' ||
resource === 'policies' ||
resource === 'tenants'
);
}
private globalPolicySupportsWrite(resource: string): boolean {
return resource === 'proxy' || resource === 'restricted-components';
}
private updateResourceIdentifierVisibility(resource: string): void {
if (resource === 'restricted-components') {
this.supportsResourceIdentifier = true;
this.policyForm.addControl('resourceIdentifier', new FormControl(''));
} else {
this.supportsResourceIdentifier = false;
this.policyForm.removeControl('resourceIdentifier');
}
}
actionChanged(): void {
this.store.dispatch(
selectGlobalAccessPolicy({
request: {
resourceAction: {
resource: this.policyForm.get('resource')?.value,
action: this.policyForm.get('action')?.value,
resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
}
}
})
);
}
resourceIdentifierChanged(): void {
this.store.dispatch(
selectGlobalAccessPolicy({
request: {
resourceAction: {
resource: this.policyForm.get('resource')?.value,
action: this.policyForm.get('action')?.value,
resourceIdentifier: this.policyForm.get('resourceIdentifier')?.value
}
}
})
);
}
getTemplateForInheritedPolicy(policy: AccessPolicyEntity): TemplateRef<any> {
if (policy.component.resource === '/policies') {
return this.inheritedFromPolicies;
} else if (policy.component.resource === '/controller') {
return this.inheritedFromController;
}
return this.inheritedFromNoRestrictions;
}
createNewPolicy(): void {
this.store.dispatch(createAccessPolicy());
}
removeTenantFromPolicy(request: RemoveTenantFromPolicyRequest): void {
this.store.dispatch(
promptRemoveTenantFromPolicy({
request
})
);
}
addTenantToPolicy(): void {
this.store.dispatch(openAddTenantToPolicyDialog());
}
deletePolicy(): void {
this.store.dispatch(promptDeleteAccessPolicy());
}
refreshGlobalAccessPolicy(): void {
this.store.dispatch(reloadAccessPolicy());
}
ngOnDestroy(): void {
// reload the current user to ensure the latest global policies
this.store.dispatch(loadCurrentUser());
this.store.dispatch(resetAccessPolicyState());
this.store.dispatch(resetTenantsState());
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { GlobalAccessPolicies } from './global-access-policies.component';
import { CommonModule } from '@angular/common';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { GlobalAccessPoliciesRoutingModule } from './global-access-policies-routing.module';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { PolicyTable } from '../common/policy-table/policy-table.component';
@NgModule({
declarations: [GlobalAccessPolicies],
exports: [GlobalAccessPolicies],
imports: [
CommonModule,
GlobalAccessPoliciesRoutingModule,
NgxSkeletonLoaderModule,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
NifiTooltipDirective,
PolicyTable
]
})
export class GlobalAccessPoliciesModule {}

View File

@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ConnectableBehavior', () => {
let service: ConnectableBehavior;
@ -55,6 +57,10 @@ describe('ConnectableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('DraggableBehavior', () => {
let service: DraggableBehavior;
@ -60,6 +62,10 @@ describe('DraggableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('EditableBehaviorService', () => {
let service: EditableBehavior;
@ -60,6 +62,10 @@ describe('EditableBehaviorService', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('QuickSelectBehavior', () => {
let service: QuickSelectBehavior;
@ -55,6 +57,10 @@ describe('QuickSelectBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -30,6 +30,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('SelectableBehavior', () => {
let service: SelectableBehavior;
@ -54,6 +56,10 @@ describe('SelectableBehavior', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('BirdseyeView', () => {
let service: BirdseyeView;
@ -60,6 +62,10 @@ describe('BirdseyeView', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -30,6 +30,7 @@ import {
navigateToControllerServicesForProcessGroup,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
navigateToProvenanceForComponent,
navigateToViewStatusHistoryForComponent,
reloadFlow,
@ -740,13 +741,57 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
},
{
condition: (selection: any) => {
// TODO - canManagePolicies
return false;
return (
this.canvasUtils.supportsManagedAuthorizer() && this.canvasUtils.canManagePolicies(selection)
);
},
clazz: 'fa fa-key',
text: 'Manage access policies',
action: () => {
// TODO - managePolicies
action: (selection: any) => {
if (selection.empty()) {
this.store.dispatch(
navigateToManageComponentPolicies({
request: {
resource: 'process-groups',
id: this.canvasUtils.getProcessGroupId()
}
})
);
} else {
const selectionData = selection.datum();
const componentType: ComponentType = selectionData.type;
let resource: string = 'process-groups';
switch (componentType) {
case ComponentType.Processor:
resource = 'processors';
break;
case ComponentType.InputPort:
resource = 'input-ports';
break;
case ComponentType.OutputPort:
resource = 'output-ports';
break;
case ComponentType.Funnel:
resource = 'funnels';
break;
case ComponentType.Label:
resource = 'labels';
break;
case ComponentType.RemoteProcessGroup:
resource = 'remote-process-groups';
break;
}
this.store.dispatch(
navigateToManageComponentPolicies({
request: {
resource,
id: selectionData.id
}
})
);
}
}
},
{

View File

@ -31,6 +31,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('CanvasUtils', () => {
let service: CanvasUtils;
@ -55,6 +57,10 @@ describe('CanvasUtils', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -35,6 +35,9 @@ import { NiFiCommon } from '../../../service/nifi-common.service';
import { CurrentUser } from '../../../state/current-user';
import { initialState as initialUserState } from '../../../state/current-user/current-user.reducer';
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
import { FlowConfiguration } from '../../../state/flow-configuration';
import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
@Injectable({
providedIn: 'root'
@ -49,6 +52,7 @@ export class CanvasUtils {
private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
private canvasPermissions: Permissions = initialFlowState.flow.permissions;
private currentUser: CurrentUser = initialUserState.user;
private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
private connections: any[] = [];
private readonly humanizeDuration: Humanizer;
@ -94,6 +98,13 @@ export class CanvasUtils {
.subscribe((user) => {
this.currentUser = user;
});
this.store
.select(selectFlowConfiguration)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((flowConfiguration) => {
this.flowConfiguration = flowConfiguration;
});
}
public hasDownstream(selection: any): boolean {
@ -453,6 +464,40 @@ export class CanvasUtils {
return selection.size() === 1 && selection.classed('funnel');
}
/**
* Determines whether the user can configure or open the policy management page.
*/
public canManagePolicies(selection: any): boolean {
// ensure 0 or 1 components selected
if (selection.size() <= 1) {
// if something is selected, ensure it's not a connection
if (!selection.empty() && this.isConnection(selection)) {
return false;
}
// ensure access to read tenants
return this.canAccessTenants();
}
return false;
}
public supportsManagedAuthorizer(): boolean {
if (this.flowConfiguration) {
return this.flowConfiguration.supportsManagedAuthorizer;
}
return false;
}
/**
* Determines whether the current user can access tenants.
*
* @returns {boolean}
*/
public canAccessTenants(): boolean {
return this.currentUser.tenantsPermissions.canRead === true;
}
/**
* Determines whether the current user can access provenance for the specified component.
*

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import * as fromUser from '../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../state/parameter';
import * as fromParameter from '../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
describe('CanvasView', () => {
let service: CanvasView;
@ -60,6 +62,10 @@ describe('CanvasView', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ConnectionManager', () => {
let service: ConnectionManager;
@ -60,6 +62,10 @@ describe('ConnectionManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('FunnelManager', () => {
let service: FunnelManager;
@ -60,6 +62,10 @@ describe('FunnelManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('LabelManager', () => {
let service: LabelManager;
@ -60,6 +62,10 @@ describe('LabelManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('PortManager', () => {
let service: PortManager;
@ -60,6 +62,10 @@ describe('PortManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ProcessGroupManager', () => {
let service: ProcessGroupManager;
@ -60,6 +62,10 @@ describe('ProcessGroupManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('ProcessorManager', () => {
let service: ProcessorManager;
@ -60,6 +62,10 @@ describe('ProcessorManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -32,6 +32,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import * as fromUser from '../../../../state/current-user/current-user.reducer';
import { parameterFeatureKey } from '../../state/parameter';
import * as fromParameter from '../../state/parameter/parameter.reducer';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
describe('RemoteProcessGroupManager', () => {
let service: RemoteProcessGroupManager;
@ -60,6 +62,10 @@ describe('RemoteProcessGroupManager', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -45,6 +45,7 @@ import {
MoveComponentsRequest,
NavigateToComponentRequest,
NavigateToControllerServicesRequest,
NavigateToManageComponentPoliciesRequest,
OpenComponentDialogRequest,
OpenGroupComponentsDialogRequest,
LoadChildProcessGroupRequest,
@ -295,6 +296,11 @@ export const navigateToEditComponent = createAction(
props<{ request: OpenComponentDialogRequest }>()
);
export const navigateToManageComponentPolicies = createAction(
`${CANVAS_PREFIX} Navigate To Manage Component Policies`,
props<{ request: NavigateToManageComponentPoliciesRequest }>()
);
export const editComponent = createAction(
`${CANVAS_PREFIX} Edit Component`,
props<{ request: EditComponentDialogRequest }>()

View File

@ -674,6 +674,18 @@ export class FlowEffects {
{ dispatch: false }
);
navigateToManageComponentPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.navigateToManageComponentPolicies),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/access-policies', 'read', 'component', request.resource, request.id]);
})
),
{ dispatch: false }
);
navigateToViewStatusHistoryForComponent$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -215,6 +215,11 @@ export interface OpenComponentDialogRequest {
type: ComponentType;
}
export interface NavigateToManageComponentPoliciesRequest {
resource: string;
id: string;
}
export interface EditComponentDialogRequest {
type: ComponentType;
uri: string;

View File

@ -63,6 +63,7 @@ import { initialState } from '../../state/flow/flow.reducer';
import { ContextMenuDefinitionProvider } from '../../../../ui/common/context-menu/context-menu.component';
import { CanvasContextMenu } from '../../service/canvas-context-menu.service';
import { getStatusHistoryAndOpenDialog } from '../../../../state/status-history/status-history.actions';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
@Component({
selector: 'fd-canvas',
@ -270,6 +271,7 @@ export class Canvas implements OnInit, OnDestroy {
this.createSvg();
this.canvasView.init(this.viewContainerRef, this.svg, this.canvas);
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(startProcessGroupPolling());
}

View File

@ -26,6 +26,11 @@ import {
import { NavigationControl } from './navigation-control/navigation-control.component';
import { OperationControl } from './operation-control/operation-control.component';
import { AsyncPipe } from '@angular/common';
import { NiFiState } from '../../../../../state';
import {
selectFlowConfiguration,
selectSupportsManagedAuthorizer
} from '../../../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'graph-controls',
@ -38,6 +43,7 @@ export class GraphControls {
navigationCollapsed$ = this.store.select(selectNavigationCollapsed);
operationCollapsed$ = this.store.select(selectOperationCollapsed);
breadcrumbEntity$ = this.store.select(selectBreadcrumbs);
supportsManagedAuthorizer$ = this.store.select(selectSupportsManagedAuthorizer);
constructor(private store: Store<CanvasState>) {}
constructor(private store: Store<NiFiState>) {}
}

View File

@ -53,6 +53,7 @@
<button
class="nifi-button mr-2"
type="button"
*ngIf="supportsManagedAuthorizer()"
[disabled]="!canManageAccess(selection)"
(click)="manageAccess(selection)">
<i class="fa fa-key"></i>

View File

@ -62,7 +62,6 @@ div.operation-control {
.operation-context-name {
font-size: 15px;
font-family: Roboto;
color: #262626;
height: 20px;
width: 225px;
@ -73,14 +72,12 @@ div.operation-control {
.operation-context-type {
font-size: 12px;
font-family: Roboto;
color: #728e9b;
}
.operation-context-id {
height: 18px;
font-size: 12px;
font-family: Roboto;
color: #775351;
}
}

View File

@ -21,6 +21,7 @@ import {
getParameterContextsAndOpenGroupComponentsDialog,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
setOperationCollapsed,
startComponents,
startCurrentProcessGroup,
@ -40,6 +41,7 @@ import {
} from '../../../../state/flow';
import { NgIf } from '@angular/common';
import { BreadcrumbEntity } from '../../../../state/shared';
import { ComponentType } from '../../../../../../state/shared';
@Component({
selector: 'operation-control',
@ -199,13 +201,59 @@ export class OperationControl {
}
}
supportsManagedAuthorizer(): boolean {
return this.canvasUtils.supportsManagedAuthorizer();
}
canManageAccess(selection: any): boolean {
// TODO
return false;
return this.canvasUtils.canManagePolicies(selection);
}
manageAccess(selection: any): void {
// TODO
if (selection.empty()) {
this.store.dispatch(
navigateToManageComponentPolicies({
request: {
resource: 'process-groups',
id: this.breadcrumbEntity.id
}
})
);
} else {
const selectionData = selection.datum();
const componentType: ComponentType = selectionData.type;
let resource: string = 'process-groups';
switch (componentType) {
case ComponentType.Processor:
resource = 'processors';
break;
case ComponentType.InputPort:
resource = 'input-ports';
break;
case ComponentType.OutputPort:
resource = 'output-ports';
break;
case ComponentType.Funnel:
resource = 'funnels';
break;
case ComponentType.Label:
resource = 'labels';
break;
case ComponentType.RemoteProcessGroup:
resource = 'remote-process-groups';
break;
}
this.store.dispatch(
navigateToManageComponentPolicies({
request: {
resource,
id: selectionData.id
}
})
);
}
}
canEnable(selection: any): boolean {

View File

@ -122,19 +122,31 @@
<i class="fa fa-fw mr-2"></i>
System Diagnostics
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
<ng-container *ngIf="flowConfiguration$ | async as flowConfiguration">
<ng-container *ngIf="flowConfiguration.supportsManagedAuthorizer">
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/access-policies', 'global']"
[disabled]="
!user.tenantsPermissions.canRead ||
!user.policiesPermissions.canRead ||
!user.policiesPermissions.canWrite
">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
</ng-container>
</ng-container>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-question-circle mr-2"></i>

View File

@ -38,6 +38,8 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@ -105,6 +107,10 @@ describe('HeaderComponent', () => {
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -40,6 +40,7 @@ import { RouterLink } from '@angular/router';
import { FlowStatus } from './flow-status/flow-status.component';
import { getNodeStatusHistoryAndOpenDialog } from '../../../../../state/status-history/status-history.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../../state/system-diagnostics/system-diagnostics.actions';
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'fd-header',
@ -66,6 +67,7 @@ export class HeaderComponent {
clusterSummary$ = this.store.select(selectClusterSummary);
controllerBulletins$ = this.store.select(selectControllerBulletins);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
constructor(

View File

@ -43,6 +43,9 @@
[controllerServices]="serviceState.controllerServices"
[formatScope]="formatScope(serviceState.breadcrumb)"
[definedByCurrentGroup]="definedByCurrentGroup(serviceState.breadcrumb)"
[currentUser]="(currentUser$ | async)!"
[flowConfiguration]="(flowConfiguration$ | async)!"
[canModifyParent]="canModifyParent(serviceState.breadcrumb)"
(selectControllerService)="selectControllerService($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)"

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, OnDestroy } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { filter, switchMap, take, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -41,19 +41,25 @@ import {
import { initialState } from '../../state/controller-services/controller-services.reducer';
import { ControllerServiceEntity } from '../../../../state/shared';
import { BreadcrumbEntity } from '../../state/shared';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
@Component({
selector: 'controller-services',
templateUrl: './controller-services.component.html',
styleUrls: ['./controller-services.component.scss']
})
export class ControllerServices implements OnDestroy {
export class ControllerServices implements OnInit, OnDestroy {
serviceState$ = this.store.select(selectControllerServicesState);
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
private currentProcessGroupId!: string;
constructor(private store: Store<ControllerServicesState>) {
constructor(private store: Store<NiFiState>) {
// load the controller services using the process group id from the route
this.store
.select(selectProcessGroupIdFromRoute)
@ -98,6 +104,10 @@ export class ControllerServices implements OnDestroy {
});
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
}
isInitialLoading(state: ControllerServicesState): boolean {
// using the current timestamp to detect the initial load event
return state.loadedTimestamp == initialState.loadedTimestamp;
@ -189,6 +199,28 @@ export class ControllerServices implements OnDestroy {
);
}
canModifyParent(breadcrumb: BreadcrumbEntity): (entity: ControllerServiceEntity) => boolean {
const breadcrumbs: BreadcrumbEntity[] = [];
let currentBreadcrumb: BreadcrumbEntity | undefined = breadcrumb;
while (currentBreadcrumb != null) {
breadcrumbs.push(currentBreadcrumb);
currentBreadcrumb = currentBreadcrumb.parentBreadcrumb;
}
return (entity: ControllerServiceEntity): boolean => {
const entityBreadcrumb: BreadcrumbEntity | undefined = breadcrumbs.find(
(bc) => bc.id === entity.parentGroupId
);
if (entityBreadcrumb) {
return entityBreadcrumb.permissions.canWrite;
}
return false;
};
}
selectControllerService(entity: ControllerServiceEntity): void {
// this service listing shows all services in the current group and any
// ancestor group. in this context we don't want the user to navigate away

View File

@ -30,6 +30,8 @@
<parameter-context-table
[parameterContexts]="parameterContextListingState.parameterContexts"
[selectedParameterContextId]="selectedParameterContextId$ | async"
[currentUser]="(currentUser$ | async)!"
[flowConfiguration]="(flowConfiguration$ | async)!"
(selectParameterContext)="selectParameterContext($event)"
(editParameterContext)="editParameterContext($event)"
(deleteParameterContext)="deleteParameterContext($event)"></parameter-context-table>

View File

@ -35,6 +35,9 @@ import {
import { initialState } from '../../state/parameter-context-listing/parameter-context-listing.reducer';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
@Component({
selector: 'parameter-context-listing',
@ -44,6 +47,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class ParameterContextListing implements OnInit {
parameterContextListingState$ = this.store.select(selectParameterContextListingState);
selectedParameterContextId$ = this.store.select(selectParameterContextIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
constructor(private store: Store<ParameterContextListingState>) {
this.store
@ -72,6 +77,7 @@ export class ParameterContextListing implements OnInit {
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadParameterContexts());
}

View File

@ -24,6 +24,7 @@ import { MatTableModule } from '@angular/material/table';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { ParameterContextTable } from './parameter-context-table/parameter-context-table.component';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterLink } from '@angular/router';
@NgModule({
declarations: [ParameterContextListing, ParameterContextTable],
@ -34,7 +35,8 @@ import { MatDialogModule } from '@angular/material/dialog';
MatSortModule,
MatTableModule,
MatDialogModule,
NifiTooltipDirective
NifiTooltipDirective,
RouterLink
]
})
export class ParameterContextListingModule {}

View File

@ -82,7 +82,8 @@
<div
class="pointer fa fa-key"
*ngIf="canManageAccessPolicies()"
(click)="managePoliciesClicked(item, $event)"
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)"
title="Access Policies"></div>
<!-- TODO go to parameter provider -->
</div>

View File

@ -20,6 +20,8 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { ParameterContextEntity } from '../../../state/parameter-context-listing';
import { FlowConfiguration } from '../../../../../state/flow-configuration';
import { CurrentUser } from '../../../../../state/current-user';
@Component({
selector: 'parameter-context-table',
@ -43,7 +45,10 @@ export class ParameterContextTable implements AfterViewInit {
return '';
};
}
@Input() selectedParameterContextId!: string;
@Input() flowConfiguration!: FlowConfiguration;
@Input() currentUser!: CurrentUser;
@Output() selectParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@Output() editParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@ -86,8 +91,10 @@ export class ParameterContextTable implements AfterViewInit {
}
canDelete(entity: ParameterContextEntity): boolean {
// TODO canModifyParameterContexts
return this.canRead(entity) && this.canWrite(entity);
const canModifyParameterContexts: boolean =
this.currentUser.parameterContextPermissions.canRead &&
this.currentUser.parameterContextPermissions.canWrite;
return canModifyParameterContexts && this.canRead(entity) && this.canWrite(entity);
}
deleteClicked(entity: ParameterContextEntity, event: MouseEvent): void {
@ -96,12 +103,11 @@ export class ParameterContextTable implements AfterViewInit {
}
canManageAccessPolicies(): boolean {
// TODO nfCanvasUtils.isManagedAuthorizer() && nfCommon.canAccessTenants()
return false;
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
managePoliciesClicked(entity: ParameterContextEntity, event: MouseEvent): void {
event.stopPropagation();
getPolicyLink(entity: ParameterContextEntity): string[] {
return ['/access-policies', 'read', 'component', 'parameter-contexts', entity.id];
}
select(entity: ParameterContextEntity): void {

View File

@ -32,6 +32,9 @@
[controllerServices]="serviceState.controllerServices"
[formatScope]="formatScope"
[definedByCurrentGroup]="definedByCurrentGroup"
[currentUser]="currentUser"
[canModifyParent]="canModifyParent(currentUser)"
[flowConfiguration]="(flowConfiguration$ | async)!"
(selectControllerService)="selectControllerService($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)"

View File

@ -41,8 +41,9 @@ import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { NiFiState } from '../../../../state';
import { state } from '@angular/animations';
import { resetEnableControllerServiceState } from '../../../../state/contoller-service-state/controller-service-state.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { CurrentUser } from '../../../../state/current-user';
@Component({
selector: 'management-controller-services',
@ -53,6 +54,7 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
serviceState$ = this.store.select(selectManagementControllerServicesState);
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
constructor(private store: Store<NiFiState>) {
this.store
@ -82,6 +84,7 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadManagementControllerServices());
}
@ -146,6 +149,11 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
);
}
canModifyParent(currentUser: CurrentUser): (entity: ControllerServiceEntity) => boolean {
return (entity: ControllerServiceEntity) =>
currentUser.controllerPermissions.canRead && currentUser.controllerPermissions.canWrite;
}
selectControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
selectControllerService({

View File

@ -121,8 +121,13 @@
(click)="deleteClicked(item)"
title="Delete"></div>
<div class="pointer fa fa-tasks" *ngIf="canViewState(item)" title="View State"></div>
<div
class="pointer fa fa-key"
*ngIf="canManageAccessPolicies()"
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)"
title="Access Policies"></div>
</div>
<div class="pointer fa fa-key" *ngIf="canManageAccessPolicies()" title="Access Policies"></div>
</td>
</ng-container>

View File

@ -24,6 +24,8 @@ import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bu
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../../../../../state/shared';
import { FlowConfiguration } from '../../../../../state/flow-configuration';
import { CurrentUser } from '../../../../../state/current-user';
@Component({
selector: 'reporting-task-table',
@ -48,6 +50,8 @@ export class ReportingTaskTable implements AfterViewInit {
};
}
@Input() selectedReportingTaskId!: string;
@Input() flowConfiguration!: FlowConfiguration;
@Input() currentUser!: CurrentUser;
@Output() selectReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@ -214,7 +218,8 @@ export class ReportingTaskTable implements AfterViewInit {
}
canDelete(entity: ReportingTaskEntity): boolean {
const canWriteParent: boolean = true; // TODO canModifyController()
const canWriteParent: boolean =
this.currentUser.controllerPermissions.canRead && this.currentUser.controllerPermissions.canWrite;
return (
(this.isDisabled(entity) || this.isStopped(entity)) &&
this.canRead(entity) &&
@ -237,8 +242,11 @@ export class ReportingTaskTable implements AfterViewInit {
}
canManageAccessPolicies(): boolean {
// TODO
return false;
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
getPolicyLink(entity: ReportingTaskEntity): string[] {
return ['/access-policies', 'read', 'component', 'reporting-tasks', entity.id];
}
select(entity: ReportingTaskEntity): void {

View File

@ -30,6 +30,8 @@
<reporting-task-table
[selectedReportingTaskId]="selectedReportingTaskId$ | async"
[reportingTasks]="reportingTaskState.reportingTasks"
[currentUser]="currentUser"
[flowConfiguration]="(flowConfiguration$ | async)!"
(configureReportingTask)="configureReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)"
(deleteReportingTask)="deleteReportingTask($event)"

View File

@ -40,6 +40,8 @@ import {
import { initialState } from '../../state/reporting-tasks/reporting-tasks.reducer';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'reporting-tasks',
@ -50,6 +52,7 @@ export class ReportingTasks implements OnInit, OnDestroy {
reportingTaskState$ = this.store.select(selectReportingTasksState);
selectedReportingTaskId$ = this.store.select(selectReportingTaskIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
constructor(private store: Store<NiFiState>) {
this.store
@ -79,6 +82,7 @@ export class ReportingTasks implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadReportingTasks());
}

View File

@ -24,6 +24,7 @@ import { MatTableModule } from '@angular/material/table';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { ReportingTaskTable } from './reporting-task-table/reporting-task-table.component';
import { ControllerServiceTable } from '../../../../ui/common/controller-service/controller-service-table/controller-service-table.component';
import { RouterLink } from '@angular/router';
@NgModule({
declarations: [ReportingTasks, ReportingTaskTable],
@ -34,7 +35,8 @@ import { ControllerServiceTable } from '../../../../ui/common/controller-service
MatSortModule,
MatTableModule,
NifiTooltipDirective,
ControllerServiceTable
ControllerServiceTable,
RouterLink
]
})
export class ReportingTasksModule {}

View File

@ -23,17 +23,19 @@
<ng-template #loaded>
<div class="flex flex-col h-full gap-y-2">
<div class="flex-1" *ngIf="currentUser$ | async as user">
<user-table
[tenants]="{ users: userListingState.users, userGroups: userListingState.userGroups }"
[selectedTenantId]="selectedTenantId$ | async"
[currentUser]="(currentUser$ | async)!"
[configurableUsersAndGroups]="true"
(createTenant)="createTenant()"
(selectTenant)="selectTenant($event)"
(editTenant)="editTenant($event)"
(deleteUser)="deleteUser($event)"
(deleteUserGroup)="deleteUserGroup($event)"
(viewAccessPolicies)="viewAccessPolicies($event)"></user-table>
<ng-container *ngIf="(flowConfiguration$ | async)! as flowConfiguration">
<user-table
[tenants]="{ users: userListingState.users, userGroups: userListingState.userGroups }"
[selectedTenantId]="selectedTenantId$ | async"
[currentUser]="user"
[configurableUsersAndGroups]="flowConfiguration.supportsConfigurableUsersAndGroups"
(createTenant)="createTenant()"
(selectTenant)="selectTenant($event)"
(editTenant)="editTenant($event)"
(deleteUser)="deleteUser($event)"
(deleteUserGroup)="deleteUserGroup($event)"
(viewAccessPolicies)="viewAccessPolicies($event)"></user-table>
</ng-container>
</div>
<div class="flex justify-between">
<div class="refresh-container flex items-center gap-x-2">

View File

@ -42,6 +42,8 @@ import {
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UserEntity, UserGroupEntity } from '../../../../state/shared';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'user-listing',
@ -49,6 +51,7 @@ import { UserEntity, UserGroupEntity } from '../../../../state/shared';
styleUrls: ['./user-listing.component.scss']
})
export class UserListing implements OnInit {
flowConfiguration$ = this.store.select(selectFlowConfiguration);
userListingState$ = this.store.select(selectUserListingState);
selectedTenantId$ = this.store.select(selectTenantIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
@ -124,6 +127,7 @@ export class UserListing implements OnInit {
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(loadTenants());
}

View File

@ -19,24 +19,11 @@ import { NgModule } from '@angular/core';
import { UserListing } from './user-listing.component';
import { CommonModule } from '@angular/common';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { UserTable } from './user-table/user-table.component';
@NgModule({
declarations: [UserListing, UserTable],
declarations: [UserListing],
exports: [UserListing],
imports: [
CommonModule,
NgxSkeletonLoaderModule,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule
]
imports: [CommonModule, NgxSkeletonLoaderModule, UserTable]
})
export class UserListingModule {}

View File

@ -172,8 +172,8 @@ describe('UserTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserTable],
imports: [
UserTable,
MatTableModule,
MatSortModule,
MatInputModule,

View File

@ -16,13 +16,17 @@
*/
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Sort } from '@angular/material/sort';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { debounceTime } from 'rxjs';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { CurrentUser } from '../../../../../state/current-user';
import { AccessPolicySummaryEntity, UserEntity, UserGroupEntity } from '../../../../../state/shared';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { NgIf } from '@angular/common';
import { MatInputModule } from '@angular/material/input';
export interface TenantItem {
id: string;
@ -39,7 +43,17 @@ export interface Tenants {
@Component({
selector: 'user-table',
standalone: true,
templateUrl: './user-table.component.html',
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
NgIf,
MatTableModule,
MatSortModule,
MatInputModule
],
styleUrls: ['./user-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class UserTable implements AfterViewInit {

View File

@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class FlowConfigurationService {
private static readonly API: string = '../nifi-api';
constructor(private httpClient: HttpClient) {}
getFlowConfiguration(): Observable<any> {
return this.httpClient.get(`${FlowConfigurationService.API}/flow/config`);
}
}

View File

@ -33,7 +33,7 @@ export const authorizationGuard = (authorizationCheck: (user: CurrentUser) => bo
return true;
}
// TODO - replace with 404 error page
// TODO - replace with 403 error page
return router.parseUrl('/');
})
);

View File

@ -0,0 +1,43 @@
/*
* 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 { CanMatchFn, Route, Router, UrlSegment } from '@angular/router';
import { inject } from '@angular/core';
import { map } from 'rxjs';
import { Store } from '@ngrx/store';
import { FlowConfiguration, FlowConfigurationState } from '../../state/flow-configuration';
import { selectFlowConfiguration } from '../../state/flow-configuration/flow-configuration.selectors';
export const checkFlowConfiguration = (
flowConfigurationCheck: (flowConfiguration: FlowConfiguration) => boolean
): CanMatchFn => {
return (route: Route, state: UrlSegment[]) => {
const router: Router = inject(Router);
const store: Store<FlowConfigurationState> = inject(Store<FlowConfigurationState>);
return store.select(selectFlowConfiguration).pipe(
map((flowConfiguration) => {
if (flowConfiguration && flowConfigurationCheck(flowConfiguration)) {
return true;
}
// TODO - replace with 409 error page
return router.parseUrl('/');
})
);
};
};

View File

@ -504,4 +504,11 @@ export class NiFiCommon {
public getPolicyTypeListing(value: string): SelectOption | undefined {
return this.policyTypeListing.find((policy: SelectOption) => value === policy.value);
}
/**
* Gets all policy types for every global resource.
*/
public getAllPolicyTypeListing(): SelectOption[] {
return this.policyTypeListing;
}
}

View File

@ -36,7 +36,7 @@ export class AboutEffects {
this.aboutService.getAbout().pipe(
map((response) =>
AboutActions.loadAboutSuccess({
response: response
response
})
),
catchError((error) => of(AboutActions.aboutApiError({ error: error.error })))

View File

@ -16,7 +16,11 @@
*/
import { createAction, props } from '@ngrx/store';
import { LoadExtensionTypesForCanvasResponse, LoadExtensionTypesForSettingsResponse } from './index';
import {
LoadExtensionTypesForCanvasResponse,
LoadExtensionTypesForPoliciesResponse,
LoadExtensionTypesForSettingsResponse
} from './index';
export const loadExtensionTypesForCanvas = createAction('[Extension Types] Load Extension Types For Canvas');
@ -32,6 +36,13 @@ export const loadExtensionTypesForSettingsSuccess = createAction(
props<{ response: LoadExtensionTypesForSettingsResponse }>()
);
export const loadExtensionTypesForPolicies = createAction('[Extension Types] Load Extension Types For Policies');
export const loadExtensionTypesForPoliciesSuccess = createAction(
'[Extension Types] Load Extension Types For Canvas Success',
props<{ response: LoadExtensionTypesForPoliciesResponse }>()
);
export const extensionTypesApiError = createAction(
'[Extension Types] Extension Types Api Error',
props<{ error: string }>()

View File

@ -86,4 +86,39 @@ export class ExtensionTypesEffects {
)
)
);
loadExtensionTypesForPolicies$ = createEffect(() =>
this.actions$.pipe(
ofType(ExtensionTypesActions.loadExtensionTypesForPolicies),
switchMap(() =>
combineLatest([
this.extensionTypesService.getProcessorTypes(),
this.extensionTypesService.getControllerServiceTypes(),
this.extensionTypesService.getReportingTaskTypes(),
this.extensionTypesService.getParameterProviderTypes(),
this.extensionTypesService.getFlowAnalysisRuleTypes()
]).pipe(
map(
([
processorTypes,
controllerServiceTypes,
reportingTaskTypes,
parameterProviderTypes,
flowAnalysisRuleTypes
]) =>
ExtensionTypesActions.loadExtensionTypesForPoliciesSuccess({
response: {
processorTypes: processorTypes.processorTypes,
controllerServiceTypes: controllerServiceTypes.controllerServiceTypes,
reportingTaskTypes: reportingTaskTypes.reportingTaskTypes,
parameterProviderTypes: parameterProviderTypes.parameterProviderTypes,
flowAnalysisRuleTypes: flowAnalysisRuleTypes.flowAnalysisRuleTypes
}
})
),
catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error: error.error })))
)
)
)
);
}

Some files were not shown because too many files have changed in this diff Show More