diff --git a/mlflow_oidc_auth/views.py b/mlflow_oidc_auth/views.py index 6c6c46f..9e78d1e 100644 --- a/mlflow_oidc_auth/views.py +++ b/mlflow_oidc_auth/views.py @@ -367,7 +367,16 @@ def create_access_token(): def get_current_user(): user = store.get_user(_get_username()) - return jsonify(user.to_json()) + user_json = user.to_json() + user_json["experiment_permissions"] = [ + { + "name": mlflow_client.get_experiment(permission.experiment_id).name, + "id": permission.experiment_id, + "permission": permission.permission + } + for permission in user.experiment_permissions + ] + return jsonify(user_json) def update_username_password(): diff --git a/web-ui/package.json b/web-ui/package.json index 0112a42..3a203b8 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -1,6 +1,7 @@ { "name": "mlflow-oidc-auth-front", "version": "0.0.0", + "license":"Apache-2.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/web-ui/src/app/app.component.ts b/web-ui/src/app/app.component.ts index 618ff50..eaa5f12 100644 --- a/web-ui/src/app/app.component.ts +++ b/web-ui/src/app/app.component.ts @@ -9,7 +9,7 @@ import { AuthService, DataService } from './shared/services'; export class AppComponent implements OnInit { title = 'mlflow-oidc-auth-front'; - name: string = 'Alex'; + name: string = ''; constructor( private readonly dataService: DataService, @@ -18,8 +18,10 @@ export class AppComponent implements OnInit { } ngOnInit(): void { - this.dataService.getCurrentUser().subscribe(({ username }) => { - this.authService.setUser(username); - }); + this.dataService.getCurrentUser() + .subscribe((userInfo) => { + this.authService.setUserInfo(userInfo); + this.name = userInfo.display_name; + }); } } diff --git a/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.html b/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.html index 4cef41d..5b6cfa8 100644 --- a/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.html +++ b/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.html @@ -1,7 +1,7 @@

Model Access

- @@ -11,14 +11,14 @@

Model Access

[columnConfig]="modelColumnConfig" [data]="modelDataSource" [isActionsActive]="true" - (editEvent)="handleUserEditForModel($event)" + (editEvent)="handleUserEditForModel()" >

Experiment Access

- @@ -28,6 +28,6 @@

Experiment Access

[columnConfig]="experimentColumnConfig" [data]="experimentDataSource" [isActionsActive]="true" - (editEvent)="handleUserEditForExperiment($event)" + (editEvent)="handleUserEditForExperiment()" >
diff --git a/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.ts b/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.ts index 4053d45..4833aa4 100644 --- a/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.ts +++ b/web-ui/src/app/features/admin-page/components/details/user-permission-details/user-permission-details.component.ts @@ -1,7 +1,13 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; -import { EditPermissionsModalComponent, GrantPermissionModalComponent } from '../../../../../shared/components'; +import { + EditPermissionsModalComponent, + GrantPermissionModalComponent, + GrantPermissionModalData, +} from '../../../../../shared/components'; +import { DataService } from '../../../../../shared/services'; +import { filter, switchMap } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'ml-user-permission-details', @@ -9,6 +15,7 @@ import { EditPermissionsModalComponent, GrantPermissionModalComponent } from '.. styleUrls: ['./user-permission-details.component.scss'], }) export class UserPermissionDetailsComponent implements OnInit { + userId: string | null = null; modelColumnConfig = [ { title: 'Modal name', @@ -55,17 +62,18 @@ export class UserPermissionDetailsComponent implements OnInit { constructor( - private router: Router, - private route: ActivatedRoute, - public dialog: MatDialog, + private readonly dialog: MatDialog, + private readonly dataService: DataService, + private readonly route: ActivatedRoute, ) { } ngOnInit(): void { + this.userId = this.route.snapshot.paramMap.get('id'); } - handleUserEditForModel($event: any) { + handleUserEditForModel() { this.dialog .open(EditPermissionsModalComponent) .afterClosed() @@ -74,7 +82,7 @@ export class UserPermissionDetailsComponent implements OnInit { }) } - handleUserEditForExperiment($event: any) { + handleUserEditForExperiment() { this.dialog .open(EditPermissionsModalComponent) .afterClosed() @@ -88,4 +96,50 @@ export class UserPermissionDetailsComponent implements OnInit { .afterClosed() .subscribe(console.log); } + + addModelPermissionToUser() { + this.dataService.getAllModels() + .pipe( + switchMap(({ models }) => this.dialog.open(GrantPermissionModalComponent, { + data: { + type: 'model', + entities: models, + userName: this.userId ? this.userId : '', + } + }) + .afterClosed()), + filter(Boolean), + ) + .subscribe((data) => { + const { entity, permission, user } = data; + this.dataService.createModelPermission({ + user_name: user, + model_name: entity, + new_permission: permission, + }).subscribe(); + }); + } + + addExperimentPermissionToUser() { + this.dataService.getAllExperiments() + .pipe( + switchMap(({ experiments }) => this.dialog.open(GrantPermissionModalComponent, { + data: { + type: 'experiment', + entities: experiments, + userName: this.userId ? this.userId : '', + } + }) + .afterClosed()), + filter(Boolean), + ) + .subscribe((data) => { + const { entity, permission, user } = data; + this.dataService.createExperimentPermission({ + user_name: user, + experiment_name: entity, + new_permission: permission, + }).subscribe(); + }); + } } diff --git a/web-ui/src/app/features/home-page/components/home-page/home-page.component.html b/web-ui/src/app/features/home-page/components/home-page/home-page.component.html index f5f9d02..08b5641 100644 --- a/web-ui/src/app/features/home-page/components/home-page/home-page.component.html +++ b/web-ui/src/app/features/home-page/components/home-page/home-page.component.html @@ -8,13 +8,26 @@
- - +

Experiments

+ +

Models

+
-
+ + +
+ You have no experiments +
+
+ + +
+ You have no models +
+
diff --git a/web-ui/src/app/features/home-page/components/home-page/home-page.component.ts b/web-ui/src/app/features/home-page/components/home-page/home-page.component.ts index 9e2eed8..98e150b 100644 --- a/web-ui/src/app/features/home-page/components/home-page/home-page.component.ts +++ b/web-ui/src/app/features/home-page/components/home-page/home-page.component.ts @@ -6,6 +6,7 @@ import { } from '../../../../shared/components'; import { finalize, forkJoin, switchMap } from 'rxjs'; import { AuthService, DataService } from '../../../../shared/services'; +import { ExperimentModel, ModelModel, UserResponseModel } from '../../../../shared/interfaces/data.interfaces'; @Component({ selector: 'ml-home-page', @@ -13,7 +14,7 @@ import { AuthService, DataService } from '../../../../shared/services'; styleUrls: ['./home-page.component.scss'], }) export class HomePageComponent implements OnInit { - currentUser?: string; + currentUserInfo: UserResponseModel | null = null; loading = false; experimentsColumnConfig = [ { @@ -22,11 +23,9 @@ export class HomePageComponent implements OnInit { }, { title: 'Permissions', - key: 'permissions', + key: 'permission', }, ]; - experimentsDataSource = []; - modelsColumnConfig = [ { title: 'Model name', @@ -34,10 +33,12 @@ export class HomePageComponent implements OnInit { }, { title: 'Permissions', - key: 'permissions', + key: 'permission', }, ]; - modelsDataSource = []; + experimentsDataSource: ExperimentModel[] = []; + + modelsDataSource: ModelModel[] = []; constructor( private readonly dialog: MatDialog, @@ -47,21 +48,15 @@ export class HomePageComponent implements OnInit { } ngOnInit(): void { - this.currentUser = this.authService.getUser(); + this.currentUserInfo = this.authService.getUserInfo(); - if (this.currentUser) { - this.loading = true; - forkJoin([ - this.dataService.getExperimentsForUser(this.currentUser), - this.dataService.getModelsForUser(this.currentUser), - ]) - .pipe( - finalize(() => this.loading = false), - ) - .subscribe(([experiments, models]) => { - this.experimentsDataSource = experiments; - this.modelsDataSource = models; - }); + if (this.currentUserInfo) { + const { username } = this.currentUserInfo; + + if (username) { + this.experimentsDataSource = this.currentUserInfo.experiment_permissions; + this.modelsDataSource = this.currentUserInfo.registered_model_permissions; + } } } diff --git a/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.html b/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.html index 4b43779..804b99a 100644 --- a/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.html +++ b/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.html @@ -1,22 +1,26 @@ -

"User name" permissions for model/experiment "name"

- -

Permissions

- - Exp - - - - Permissions - - +

Add {{data.type}} permissions for {{data.userName}}

+ +
+ + {{data.type | titlecase}} + + + + Permissions + + +
+
- - + + diff --git a/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.ts b/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.ts index 88f596e..de52887 100644 --- a/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.ts +++ b/web-ui/src/app/shared/components/grant-permissoin-modal/grant-permission-modal.component.ts @@ -1,4 +1,12 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +export interface GrantPermissionModalData { + userName: string; + type: 'model' | 'experiment'; + entities: string[]; +} @Component({ selector: 'ml-grant-permission-modal', @@ -6,13 +14,20 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./grant-permission-modal.component.scss'] }) export class GrantPermissionModalComponent implements OnInit { + form!: FormGroup; - constructor() { } - - ngOnInit(): void { + constructor( + @Inject(MAT_DIALOG_DATA) public data: GrantPermissionModalData, + private readonly fb: FormBuilder, + ) { } - onNoClick() { - + ngOnInit(): void { + this.form = this.fb.group({ + user: this.data.userName, + type: this.data.type, + permission: [null, Validators.required], + entity: [null, Validators.required], + }) } } diff --git a/web-ui/src/app/shared/interfaces/data.interfaces.ts b/web-ui/src/app/shared/interfaces/data.interfaces.ts new file mode 100644 index 0000000..0e9ca70 --- /dev/null +++ b/web-ui/src/app/shared/interfaces/data.interfaces.ts @@ -0,0 +1,38 @@ +export interface UserResponseModel { + display_name: string; + experiment_permissions: ExperimentModel[]; + id: number; + is_admin: boolean; + registered_model_permissions: ModelModel[]; + username: string; +} + +export interface ExperimentModel { + id: string; + name: string; + permission: string; +} + +export interface ModelModel { + name: string; + permission: string; +} + +export interface ExperimentsResponseModel { + experiments: { + id: string, + name: string, + permissions: string }[] +} + +export interface CreateExperimentPermissionRequestBodyModel { + experiment_name: string; + user_name: string; + new_permission: string; +} + +export interface CreateModelPermissionRequestBodyModel { + "model_name": string; + "user_name": string; + "new_permission": string; +} diff --git a/web-ui/src/app/shared/services/auth.service.ts b/web-ui/src/app/shared/services/auth.service.ts index 8a1caf8..23e6a0a 100644 --- a/web-ui/src/app/shared/services/auth.service.ts +++ b/web-ui/src/app/shared/services/auth.service.ts @@ -1,19 +1,20 @@ import { Injectable } from '@angular/core'; +import { UserResponseModel } from '../interfaces/data.interfaces'; @Injectable({ providedIn: 'root', }) export class AuthService { - private user?: string; + private user: UserResponseModel | null = null; constructor() { } - getUser() { - return this.user; + getUserInfo(): UserResponseModel | null { + return this.user ? this.user : null; } - setUser(user: string) { + setUserInfo(user: UserResponseModel) { this.user = user; } } diff --git a/web-ui/src/app/shared/services/data.service.ts b/web-ui/src/app/shared/services/data.service.ts index d568142..b39c7c9 100644 --- a/web-ui/src/app/shared/services/data.service.ts +++ b/web-ui/src/app/shared/services/data.service.ts @@ -1,5 +1,11 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { + CreateExperimentPermissionRequestBodyModel, CreateModelPermissionRequestBodyModel, + ExperimentsResponseModel, + UserResponseModel, +} from '../interfaces/data.interfaces'; +import { map } from 'rxjs' @Injectable({ providedIn: 'root', @@ -12,7 +18,7 @@ export class DataService { } getCurrentUser() { - return this.http.get<{ username: string }>('/api/2.0/mlflow/users/current'); + return this.http.get('/api/2.0/mlflow/users/current'); } getAccessKey() { @@ -32,10 +38,21 @@ export class DataService { } getExperimentsForUser(userName: string) { - return this.http.get<[]>(`/api/2.0/mlflow/users/${userName}/experiments`); + return this.http.get(`/api/2.0/mlflow/users/${userName}/experiments`) + .pipe( + map(response => response.experiments), + ); } getModelsForUser(userName: string) { return this.http.get<[]>(`/api/2.0/mlflow/users/${userName}/registered-models`); } + + createExperimentPermission(body: CreateExperimentPermissionRequestBodyModel) { + return this.http.post('/api/2.0/mlflow/experiments/permissions/create', body); + } + + createModelPermission(body: CreateModelPermissionRequestBodyModel) { + return this.http.post('/api/2.0/mlflow/registered-models/permissions/create', body); + } } diff --git a/web-ui/src/app/shared/shared.module.ts b/web-ui/src/app/shared/shared.module.ts index e8c761e..d7d94f6 100644 --- a/web-ui/src/app/shared/shared.module.ts +++ b/web-ui/src/app/shared/shared.module.ts @@ -9,7 +9,7 @@ import { TableComponent, } from './components'; import { MaterialModule } from './material/material.module'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TableSearchPipe } from './pipes/table-search.pipe'; import { RouterLinkWithHref } from '@angular/router'; @@ -46,6 +46,7 @@ const SHARED_PIPES = [ NgbModule, RouterLinkWithHref, HttpClientModule, + ReactiveFormsModule, ], }) export class SharedModule { }