diff --git a/.rubocop.yml b/.rubocop.yml
index 824c97eb3..752902541 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -83,6 +83,9 @@ Naming/FileName:
Rails/EnumHash:
Enabled: false
+Rails/SkipsModelValidations:
+ AllowedMethods: ["touch"]
+
# Keep for now, easier with superclass definitions
Style/ClassAndModuleChildren:
Enabled: false
diff --git a/Gemfile b/Gemfile
index 0691a0464..38ea0c793 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,6 +18,7 @@ gem 'faker'
gem 'haml'
gem 'maxmind-db'
gem 'openid_connect'
+gem 'paper_trail', '~> 12.3'
gem 'password_strength'
gem 'pg'
gem 'pundit'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2f9d7485b..832b6dbee 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -195,6 +195,9 @@ GEM
validate_email
validate_url
webfinger (>= 1.0.1)
+ paper_trail (12.3.0)
+ activerecord (>= 5.2)
+ request_store (~> 1.1)
parallel (1.21.0)
parser (3.1.1.0)
ast (~> 2.4.1)
@@ -263,6 +266,8 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.2.1)
+ request_store (1.5.1)
+ rack (>= 1.4)
rexml (3.2.5)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
@@ -384,6 +389,7 @@ DEPENDENCIES
mysql2
net-ldap
openid_connect
+ paper_trail (~> 12.3)
password_strength
pg
pry
diff --git a/app/controllers/api/encryptables_controller.rb b/app/controllers/api/encryptables_controller.rb
index 7438ab22f..bf8d8eaf5 100644
--- a/app/controllers/api/encryptables_controller.rb
+++ b/app/controllers/api/encryptables_controller.rb
@@ -5,6 +5,8 @@ class Api::EncryptablesController < ApiController
self.permitted_attrs = [:name, :description, :tag]
+ before_action :set_paper_trail_whodunnit
+
helper_method :team
# GET /api/encryptables
@@ -19,6 +21,7 @@ def index
def show
authorize entry
entry.decrypt(decrypted_team_password(team))
+ log_read_access
render_entry
end
@@ -69,6 +72,16 @@ def model_class
end
# rubocop:enable Metrics/MethodLength
+ def fetch_entries
+ if encryptable_file?
+ super
+ elsif tag_param.present?
+ user_encryptables.find_by(tag: tag_param)
+ else
+ Encryptables::FilteredList.new(current_user, params).fetch_entries
+ end
+ end
+
def build_entry
return build_encryptable_file if encryptable_file?
@@ -128,4 +141,11 @@ def permitted_attrs
[]
end
end
+
+ def log_read_access
+ v = encryptable.paper_trail.save_with_version
+ v.event = :viewed
+ v.created_at = DateTime.now
+ v.save!
+ end
end
diff --git a/app/controllers/api/logs_controller.rb b/app/controllers/api/logs_controller.rb
new file mode 100644
index 000000000..3f1f1fcce
--- /dev/null
+++ b/app/controllers/api/logs_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Api::LogsController < ApiController
+
+ def index(options = {})
+ authorize(team, :index?, policy_class: LogPolicy)
+ render({ json: fetch_entries,
+ each_serializer: @model_serializer ||= LogsSerializer }
+ .merge(render_options)
+ .merge(options.fetch(:render_options, {})))
+ end
+
+ def fetch_entries
+ PaperTrail.serializer = JSON
+ limit = params[:load] || 20
+ offset = params[:offset] || 0
+ Version
+ .includes(:user)
+ .where(item_id: params[:encryptable_id])
+ .order(created_at: :desc)
+ .offset(offset)
+ .limit(limit)
+ end
+
+ def team
+ @team ||= Encryptable.find(params[:encryptable_id]).folder.team
+ end
+end
diff --git a/app/models/encryptable.rb b/app/models/encryptable.rb
index 6df73e7ba..67471cd34 100644
--- a/app/models/encryptable.rb
+++ b/app/models/encryptable.rb
@@ -17,6 +17,12 @@
class Encryptable < ApplicationRecord
+ has_paper_trail on: [:touch, :update], ignore: [:tag, :type], versions: {
+ class_name: 'Version'
+ }
+
+ before_destroy :destroy_versions
+
serialize :encrypted_data, ::EncryptedData
attr_readonly :type
@@ -71,4 +77,8 @@ def decrypt_attr(attr, team_password)
instance_variable_set("@cleartext_#{attr}", cleartext_value)
end
+ def destroy_versions
+ versions.destroy_all
+ end
+
end
diff --git a/app/models/user.rb b/app/models/user.rb
index e3236556e..327bcd736 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -31,6 +31,8 @@ class User < ApplicationRecord
validates :username, uniqueness: :username
validates :username, presence: true
+ has_many :versions, foreign_key: :whodunnit, dependent: :destroy, class_name: 'Version'
+
def update_password(old, new)
return unless auth_db?
diff --git a/app/models/version.rb b/app/models/version.rb
new file mode 100644
index 000000000..8ab1f54d3
--- /dev/null
+++ b/app/models/version.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Version < PaperTrail::Version
+ belongs_to :user, class_name: 'User', foreign_key: :whodunnit
+ belongs_to :encryptable, class_name: 'Encryptable', foreign_key: :item_id
+end
diff --git a/app/policies/log_policy.rb b/app/policies/log_policy.rb
new file mode 100644
index 000000000..93eabd6cb
--- /dev/null
+++ b/app/policies/log_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class LogPolicy < TeamPolicy
+ def index?
+ team_member?
+ end
+
+ def show?
+ @team.teammember?(@user.id) && !@user.is_a?(User::Api)
+ end
+end
diff --git a/app/presenters/encryptables/filtered_list.rb b/app/presenters/encryptables/filtered_list.rb
new file mode 100644
index 000000000..785dd0f47
--- /dev/null
+++ b/app/presenters/encryptables/filtered_list.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ::Encryptables
+ class FilteredList < ::FilteredList
+
+ def fetch_entries
+ filtered_encryptables = encryptables
+
+ filtered_encryptables = filter_by_recent if recent?
+ filtered_encryptables = filter_by_query(filtered_encryptables) if query
+
+
+ filtered_encryptables
+ end
+
+ private
+
+ def query
+ @params[:q]&.strip&.downcase
+ end
+
+ def recent?
+ true?(@params[:recent])
+ end
+
+ def encryptables
+ @current_user.encryptables
+ end
+
+ def filter_by_query(encryptables)
+ encryptables.where(
+ 'lower(encryptables.description) LIKE :query
+ OR lower(encryptables.name) LIKE :query',
+ query: "%#{query}%"
+ )
+ end
+
+ def filter_by_recent
+ Version
+ .includes(:encryptable, encryptable: [:folder])
+ .where(whodunnit: @current_user)
+ .order(created_at: :desc)
+ .group(:item_id, :item_type)
+ .select(:item_id, :item_type)
+ .limit(5)
+ .map(&:encryptable)
+ end
+ end
+end
diff --git a/app/presenters/filtered_list.rb b/app/presenters/filtered_list.rb
index 92b3a2b62..fb3dce893 100644
--- a/app/presenters/filtered_list.rb
+++ b/app/presenters/filtered_list.rb
@@ -15,4 +15,8 @@ def fetch_entries
def list_param(key)
@params[key].to_a.map(&:to_i)
end
+
+ def true?(value)
+ %w[1 yes true].include?(value.to_s.downcase)
+ end
end
diff --git a/app/serializers/logs_serializer.rb b/app/serializers/logs_serializer.rb
new file mode 100644
index 000000000..4fd6c298b
--- /dev/null
+++ b/app/serializers/logs_serializer.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class LogsSerializer < ApplicationSerializer
+ attributes :id, :item_type, :event, :user_id, :username, :created_at
+
+ belongs_to :encryptable
+
+ def user_id
+ object.whodunnit
+ end
+
+ def encryptable
+ object.item
+ end
+
+ def username
+ object.user.username
+ end
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
index d5c0cd70c..554c3c50a 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -11,7 +11,9 @@
end
end
- resources :encryptables, except: [:new, :edit]
+ resources :encryptables, except: [:new, :edit] do
+ resources :logs, only: [:index]
+ end
resources :api_users, except: [:new, :edit] do
member do
diff --git a/db/migrate/20220401130939_create_versions.rb b/db/migrate/20220401130939_create_versions.rb
new file mode 100644
index 000000000..f6e7ba97a
--- /dev/null
+++ b/db/migrate/20220401130939_create_versions.rb
@@ -0,0 +1,38 @@
+# This migration creates the `versions` table, the only schema PT requires.
+# All other migrations PT provides are optional.
+class CreateVersions < ActiveRecord::Migration[7.0]
+
+ # The largest text column available in all supported RDBMS is
+ # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
+ # so that MySQL will use `longtext` instead of `text`. Otherwise,
+ # when serializing very large objects, `text` might not be big enough.
+ TEXT_BYTES = 1_073_741_823
+
+ def change
+ create_table :versions do |t|
+ t.string :item_type, null: false
+ t.bigint :item_id, null: false
+ t.string :event, null: false
+ t.string :whodunnit
+ t.text :object, limit: TEXT_BYTES
+
+ # Known issue in MySQL: fractional second precision
+ # -------------------------------------------------
+ #
+ # MySQL timestamp columns do not support fractional seconds unless
+ # defined with "fractional seconds precision". MySQL users should manually
+ # add fractional seconds precision to this migration, specifically, to
+ # the `created_at` column.
+ # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
+ #
+ # MySQL users should also upgrade to at least rails 4.2, which is the first
+ # version of ActiveRecord with support for fractional seconds in MySQL.
+ # (https://github.com/rails/rails/pull/14359)
+ #
+ # MySQL users should use the following line for `created_at`
+ t.datetime :created_at, limit: 6
+ #t.datetime :created_at
+ end
+ add_index :versions, %i(item_type item_id)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d1398a757..6371a11bd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2022_03_29_122335) do
+ActiveRecord::Schema[7.0].define(version: 2022_04_01_130939) do
create_table "encryptables", force: :cascade do |t|
t.string "name", limit: 255, default: "", null: false
t.integer "folder_id"
@@ -92,4 +92,14 @@
t.index ["default_ccli_user_id"], name: "index_users_on_default_ccli_user_id"
end
+ create_table "versions", force: :cascade do |t|
+ t.string "item_type", null: false
+ t.bigint "item_id", null: false
+ t.string "event", null: false
+ t.string "whodunnit"
+ t.text "object", limit: 1073741823
+ t.datetime "created_at"
+ t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
+ end
+
end
diff --git a/frontend/app/adapters/version.js b/frontend/app/adapters/version.js
new file mode 100644
index 000000000..a0dcb368e
--- /dev/null
+++ b/frontend/app/adapters/version.js
@@ -0,0 +1,19 @@
+import ApplicationAdapter from "./application";
+
+export default ApplicationAdapter.extend({
+ namespace: "api/encryptables",
+
+ pathForType() {
+ return "logs";
+ },
+
+ urlForQuery(query, modelName) {
+ if (query.encryptableId) {
+ let url = `/${this.namespace}/${query.encryptableId}/logs`;
+
+ delete query.encryptableId;
+ return url;
+ }
+ return super.urlForQuery(query, modelName);
+ }
+});
diff --git a/frontend/app/components/encryptable/show.js b/frontend/app/components/encryptable/show.js
index 9f326df3f..e5959bb65 100644
--- a/frontend/app/components/encryptable/show.js
+++ b/frontend/app/components/encryptable/show.js
@@ -9,9 +9,14 @@ export default class ShowComponent extends Component {
@service intl;
@service notify;
+ @tracked logs = [];
+ loadAmount = 10;
+ offset = 0;
+
constructor() {
super(...arguments);
+ this.getLogs();
window.scrollTo(0, 0);
}
@@ -24,9 +29,13 @@ export default class ShowComponent extends Component {
@tracked
isPasswordVisible = false;
+ @tracked
+ canLoadMore = true;
+
@action
toggleEncryptableEdit() {
this.isEncryptableEditing = !this.isEncryptableEditing;
+ this.getLogs();
}
@action
@@ -57,4 +66,33 @@ export default class ShowComponent extends Component {
onCopied(attribute) {
this.notify.info(this.intl.t(`flashes.encryptables.${attribute}_copied`));
}
+
+ @action
+ getLogs() {
+ this.store
+ .query("version", {
+ encryptableId: this.args.encryptable.id,
+ load: this.loadAmount,
+ offset: this.offset
+ })
+ .then((res) => {
+ this.logs = res
+ .toArray()
+ .filter((log) => {
+ return !this.logs.includes(log);
+ })
+ .concat(this.logs);
+ this.toggleLoadMore();
+ });
+ }
+
+ @action
+ loadMore() {
+ this.offset += this.loadAmount;
+ this.getLogs();
+ }
+
+ toggleLoadMore() {
+ this.canLoadMore = this.loadAmount <= this.logs.length;
+ }
}
diff --git a/frontend/app/components/encryptable/table-row.js b/frontend/app/components/encryptable/table-row.js
new file mode 100644
index 000000000..8847ef24c
--- /dev/null
+++ b/frontend/app/components/encryptable/table-row.js
@@ -0,0 +1,7 @@
+import Component from "@glimmer/component";
+
+export default class TableRowComponent extends Component {
+ constructor() {
+ super(...arguments);
+ }
+}
diff --git a/frontend/app/components/encryptable/table.js b/frontend/app/components/encryptable/table.js
new file mode 100644
index 000000000..b028d289f
--- /dev/null
+++ b/frontend/app/components/encryptable/table.js
@@ -0,0 +1,13 @@
+import Component from "@glimmer/component";
+import { inject as service } from "@ember/service";
+
+export default class TableComponent extends Component {
+ @service store;
+ @service router;
+ @service intl;
+ @service notify;
+
+ constructor() {
+ super(...arguments);
+ }
+}
diff --git a/frontend/app/models/encryptable.js b/frontend/app/models/encryptable.js
index ad08fd894..9ad2be73f 100644
--- a/frontend/app/models/encryptable.js
+++ b/frontend/app/models/encryptable.js
@@ -1,4 +1,4 @@
-import Model, { attr, belongsTo } from "@ember-data/model";
+import Model, { attr, hasMany, belongsTo } from "@ember-data/model";
export default class Encryptable extends Model {
@attr("string") name;
@@ -6,6 +6,7 @@ export default class Encryptable extends Model {
@attr("string") createdAt;
@attr("string") updatedAt;
@belongsTo("folder") folder;
+ @hasMany("version") versions;
get isOseSecret() {
return this.constructor.modelName === "encryptable-ose-secret";
diff --git a/frontend/app/models/version.js b/frontend/app/models/version.js
new file mode 100644
index 000000000..3fb94e1a9
--- /dev/null
+++ b/frontend/app/models/version.js
@@ -0,0 +1,9 @@
+import Model, { attr, belongsTo } from "@ember-data/model";
+
+export default class Version extends Model {
+ @attr("string") event;
+ @attr("number") userId;
+ @attr("string") username;
+ @attr("string") createdAt;
+ @belongsTo("encryptable") encryptable;
+}
diff --git a/frontend/app/router.js b/frontend/app/router.js
index f75906592..eabcf5eba 100644
--- a/frontend/app/router.js
+++ b/frontend/app/router.js
@@ -11,6 +11,7 @@ Router.map(function () {
this.route("new");
this.route("edit", { path: "/edit/:id" });
this.route("show", { path: "/:id" });
+ this.route("logs", { path: "/:encryptable_id/logs" });
this.route(
"file-entries",
{ path: "/:encryptable_id/file-entries" },
diff --git a/frontend/app/routes/dashboard.js b/frontend/app/routes/dashboard.js
index 3094f7e35..a09592dba 100644
--- a/frontend/app/routes/dashboard.js
+++ b/frontend/app/routes/dashboard.js
@@ -12,22 +12,31 @@ export default class DashboardRoute extends BaseRoute {
};
async model(params) {
- params["limit"] = 20;
const favouriteTeams = await this.getFavouriteTeams(params);
- const teams = this.getTeams(params);
+ const teams = await this.getTeams(params);
+ const recentCredentials = await this.getRecentCredentials(params);
+
return RSVP.hash({
favouriteTeams,
- teams
+ teams,
+ recentCredentials
});
}
async getFavouriteTeams(params) {
+ params["limit"] = 20;
params["favourite"] = true;
return await this.store.query("team", params);
}
async getTeams(params) {
+ params["limit"] = 20;
params["favourite"] = false;
return await this.store.query("team", params);
}
+
+ async getRecentCredentials(params) {
+ params["recent"] = true;
+ return await this.store.query("encryptable", params);
+ }
}
diff --git a/frontend/app/templates/components/dashboard-card.hbs b/frontend/app/templates/components/dashboard-card.hbs
index 3ce9f241e..03dbcde89 100644
--- a/frontend/app/templates/components/dashboard-card.hbs
+++ b/frontend/app/templates/components/dashboard-card.hbs
@@ -9,6 +9,6 @@
{{#if this.team.isPersonalTeam }}
{{else}}
-
+
{{/if}}
\ No newline at end of file
diff --git a/frontend/app/templates/components/encryptable/show.hbs b/frontend/app/templates/components/encryptable/show.hbs
index 7c1772ca3..671aa86ab 100644
--- a/frontend/app/templates/components/encryptable/show.hbs
+++ b/frontend/app/templates/components/encryptable/show.hbs
@@ -34,59 +34,77 @@
{{t "encryptable/credentials.show.last_update"}}: {{moment-format @encryptable.updatedAt "DD.MM.YYYY hh:mm"}}
-
- {{#if @encryptable.description}}
-
-
-
{{@encryptable.description}}
-
-
- {{/if}}
-
-
-
{{t "encryptable/credentials.show.attachments"}}
-
-
-
- {{t "encryptable/credentials.show.add_attachment"}}
-
-
+
+
+ {{#if @encryptable.description}}
+
+
+
{{@encryptable.description}}
+
+
+ {{/if}}
+
+
+
+
{{t "encryptable/credentials.show.attachments"}}
+
+
+
+ {{t "encryptable/credentials.show.add_attachment"}}
+
+
+
+
+
+
+
+ {{t "encryptable/credentials.show.file"}} |
+ {{t "description"}} |
+ {{t "actions"}} |
+
+ {{#each @encryptable.encryptableFiles as |encryptableFile|}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+
+ {{#if this.canLoadMore}}
+
+ {{t "encryptable/credentials.show.load_more"}}
+
+ {{/if}}
+
+
-
-
-
- {{t "encryptable/credentials.show.file"}} |
- {{t "description"}} |
- {{t "actions"}} |
-
- {{#each @encryptable.encryptableFiles as |encryptableFile|}}
-
- {{/each}}
-
-
diff --git a/frontend/app/templates/components/encryptable/table-row.hbs b/frontend/app/templates/components/encryptable/table-row.hbs
new file mode 100644
index 000000000..cd49c1eb7
--- /dev/null
+++ b/frontend/app/templates/components/encryptable/table-row.hbs
@@ -0,0 +1,5 @@
+
+ {{t (concat "encryptable/credentials.log." @paperTrailVersion.event)}} |
+ {{@paperTrailVersion.username}} |
+ {{moment-format @paperTrailVersion.createdAt "DD.MM.YYYY hh:mm"}} |
+
diff --git a/frontend/app/templates/components/encryptable/table.hbs b/frontend/app/templates/components/encryptable/table.hbs
new file mode 100644
index 000000000..bfe687565
--- /dev/null
+++ b/frontend/app/templates/components/encryptable/table.hbs
@@ -0,0 +1,14 @@
+
+
+
+ {{t "encryptable/credentials.show.action"}} |
+ {{t "user" }} |
+ {{t "encryptable/credentials.show.date_time" }} |
+
+
+
+ {{#each @logs as |paperTrailVersion|}}
+
+ {{/each}}
+
+
diff --git a/frontend/app/templates/dashboard.hbs b/frontend/app/templates/dashboard.hbs
index 1046fa61a..bfc3e46c8 100644
--- a/frontend/app/templates/dashboard.hbs
+++ b/frontend/app/templates/dashboard.hbs
@@ -1,20 +1,32 @@
+ {{#if @model.recentCredentials}}
+
+
+ {{#each this.model.recentCredentials as |encryptable|}}
+
+
+
+ {{/each}}
+
+ {{/if}}
+
{{#if @model.favouriteTeams}}
{{#each this.model.favouriteTeams as |team|}}
-
+
{{/each}}
{{/if}}
+
{{#if this.model.teams}}
{{#each this.model.teams as |team|}}
-
+
{{/each}}
diff --git a/frontend/config/environment.js b/frontend/config/environment.js
index 23ee2b6da..8709564cf 100644
--- a/frontend/config/environment.js
+++ b/frontend/config/environment.js
@@ -9,6 +9,7 @@ module.exports = function (environment) {
sentryDsn: "",
appVersion: undefined,
currentUserId: undefined,
+ encryptable_id: undefined,
"changeset-validations": { rawOutput: true },
EmberENV: {
FEATURES: {
diff --git a/frontend/tests/integration/components/account/show-test.js b/frontend/tests/integration/components/account/show-test.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/tests/integration/components/dashboard-card-test.js b/frontend/tests/integration/components/dashboard-card-test.js
index 42a1806c1..316b4b05a 100644
--- a/frontend/tests/integration/components/dashboard-card-test.js
+++ b/frontend/tests/integration/components/dashboard-card-test.js
@@ -20,7 +20,7 @@ module("Integration | Component | dashboard-card", function (hooks) {
// Template block usage:
await render(hbs`
-
+
`);
diff --git a/frontend/tests/integration/components/encryptable/show-test.js b/frontend/tests/integration/components/encryptable/show-test.js
index c4ffafd7d..776c16bf7 100644
--- a/frontend/tests/integration/components/encryptable/show-test.js
+++ b/frontend/tests/integration/components/encryptable/show-test.js
@@ -9,7 +9,32 @@ import { isPresent } from "@ember/utils";
const storeStub = Service.extend({
query(modelName, params) {
if (params) {
- return [];
+ return Promise.all([
+ {
+ userId: 1,
+ username: "alice",
+ event: "viewed",
+ createdAt: "2021-06-14 09:23:02.750627",
+ encryptable: {
+ get() {
+ return 1;
+ },
+ id: 1
+ }
+ },
+ {
+ userId: 2,
+ username: "bob",
+ event: "update",
+ createdAt: "2021-06-15 09:23:02.750627",
+ encryptable: {
+ get() {
+ return 1;
+ },
+ id: 1
+ }
+ }
+ ]);
}
}
});
@@ -21,9 +46,6 @@ module("Integration | Component | encryptable/show", function (hooks) {
this.owner.unregister("service:store");
this.owner.register("service:store", storeStub);
setLocale("en");
- });
-
- test("it renders with data and shows edit buttons credentials encryptable entry", async function (assert) {
this.set("encryptable", {
id: 1,
name: "Ninjas test encryptable",
@@ -49,9 +71,37 @@ module("Integration | Component | encryptable/show", function (hooks) {
return 1;
}
}
+ ],
+ versions: [
+ {
+ userId: 1,
+ username: "alice",
+ event: "viewed",
+ createdAt: "2021-06-14 09:23:02.750627",
+ encryptable: {
+ get() {
+ return 1;
+ },
+ id: 1
+ }
+ },
+ {
+ userId: 2,
+ username: "bob",
+ event: "update",
+ createdAt: "2021-06-15 09:23:02.750627",
+ encryptable: {
+ get() {
+ return 1;
+ },
+ id: 1
+ }
+ }
]
});
+ });
+ test("it renders with data and shows edit buttons credentials encryptable entry", async function (assert) {
await render(
hbs``
);
@@ -71,4 +121,14 @@ module("Integration | Component | encryptable/show", function (hooks) {
assert.ok(isPresent(deleteButton));
assert.ok(isPresent(editButton));
});
+
+ test("log and credentials tabs ase present", async function (assert) {
+ await render(
+ hbs``
+ );
+ let credTab = document.getElementById("credentials-tab");
+ let logTab = document.getElementById("log-tab");
+ assert.ok(isPresent(credTab));
+ assert.ok(isPresent(logTab));
+ });
});
diff --git a/frontend/translations/ch_be.yml b/frontend/translations/ch_be.yml
index 94c3ed9ba..fff3ab3df 100644
--- a/frontend/translations/ch_be.yml
+++ b/frontend/translations/ch_be.yml
@@ -87,8 +87,14 @@ ch_be:
copy_password: Passwort kopierä
copy_username: Benutzername kopierä
add_attachment: Anhang hinzufüegä
+ load_more: Meh aazeige
+ date_time: Datum/Zyt
+ action: Aktion
new:
title: Nöi Zuägangsdate
+ log:
+ update: Bearbeitet
+ viewed: Aagluegt
admin:
title: Admin
diff --git a/frontend/translations/de.yml b/frontend/translations/de.yml
index 7015ce14b..b54645c83 100644
--- a/frontend/translations/de.yml
+++ b/frontend/translations/de.yml
@@ -87,8 +87,14 @@ de:
copy_password: Passwort kopieren
copy_username: Benutzername kopieren
add_attachment: Anhang hinzufügen
+ load_more: Mehr anzeigen
+ date_time: Datum/Uhrzeit
+ action: Aktion
new:
title: Neue Zugangsdaten
+ log:
+ update: Bearbeitet
+ viewed: Angesehen
admin:
title: Admin
diff --git a/frontend/translations/en.yml b/frontend/translations/en.yml
index 4b2db4238..420a0cb0e 100644
--- a/frontend/translations/en.yml
+++ b/frontend/translations/en.yml
@@ -87,8 +87,14 @@ en:
copy_password: Copy password
copy_username: Copy username
add_attachment: Add Attachment
+ load_more: Load More
+ date_time: Date/Time
+ action: Action
new:
title: New Credentials
+ log:
+ update: edited
+ viewed: viewed
admin:
title: Admin
diff --git a/frontend/translations/fr.yml b/frontend/translations/fr.yml
index 535cb46bf..18c974556 100644
--- a/frontend/translations/fr.yml
+++ b/frontend/translations/fr.yml
@@ -87,8 +87,15 @@ fr:
copy_password: Copier le mot de passe
copy_username: Copier le nom d'utilisateur
add_attachment: Ajouter un attachement
+ load_more: Charger plus
+ date_time: Date/Temps
+ action: Action
new:
title: Nouveau compte
+ log:
+ update: modifié
+ viewed: regardé
+
admin:
title: Admin
diff --git a/spec/controllers/api/encryptables_controller_spec.rb b/spec/controllers/api/encryptables_controller_spec.rb
index 986164f1e..87396fc39 100644
--- a/spec/controllers/api/encryptables_controller_spec.rb
+++ b/spec/controllers/api/encryptables_controller_spec.rb
@@ -13,6 +13,7 @@
let(:attributes) { %w[name cleartext_password cleartext_username] }
let!(:ose_secret) { create_ose_secret }
let(:credentials1) { encryptables(:credentials1) }
+ let(:encryptable) { encryptables.first }
context 'GET index' do
it 'returns encryptable with matching name' do
@@ -36,7 +37,7 @@
expect_json_object_includes_keys(credentials1_json_relationships, nested_models)
end
- it 'returns all enncryptables if empty query param given' do
+ it 'returns all encryptables if empty query param given' do
login_as(:alice)
get :index, params: { 'q': '' }, xhr: true
@@ -128,6 +129,87 @@
expect(response.status).to eq(403)
end
+
+ context 'recent Credentials' do
+ let!(:recent_credentials) do
+ folder = teams(:team1).folders.first
+ private_key = decrypt_private_key(bob)
+ team_password = folder.team.decrypt_team_password(bob, private_key)
+ Fabricate.times(
+ 6,
+ :credential,
+ folder: folder,
+ team_password: team_password
+ )
+ end
+
+ it 'returns most recent credentials' do
+ login_as(:alice)
+
+ recent_credentials.each do |credential|
+ log_read_access(alice.id, credential)
+ end
+
+ get :index, params: { recent: true }, xhr: true
+
+ expect(response.status).to be(200)
+ expect(data.size).to eq(5)
+ attributes = data.first['attributes']
+ expect(attributes['name']).to eq recent_credentials.last.name
+ expect(attributes['description']).to eq recent_credentials.last.description
+ end
+
+ it 'shows most recently used credential first in list' do
+ login_as(:alice)
+
+
+ recent_credentials.each do |credential|
+ log_read_access(alice.id, credential)
+ end
+ log_read_access(alice.id, credentials1)
+
+ get :index, params: { recent: true }, xhr: true
+
+ expect(response.status).to be(200)
+ expect(data.size).to eq(5)
+ attributes = data.first['attributes']
+ expect(attributes['name']).to eq credentials1.name
+ expect(attributes['description']).to eq credentials1.description
+
+ end
+ it 'does not show credentials with no access' do
+ login_as(:bob)
+
+ recent_credentials1 = recent_credentials.first
+ log_read_access(alice.id, recent_credentials1)
+
+ get :index, params: { recent: true }, xhr: true
+
+ expect(response.status).to be(200)
+ expect(data.size).to eq(0)
+ end
+
+ it 'does not show deleted credentials' do
+ login_as(:alice)
+
+ recent_credentials1 = recent_credentials.first
+ log_read_access(alice.id, recent_credentials1)
+
+ get :index, params: { recent: true }, xhr: true
+
+ expect(data.size).to eq(1)
+ attributes = data.first['attributes']
+ expect(attributes['name']).to eq recent_credentials1.name
+ expect(attributes['description']).to eq recent_credentials1.description
+
+ recent_credentials1.destroy!
+
+ get :index, params: { recent: true }, xhr: true
+
+ expect(response.status).to be(200)
+ expect(data.size).to eq(0)
+ end
+ end
end
context 'GET show' do
@@ -700,6 +782,56 @@
end
end
+ context 'papertrail', versioning: true do
+ context 'delete' do
+ it 'deletes log history if encryptable is deleted' do
+ encryptable2 = encryptables(:credentials2)
+
+ 1000.times do
+ encryptable.touch
+ end
+
+ 500.times do
+ encryptable2.touch
+ end
+
+ expect(encryptable.versions.size).to eq(1000)
+ expect(encryptable2.versions.size).to eq(500)
+
+ encryptable.destroy
+
+ expect(encryptable.versions.size).to be(0)
+ expect(encryptable2.versions.size).to eq(500)
+ end
+ end
+
+ context 'touch' do
+ it 'creates a log entry' do
+ login_as(:bob)
+
+ encryptable.touch
+ expect(encryptable.versions.count).to eq(1)
+ end
+
+ it 'contains user in whodunnit' do
+ login_as(:bob)
+
+ encryptable.touch
+ expect(encryptable.versions.last.whodunnit).to eq(bob.id.to_s)
+ end
+ end
+
+ context 'show' do
+ it 'logs show access' do
+ login_as(:bob)
+
+ get :show, params: { id: encryptable.id }
+
+ expect(encryptable.versions.last.event).to eq('viewed')
+ end
+ end
+ end
+
private
def create_ose_secret
@@ -734,4 +866,12 @@ def set_auth_headers
request.headers['Authorization-User'] = bob.username
request.headers['Authorization-Password'] = Base64.encode64('password')
end
+
+ def log_read_access(user_id, credential)
+ v = credential.paper_trail.save_with_version
+ v.whodunnit = user_id
+ v.event = :viewed
+ v.created_at = DateTime.now
+ v.save!
+ end
end
diff --git a/spec/controllers/api/logs_controller_spec.rb b/spec/controllers/api/logs_controller_spec.rb
new file mode 100644
index 000000000..7e58f9fc7
--- /dev/null
+++ b/spec/controllers/api/logs_controller_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Api::LogsController do
+ include ControllerHelpers
+
+ let(:bob) { users(:bob) }
+ let(:alice) { users(:alice) }
+ let(:api_user) { bob.api_users.create }
+ let(:credentials1) { encryptables(:credentials1) }
+
+ context 'GET index' do
+ it 'returns right amount of logs' do
+ login_as(:alice)
+ PaperTrail.request(whodunnit: alice.id) do
+ credentials1.touch
+ credentials1.touch
+ end
+
+ get :index, params: { encryptable_id: credentials1.id }
+ expect(data.count).to eq 2
+ expect(data.first['attributes']['username']).to eq 'alice'
+ end
+
+ it 'returns sorted results' do
+ login_as(:bob)
+ PaperTrail.request(whodunnit: bob.id) do
+ credentials1.touch
+ credentials1.touch
+ end
+ get :index, params: { encryptable_id: credentials1.id }
+ expect(data.first['attributes']['created_at']).to be > data.second['attributes']['created_at']
+ end
+
+ it 'denies access if not in team' do
+ login_as(:alice)
+
+ team2 = teams(:team2)
+ encryptable = team2.folders.first.encryptables.first
+
+ get :index, params: { encryptable_id: encryptable.id }
+ expect(response.status).to be 403
+ end
+ end
+end
diff --git a/spec/fabricators/encryptables/credentials_fabricator.rb b/spec/fabricators/encryptables/credentials_fabricator.rb
index dd4cd5d7a..8797270d9 100644
--- a/spec/fabricators/encryptables/credentials_fabricator.rb
+++ b/spec/fabricators/encryptables/credentials_fabricator.rb
@@ -7,7 +7,7 @@
Fabricator(:credential, from: 'Encryptable::Credentials') do
transient :team_password
- name { Faker::Team.creature }
+ name { sequence(:name) { |i| Faker::Team.creature + " #{i}" } }
cleartext_username { Faker::Internet.user_name }
cleartext_password { Faker::Internet.password }
before_save do |account, attrs|
diff --git a/spec/models/encryptable_spec.rb b/spec/models/encryptable_spec.rb
index ad5b6bb09..531ae2a0f 100644
--- a/spec/models/encryptable_spec.rb
+++ b/spec/models/encryptable_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-
describe Encryptable do
-
let(:bob) { users(:bob) }
let(:bobs_private_key) { bob.decrypt_private_key('password') }
let(:encryptable) { encryptables(:credentials1) }
diff --git a/spec/system/dashboard_system_spec.rb b/spec/system/dashboard_system_spec.rb
index a65cccb5a..a43404113 100644
--- a/spec/system/dashboard_system_spec.rb
+++ b/spec/system/dashboard_system_spec.rb
@@ -5,13 +5,17 @@
describe 'Dashboard', type: :system, js: true do
include SystemHelpers
+ let(:credentials) { encryptables(:credentials1) }
+ let(:team) { teams(:team1) }
+
+
it 'renders dashboard grid' do
- login_as_user(:alice)
+ create_recent_credentials
expect(page.current_path).to eq('/dashboard')
-
expect(page).to have_selector('pzsh-hero', visible: true)
expect(page).to have_selector('pzsh-banner', visible: true)
+ expect(page).to have_text('Recent Credentials', count: 1)
expect(page).to have_text('Favourites', count: 1)
expect(page).to have_text('Teams', count: 1)
@@ -21,14 +25,35 @@
end
it 'navigates to team on team card click' do
- login_as_user(:alice)
+ create_recent_credentials
+
expect(page.current_path).to eq('/dashboard')
expect(page).to have_selector('pzsh-hero', visible: true)
- all('div.dashboard-grid-card').first.click
- expect(page.current_path).to eq('/teams/542659241')
+ first('div.dashboard-grid-card', text: team.name).click
+
+ expect(page.current_path).to eq("/teams/#{team.id}")
expect(page).to have_selector 'div.content'
end
+
+ it 'lists recently accessed encryptable' do
+ create_recent_credentials
+
+ expect(page.current_path).to eq('/dashboard')
+
+ expect(page).to have_selector('pzsh-hero', visible: true)
+
+ first('div.dashboard-grid-card', text: credentials.name).click
+
+ expect(page.current_path).to eq("/encryptables/#{credentials.id}")
+ expect(page).to have_selector 'div.content'
+ end
+
+ def create_recent_credentials
+ login_as_user(:alice)
+ visit("/encryptables/#{credentials.id}")
+ visit('/dashboard')
+ end
end
diff --git a/spec/system/encryptable_log_system_spec.rb b/spec/system/encryptable_log_system_spec.rb
new file mode 100644
index 000000000..755015047
--- /dev/null
+++ b/spec/system/encryptable_log_system_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'pry'
+
+describe 'encryptable log', type: :system, js: true do
+ include SystemHelpers
+
+ describe 'logs view access' do
+ it 'contains credentials and logs table in page' do
+ login_as_user(:bob)
+ visit("/encryptables/#{encryptables(:credentials1).id}")
+ expect(page).to have_text('Credentials')
+ click_link('Logs')
+
+ expect(page).to have_css('table')
+ within 'table' do
+ table_rows = all('tr')
+
+ expect(table_rows.length).to eq(2)
+ top_row = table_rows[1]
+
+ within top_row do
+ expect(page).to have_text('viewed')
+ expect(page).to have_text('bob')
+ end
+ end
+ end
+
+ it 'contains log for update' do
+ login_as_user(:bob)
+ visit("/encryptables/#{encryptables(:credentials1).id}")
+ edit_encryptable
+ click_link('Logs')
+
+ within 'table' do
+ table_rows = all('tr')
+
+ expect(table_rows.length).to eq(3)
+
+ top_row = table_rows[1]
+
+ within top_row do
+ expect(page).to have_text('edited')
+ expect(page).to have_text('bob')
+ end
+ end
+ end
+ end
+
+ private
+
+ def edit_encryptable
+ find('#edit_account_button').click
+ within('form.ember-view[role="form"]', visible: false) do
+ find("input[name='cleartextUsername']", visible: false).set 'username2'
+ end
+ click_button('Save', visible: false)
+ end
+end