Просмотр исходного кода

16223 user auth token list and revoke (#17434)

* Start user auth token list on user profile (#16223)

* Now session found and first, better os regex (#16223)

* Revoke for user and admin user token manage (#16223)

* Tidying and styling (#16223)

* Tidying and styling (#16223)

* Update to use #16222 (#16223)

* Update for changes to 16222

* update per api issue
Shavonn Brown 6 лет назад
Родитель
Сommit
40f95a95c0

+ 1 - 1
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -126,7 +126,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
             getOptionLabel={i => i.title}
             onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
             options={dashboards}
-            placeholder="Chose default dashboard"
+            placeholder="Choose default dashboard"
             width={20}
           />
         </div>

+ 46 - 0
public/app/features/admin/AdminEditUserCtrl.ts

@@ -1,12 +1,15 @@
 import _ from 'lodash';
+import { dateTime } from '@grafana/ui';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import { NavModelSrv } from 'app/core/core';
 import { User } from 'app/core/services/context_srv';
+import { UserSession } from 'app/types';
 
 export default class AdminEditUserCtrl {
   /** @ngInject */
   constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) {
     $scope.user = {};
+    $scope.sessions = [];
     $scope.newOrg = { name: '', role: 'Editor' };
     $scope.permissions = {};
     $scope.navModel = navModelSrv.getNav('admin', 'global-users', 0);
@@ -14,6 +17,7 @@ export default class AdminEditUserCtrl {
     $scope.init = () => {
       if ($routeParams.id) {
         $scope.getUser($routeParams.id);
+        $scope.getUserSessions($routeParams.id);
         $scope.getUserOrgs($routeParams.id);
       }
     };
@@ -26,6 +30,48 @@ export default class AdminEditUserCtrl {
       });
     };
 
+    $scope.getUserSessions = (id: number) => {
+      backendSrv.get('/api/admin/users/' + id + '/auth-tokens').then((sessions: UserSession[]) => {
+        sessions.reverse();
+
+        $scope.sessions = sessions.map((session: UserSession) => {
+          return {
+            id: session.id,
+            isActive: session.isActive,
+            seenAt: dateTime(session.seenAt).fromNow(),
+            createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
+            clientIp: session.clientIp,
+            browser: session.browser,
+            browserVersion: session.browserVersion,
+            os: session.os,
+            osVersion: session.osVersion,
+            device: session.device,
+          };
+        });
+      });
+    };
+
+    $scope.revokeUserSession = (tokenId: number) => {
+      backendSrv
+        .post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', {
+          authTokenId: tokenId,
+        })
+        .then(() => {
+          $scope.sessions = $scope.sessions.filter((session: UserSession) => {
+            if (session.id === tokenId) {
+              return false;
+            }
+            return true;
+          });
+        });
+    };
+
+    $scope.revokeAllUserSessions = (tokenId: number) => {
+      backendSrv.post('/api/admin/users/' + $scope.user_id + '/logout').then(() => {
+        $scope.sessions = [];
+      });
+    };
+
     $scope.setPassword = () => {
       if (!$scope.passwordForm.$valid) {
         return;

+ 57 - 23
public/app/features/admin/partials/edit_user.html

@@ -70,32 +70,66 @@
 		</div>
 	</form>
 
-	<table class="filter-table">
-		<thead>
-			<tr>
-				<th>Name</th>
-				<th>Role</th>
-				<th></th>
+	<div class="gf-form-group">
+		<table class="filter-table">
+			<thead>
+				<tr>
+					<th>Name</th>
+					<th>Role</th>
+					<th></th>
+				</tr>
+			</thead>
+			<tr ng-repeat="org in orgs">
+				<td>
+					{{org.name}} <span class="label label-info" ng-show="org.orgId === user.orgId">Current</span>
+				</td>
+				<td>
+			<div class="gf-form">
+				<span class="gf-form-select-wrapper">
+					<select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(org)">
+					</select>
+				</span>
+			</div>
+				</td>
+				<td style="width: 1%">
+					<a ng-click="removeOrgUser(org)" class="btn btn-danger btn-small">
+						<i class="fa fa-remove"></i>
+					</a>
+				</td>
 			</tr>
+		</table>
+	</div>
+
+	<h3 class="page-heading">Sessions</h3>
+
+	<div class="gf-form-group">
+	  <table class="filter-table form-inline">
+		<thead>
+		  <tr>
+			<th>Last seen</th>
+			<th>Logged on</th>
+			<th>IP address</th>
+			<th>Browser &amp; OS</th>
+			<th></th>
+		  </tr>
 		</thead>
-		<tr ng-repeat="org in orgs">
-			<td>
-				{{org.name}} <span class="label label-info" ng-show="org.orgId === user.orgId">Current</span>
-			</td>
+		<tbody>
+		  <tr ng-repeat="session in sessions">
+			<td ng-if="session.isActive">Now</td>
+			<td ng-if="!session.isActive">{{session.seenAt}}</td>
+			<td>{{session.createdAt}}</td>
+			<td>{{session.clientIp}}</td>
+			<td>{{session.browser}} on {{session.os}} {{session.osVersion}}</td>
 			<td>
-        <div class="gf-form">
-            <span class="gf-form-select-wrapper">
-                <select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(org)">
-                </select>
-            </span>
-        </div>
+				<button class="btn btn-danger btn-small" ng-click="revokeUserSession(session.id)">
+					<i class="fa fa-power-off"></i>
+				</button>
 			</td>
-			<td style="width: 1%">
-				<a ng-click="removeOrgUser(org)" class="btn btn-danger btn-small">
-					<i class="fa fa-remove"></i>
-				</a>
-			</td>
-		</tr>
-	</table>
+		  </tr>
+		</tbody>
+	  </table>
+	</div>
+
+	<button ng-if="sessions.length" class="btn btn-danger" ng-click="revokeAllUserSessions()">Logout user from all devices</button>
 
 </div>

+ 1 - 3
public/app/features/alerting/state/reducers.ts

@@ -13,9 +13,7 @@ function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
     stateText: stateModel.text,
     stateIcon: stateModel.iconClass,
     stateClass: stateModel.stateClass,
-    stateAge: dateTime(dto.newStateDate)
-      .fromNow()
-      .replace(' ago', ''),
+    stateAge: dateTime(dto.newStateDate).fromNow(true),
   };
 
   if (rule.state !== 'paused') {

+ 50 - 0
public/app/features/profile/ProfileCtrl.ts

@@ -1,11 +1,14 @@
 import config from 'app/core/config';
 import { coreModule } from 'app/core/core';
+import { dateTime } from '@grafana/ui';
+import { UserSession } from 'app/types';
 
 export class ProfileCtrl {
   user: any;
   oldTheme: any;
   teams: any = [];
   orgs: any = [];
+  sessions: object[] = [];
   userForm: any;
   showTeamsList = false;
   showOrgsList = false;
@@ -15,6 +18,7 @@ export class ProfileCtrl {
   /** @ngInject */
   constructor(private backendSrv, private contextSrv, private $location, navModelSrv) {
     this.getUser();
+    this.getUserSessions();
     this.getUserTeams();
     this.getUserOrgs();
     this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0);
@@ -27,6 +31,52 @@ export class ProfileCtrl {
     });
   }
 
+  getUserSessions() {
+    this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => {
+      sessions.reverse();
+
+      const found = sessions.findIndex((session: UserSession) => {
+        return session.isActive;
+      });
+
+      if (found) {
+        const now = sessions[found];
+        sessions.splice(found, found);
+        sessions.unshift(now);
+      }
+
+      this.sessions = sessions.map((session: UserSession) => {
+        return {
+          id: session.id,
+          isActive: session.isActive,
+          seenAt: dateTime(session.seenAt).fromNow(),
+          createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
+          clientIp: session.clientIp,
+          browser: session.browser,
+          browserVersion: session.browserVersion,
+          os: session.os,
+          osVersion: session.osVersion,
+          device: session.device,
+        };
+      });
+    });
+  }
+
+  revokeUserSession(tokenId: number) {
+    this.backendSrv
+      .post('/api/user/revoke-auth-token', {
+        authTokenId: tokenId,
+      })
+      .then(() => {
+        this.sessions = this.sessions.filter((session: UserSession) => {
+          if (session.id === tokenId) {
+            return false;
+          }
+          return true;
+        });
+      });
+  }
+
   getUserTeams() {
     this.backendSrv.get('/api/user/teams').then(teams => {
       this.teams = teams;

+ 29 - 0
public/app/features/profile/partials/profile.html

@@ -74,3 +74,32 @@
       </tbody>
     </table>
   </div>
+
+  <h3 class="page-heading">Sessions</h3>
+  <div class="gf-form-group">
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th>Last seen</th>
+          <th>Logged on</th>
+          <th>IP address</th>
+          <th>Browser &amp; OS</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="session in ctrl.sessions">
+          <td ng-if="session.isActive">Now</td>
+          <td ng-if="!session.isActive">{{session.seenAt}}</td>
+          <td>{{session.createdAt}}</td>
+          <td>{{session.clientIp}}</td>
+          <td>{{session.browser}} on {{session.os}} {{session.osVersion}}</td>
+          <td>
+            <button class="btn btn-danger btn-small" ng-click="ctrl.revokeUserSession(session.id)">
+              <i class="fa fa-power-off"></i>
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>

+ 13 - 0
public/app/types/user.ts

@@ -48,3 +48,16 @@ export interface UserState {
   orgId: number;
   timeZone: string;
 }
+
+export interface UserSession {
+  id: number;
+  createdAt: string;
+  clientIp: string;
+  isActive: boolean;
+  seenAt: string;
+  browser: string;
+  browserVersion: string;
+  os: string;
+  osVersion: string;
+  device: string;
+}