Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,20 @@

</nz-form-item>

<nz-form-item *ngIf="showOTPCodeInput" class="form-field otp-code">
<nz-form-label class="zone-label" nzFor='otpCode' nzNoColon>{{ "OTP Code" | translate }}</nz-form-label>
<nz-form-control class="input-field">
<input
#otpCode
[(ngModel)]="manualAuthInfo.otp_code"
[placeholder]="'OTP Code' | translate"
autocomplete="one-time-code"
inputmode="numeric"
maxlength="16"
name="otpCode"
nz-input
>

Check warning on line 90 in src/app/elements/connect/connect-dialog/select-account/select-account.component.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

DOM elements should use the "autocomplete" attribute correctly.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWo9QveQgd5znXqyA&open=AZ7JWo9QveQgd5znXqyA&pullRequest=1531
</nz-form-control>
</nz-form-item>


Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
}
}

.otp-code {
::ng-deep .ant-input {
width: 70%;
}
}

.input-field {
width: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
return this.accountSelected.username === '@INPUT' || this.accountSelected.username === '@USER';
}

get showOTPCodeInput() {
return !!this.accountSelected &&
this.accountSelected.username !== '@ANON' &&
!!this.accountSelected.has_otp_secret_key;
}

public compareFn = (f1: Account, f2: Account) => {
if (!f1 || !f2) return false;
return f1.alias === f2.alias && f1.id === f2.id;
Expand Down Expand Up @@ -298,6 +304,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
if (!this.accountSelected) {
return;
}
this.manualAuthInfo.otp_code = '';
if (this.accountSelected.has_secret) {
return;
}
Expand All @@ -317,6 +324,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
}
if (this.localAuthItems && this.localAuthItems.length > 0) {
this.manualAuthInfo = Object.assign(this.manualAuthInfo, this.localAuthItems[0]);
this.manualAuthInfo.otp_code = '';
}
this.setUsernamePlaceholder();
setTimeout(() => {
Expand All @@ -342,6 +350,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
this.filteredOptions = this.localAuthItems.filter(authInfo => {
if (authInfo.username.toLowerCase() === filterValue) {
this.manualAuthInfo = Object.assign(this.manualAuthInfo, authInfo);
this.manualAuthInfo.otp_code = '';
}
return authInfo.username.toLowerCase().includes(filterValue);
});
Expand Down
2 changes: 2 additions & 0 deletions src/app/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Account {
name: string;
username: string;
has_secret: boolean;
has_otp_secret_key: boolean;
secret: string;
actions: Array<Action>;
id?: string;
Expand Down Expand Up @@ -401,6 +402,7 @@ export class AuthInfo {
alias: string;
username: string;
secret: string;
otp_code: string;
rememberAuth: boolean;
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/services/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export class AppService {
for (const auth of auths) {
const newAuth = Object.assign({}, auth);
newAuth.secret = this.decrypt(newAuth.secret);
newAuth.otp_code = '';
newAuths.push(newAuth);
}
return newAuths;
Expand All @@ -356,6 +357,7 @@ export class AppService {
setAccountLocalAuth(asset: Asset, account: Account, auth: AuthInfo) {
const assetId = asset.id;
const newAuth = Object.assign({ alias: account.alias, username: account.username }, auth);
newAuth.otp_code = '';

// 如果 auth.alias 是 undefined,保持使用 account.alias
if (auth.alias === undefined && account.alias !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@
<div class="loading-bar-progress"></div>
</div>
</ng-container>

<ng-container *ngIf="content.customContent?.type === 'otp_code'">
<div class="otp-code-box">
<input
#otpCodeInput
[(ngModel)]="otpCode"
(ngModelChange)="onOTPCodeInput()"
[placeholder]="'OTP Code' | translate"
autocomplete="one-time-code"
inputmode="numeric"
maxlength="16"
nz-input
/>

Check warning on line 44 in src/app/services/connect-token/acl-dialog/acl-dialog.component.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

DOM elements should use the "autocomplete" attribute correctly.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWpAwveQgd5znXqyB&open=AZ7JWpAwveQgd5znXqyB&pullRequest=1531
<div *ngIf="otpCodeError" class="otp-code-error">{{ otpCodeError }}</div>
</div>
</ng-container>
</ng-template>
</nz-alert>
</div>
Expand All @@ -47,7 +63,12 @@

<div *nzModalFooter class="nz-modal-footer">
<ng-container *ngFor="let action of content.actions">
<button nz-button [nzType]="action.type || 'default'" (click)="action.callback()">
<button
nz-button
[nzLoading]="code === 'otp_code_verify' && action.type === 'primary' && otpCodeSubmitting"
[nzType]="action.type || 'default'"
(click)="action.callback()"
>
{{ action.text | translate }}
</button>
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
border-radius: 4px;
}
}

.otp-code-box {
margin-top: 16px;
}

.otp-code-error {
color: #ff4d4f;
font-size: 12px;
margin-top: 8px;
}
}


Expand Down
143 changes: 139 additions & 4 deletions src/app/services/connect-token/acl-dialog/acl-dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import {HttpService} from '@app/services/http';
import {FaceService} from '@app/services/face';
import {HttpErrorResponse} from '@angular/common/http';
import {Component, Inject, OnInit} from '@angular/core';
import {Component, ElementRef, Inject, OnInit, ViewChild} from '@angular/core';
import {Asset, ConnectData, ConnectionToken} from '@app/model';
import {NzNotificationService} from 'ng-zorro-antd/notification';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
Expand Down Expand Up @@ -32,12 +32,16 @@
styleUrls: ['acl-dialog.component.scss']
})
export class ElementACLDialogComponent implements OnInit {
@ViewChild('otpCodeInput', { static: false }) otpCodeInput: ElementRef;
public asset: Asset;
public connectInfo: ConnectData;
public code: string;
public connectionToken: ConnectionToken = null;
public error: HttpErrorResponse;
public otherError: string;
public otpCode: string = '';
public otpCodeError: string = '';
public otpCodeSubmitting = false;
public ticketAssignees: string = '-';
// Token 的行为,创建或者兑换 Token, create, exchange
public tokenAction: string = 'create';
Expand Down Expand Up @@ -82,15 +86,43 @@
}

ngOnInit() {
// 创建 Token 的时候,需要传入 Asset 和 ConnectInfo
this.content = this.getDialogContent(this.data.code);
this.asset = this.data.asset;
this.connectInfo = this.data.connectInfo;
this.code = this.data.code;
this.error = this.data.error;
// 兑换 Token 的时候,需要传入 Token ID
this.tokenID = this.data.tokenID;
// 控制 token 的行为, 创建还是兑换
this.tokenAction = this.data.tokenAction;
this.code = this.resolveDialogCode(this.data.code, this.error);
this.content = this.getDialogContent(this.code);
if (this.code === 'otp_code_verify') {
this.focusOTPCodeInput();
}
}

onOTPCodeInput() {
this.otpCodeError = '';
}

onConfirmOTPCode() {
const otpCode = (this.otpCode || '').trim();
if (!otpCode) {
this.otpCodeError = this._i18n.instant('Please input OTP code');
this.focusOTPCodeInput();
return;
}
this.otpCodeError = '';
this.otpCodeSubmitting = true;
this.requestTokenWithOTPCode(otpCode).subscribe(
(connToken: ConnectionToken) => {
this.otpCodeSubmitting = false;
this.dialogRef.close(connToken);
},
(error: HttpErrorResponse) => {
this.otpCodeSubmitting = false;
this.applyDialogError(error, true);
}
);
}

async onCopySuccess(evt) {
Expand Down Expand Up @@ -272,6 +304,91 @@
this.dialogRef.close(null);
}

private requestTokenWithOTPCode(otpCode: string) {
if (this.tokenAction === 'exchange') {
return this._http.exchangeConnectToken(this.tokenID, false, false, undefined, otpCode);
}
if (this.data.connectData && this.data.connectData.direct) {

Check warning on line 311 in src/app/services/connect-token/acl-dialog/acl-dialog.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWpBQveQgd5znXqyC&open=AZ7JWpBQveQgd5znXqyC&pullRequest=1531
return this._http.adminConnectToken(
this.asset,
this.data.connectData,
false,
false,
undefined,
otpCode
);
}
return this._http.createConnectToken(this.asset, this.connectInfo, false, false, undefined, otpCode);
}

private resolveDialogCode(code: string, error?: HttpErrorResponse): string {
if (this.hasOTPCodeError(error)) {
return 'otp_code_verify';
}
return code || 'other';
}

private hasOTPCodeError(error?: HttpErrorResponse): boolean {
return !!(error && error.error && typeof error.error === 'object' && error.error.otp_code);

Check warning on line 332 in src/app/services/connect-token/acl-dialog/acl-dialog.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWpBRveQgd5znXqyD&open=AZ7JWpBRveQgd5znXqyD&pullRequest=1531
}

private getFieldErrorMessage(error: HttpErrorResponse, field: string): string {
if (!error || !error.error || typeof error.error !== 'object') {

Check warning on line 336 in src/app/services/connect-token/acl-dialog/acl-dialog.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWpBRveQgd5znXqyE&open=AZ7JWpBRveQgd5znXqyE&pullRequest=1531
return '';
}
const value = error.error[field];
if (Array.isArray(value)) {
return value.join(' ');
}
return value || '';
}

private getOtherErrorMessage(error: HttpErrorResponse): string {
if (!error) {
return '';
}
let value: any = error.error;
if (!value) {
return error.message;
}
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
return value.join(' ');
}
if (value.detail) {
return value.detail;
}
return JSON.stringify(value);
}

private applyDialogError(error: HttpErrorResponse, keepOTPCode = false) {
this.error = error;
this.data.error = error;
this.code = this.resolveDialogCode(error?.error?.code, error);
if (this.code === 'otp_code_verify') {
this.otpCodeError = this.getFieldErrorMessage(error, 'otp_code');
if (!keepOTPCode) {
this.otpCode = '';
}
this.content = this.getDialogContent(this.code);
this.focusOTPCodeInput();
return;
}
this.otpCodeError = '';
this.otherError = this.getOtherErrorMessage(error);
this.content = this.getDialogContent(this.code);
}

private focusOTPCodeInput() {
setTimeout(() => {
if (this.otpCodeInput && this.otpCodeInput.nativeElement) {

Check warning on line 386 in src/app/services/connect-token/acl-dialog/acl-dialog.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AZ7JWpBRveQgd5znXqyF&open=AZ7JWpBRveQgd5znXqyF&pullRequest=1531
this.otpCodeInput.nativeElement.focus();
}
}, 0);
}

checkTicket() {
const checkMethod = this.connectionToken.from_ticket_info.check_ticket_api.method.toLowerCase();
const checkURL = this.connectionToken.from_ticket_info.check_ticket_api.url;
Expand Down Expand Up @@ -343,6 +460,24 @@
}
]
},
otp_code_verify: {
title: 'OTP verification',
message: 'This account requires OTP code. Please input it to continue.',
customContent: {
type: 'otp_code'
},
actions: [
{
text: 'Cancel',
callback: () => vm.closeDialog()
},
{
text: 'Confirm',
type: 'primary',
callback: () => vm.onConfirmOTPCode()
}
]
},
acl_face_online: {
title: 'Login reminder',
message: 'Face online required',
Expand Down
Loading