Skip to content

Commit 1fd2d36

Browse files
authored
Merge pull request ceph#55860 from ceph/add-multi-cluster-token-ttl
mgr/dashboard: Add multi cluster token ttl Reviewed-by: Nizamudeen A <[email protected]>
2 parents 0b2df70 + dfc9aef commit 1fd2d36

File tree

10 files changed

+217
-29
lines changed

10 files changed

+217
-29
lines changed

src/pybind/mgr/dashboard/controllers/auth.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import logging
66
import sys
7+
from typing import Optional
78

89
import cherrypy
910

@@ -30,16 +31,33 @@
3031
"pwdUpdateRequired": (bool, "Is password update required?")
3132
}
3233

34+
AUTH_SCHEMA = {
35+
"token": (str, "Authentication Token"),
36+
"username": (str, "Username"),
37+
"permissions": ({
38+
"cephfs": ([str], "")
39+
}, "List of permissions acquired"),
40+
"pwdExpirationDate": (str, "Password expiration date"),
41+
"sso": (bool, "Uses single sign on?"),
42+
"pwdUpdateRequired": (bool, "Is password update required?")
43+
}
44+
3345

3446
@APIRouter('/auth', secure=False)
3547
@APIDoc("Initiate a session with Ceph", "Auth")
3648
class Auth(RESTController, ControllerAuthMixin):
3749
"""
3850
Provide authenticates and returns JWT token.
3951
"""
40-
# pylint: disable=R0912
41-
42-
def create(self, username, password):
52+
@EndpointDoc("Dashboard Authentication",
53+
parameters={
54+
'username': (str, 'Username'),
55+
'password': (str, 'Password'),
56+
'ttl': (int, 'Token Time to Live (in hours)')
57+
},
58+
responses={201: AUTH_SCHEMA})
59+
def create(self, username, password, ttl: Optional[int] = None):
60+
# pylint: disable=R0912
4361
user_data = AuthManager.authenticate(username, password)
4462
user_perms, pwd_expiration_date, pwd_update_required = None, None, None
4563
max_attempt = Settings.ACCOUNT_LOCKOUT_ATTEMPTS
@@ -60,7 +78,7 @@ def create(self, username, password):
6078
logger.info('Login successful: %s', username)
6179
mgr.ACCESS_CTRL_DB.reset_attempt(username)
6280
mgr.ACCESS_CTRL_DB.save()
63-
token = JwtManager.gen_token(username)
81+
token = JwtManager.gen_token(username, ttl=ttl)
6482

6583
# For backward-compatibility: PyJWT versions < 2.0.0 return bytes.
6684
token = token.decode('utf-8') if isinstance(token, bytes) else token

src/pybind/mgr/dashboard/controllers/multi_cluster.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ def _proxy(self, method, base_url, path, params=None, payload=None, verify=False
5555
@EndpointDoc("Authenticate to a remote cluster")
5656
def auth(self, url: str, cluster_alias: str, username: str,
5757
password=None, token=None, hub_url=None, cluster_fsid=None,
58-
prometheus_api_url=None, ssl_verify=False, ssl_certificate=None):
58+
prometheus_api_url=None, ssl_verify=False, ssl_certificate=None, ttl=None):
59+
5960
try:
6061
hub_fsid = mgr.get('config')['fsid']
6162
except KeyError:
@@ -64,7 +65,8 @@ def auth(self, url: str, cluster_alias: str, username: str,
6465
if password:
6566
payload = {
6667
'username': username,
67-
'password': password
68+
'password': password,
69+
'ttl': ttl
6870
}
6971
cluster_token = self.check_cluster_connection(url, payload, username,
7072
ssl_verify, ssl_certificate)
@@ -199,12 +201,13 @@ def set_config(self, config: object):
199201
@UpdatePermission
200202
# pylint: disable=W0613
201203
def reconnect_cluster(self, url: str, username=None, password=None, token=None,
202-
ssl_verify=False, ssl_certificate=None):
204+
ssl_verify=False, ssl_certificate=None, ttl=None):
203205
multicluster_config = self.load_multi_cluster_config()
204206
if username and password:
205207
payload = {
206208
'username': username,
207-
'password': password
209+
'password': password,
210+
'ttl': ttl
208211
}
209212

210213
cluster_token = self.check_cluster_connection(url, payload, username,
@@ -290,6 +293,15 @@ def is_token_expired(self, jwt_token):
290293
current_time = time.time()
291294
return expiration_time < current_time
292295

296+
def get_time_left(self, jwt_token):
297+
split_message = jwt_token.split(".")
298+
base64_message = split_message[1]
299+
decoded_token = json.loads(base64.urlsafe_b64decode(base64_message + "===="))
300+
expiration_time = decoded_token['exp']
301+
current_time = time.time()
302+
time_left = expiration_time - current_time
303+
return max(0, time_left)
304+
293305
def check_token_status_expiration(self, token):
294306
if self.is_token_expired(token):
295307
return 1
@@ -303,7 +315,9 @@ def check_token_status_array(self, clusters_token_array):
303315
token = item['token']
304316
user = item['user']
305317
status = self.check_token_status_expiration(token)
306-
token_status_map[cluster_name] = {'status': status, 'user': user}
318+
time_left = self.get_time_left(token)
319+
token_status_map[cluster_name] = {'status': status, 'user': user,
320+
'time_left': time_left}
307321

308322
return token_status_map
309323

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@
188188
i18n>This field is required.</span>
189189
</div>
190190
</div>
191+
<div class="form-group row"
192+
*ngIf="!remoteClusterForm.getValue('showToken') && action !== 'edit'">
193+
<label class="cd-col-form-label"
194+
for="ttl"
195+
i18n>Login Expiration</label>
196+
<div class="cd-col-form-input">
197+
<select class="form-select"
198+
id="ttl"
199+
formControlName="ttl"
200+
name="ttl">
201+
<option value="1">1 day</option>
202+
<option value="7">1 week</option>
203+
<option value="15"
204+
[selected]="true">15 days</option>
205+
<option value="30">30 days</option>
206+
</select>
207+
</div>
208+
</div>
191209
<div class="form-group row"
192210
*ngIf="action !== 'edit'">
193211
<div class="cd-col-form-offset">

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
141141
]
142142
}),
143143
ssl: new FormControl(false),
144+
ttl: new FormControl(15),
144145
ssl_cert: new FormControl('', {
145146
validators: [
146147
CdValidators.requiredIf({
@@ -178,6 +179,10 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
178179
this.activeModal.close();
179180
}
180181

182+
convertToHours(value: number): number {
183+
return value * 24; // Convert days to hours
184+
}
185+
181186
onSubmit() {
182187
const url = this.remoteClusterForm.getValue('remoteClusterUrl');
183188
const updatedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
@@ -186,7 +191,9 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
186191
const password = this.remoteClusterForm.getValue('password');
187192
const token = this.remoteClusterForm.getValue('apiToken');
188193
const clusterFsid = this.remoteClusterForm.getValue('clusterFsid');
194+
const prometheusApiUrl = this.remoteClusterForm.getValue('prometheusApiUrl');
189195
const ssl = this.remoteClusterForm.getValue('ssl');
196+
const ttl = this.convertToHours(this.remoteClusterForm.getValue('ttl'));
190197
const ssl_certificate = this.remoteClusterForm.getValue('ssl_cert')?.trim();
191198

192199
const commonSubscribtion = {
@@ -212,7 +219,7 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
212219
case 'reconnect':
213220
this.subs.add(
214221
this.multiClusterService
215-
.reConnectCluster(updatedUrl, username, password, token, ssl, ssl_certificate)
222+
.reConnectCluster(updatedUrl, username, password, token, ssl, ssl_certificate, ttl)
216223
.subscribe(commonSubscribtion)
217224
);
218225
break;
@@ -227,8 +234,10 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
227234
token,
228235
window.location.origin,
229236
clusterFsid,
237+
prometheusApiUrl,
230238
ssl,
231-
ssl_certificate
239+
ssl_certificate,
240+
ttl
232241
)
233242
.subscribe(commonSubscribtion)
234243
);

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,24 @@ <h4 class="mt-3">This cluster is already managed by cluster -
6161
</a>
6262
</ng-template>
6363

64+
<ng-template #durationTpl
65+
let-column="column"
66+
let-value="value"
67+
let-row="row">
68+
<span *ngIf="row.remainingTimeWithoutSeconds > 0 && row.cluster_alias !== 'local-cluster'">
69+
<i *ngIf="row.remainingDays < 8"
70+
i18n-title
71+
title="Cluster's token is about to expire"
72+
[class.icon-danger-color]="row.remainingDays < 2"
73+
[class.icon-warning-color]="row.remainingDays < 8"
74+
class="{{ icons.warning }}"></i>
75+
<span title="{{ value | cdDate }}">{{ row.remainingTimeWithoutSeconds / 1000 | duration }}</span>
76+
</span>
77+
<span *ngIf="row.remainingTimeWithoutSeconds <= 0 && row.remainingDays <=0 && row.cluster_alias !== 'local-cluster'">
78+
<i i18n-title
79+
title="Cluster's token has expired"
80+
class="{{ icons.danger }}"></i>
81+
<span class="text-danger">Token expired</span>
82+
</span>
83+
<span *ngIf="row.cluster_alias === 'local-cluster'">N/A</span>
84+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { MultiCluster } from '~/app/shared/models/multi-cluster';
1919
import { Router } from '@angular/router';
2020
import { CookiesService } from '~/app/shared/services/cookie.service';
2121
import { Observable, Subscription } from 'rxjs';
22-
import { SettingsService } from '~/app/shared/api/settings.service';
2322

2423
@Component({
2524
selector: 'cd-multi-cluster-list',
@@ -31,7 +30,8 @@ export class MultiClusterListComponent implements OnInit, OnDestroy {
3130
table: TableComponent;
3231
@ViewChild('urlTpl', { static: true })
3332
public urlTpl: TemplateRef<any>;
34-
33+
@ViewChild('durationTpl', { static: true })
34+
durationTpl: TemplateRef<any>;
3535
private subs = new Subscription();
3636
permissions: Permissions;
3737
tableActions: CdTableAction[];
@@ -50,7 +50,6 @@ export class MultiClusterListComponent implements OnInit, OnDestroy {
5050

5151
constructor(
5252
private multiClusterService: MultiClusterService,
53-
private settingsService: SettingsService,
5453
private router: Router,
5554
public actionLabels: ActionLabelsI18n,
5655
private notificationService: NotificationService,
@@ -95,15 +94,25 @@ export class MultiClusterListComponent implements OnInit, OnDestroy {
9594
this.subs.add(
9695
this.multiClusterService.subscribe((resp: object) => {
9796
if (resp && resp['config']) {
97+
this.hubUrl = resp['hub_url'];
98+
this.currentUrl = resp['current_url'];
9899
const clusterDetailsArray = Object.values(resp['config']).flat();
99100
this.data = clusterDetailsArray;
100101
this.checkClusterConnectionStatus();
102+
this.data.forEach((cluster: any) => {
103+
cluster['remainingTimeWithoutSeconds'] = 0;
104+
if (cluster['ttl'] && cluster['ttl'] > 0) {
105+
cluster['ttl'] = cluster['ttl'] * 1000;
106+
cluster['remainingTimeWithoutSeconds'] = this.getRemainingTimeWithoutSeconds(
107+
cluster['ttl']
108+
);
109+
cluster['remainingDays'] = this.getRemainingDays(cluster['ttl']);
110+
}
111+
});
101112
}
102113
})
103114
);
104115

105-
this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
106-
107116
this.columns = [
108117
{
109118
prop: 'cluster_alias',
@@ -138,6 +147,12 @@ export class MultiClusterListComponent implements OnInit, OnDestroy {
138147
prop: 'user',
139148
name: $localize`User`,
140149
flexGrow: 2
150+
},
151+
{
152+
prop: 'ttl',
153+
name: $localize`Token expires`,
154+
flexGrow: 2,
155+
cellTemplate: this.durationTpl
141156
}
142157
];
143158

@@ -149,21 +164,35 @@ export class MultiClusterListComponent implements OnInit, OnDestroy {
149164
);
150165
}
151166

152-
ngOnDestroy() {
167+
ngOnDestroy(): void {
153168
this.subs.unsubscribe();
154169
}
155170

171+
getRemainingDays(time: number): number {
172+
if (time === undefined || time == null) {
173+
return undefined;
174+
}
175+
if (time < 0) {
176+
return 0;
177+
}
178+
const toDays = 1000 * 60 * 60 * 24;
179+
return Math.max(0, Math.floor(time / toDays));
180+
}
181+
182+
getRemainingTimeWithoutSeconds(time: number): number {
183+
return Math.floor(time / (1000 * 60)) * 60 * 1000;
184+
}
185+
156186
checkClusterConnectionStatus() {
157187
if (this.clusterTokenStatus && this.data) {
158188
this.data.forEach((cluster: MultiCluster) => {
159-
const clusterStatus = this.clusterTokenStatus[cluster.name.trim()];
160-
189+
const clusterStatus = this.clusterTokenStatus[cluster.name];
161190
if (clusterStatus !== undefined) {
162191
cluster.cluster_connection_status = clusterStatus.status;
192+
cluster.ttl = clusterStatus.time_left;
163193
} else {
164194
cluster.cluster_connection_status = 2;
165195
}
166-
167196
if (cluster.cluster_alias === 'local-cluster') {
168197
cluster.cluster_connection_status = 0;
169198
}

0 commit comments

Comments
 (0)