Skip to content

Commit

Permalink
feat(ui,api): allow maintainer to get services status (#5638)
Browse files Browse the repository at this point in the history
  • Loading branch information
richardlt authored Jan 14, 2021
1 parent d1af983 commit 3fc30e4
Show file tree
Hide file tree
Showing 39 changed files with 346 additions and 267 deletions.
18 changes: 9 additions & 9 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (api *API) InitRouter() {

// Auth
r.Handle("/auth/driver", ScopeNone(), r.GET(api.getAuthDriversHandler, service.OverrideAuth(service.NoAuthMiddleware)))
r.Handle("/auth/me", Scope(sdk.AuthConsumerScopeAction), r.GET(api.getAuthMe))
r.Handle("/auth/me", ScopeNone(), r.GET(api.getAuthMe))
r.Handle("/auth/scope", ScopeNone(), r.GET(api.getAuthScopesHandler, service.OverrideAuth(service.NoAuthMiddleware)))
r.Handle("/auth/consumer/local/signup", ScopeNone(), r.POST(api.postAuthLocalSignupHandler, service.OverrideAuth(service.NoAuthMiddleware)))
r.Handle("/auth/consumer/local/signin", ScopeNone(), r.POST(api.postAuthLocalSigninHandler, service.OverrideAuth(service.NoAuthMiddleware), MaintenanceAware()))
Expand Down Expand Up @@ -71,11 +71,11 @@ func (api *API) InitRouter() {
r.Handle("/admin/database/migration/unlock/{id}", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.postDatabaseMigrationUnlockedHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/database/migration", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getDatabaseMigrationHandler, service.OverrideAuth(api.authAdminMiddleware)))

r.Handle("/admin/debug/profiles", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getDebugProfilesHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/debug/goroutines", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getDebugGoroutinesHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/debug/trace", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getTraceHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getTraceHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/debug/cpu", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getCPUProfileHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getCPUProfileHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/debug/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getProfileHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getProfileHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/debug/profiles", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getDebugProfilesHandler, service.OverrideAuth(api.authMaintainerMiddleware)))
r.Handle("/admin/debug/goroutines", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getDebugGoroutinesHandler, service.OverrideAuth(api.authMaintainerMiddleware)))
r.Handle("/admin/debug/trace", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getTraceHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getTraceHandler, service.OverrideAuth(api.authMaintainerMiddleware)))
r.Handle("/admin/debug/cpu", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getCPUProfileHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getCPUProfileHandler, service.OverrideAuth(api.authMaintainerMiddleware)))
r.Handle("/admin/debug/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.getProfileHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getProfileHandler, service.OverrideAuth(api.authMaintainerMiddleware)))

r.Handle("/admin/plugin", Scope(sdk.AuthConsumerScopeAdmin), r.POST(api.postGRPCluginHandler, service.OverrideAuth(api.authAdminMiddleware)), r.GET(api.getAllGRPCluginHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/plugin/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getGRPCluginHandler, service.OverrideAuth(api.authAdminMiddleware)), r.PUT(api.putGRPCluginHandler, service.OverrideAuth(api.authAdminMiddleware)), r.DELETE(api.deleteGRPCluginHandler, service.OverrideAuth(api.authAdminMiddleware)))
Expand All @@ -84,9 +84,9 @@ func (api *API) InitRouter() {
r.Handle("/admin/plugin/{name}/binary/{os}/{arch}/infos", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getGRPCluginBinaryInfosHandler))

// Admin service
r.Handle("/admin/service/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServiceHandler, service.OverrideAuth(api.authAdminMiddleware)), r.DELETE(api.deleteAdminServiceHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/services", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServicesHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/services/call", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)), r.POST(api.postAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)), r.PUT(api.putAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)), r.DELETE(api.deleteAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/service/{name}", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServiceHandler, service.OverrideAuth(api.authMaintainerMiddleware)), r.DELETE(api.deleteAdminServiceHandler, service.OverrideAuth(api.authAdminMiddleware)))
r.Handle("/admin/services", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServicesHandler, service.OverrideAuth(api.authMaintainerMiddleware)))
r.Handle("/admin/services/call", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminServiceCallHandler, service.OverrideAuth(api.authMaintainerMiddleware)), r.POST(api.postAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)), r.PUT(api.putAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)), r.DELETE(api.deleteAdminServiceCallHandler, service.OverrideAuth(api.authAdminMiddleware)))

// Admin database
r.Handle("/admin/database/signature", Scope(sdk.AuthConsumerScopeAdmin), r.GET(api.getAdminDatabaseSignatureResume, service.OverrideAuth(api.authAdminMiddleware)))
Expand Down
7 changes: 7 additions & 0 deletions engine/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,14 @@ func (api *API) getAuthMe() service.Handler {
if c == nil || s == nil {
return sdk.WithStack(sdk.ErrUnauthorized)
}

// Clean user and consumer aggregated data
u := *c.AuthentifiedUser
u.Groups = nil
c.AuthentifiedUser = nil

return service.WriteJSON(w, sdk.AuthCurrentConsumerResponse{
User: u,
Consumer: *c,
Session: *s,
}, http.StatusOK)
Expand Down
19 changes: 18 additions & 1 deletion engine/api/router_middleware_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,31 @@ func (api *API) authAdminMiddleware(ctx context.Context, w http.ResponseWriter,
return ctx, err
}

// Excluse consumers not admin or admin that are used for services
// Exclude consumers not admin or admin that are used for services
if !isAdmin(ctx) || isService(ctx) {
return ctx, sdk.WithStack(sdk.ErrForbidden)
}

return ctx, nil
}

func (api *API) authMaintainerMiddleware(ctx context.Context, w http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) {
ctx, end := telemetry.Span(ctx, "router.authMaintainerMiddleware")
defer end()

ctx, err := api.authMiddleware(ctx, w, req, rc)
if err != nil {
return ctx, err
}

// Excluse consumers not maintainer or admin that are used for services
if !isMaintainer(ctx) || isService(ctx) {
return ctx, sdk.WithStack(sdk.ErrForbidden)
}

return ctx, nil
}

func (api *API) authMiddleware(ctx context.Context, w http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) {
ctx, end := telemetry.Span(ctx, "router.authMiddleware")
defer end()
Expand Down
42 changes: 42 additions & 0 deletions engine/api/router_middleware_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,48 @@ func Test_authAdminMiddleware(t *testing.T) {
assert.Equal(t, admin.ID, getAPIConsumer(ctx).AuthentifiedUserID)
}

func Test_authMaintainerMiddleware(t *testing.T) {
api, db, _ := newTestAPI(t)

_, jwtLambda := assets.InsertLambdaUser(t, db)
maintainer, jwtMaintainer := assets.InsertMaintainerUser(t, db)
admin, jwtAdmin := assets.InsertAdminUser(t, db)

config := &service.HandlerConfig{}

req := assets.NewRequest(t, http.MethodGet, "", nil)
w := httptest.NewRecorder()
ctx, err := api.jwtMiddleware(context.TODO(), w, req, config)
require.NoError(t, err)
ctx, err = api.authMaintainerMiddleware(ctx, w, req, config)
assert.Error(t, err, "an error should be returned because no jwt was given and maintainer auth is required")

req = assets.NewJWTAuthentifiedRequest(t, jwtLambda, http.MethodGet, "", nil)
w = httptest.NewRecorder()
ctx, err = api.jwtMiddleware(context.TODO(), w, req, config)
require.NoError(t, err)
ctx, err = api.authMaintainerMiddleware(ctx, w, req, config)
assert.Error(t, err, "an error should be returned because a jwt was given for a lambda user")

req = assets.NewJWTAuthentifiedRequest(t, jwtMaintainer, http.MethodGet, "", nil)
w = httptest.NewRecorder()
ctx, err = api.jwtMiddleware(context.TODO(), w, req, config)
require.NoError(t, err)
ctx, err = api.authMaintainerMiddleware(ctx, w, req, config)
assert.NoError(t, err, "no error should be returned because a jwt was given for an maintainer user")
require.NotNil(t, getAPIConsumer(ctx))
assert.Equal(t, maintainer.ID, getAPIConsumer(ctx).AuthentifiedUserID)

req = assets.NewJWTAuthentifiedRequest(t, jwtAdmin, http.MethodGet, "", nil)
w = httptest.NewRecorder()
ctx, err = api.jwtMiddleware(context.TODO(), w, req, config)
require.NoError(t, err)
ctx, err = api.authMaintainerMiddleware(ctx, w, req, config)
assert.NoError(t, err, "no error should be returned because a jwt was given for an admin user")
require.NotNil(t, getAPIConsumer(ctx))
assert.Equal(t, admin.ID, getAPIConsumer(ctx).AuthentifiedUserID)
}

func Test_authMiddleware_WithAuthConsumerScoped(t *testing.T) {
api, db, _ := newTestAPI(t)

Expand Down
5 changes: 3 additions & 2 deletions sdk/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ type AuthDriverUserInfo struct {

// AuthCurrentConsumerResponse describe the current consumer and the current session
type AuthCurrentConsumerResponse struct {
Consumer AuthConsumer `json:"consumer"`
Session AuthSession `json:"session"`
User AuthentifiedUser `json:"user"`
Consumer AuthConsumer `json:"consumer"`
Session AuthSession `json:"session"`
}

// AuthConsumerType constant to identify what is the driver used to create a consumer.
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="ui active massive text loader" *ngIf="loading">CDS is loading</div>

<div class="app" *ngIf="!loading">
<div class="page" *ngIf="!isAPIAvailable || (maintenance && isConnected && user?.ring !== 'ADMIN')">
<div class="page" *ngIf="!isAPIAvailable || (maintenance && isConnected && currentAuthSummary?.user?.ring !== 'ADMIN')">
<div class="content">
<div class="wrapper loading">
<app-scrollview class="scrollview">
Expand All @@ -17,10 +17,10 @@
</div>
</div>

<ng-container *ngIf="isAPIAvailable && (!isConnected || !maintenance || user?.ring === 'ADMIN')">
<ng-container *ngIf="isAPIAvailable && (!isConnected || !maintenance || currentAuthSummary?.user?.ring === 'ADMIN')">
<app-navbar *ngIf="isConnected && !hideNavBar"></app-navbar>

<div class="banner" *ngIf="(maintenance) && (!isConnected || user?.ring == 'ADMIN')">
<div class="banner" *ngIf="(maintenance) && (!isConnected || currentAuthSummary?.user?.ring == 'ADMIN')">
{{ 'maintenance_title' | translate }}
</div>
<div class="banner update" (click)="refresh()" *ngIf="showUIUpdatedBanner">
Expand Down
10 changes: 5 additions & 5 deletions ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AppService } from './app.service';
import { Application } from './model/application.model';
import { Pipeline } from './model/pipeline.model';
import { Project } from './model/project.model';
import { AuthentifiedUser } from './model/user.model';
import { AuthSummary } from './model/user.model';
import { ApplicationService } from './service/application/application.service';
import { AuthenticationService } from './service/authentication/authentication.service';
import { BroadcastService } from './service/broadcast/broadcast.service';
Expand All @@ -38,7 +38,7 @@ import { TimelineStore } from './service/timeline/timeline.store';
import { UserService } from './service/user/user.service';
import { SharedModule } from './shared/shared.module';
import { ToastService } from './shared/toast/ToastService';
import { FetchCurrentUser } from './store/authentication.action';
import { FetchCurrentAuth } from './store/authentication.action';
import { NgxsStoreModule } from './store/store.module';
import { NavbarModule } from './views/navbar/navbar.module';

Expand Down Expand Up @@ -115,11 +115,11 @@ describe('App: CDS', () => {
http.expectOne((req: HttpRequest<any>) => req.url === '/mon/status').flush(<MonitoringStatus>{});

const store = TestBed.get(Store);
store.dispatch(new FetchCurrentUser());
store.dispatch(new FetchCurrentAuth());


http.expectOne(((req: HttpRequest<any>) => req.url === '/user/me')).flush(<AuthentifiedUser>{
username: 'someone',
http.expectOne(((req: HttpRequest<any>) => req.url === '/auth/me')).flush(<AuthSummary>{
user: { username: 'someone' }
});

expect(fixture.componentInstance.isConnected).toBeTruthy('IsConnected flag must be true');
Expand Down
16 changes: 8 additions & 8 deletions ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { EventService } from 'app/event.service';
import { GetCDSStatus } from 'app/store/cds.action';
import { CDSState } from 'app/store/cds.state';
import { WebSocketSubject } from 'rxjs/internal-compatibility';
import { interval, Observable, of, zip } from 'rxjs';
import { interval, of, zip } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators';
import { Subscription } from 'rxjs/Subscription';
import * as format from 'string-format-obj';
import { AppService } from './app.service';
import { AuthentifiedUser } from './model/user.model';
import { AuthSummary } from './model/user.model';
import { NotificationService } from './service/notification/notification.service';
import { HelpService, MonitoringService } from './service/services.module';
import { ThemeStore } from './service/theme/theme.store';
Expand Down Expand Up @@ -49,7 +49,7 @@ export class AppComponent implements OnInit, OnDestroy {
eventsRouteSubscription: Subscription;
maintenance: boolean;
cdsstateSub: Subscription;
user: AuthentifiedUser;
currentAuthSummary: AuthSummary;
previousURL: string;
websocket: WebSocketSubject<any>;
loading = true;
Expand Down Expand Up @@ -117,15 +117,15 @@ export class AppComponent implements OnInit, OnDestroy {
load(): void {
this._helpService.getHelp().subscribe(h => this._store.dispatch(new AddHelp(h)));
this._store.dispatch(new GetCDSStatus());
this._store.select(AuthenticationState.user).subscribe(user => {
if (!user) {
delete this.user;
this._store.select(AuthenticationState.summary).subscribe(s => {
if (!s) {
this.currentAuthSummary = null;
this.isConnected = false;
this._eventService.stopWebsocket();
} else {
this.user = user;
this.currentAuthSummary = s;
this.isConnected = true;
localStorage.setItem('CDS-USER', this.user.username);
localStorage.setItem('CDS-USER', this.currentAuthSummary.user.username);
this._eventService.startWebsocket();
}
});
Expand Down
18 changes: 9 additions & 9 deletions ui/src/app/guard/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
import { Store } from '@ngxs/store';
import { AuthentifiedUser } from 'app/model/user.model';
import { AuthentifiedUser, AuthSummary } from 'app/model/user.model';
import { AuthenticationState } from 'app/store/authentication.state';
import { Observable } from 'rxjs';
import { filter, first, map } from 'rxjs/operators';

@Injectable()
export class AdminGuard implements CanActivate, CanActivateChild {
export class MaintainerGuard implements CanActivate, CanActivateChild {

constructor(
private _store: Store,
private _router: Router
) { }

isAdmin(): Observable<boolean> {
return this._store.select(AuthenticationState.user)
isMaintainer(): Observable<boolean> {
return this._store.select(AuthenticationState.summary)
.pipe(
map((u: AuthentifiedUser): boolean => {
if (!u) {
map((s: AuthSummary): boolean => {
if (!s) {
return null;
}
if (!u.isAdmin()) {
if (!s.isMaintainer()) {
this._router.navigate(['/']);
return null;
}
Expand All @@ -36,13 +36,13 @@ export class AdminGuard implements CanActivate, CanActivateChild {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.isAdmin();
return this.isMaintainer();
}

canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.isAdmin();
return this.isMaintainer();
}
}
45 changes: 24 additions & 21 deletions ui/src/app/guard/authentication.guard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, NavigationExtras, Router, RouterStateSnapshot } from '@angular/router';
import { Store } from '@ngxs/store';
import { AuthentifiedUser } from 'app/model/user.model';
import { FetchCurrentUser } from 'app/store/authentication.action';
import { FetchCurrentAuth } from 'app/store/authentication.action';
import { AuthenticationState } from 'app/store/authentication.state';
import { Observable } from 'rxjs';
import { filter, first, map } from 'rxjs/operators';
Expand All @@ -15,40 +14,44 @@ export class AuthenticationGuard implements CanActivate, CanActivateChild {
private _router: Router
) { }

getCurrentUser(state: RouterStateSnapshot): Observable<boolean> {
return this._store.select(AuthenticationState.user)
redirectSignin(url: string): void {
this._router.navigate(['/auth/signin'], <NavigationExtras>{
queryParams: {
redirect: url
}
});
}

getCurrentAuth(state: RouterStateSnapshot): Observable<boolean> {
return this._store.select(AuthenticationState.summary)
.pipe(
map((u: AuthentifiedUser): boolean => {
if (!u) {
this._store.dispatch(new FetchCurrentUser()).subscribe(
() => { },
() => {
this._router.navigate(['/auth/signin'], <NavigationExtras>{
queryParams: {
redirect: state.url
}
});
}
);
return null;
map(s => {
if (s) {
return true;
}
return true;
this._store.dispatch(new FetchCurrentAuth()).subscribe(
_ => { },
_ => {
this.redirectSignin(state.url);
});
return null;
}),
filter(exists => exists !== null),
first())
first()
);
}

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.getCurrentUser(state);
return this.getCurrentAuth(state);
}

canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.getCurrentUser(state);
return this.getCurrentAuth(state);
}
}
3 changes: 2 additions & 1 deletion ui/src/app/model/authentication.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class AuthDriverManifest {
}

export class AuthCurrentConsumerResponse {
user: AuthentifiedUser;
consumer: AuthConsumer;
session: AuthSession;
}
Expand Down Expand Up @@ -92,7 +93,7 @@ export class AuthSession {
expire_at: string;
created: string;
current: boolean;
mfa; boolean;
mfa: boolean;

// UI fields
consumer: AuthConsumer;
Expand Down
Loading

0 comments on commit 3fc30e4

Please sign in to comment.