]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: smb join-auth and usersgroups resources listing 61183/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 25 Dec 2024 12:11:53 +0000 (13:11 +0100)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Mon, 10 Feb 2025 05:25:12 +0000 (06:25 +0100)
Fixes: https://tracker.ceph.com/issues/69361
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
26 files changed:
src/pybind/mgr/dashboard/controllers/smb.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-details/smb-usersgroups-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tests/test_smb.py

index 5a3592a232f0e3fa1c7e05fbbad535a28bfa0758..ae06b82ad608b0dbdae96c4f1b6b7716bcb0af1f 100644 (file)
@@ -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")
index f7a139e5b9d9fab6f5ed20e485f8c915c32c65f9..49d02b430dd5fd28faa8a30b8560fa948a673a58 100644 (file)
@@ -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,
index da7b05713faa8c2caf0bad147c6fdc7d226f1a93..5bebe3cc435d0177ac83d36bdf853e18ab58ddbd 100644 (file)
@@ -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
       }
index d5d302bf0224b1e6da5ebd4453bb10b34467179f..c9c8bb9c52423c4df853c008591c88fc20e48f04 100644 (file)
@@ -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 (file)
index 0000000..f1818e7
--- /dev/null
@@ -0,0 +1,9 @@
+<cd-table
+  [data]="joinAuth$ | async"
+  columnMode="flex"
+  [columns]="columns"
+  selectionType="single"
+  [hasDetails]="false"
+  (fetchData)="loadJoinAuth()"
+>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..d74f0ef
--- /dev/null
@@ -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<SmbJoinAuthListComponent>;
+
+  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 (file)
index 0000000..f45cda1
--- /dev/null
@@ -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<SMBJoinAuth[]>;
+  subject$ = new BehaviorSubject<SMBJoinAuth[]>([]);
+
+  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 (file)
index 0000000..d2b6f3a
--- /dev/null
@@ -0,0 +1,66 @@
+<legend
+  *ngIf="selectedTab == Tabs.clusters"
+  i18n
+>
+  Clusters
+  <cd-help-text>
+    Logical management units that may map to one or more managed Samba service
+  </cd-help-text>
+</legend>
+
+<legend
+  *ngIf="selectedTab == Tabs.joinAuths"
+  i18n
+>
+  Active directory access resources
+  <cd-help-text>
+    Logical management units for authorization on Active Directory servers
+  </cd-help-text>
+</legend>
+
+<legend
+  *ngIf="selectedTab == Tabs.usersgroups"
+  i18n
+>
+  Standalone access resources
+  <cd-help-text>
+    Logical management units for authorization on Standalone servers
+  </cd-help-text>
+</legend>
+
+<cds-tabs
+  type="contained"
+  followFocus="true"
+  isNavigation="true"
+  [cacheActive]="false">
+  <cds-tab
+    heading="Clusters"
+    [tabContent]="clusters"
+    i18n-heading
+    (selected)="onSelected(Tabs.clusters)">
+  </cds-tab>
+  <cds-tab
+    heading="Active Directory"
+    [tabContent]="joinAuth"
+    i18n-heading
+    (selected)="onSelected(Tabs.joinAuths)">
+  </cds-tab>
+  <cds-tab
+    heading="Standalone"
+    [tabContent]="usersgroups"
+    i18n-heading
+    (selected)="onSelected(Tabs.usersgroups)">
+  </cds-tab>
+</cds-tabs>
+
+<ng-template #clusters>
+  <cd-smb-cluster-list></cd-smb-cluster-list>
+</ng-template>
+
+<ng-template #joinAuth>
+  <cd-smb-join-auth-list></cd-smb-join-auth-list>
+</ng-template>
+
+<ng-template #usersgroups>
+  <cd-smb-users-list></cd-smb-users-list>
+</ng-template>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..f5f41da
--- /dev/null
@@ -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<SmbTabsComponent>;
+
+  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 (file)
index 0000000..48a073e
--- /dev/null
@@ -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 (file)
index 0000000..49063b1
--- /dev/null
@@ -0,0 +1,19 @@
+<cds-tabs
+  type="contained"
+  followFocus="true"
+  isNavigation="true"
+  [cacheActive]="false"
+>
+  <cds-tab
+    heading="Users"
+    i18n-heading>
+    <cd-table
+      [data]="selection?.values.users"
+      columnMode="flex"
+      [columns]="columns"
+      selectionType="single"
+      [hasDetails]="false"
+    >
+    </cd-table>
+  </cds-tab>
+</cds-tabs>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..0f67b20
--- /dev/null
@@ -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<SmbUsersgroupsDetailsComponent>;
+
+  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 (file)
index 0000000..c082c3d
--- /dev/null
@@ -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 (file)
index 0000000..7aa7ee3
--- /dev/null
@@ -0,0 +1,28 @@
+<ng-container *ngIf="usersGroups$ | async as usersGroups">
+  <cd-table
+    [data]="usersGroups"
+    columnMode="flex"
+    [columns]="columns"
+    selectionType="single"
+    [hasDetails]="true"
+    (setExpandedRow)="setExpandedRow($event)"
+    (fetchData)="loadUsersGroups()"
+>
+  <cd-smb-usersgroups-details
+    *cdTableDetail
+    [selection]="expandedRow"
+  ></cd-smb-usersgroups-details>
+</cd-table>
+</ng-container>
+
+<ng-template
+  #groupsNamesTpl
+  let-row="data.row"
+>
+  <cds-tag
+    *ngFor="let group of row?.values.groups"
+    size="md"
+  >
+    {{ group.name }}
+  </cds-tag>
+</ng-template>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..6d15c80
--- /dev/null
@@ -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<SmbUsersgroupsListComponent>;
+
+  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 (file)
index 0000000..869a211
--- /dev/null
@@ -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<any>;
+  columns: CdTableColumn[];
+  permission: Permission;
+  tableActions: CdTableAction[];
+  context: CdTableFetchDataContext;
+
+  usersGroups$: Observable<SMBUsersGroups[]>;
+  subject$ = new BehaviorSubject<SMBUsersGroups[]>([]);
+
+  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([]);
+  }
+}
index 87cc3f1288358abd444b1c0c99f7eb0b3c718fb4..3c1286da775f03bd1a8a32e89ce9b1bf8307783f 100644 (file)
@@ -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';
index f96504de41225dc875349c7fad9f69a12e6114eb..2211e8629bb4557558b7533785d3711168ff2423 100644 (file)
@@ -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) {
index b458e2d7796e12d1d6a91aff370ba5d2f24358db..8beee92bd4327f8d1e636e93be90f4c01f2d0f6f 100644 (file)
@@ -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');
+  });
 });
index 1a175bf53dddc68aee8d7c53be7f600fc583818c..ac2e460b08139434a3ac781f2a1f26637d840c2f 100644 (file)
@@ -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<SMBShare[]> {
     return this.http.get<SMBShare[]>(`${this.baseURL}/share?cluster_id=${clusterId}`);
   }
+
+  listJoinAuths(): Observable<SMBJoinAuth[]> {
+    return this.http.get<SMBJoinAuth[]>(`${this.baseURL}/joinauth`);
+  }
+
+  listUsersGroups(): Observable<SMBUsersGroups[]> {
+    return this.http.get<SMBUsersGroups[]>(`${this.baseURL}/usersgroups`);
+  }
 }
index 3f4e9876558273aa3dbd32775f47db573ee57eef..e38c714de027b3dcc58b53517a625a9da99af02a 100644 (file)
@@ -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: []
index c56a592e2fd25f734517897b0b7e13d550769aab..8160f40d3df580fff5d591078e1e65b38abd6095 100644 (file)
@@ -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'])