From 2496f2b4b21d631aaf8c23c65e06819b9d187440 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Wed, 25 Dec 2024 13:11:53 +0100 Subject: [PATCH] mgr/dashboard: smb join-auth and usersgroups resources listing Fixes: https://tracker.ceph.com/issues/69361 Signed-off-by: Pedro Gonzalez Gomez --- src/pybind/mgr/dashboard/controllers/smb.py | 79 +++++++++ .../frontend/src/app/app-routing.module.ts | 4 +- .../smb-cluster-form.component.ts | 4 +- .../smb-cluster-tabs.component.spec.ts | 4 +- .../smb-join-auth-list.component.html | 9 + .../smb-join-auth-list.component.scss | 0 .../smb-join-auth-list.component.spec.ts | 27 +++ .../smb-join-auth-list.component.ts | 69 ++++++++ .../ceph/smb/smb-tabs/smb-tabs.component.html | 66 ++++++++ .../ceph/smb/smb-tabs/smb-tabs.component.scss | 0 .../smb/smb-tabs/smb-tabs.component.spec.ts | 38 +++++ .../ceph/smb/smb-tabs/smb-tabs.component.ts | 24 +++ .../smb-usersgroups-details.component.html | 19 +++ .../smb-usersgroups-details.component.scss | 0 .../smb-usersgroups-details.component.spec.ts | 22 +++ .../smb-usersgroups-details.component.ts | 24 +++ .../smb-usersgroups-list.component.html | 28 ++++ .../smb-usersgroups-list.component.scss | 0 .../smb-usersgroups-list.component.spec.ts | 27 +++ .../smb-usersgroups-list.component.ts | 83 +++++++++ .../frontend/src/app/ceph/smb/smb.model.ts | 41 ++++- .../frontend/src/app/ceph/smb/smb.module.ts | 20 ++- .../src/app/shared/api/smb.service.spec.ts | 18 +- .../src/app/shared/api/smb.service.ts | 16 +- src/pybind/mgr/dashboard/openapi.yaml | 157 ++++++++++++++++++ src/pybind/mgr/dashboard/tests/test_smb.py | 102 +++++++++++- 26 files changed, 862 insertions(+), 19 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py index 5a3592a232f..ae06b82ad60 100644 --- a/src/pybind/mgr/dashboard/controllers/smb.py +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -88,6 +88,41 @@ SHARE_SCHEMA = { }, "Configuration for the CephFS share") } +JOIN_AUTH_SCHEMA = { + "resource_type": (str, "ceph.smb.join.auth"), + "auth_id": (str, "Unique identifier for the join auth resource"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "auth": ({ + "username": (str, "Username for authentication"), + "password": (str, "Password for authentication") + }, "Authentication credentials"), + "linked_to_cluster": (str, "Optional string containing a cluster ID. \ + If set, the resource is linked to the cluster and will be automatically removed \ + when the cluster is removed") +} + +LIST_JOIN_AUTH_SCHEMA = [JOIN_AUTH_SCHEMA] + +USERSGROUPS_SCHEMA = { + "resource_type": (str, "ceph.smb.usersgroups"), + "users_groups_id": (str, "A short string identifying the usersgroups resource"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "values": ({ + "users": ([{ + "name": (str, "The user name"), + "password": (str, "The password for the user") + }], "List of user objects, each containing a name and password"), + "groups": ([{ + "name": (str, "The name of the group") + }], "List of group objects, each containing a name") + }, "Required object containing users and groups information"), + "linked_to_cluster": (str, "Optional string containing a cluster ID. \ + If set, the resource is linked to the cluster and will be automatically removed \ + when the cluster is removed") +} + +LIST_USERSGROUPS_SCHEMA = [USERSGROUPS_SCHEMA] + def raise_on_failure(func): @wraps(func) @@ -227,6 +262,50 @@ class SMBShare(RESTController): return mgr.remote('smb', 'apply_resources', json.dumps(resource)).one().to_simplified() +@APIRouter('/smb/joinauth', Scope.SMB) +@APIDoc("SMB Join Auth API", "SMB") +class SMBJoinAuth(RESTController): + _resource: str = 'ceph.smb.join.auth' + + @ReadPermission + @EndpointDoc("List smb join authorization resources", + responses={200: LIST_JOIN_AUTH_SCHEMA}) + def list(self, join_auth: str = '') -> List[Share]: + """ + List all smb join auth resources + + :return: Returns list of join auth. + :rtype: List[Dict] + """ + res = mgr.remote( + 'smb', + 'show', + [f'{self._resource}.{join_auth}' if join_auth else self._resource]) + return res['resources'] if 'resources' in res else [res] + + +@APIRouter('/smb/usersgroups', Scope.SMB) +@APIDoc("SMB Users Groups API", "SMB") +class SMBUsersgroups(RESTController): + _resource: str = 'ceph.smb.usersgroups' + + @ReadPermission + @EndpointDoc("List smb user resources", + responses={200: LIST_USERSGROUPS_SCHEMA}) + def list(self, users_groups: str = '') -> List[Share]: + """ + List all smb usersgroups resources + + :return: Returns list of usersgroups. + :rtype: List[Dict] + """ + res = mgr.remote( + 'smb', + 'show', + [f'{self._resource}.{users_groups}' if users_groups else self._resource]) + return res['resources'] if 'resources' in res else [res] + + @UIRouter('/smb') class SMBStatus(RESTController): @EndpointDoc("Get SMB feature status") diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index f7a139e5b9d..49d02b430dd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -51,8 +51,8 @@ import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progres import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component'; import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component'; import { MultiClusterDetailsComponent } from './ceph/cluster/multi-cluster/multi-cluster-details/multi-cluster-details.component'; -import { SmbClusterListComponent } from './ceph/smb/smb-cluster-list/smb-cluster-list.component'; import { SmbClusterFormComponent } from './ceph/smb/smb-cluster-form/smb-cluster-form.component'; +import { SmbTabsComponent } from './ceph/smb/smb-tabs/smb-tabs.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -446,7 +446,7 @@ const routes: Routes = [ breadcrumbs: 'File/SMB' }, children: [ - { path: '', component: SmbClusterListComponent }, + { path: '', component: SmbTabsComponent }, { path: `${URLVerbs.CREATE}`, component: SmbClusterFormComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts index da7b05713fa..5bebe3cc435 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts @@ -9,7 +9,7 @@ import { CLUSTERING, PLACEMENT, RequestModel, - RESOURCE_TYPE, + CLUSTER_RESOURCE, RESOURCE, DomainSettings, JoinSource @@ -214,7 +214,7 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { const requestModel: RequestModel = { cluster_resource: { - resource_type: RESOURCE_TYPE, + resource_type: CLUSTER_RESOURCE, cluster_id: rawFormValue.cluster_id, auth_mode: rawFormValue.auth_mode } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts index d5d302bf022..c9c8bb9c524 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SmbClusterTabsComponent } from './smb-cluster-tabs.component'; -import { RESOURCE_TYPE, SMBCluster } from '../smb.model'; +import { CLUSTER_RESOURCE, SMBCluster } from '../smb.model'; import { By } from '@angular/platform-browser'; describe('SmbClusterTabsComponent', () => { @@ -32,7 +32,7 @@ describe('SmbClusterTabsComponent', () => { const selectedSmbCluster = (clusterId: string) => { const smbCluster: SMBCluster = { - resource_type: RESOURCE_TYPE, + resource_type: CLUSTER_RESOURCE, cluster_id: clusterId, auth_mode: 'user' }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html new file mode 100644 index 00000000000..f1818e7ae3a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html @@ -0,0 +1,9 @@ + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.spec.ts new file mode 100644 index 00000000000..d74f0ef3903 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SmbJoinAuthListComponent } from './smb-join-auth-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('SmbJoinAuthListComponent', () => { + let component: SmbJoinAuthListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SmbJoinAuthListComponent], + imports: [SharedModule, HttpClientTestingModule, ToastrModule.forRoot(), RouterTestingModule] + }).compileComponents(); + + fixture = TestBed.createComponent(SmbJoinAuthListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts new file mode 100644 index 00000000000..f45cda1084f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable, BehaviorSubject, of } from 'rxjs'; +import { switchMap, catchError } from 'rxjs/operators'; +import { SmbService } from '~/app/shared/api/smb.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { Permission } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { SMBJoinAuth } from '../smb.model'; + +@Component({ + selector: 'cd-smb-join-auth-list', + templateUrl: './smb-join-auth-list.component.html', + styleUrls: ['./smb-join-auth-list.component.scss'] +}) +export class SmbJoinAuthListComponent implements OnInit { + columns: CdTableColumn[]; + permission: Permission; + tableActions: CdTableAction[]; + context: CdTableFetchDataContext; + + joinAuth$: Observable; + subject$ = new BehaviorSubject([]); + + constructor( + private authStorageService: AuthStorageService, + public actionLabels: ActionLabelsI18n, + private smbService: SmbService + ) { + this.permission = this.authStorageService.getPermissions().smb; + } + + ngOnInit() { + this.columns = [ + { + name: $localize`ID`, + prop: 'auth_id', + flexGrow: 2 + }, + { + name: $localize`Username`, + prop: 'auth.username', + flexGrow: 2 + }, + { + name: $localize`Linked to Cluster`, + prop: 'linked_to_cluster', + flexGrow: 2 + } + ]; + + this.joinAuth$ = this.subject$.pipe( + switchMap(() => + this.smbService.listJoinAuths().pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ) + ) + ); + } + + loadJoinAuth() { + this.subject$.next([]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html new file mode 100644 index 00000000000..d2b6f3aac06 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html @@ -0,0 +1,66 @@ + + Clusters + + Logical management units that may map to one or more managed Samba service + + + + + Active directory access resources + + Logical management units for authorization on Active Directory servers + + + + + Standalone access resources + + Logical management units for authorization on Standalone servers + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.spec.ts new file mode 100644 index 00000000000..f5f41da37d0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SmbTabsComponent } from './smb-tabs.component'; +import { By } from '@angular/platform-browser'; + +describe('SmbTabsComponent', () => { + let component: SmbTabsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SmbTabsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SmbTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the heading text in the tab', () => { + const tabs = fixture.debugElement.queryAll(By.css('cds-tab')); + expect(tabs.length).toBe(3); + expect(tabs[0].attributes['heading']).toBe('Clusters'); + expect(tabs[1].attributes['heading']).toBe('Active Directory'); + expect(tabs[2].attributes['heading']).toBe('Standalone'); + }); + + // If the tabs cacheActive is set to true data for all tabs will be fetched at once, + // smb mgr module might hit recursive error when doing multiple request to the db + it('should have cache disabled', () => { + const tabs = fixture.nativeElement.querySelector('cds-tabs'); + expect(tabs.cacheActive).toBeFalsy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.ts new file mode 100644 index 00000000000..48a073e2f0d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +enum TABS { + 'clusters', + 'joinAuths', + 'usersgroups' +} + +@Component({ + selector: 'cd-smb-tabs', + templateUrl: './smb-tabs.component.html', + styleUrls: ['./smb-tabs.component.scss'] +}) +export class SmbTabsComponent { + selectedTab: TABS; + + onSelected(tab: TABS) { + this.selectedTab = tab; + } + + public get Tabs(): typeof TABS { + return TABS; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.html new file mode 100644 index 00000000000..49063b1bd33 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.html @@ -0,0 +1,19 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.spec.ts new file mode 100644 index 00000000000..0f67b20df74 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SmbUsersgroupsDetailsComponent } from './smb-usersgroups-details.component'; + +describe('SmbUsersgroupsDetailsComponent', () => { + let component: SmbUsersgroupsDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SmbUsersgroupsDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SmbUsersgroupsDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.ts new file mode 100644 index 00000000000..c082c3da28d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.ts @@ -0,0 +1,24 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { SMBUsersGroups } from '../smb.model'; + +@Component({ + selector: 'cd-smb-usersgroups-details', + templateUrl: './smb-usersgroups-details.component.html', + styleUrls: ['./smb-usersgroups-details.component.scss'] +}) +export class SmbUsersgroupsDetailsComponent implements OnInit { + @Input() + selection: SMBUsersGroups; + columns: CdTableColumn[]; + + ngOnInit() { + this.columns = [ + { + name: $localize`Username`, + prop: 'name', + flexGrow: 2 + } + ]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html new file mode 100644 index 00000000000..7aa7ee37d31 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html @@ -0,0 +1,28 @@ + + + + + + + + + {{ group.name }} + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.spec.ts new file mode 100644 index 00000000000..6d15c800122 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SmbUsersgroupsListComponent } from './smb-usersgroups-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('SmbUsersgroupsListComponent', () => { + let component: SmbUsersgroupsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SmbUsersgroupsListComponent], + imports: [SharedModule, HttpClientTestingModule, ToastrModule.forRoot(), RouterTestingModule] + }).compileComponents(); + + fixture = TestBed.createComponent(SmbUsersgroupsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts new file mode 100644 index 00000000000..869a21115da --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts @@ -0,0 +1,83 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { catchError, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; + +import _ from 'lodash'; + +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; +import { Permission } from '~/app/shared/models/permissions'; + +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { SmbService } from '~/app/shared/api/smb.service'; +import { SMBUsersGroups } from '../smb.model'; + +@Component({ + selector: 'cd-smb-users-list', + templateUrl: './smb-usersgroups-list.component.html', + styleUrls: ['./smb-usersgroups-list.component.scss'] +}) +export class SmbUsersgroupsListComponent extends ListWithDetails implements OnInit { + @ViewChild('groupsNamesTpl', { static: true }) + groupsNamesTpl: TemplateRef; + columns: CdTableColumn[]; + permission: Permission; + tableActions: CdTableAction[]; + context: CdTableFetchDataContext; + + usersGroups$: Observable; + subject$ = new BehaviorSubject([]); + + constructor( + private authStorageService: AuthStorageService, + public actionLabels: ActionLabelsI18n, + private smbService: SmbService + ) { + super(); + this.permission = this.authStorageService.getPermissions().smb; + } + + ngOnInit() { + this.columns = [ + { + name: $localize`ID`, + prop: 'users_groups_id', + flexGrow: 2 + }, + { + name: $localize`Number of users`, + prop: 'values.users.length', + flexGrow: 2 + }, + { + name: $localize`Groups`, + prop: 'values.groups', + cellTemplate: this.groupsNamesTpl, + flexGrow: 2 + }, + { + name: $localize`Linked to`, + prop: 'values.linked_to_cluster', + flexGrow: 2 + } + ]; + + this.usersGroups$ = this.subject$.pipe( + switchMap(() => + this.smbService.listUsersGroups().pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ) + ) + ); + } + + loadUsersGroups() { + this.subject$.next([]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index 87cc3f12883..3c1286da775 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -52,8 +52,6 @@ export const PLACEMENT = { label: 'label' }; -export const RESOURCE_TYPE = 'ceph.smb.cluster'; - export interface SMBShare { cluster_id: string; share_id: string; @@ -79,3 +77,42 @@ interface SMBShareLoginControl { access: 'read' | 'read-write' | 'none' | 'admin'; category?: 'user' | 'group'; } + +export interface SMBJoinAuth { + resource_type: string; + auth_id: string; + intent: Intent; + auth: Auth; + linked_to_cluster?: string; +} + +export interface SMBUsersGroups { + resource_type: string; + users_groups_id: string; + intent: Intent; + values: Value; + linked_to_cluster?: string; +} + +interface Auth { + username: string; + password: string; +} + +interface User { + name: string; + password: string; +} + +interface Group { + name: string; +} + +interface Value { + users: User[]; + groups: Group[]; +} + +type Intent = 'present' | 'removed'; + +export const CLUSTER_RESOURCE = 'ceph.smb.cluster'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts index f96504de412..2211e8629bb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts @@ -2,7 +2,6 @@ import Close from '@carbon/icons/es/close/32'; import { SmbClusterListComponent } from './smb-cluster-list/smb-cluster-list.component'; import { SmbClusterFormComponent } from './smb-cluster-form/smb-cluster-form.component'; import { AppRoutingModule } from '~/app/app-routing.module'; -import { provideCharts, withDefaultRegisterables, BaseChartDirective } from 'ng2-charts'; import { DataTableModule } from '~/app/shared/datatable/datatable.module'; import { SmbDomainSettingModalComponent } from './smb-domain-setting-modal/smb-domain-setting-modal.component'; import { SmbClusterTabsComponent } from './smb-cluster-tabs/smb-cluster-tabs.component'; @@ -21,7 +20,8 @@ import { NumberModule, PlaceholderModule, SelectModule, - TabsModule + TabsModule, + TagModule } from 'carbon-components-angular'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @@ -29,6 +29,11 @@ import { SharedModule } from '~/app/shared/shared.module'; import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; +import { SmbUsersgroupsListComponent } from './smb-usersgroups-list/smb-usersgroups-list.component'; +import { SmbTabsComponent } from './smb-tabs/smb-tabs.component'; +import { SmbJoinAuthListComponent } from './smb-join-auth-list/smb-join-auth-list.component'; +import { SmbUsersgroupsDetailsComponent } from './smb-usersgroups-details/smb-usersgroups-details.component'; + @NgModule({ imports: [ ReactiveFormsModule, @@ -36,7 +41,6 @@ import { NgModule } from '@angular/core'; CommonModule, SharedModule, AppRoutingModule, - BaseChartDirective, CommonModule, FormsModule, ReactiveFormsModule, @@ -44,6 +48,7 @@ import { NgModule } from '@angular/core'; GridModule, SelectModule, TabsModule, + TagModule, InputModule, CheckboxModule, SelectModule, @@ -62,9 +67,12 @@ import { NgModule } from '@angular/core'; SmbClusterFormComponent, SmbDomainSettingModalComponent, SmbClusterTabsComponent, - SmbShareListComponent - ], - providers: [provideCharts(withDefaultRegisterables())] + SmbShareListComponent, + SmbUsersgroupsListComponent, + SmbUsersgroupsDetailsComponent, + SmbTabsComponent, + SmbJoinAuthListComponent + ] }) export class SmbModule { constructor(private iconService: IconService) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts index b458e2d7796..8beee92bd43 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts @@ -1,16 +1,16 @@ import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { SmbService } from './smb.service'; import { configureTestBed } from '~/testing/unit-test-helper'; +import { provideHttpClient } from '@angular/common/http'; describe('SmbService', () => { let service: SmbService; let httpTesting: HttpTestingController; configureTestBed({ - providers: [SmbService], - imports: [HttpClientTestingModule] + providers: [SmbService, provideHttpClient(), provideHttpClientTesting()] }); beforeEach(() => { @@ -46,4 +46,16 @@ describe('SmbService', () => { const req = httpTesting.expectOne('api/smb/share?cluster_id=tango'); expect(req.request.method).toBe('GET'); }); + + it('should call list join auth', () => { + service.listJoinAuths().subscribe(); + const req = httpTesting.expectOne('api/smb/joinauth'); + expect(req.request.method).toBe('GET'); + }); + + it('should call list usersgroups', () => { + service.listUsersGroups().subscribe(); + const req = httpTesting.expectOne('api/smb/usersgroups'); + expect(req.request.method).toBe('GET'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index 1a175bf53dd..ac2e460b081 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -2,7 +2,13 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { DomainSettings, SMBCluster, SMBShare } from '~/app/ceph/smb/smb.model'; +import { + DomainSettings, + SMBCluster, + SMBJoinAuth, + SMBShare, + SMBUsersGroups +} from '~/app/ceph/smb/smb.model'; @Injectable({ providedIn: 'root' @@ -35,4 +41,12 @@ export class SmbService { listShares(clusterId: string): Observable { return this.http.get(`${this.baseURL}/share?cluster_id=${clusterId}`); } + + listJoinAuths(): Observable { + return this.http.get(`${this.baseURL}/joinauth`); + } + + listUsersGroups(): Observable { + return this.http.get(`${this.baseURL}/usersgroups`); + } } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3f4e9876558..e38c714de02 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -15007,6 +15007,74 @@ paths: summary: Get an smb cluster tags: - SMB + /api/smb/joinauth: + get: + description: "\n List all smb join auth resources\n\n :return:\ + \ Returns list of join auth.\n :rtype: List[Dict]\n " + parameters: + - default: '' + in: query + name: join_auth + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + items: + properties: + auth: + description: Authentication credentials + properties: + password: + description: Password for authentication + type: string + username: + description: Username for authentication + type: string + required: + - username + - password + type: object + auth_id: + description: Unique identifier for the join auth resource + type: string + intent: + description: Desired state of the resource, e.g., 'present' + or 'removed' + type: string + linked_to_cluster: + description: Optional string containing a cluster ID. If + set, the resource is linked to the cluster and will be automatically + removed when the cluster is removed + type: string + resource_type: + description: ceph.smb.join.auth + type: string + type: object + required: + - resource_type + - auth_id + - intent + - auth + - linked_to_cluster + type: array + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: List smb join authorization resources + tags: + - SMB /api/smb/share: get: description: "\n List all smb shares or all shares for a given cluster\n\ @@ -15134,6 +15202,95 @@ paths: summary: Remove an smb share tags: - SMB + /api/smb/usersgroups: + get: + description: "\n List all smb usersgroups resources\n\n :return:\ + \ Returns list of usersgroups.\n :rtype: List[Dict]\n " + parameters: + - default: '' + in: query + name: users_groups + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + items: + properties: + intent: + description: Desired state of the resource, e.g., 'present' + or 'removed' + type: string + linked_to_cluster: + description: Optional string containing a cluster ID. If + set, the resource is linked to the cluster and will be automatically + removed when the cluster is removed + type: string + resource_type: + description: ceph.smb.usersgroups + type: string + users_groups_id: + description: A short string identifying the usersgroups resource + type: string + values: + description: Required object containing users and groups information + properties: + groups: + description: List of group objects, each containing a name + items: + properties: + name: + description: The name of the group + type: string + required: + - name + type: object + type: array + users: + description: List of user objects, each containing a name + and password + items: + properties: + name: + description: The user name + type: string + password: + description: The password for the user + type: string + required: + - name + - password + type: object + type: array + required: + - users + - groups + type: object + type: object + required: + - resource_type + - users_groups_id + - intent + - values + - linked_to_cluster + type: array + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: List smb user resources + tags: + - SMB /api/summary: get: parameters: [] diff --git a/src/pybind/mgr/dashboard/tests/test_smb.py b/src/pybind/mgr/dashboard/tests/test_smb.py index c56a592e2fd..8160f40d3df 100644 --- a/src/pybind/mgr/dashboard/tests/test_smb.py +++ b/src/pybind/mgr/dashboard/tests/test_smb.py @@ -1,7 +1,7 @@ import json from unittest.mock import Mock -from dashboard.controllers.smb import SMBCluster, SMBShare +from dashboard.controllers.smb import SMBCluster, SMBJoinAuth, SMBShare, SMBUsersgroups from .. import mgr from ..tests import ControllerTestCase @@ -219,3 +219,103 @@ class SMBShareTest(ControllerTestCase): self._delete(f'{self._endpoint}/smbCluster1/share1') self.assertStatus(204) mgr.remote.assert_called_once_with('smb', 'apply_resources', json.dumps(_res_simplified)) + + +class SMBJoinAuthTest(ControllerTestCase): + _endpoint = '/api/smb/joinauth' + + _join_auths = { + "resources": [ + { + "resource_type": "ceph.smb.join.auth", + "auth_id": "join1-admin", + "intent": "present", + "auth": { + "username": "Administrator", + "password": "Passw0rd" + } + }, + { + "resource_type": "ceph.smb.join.auth", + "auth_id": "ja2", + "intent": "present", + "auth": { + "username": "user123", + "password": "foobar" + } + } + ] + } + + @classmethod + def setup_server(cls): + cls.setup_controllers([SMBJoinAuth]) + + def test_list_one_join_auth(self): + mgr.remote = Mock(return_value=self._join_auths['resources'][0]) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody([self._join_auths['resources'][0]]) + + def test_list_multiple_clusters(self): + mgr.remote = Mock(return_value=self._join_auths) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody(self._join_auths['resources']) + + +class SMBUsersgroupsTest(ControllerTestCase): + _endpoint = '/api/smb/usersgroups' + + _usersgroups = { + "resources": [ + { + "resource_type": "ceph.smb.usersgroups", + "users_groups_id": "u2", + "intent": "present", + "values": { + "users": [ + { + "name": "user3", + "password": "pass" + } + ], + "groups": [] + } + }, + { + "resource_type": "ceph.smb.usersgroups", + "users_groups_id": "u1", + "intent": "present", + "values": { + "users": [ + { + "name": "user2", + "password": "pass" + } + ], + "groups": [] + } + } + ] + } + + @classmethod + def setup_server(cls): + cls.setup_controllers([SMBUsersgroups]) + + def test_list_one_usersgroups(self): + mgr.remote = Mock(return_value=self._usersgroups['resources'][0]) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody([self._usersgroups['resources'][0]]) + + def test_list_multiple_usersgroups(self): + mgr.remote = Mock(return_value=self._usersgroups) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody(self._usersgroups['resources']) -- 2.39.5