FEATURE: Overhaul of admin API key system (#8284)

FEATURE: Overhaul of admin API key system (#8284)

  • Allow revoking keys without deleting them
  • Auto-revoke keys after a period of no use (default 6 months)
  • Allow multiple keys per user
  • Allow attaching a description to each key, for easier auditing
  • Log changes to keys in the staff action log
  • Move all key management to one place, and improve the UI
diff --git a/app/assets/javascripts/admin/adapters/api-key.js.es6 b/app/assets/javascripts/admin/adapters/api-key.js.es6
new file mode 100644
index 0000000000..a473f66e08
--- /dev/null
+++ b/app/assets/javascripts/admin/adapters/api-key.js.es6
@@ -0,0 +1,11 @@
+import RESTAdapter from "discourse/adapters/rest";
+
+export default RESTAdapter.extend({
+  basePath() {
+    return "/admin/api/";
+  },
+
+  apiNameFor() {
+    return "key";
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-index.js.es6
new file mode 100644
index 0000000000..b087269626
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-api-keys-index.js.es6
@@ -0,0 +1,13 @@
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
+export default Ember.Controller.extend({
+  actions: {
+    revokeKey(key) {
+      key.revoke().catch(popupAjaxError);
+    },
+
+    undoRevokeKey(key) {
+      key.undoRevoke().catch(popupAjaxError);
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6
new file mode 100644
index 0000000000..f4d56c0a05
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6
@@ -0,0 +1,39 @@
+import { default as computed } from "ember-addons/ember-computed-decorators";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
+export default Ember.Controller.extend({
+  userModes: [
+    { id: "all", name: I18n.t("admin.api.all_users") },
+    { id: "single", name: I18n.t("admin.api.single_user") }
+  ],
+
+  @computed("userMode")
+  showUserSelector(mode) {
+    return mode === "single";
+  },
+
+  @computed("model.description", "model.username", "userMode")
+  saveDisabled(description, username, userMode) {
+    if (Ember.isBlank(description)) return true;
+    if (userMode === "single" && Ember.isBlank(username)) return true;
+    return false;
+  },
+
+  actions: {
+    changeUserMode(value) {
+      if (value === "all") {
+        this.model.set("username", null);
+      }
+      this.set("userMode", value);
+    },
+
+    save() {
+      this.model
+        .save()
+        .then(() => {
+          this.transitionToRoute("adminApiKeys.show", this.model.id);
+        })
+        .catch(popupAjaxError);
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-show.js.es6
new file mode 100644
index 0000000000..2e19ea5779
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-api-keys-show.js.es6
@@ -0,0 +1,54 @@
+import { bufferedProperty } from "discourse/mixins/buffered-content";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { empty } from "@ember/object/computed";
+
+export default Ember.Controller.extend(bufferedProperty("model"), {
+  isNew: empty("model.id"),
+
+  actions: {
+    saveDescription() {
+      const buffered = this.buffered;
+      const attrs = buffered.getProperties("description");
+
+      this.model
+        .save(attrs)
+        .then(() => {
+          this.set("editingDescription", false);
+          this.rollbackBuffer();
+        })
+        .catch(popupAjaxError);
+    },
+
+    cancel() {
+      const id = this.get("userField.id");
+      if (Ember.isEmpty(id)) {
+        this.destroyAction(this.userField);
+      } else {
+        this.rollbackBuffer();
+        this.set("editing", false);
+      }
+    },
+
+    editDescription() {
+      this.toggleProperty("editingDescription");
+      if (!this.editingDescription) {
+        this.rollbackBuffer();
+      }
+    },
+
+    revokeKey(key) {
+      key.revoke().catch(popupAjaxError);
+    },
+
+    deleteKey(key) {
+      key
+        .destroyRecord()
+        .then(() => this.transitionToRoute("adminApiKeys.index"))
+        .catch(popupAjaxError);
+    },
+
+    undoRevokeKey(key) {
+      key.undoRevoke().catch(popupAjaxError);
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6
index 9cd1682ff6..e69de29bb2 100644
--- a/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6
@@ -1,42 +0,0 @@
-import ApiKey from "admin/models/api-key";
-import { default as computed } from "ember-addons/ember-computed-decorators";
-import Controller from "@ember/controller";
-
-export default Controller.extend({
-  @computed("model.[]")
-  hasMasterKey(model) {
-    return !!model.findBy("user", null);
-  },
-
-  actions: {
-    generateMasterKey() {
-      ApiKey.generateMasterKey().then(key => this.model.pushObject(key));
-    },
-
-    regenerateKey(key) {
-      bootbox.confirm(
-        I18n.t("admin.api.confirm_regen"),
-        I18n.t("no_value"),
-        I18n.t("yes_value"),
-        result => {
-          if (result) {
-            key.regenerate();
-          }
-        }
-      );
-    },
-
-    revokeKey(key) {
-      bootbox.confirm(
-        I18n.t("admin.api.confirm_revoke"),
-        I18n.t("no_value"),
-        I18n.t("yes_value"),
-        result => {
-          if (result) {
-            key.revoke().then(() => this.model.removeObject(key));
-          }
-        }
-      );
-    }
-  }
-});
diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
index 9a25f679ef..47a54b8ebf 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
@@ -258,10 +258,6 @@ export default Controller.extend(CanCheckEmails, {
         .finally(() => this.toggleProperty("editingTitle"));
     },
 
-    generateApiKey() {
-      this.model.generateApiKey();
-    },
-
     saveCustomGroups() {
       const currentIds = this.customGroupIds;
       const bufferedIds = this.customGroupIdsBuffer;
@@ -294,32 +290,6 @@ export default Controller.extend(CanCheckEmails, {
 
     resetPrimaryGroup() {
       this.set("model.primary_group_id", this.originalPrimaryGroupId);
-    },
-
-    regenerateApiKey() {
-      bootbox.confirm(
-        I18n.t("admin.api.confirm_regen"),
-        I18n.t("no_value"),
-        I18n.t("yes_value"),
-        result => {
-          if (result) {
-            this.model.generateApiKey();
-          }
-        }
-      );
-    },
-
-    revokeApiKey() {
-      bootbox.confirm(
-        I18n.t("admin.api.confirm_revoke"),
-        I18n.t("no_value"),
-        I18n.t("yes_value"),
-        result => {
-          if (result) {
-            this.model.revokeApiKey();
-          }
-        }
-      );
     }
   }
 });
diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6
index 891a2923c6..e99c84e0b4 100644
--- a/app/assets/javascripts/admin/models/admin-user.js.es6
+++ b/app/assets/javascripts/admin/models/admin-user.js.es6
@@ -4,7 +4,6 @@ import { ajax } from "discourse/lib/ajax";
 import computed from "ember-addons/ember-computed-decorators";
 import { propertyNotEqual } from "discourse/lib/computed";
 import { popupAjaxError } from "discourse/lib/ajax-error";
-import ApiKey from "admin/models/api-key";
 import Group from "discourse/models/group";
 import { userPath } from "discourse/lib/url";
 
@@ -57,16 +56,6 @@ const AdminUser = Discourse.User.extend({
     );
   },
 
-  generateApiKey() {
-    return ajax(`/admin/users/${this.id}/generate_api_key`, {
-      type: "POST"
-    }).then(result => {
-      const apiKey = ApiKey.create(result.api_key);
-      this.set("api_key", apiKey);
-      return apiKey;
-    });
-  },
-
   groupAdded(added) {
     return ajax(`/admin/users/${this.id}/groups`, {
       type: "POST",

[... diff too long, it was truncated ...]

GitHub sha: 52c5cf33

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there: