checkpoint 0.2.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.gitignore +18 -9
- data/.rspec +2 -0
- data/.rubocop.yml +30 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -1
- data/LICENSE.md +27 -0
- data/README.md +23 -0
- data/Rakefile +14 -0
- data/bin/console +18 -0
- data/bin/rake +21 -0
- data/bin/rspec +21 -0
- data/bin/sequel +21 -0
- data/bin/setup +8 -0
- data/bin/yard +21 -0
- data/bin/yardoc +21 -0
- data/checkpoint.gemspec +37 -19
- data/db/migrations/1_create_permits.rb +19 -0
- data/docs/Makefile +24 -0
- data/docs/_static/.gitkeep +0 -0
- data/docs/_templates/.gitkeep +0 -0
- data/docs/authentication.rst +18 -0
- data/docs/conf.py +46 -0
- data/docs/index.rst +28 -0
- data/docs/policies.rst +211 -0
- data/docs/requirements.txt +4 -0
- data/lib/checkpoint.rb +16 -2
- data/lib/checkpoint/agent.rb +93 -0
- data/lib/checkpoint/agent/resolver.rb +33 -0
- data/lib/checkpoint/agent/token.rb +52 -0
- data/lib/checkpoint/authority.rb +67 -0
- data/lib/checkpoint/credential.rb +82 -0
- data/lib/checkpoint/credential/permission.rb +27 -0
- data/lib/checkpoint/credential/resolver.rb +87 -0
- data/lib/checkpoint/credential/role.rb +26 -0
- data/lib/checkpoint/credential/token.rb +51 -0
- data/lib/checkpoint/db.rb +161 -0
- data/lib/checkpoint/db/permit.rb +24 -0
- data/lib/checkpoint/permission_mapper.rb +29 -0
- data/lib/checkpoint/permits.rb +133 -0
- data/lib/checkpoint/query.rb +42 -0
- data/lib/checkpoint/query/action_permitted.rb +40 -0
- data/lib/checkpoint/query/role_granted.rb +55 -0
- data/lib/checkpoint/railtie.rb +92 -71
- data/lib/checkpoint/resource.rb +138 -0
- data/lib/checkpoint/resource/all_of_any_type.rb +34 -0
- data/lib/checkpoint/resource/all_of_type.rb +50 -0
- data/lib/checkpoint/resource/any_entity.rb +25 -0
- data/lib/checkpoint/resource/any_entity_of_type.rb +29 -0
- data/lib/checkpoint/resource/resolver.rb +21 -0
- data/lib/checkpoint/resource/token.rb +65 -0
- data/lib/checkpoint/version.rb +3 -1
- data/lib/tasks/migrate.rake +75 -0
- metadata +260 -19
- data/Readme.markdown +0 -103
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'checkpoint/agent/resolver'
|
4
|
+
require 'checkpoint/credential/resolver'
|
5
|
+
require 'checkpoint/resource/resolver'
|
6
|
+
require 'checkpoint/permits'
|
7
|
+
|
8
|
+
module Checkpoint
|
9
|
+
# An Authority is the central point of contact for authorization questions in
|
10
|
+
# Checkpoint. It checks whether there are permits that would allow a given
|
11
|
+
# action to be taken.
|
12
|
+
class Authority
|
13
|
+
def initialize(
|
14
|
+
agent_resolver: Agent::Resolver.new,
|
15
|
+
credential_resolver: Credential::Resolver.new,
|
16
|
+
resource_resolver: Resource::Resolver.new,
|
17
|
+
permits: Permits.new)
|
18
|
+
|
19
|
+
@agent_resolver = agent_resolver
|
20
|
+
@credential_resolver = credential_resolver
|
21
|
+
@resource_resolver = resource_resolver
|
22
|
+
@permits = permits
|
23
|
+
end
|
24
|
+
|
25
|
+
def permits?(agent, credential, resource)
|
26
|
+
# Conceptually equivalent to:
|
27
|
+
# can?(agent, action, target)
|
28
|
+
# can?(current_user, 'edit', @listing)
|
29
|
+
|
30
|
+
# user => agent tokens
|
31
|
+
# action => credential tokens
|
32
|
+
# target => resource tokens
|
33
|
+
|
34
|
+
# Permit.where(agent: agents, credential: credentials, resource: resources)
|
35
|
+
# SELECT * FROM permits
|
36
|
+
# WHERE agent IN('user:gkostin', 'account-type:umich', 'affiliation:lib-staff')
|
37
|
+
# AND credential IN('permission:edit', 'role:editor')
|
38
|
+
# AND resource IN('listing:17', 'type:listing')
|
39
|
+
|
40
|
+
# agent_type, agent_id | cred_type, cred_id | resource_type, resource_id
|
41
|
+
# ------------------------------------------------------------------------
|
42
|
+
# 'user:gkostin' | 'permission:edit' | 'listing:17'
|
43
|
+
# 'account-type:umich' | 'role:editor' | 'type:listing'
|
44
|
+
# 'affiliation:lib-staff' | | 'listing:*'
|
45
|
+
|
46
|
+
# ^^^ ^^^^ ^^^^
|
47
|
+
# if current_user has at least one row in each of of these columns,
|
48
|
+
# they have been "granted permission"
|
49
|
+
permits.for(
|
50
|
+
agent_resolver.resolve(agent),
|
51
|
+
credential_resolver.resolve(credential),
|
52
|
+
resource_resolver.resolve(resource)
|
53
|
+
).any?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Dummy authority that rejects everything
|
57
|
+
class RejectAll
|
58
|
+
def permits?(*)
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :agent_resolver, :credential_resolver, :resource_resolver, :permits
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'checkpoint/credential/resolver'
|
4
|
+
require 'checkpoint/credential/role'
|
5
|
+
require 'checkpoint/credential/permission'
|
6
|
+
require 'checkpoint/credential/token'
|
7
|
+
require 'checkpoint/permission_mapper'
|
8
|
+
|
9
|
+
module Checkpoint
|
10
|
+
# A Credential is the permission to take a particular action, or any
|
11
|
+
# instrument that can represent multiple permissions, such as a role or
|
12
|
+
# license.
|
13
|
+
#
|
14
|
+
# Credentials are abstract; that is, they are not attached to a particular
|
15
|
+
# actor or resource to be acted upon. A credential can be granted to an
|
16
|
+
# {Agent}, optionally applying to a particular resource, by way of a Permit.
|
17
|
+
# In other words, a credential can be likened to a class, while a permit can
|
18
|
+
# be likened to an instance of that class, bound to a given agent and
|
19
|
+
# possibly bound to a {Resource}.
|
20
|
+
class Credential
|
21
|
+
attr_reader :type, :id
|
22
|
+
alias name id
|
23
|
+
|
24
|
+
# Create a new generic Credential. This should generally not be called,
|
25
|
+
# preferring to use a factory or instantiate a {Permission}, {Role}, or
|
26
|
+
# custom Credential class.
|
27
|
+
#
|
28
|
+
# This class assigns the type 'credential', while most often, applications
|
29
|
+
# will want a {Permission}.
|
30
|
+
#
|
31
|
+
# The term `name` is more intuitive for credentials than `id`, as is used
|
32
|
+
# with the {Agent} and {Resource} types. This is because most applications
|
33
|
+
# will use primitive strings or symbols as the programmatic objects for
|
34
|
+
# credentials, where as `id` is often associated with a database-assigned
|
35
|
+
# identifier that should not appear in the source code. The parameter is
|
36
|
+
# called `name` here to reflect that intuitive concept, but it is really
|
37
|
+
# an alias for the `id` property of this Credential.
|
38
|
+
#
|
39
|
+
# @param name [String|Symbol] the name of this credential
|
40
|
+
def initialize(name)
|
41
|
+
@id = name.to_s
|
42
|
+
@type = 'credential'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the list of Credentials that would grant this one.
|
46
|
+
#
|
47
|
+
# This is an extension mechanism for application authors needing to
|
48
|
+
# implement hierarchical or virtual credentials and wanting to do so in
|
49
|
+
# an object-oriented way. The default implementation is to simply return
|
50
|
+
# the credential itself in an array but, for example, an a custom
|
51
|
+
# permission type could provide for aliasing by including itself and
|
52
|
+
# another instance for the synonym. Another example is modeling permissions
|
53
|
+
# granted by particular roles; this might be static, as defined in the
|
54
|
+
# source files, or dynamic, as impacted by configuration or runtime data.
|
55
|
+
#
|
56
|
+
# As an alternative, these rules could be implemented under a
|
57
|
+
# {PermissionMapper} in an application that prefers to model its credentials
|
58
|
+
# as strings or symbols, rather than more specialized objects.
|
59
|
+
#
|
60
|
+
# @see Checkpoint::PermissionMapper
|
61
|
+
# @return [Array<Credential>] the expanded list of credentials that would
|
62
|
+
# grant this one
|
63
|
+
def granted_by
|
64
|
+
[self]
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Token] a token for this credential
|
68
|
+
def token
|
69
|
+
@token ||= Token.new(type, id)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Compare two Credentials.
|
73
|
+
# @param other [Credential] the Credential to compare
|
74
|
+
# @return [Boolean] true if `other` is a Credential and its type and id
|
75
|
+
# are both eql? to {#type} and {#id}
|
76
|
+
def eql?(other)
|
77
|
+
type.eql?(other.type) && name.eql?(other.id)
|
78
|
+
end
|
79
|
+
|
80
|
+
alias == eql?
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Credential
|
5
|
+
# A Permission is simple extension to the base Credential, specifying its
|
6
|
+
# type as a permission and providing a conceptual object to be instantiated
|
7
|
+
# or passed.
|
8
|
+
#
|
9
|
+
# The most common use from outside Checkpoint will be by way of
|
10
|
+
# {Checkpoint::Query::ActionPermitted}, which will ask whether a given named
|
11
|
+
# action is permitted for a user. However, Permission could be extended or
|
12
|
+
# modified to implement aliasing or hierarchy, for example.
|
13
|
+
#
|
14
|
+
# More likely, though, is advising the resolution of Permissions through a
|
15
|
+
# {Checkpoint::PermissionMapper} or implementing a custom
|
16
|
+
# {Checkpoint::Credential::Resolver}. Subclassing or monkey-patching Permission
|
17
|
+
# should only be necessary if the application needs to extend the actual
|
18
|
+
# behavior of the Permission objects, rather than just which ones are resolved.
|
19
|
+
class Permission < Credential
|
20
|
+
TYPE = 'permission'
|
21
|
+
|
22
|
+
def type
|
23
|
+
TYPE
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'checkpoint/permission_mapper'
|
4
|
+
|
5
|
+
module Checkpoint
|
6
|
+
class Credential
|
7
|
+
# A Credential Resolver takes a concrete action name and resolves it into any
|
8
|
+
# {Credential}s that would permit the action.
|
9
|
+
#
|
10
|
+
# Checkpoint makes no particular demand on the credential model for an
|
11
|
+
# application, but offers a useful default implementation supporting
|
12
|
+
# permissions and roles. There are no default rules in Checkpoint as to which
|
13
|
+
# permissions or roles exist and, therefore, it has no default mapping of
|
14
|
+
# roles to permissions.
|
15
|
+
#
|
16
|
+
# This default resolver can be advised about an application model using roles
|
17
|
+
# and permissions customized by using one or both of two extension points:
|
18
|
+
#
|
19
|
+
# 1. Supplying a {PermissionMapper} gives a way to map action names to any
|
20
|
+
# "larger" permissions (e.g., "manage" being a shorthand for all CRUD
|
21
|
+
# operations on a Resource type) or roles that would grant a given
|
22
|
+
# permission. This affords a rather straightforward mapping of strings
|
23
|
+
# or symbols, short of building customized Credential types.
|
24
|
+
#
|
25
|
+
# 2. Implementing your own {Credential} types gives a way to model an
|
26
|
+
# application's credentials in an object-oriented way. If the resolver
|
27
|
+
# receives a {Credential} (rather than a string or symbol), it will call
|
28
|
+
# `#granted_by` on it to expand it. The Credential should be sure to
|
29
|
+
# include itself in the array it returns unless it is virtual and should
|
30
|
+
# never be considered as granted directly.
|
31
|
+
#
|
32
|
+
class Resolver
|
33
|
+
def initialize(permission_mapper: PermissionMapper.new)
|
34
|
+
@permission_mapper = permission_mapper
|
35
|
+
end
|
36
|
+
|
37
|
+
# Resolve an action into all {Credential}s that would permit it.
|
38
|
+
#
|
39
|
+
# When supplied a string or symbol, we call `permissions_for` and
|
40
|
+
# `roles_granting` on the {PermissionMapper} creating a {Permission} or
|
41
|
+
# {Role} for every result, returning them all in an array.
|
42
|
+
#
|
43
|
+
# When supplied a Credential, we call `#granted_by` on it and bypass the
|
44
|
+
# PermissionMapper. More precisely, we only check that the object responds to
|
45
|
+
# `#granted_by?`, but it would generally be a Credential subclass. The
|
46
|
+
# Credential should return an array, but we ensure the return type by
|
47
|
+
# wrapping and flattening.
|
48
|
+
#
|
49
|
+
# Note that the parameter name to `resolve` is `action`. This isn't a perfect
|
50
|
+
# name, but credentials are polymorphic such a way that there really is no
|
51
|
+
# better application-side term (cf. actor -> Agent, entity -> Resource). It
|
52
|
+
# would be something like `action_or_role`, `permission_or_role`, or a generic
|
53
|
+
# `credential`. Part of the naming intent here was to distinguish from the
|
54
|
+
# action and the ability to perform it. This inheritance relationship
|
55
|
+
# permissions and roles are both credential types is a distinguishing feature
|
56
|
+
# of Checkpoint, as opposed to models that treat permissions and roles as
|
57
|
+
# distinct concepts that must be granted in very different ways. A better
|
58
|
+
# name for this parameter may emerge over time, but it seems unlikely. The
|
59
|
+
# name `action` was selected because the most common and appropriate concrete
|
60
|
+
# thing to look for is a permission to take a named application action.
|
61
|
+
#
|
62
|
+
# @param action [String|Symbol|Credential] the action name or Credential
|
63
|
+
# to expand into any Credential that would grant it.
|
64
|
+
def resolve(action)
|
65
|
+
if action.respond_to?(:granted_by)
|
66
|
+
[action.granted_by].flatten
|
67
|
+
else
|
68
|
+
permissions_for(action) + roles_granting(action)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def permissions_for(action)
|
75
|
+
perms = permission_mapper.permissions_for(action)
|
76
|
+
perms.map { |perm| Credential::Permission.new(perm) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def roles_granting(action)
|
80
|
+
roles = permission_mapper.roles_granting(action)
|
81
|
+
roles.map { |role| Credential::Role.new(role) }
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :permission_mapper
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Credential
|
5
|
+
# A Role is simple extension to the base Credential, specifying its type
|
6
|
+
# as a role and providing a conceptual object to be instantiated or passed.
|
7
|
+
#
|
8
|
+
# The most common use from outside Checkpoint will be by way of
|
9
|
+
# {Checkpoint::Query::RoleGranted}, which will ask whether a given named
|
10
|
+
# role is granted for a user. However, Role could be extended or modified
|
11
|
+
# to implement aliasing or hierarchy, for example
|
12
|
+
#
|
13
|
+
# More likely, though, is advising the resolution of Roles through a
|
14
|
+
# {Checkpoint::PermissionMapper} or implementing a custom
|
15
|
+
# {Checkpoint::Credential::Resolver}. Subclassing or monkey-patching Role
|
16
|
+
# should only be necessary if the application needs to extend the actual
|
17
|
+
# behavior of the Role objects, rather than just which ones are resolved.
|
18
|
+
class Role < Credential
|
19
|
+
TYPE = 'role'
|
20
|
+
|
21
|
+
def type
|
22
|
+
TYPE
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkpoint
|
4
|
+
class Credential
|
5
|
+
# A Credential::Token is an identifier object for a Credential. It includes
|
6
|
+
# a type and an identifier. A {Permit} can be granted for a Token. Concrete
|
7
|
+
# actions are resolved into a number of credentials, and those credentials'
|
8
|
+
# tokens will be checked for matching permits.
|
9
|
+
class Token
|
10
|
+
attr_reader :type, :id
|
11
|
+
|
12
|
+
# Create a new Credential representing a permission or instrument that
|
13
|
+
# represents multiple permissions.
|
14
|
+
#
|
15
|
+
# @param type [String] the application-determined type of this credential.
|
16
|
+
# For example, this might be 'permission' or 'role'.
|
17
|
+
#
|
18
|
+
# @param id [String] the application-resolvable identifier for this
|
19
|
+
# credential. For example, this might be an action to be taken or the ID
|
20
|
+
# of a role.
|
21
|
+
def initialize(type, id)
|
22
|
+
@type = type.to_s
|
23
|
+
@id = id.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] a URI for this credential, including its type and id
|
27
|
+
def uri
|
28
|
+
"credential://#{type}/#{id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Token] self; for convenience of taking a Credential or token
|
32
|
+
def token
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] a token suitable for granting or matching this credential
|
37
|
+
def to_s
|
38
|
+
"#{type}:#{id}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Compare with another Credential for equality. Consider them to represent
|
42
|
+
# the same credential if `other` is a credential, has the same type, and same id.
|
43
|
+
def eql?(other)
|
44
|
+
other.is_a?(Token) && type == other.type && id == other.id
|
45
|
+
end
|
46
|
+
|
47
|
+
alias == eql?
|
48
|
+
alias inspect uri
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'logger'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Checkpoint
|
8
|
+
# Module for everything related to the Checkpoint database.
|
9
|
+
module DB
|
10
|
+
# Any error with the database that Checkpoint itself detects but cannot handle.
|
11
|
+
class DatabaseError < StandardError; end
|
12
|
+
|
13
|
+
CONNECTION_ERROR = 'The Checkpoint database is not initialized. Call initialize! first.'
|
14
|
+
|
15
|
+
ALREADY_CONNECTED = 'Already connected; refusing to connect to another database.'
|
16
|
+
|
17
|
+
MISSING_CONFIG = <<~MSG
|
18
|
+
CHECKPOINT_DATABASE_URL and DATABASE_URL are both missing and a connection
|
19
|
+
has not been configured. Cannot connect to the Checkpoint database.
|
20
|
+
See Checkpoint::DB.connect! for help.
|
21
|
+
MSG
|
22
|
+
|
23
|
+
LOAD_ERROR = <<~MSG
|
24
|
+
Error loading Checkpoint database models.
|
25
|
+
Verify connection information and that the database is migrated.
|
26
|
+
MSG
|
27
|
+
|
28
|
+
SCHEMA_HEADER = "# Checkpoint Database Version\n"
|
29
|
+
|
30
|
+
class << self
|
31
|
+
# Initialize Checkpoint
|
32
|
+
#
|
33
|
+
# This connects to the database if it has not already happened and
|
34
|
+
# requires all of the Checkpoint model classes. It is required to do the
|
35
|
+
# connection setup first because of the design decision in Sequel that
|
36
|
+
# the schema is examined at the time of extending Sequel::Model.
|
37
|
+
def initialize!
|
38
|
+
connect! unless connected?
|
39
|
+
begin
|
40
|
+
model_files.each do |file|
|
41
|
+
require_relative file
|
42
|
+
end
|
43
|
+
rescue Sequel::DatabaseError, NoMethodError => e
|
44
|
+
raise DatabaseError, LOAD_ERROR + "\n" + e.message
|
45
|
+
end
|
46
|
+
db
|
47
|
+
end
|
48
|
+
|
49
|
+
# Connect to the Checkpoint database.
|
50
|
+
#
|
51
|
+
# The default is to use the settings under {.config}, but can be
|
52
|
+
# supplied here (and they will be merged into config as a side effect).
|
53
|
+
# The keys that will be used from either source are documented here as
|
54
|
+
# the options.
|
55
|
+
#
|
56
|
+
# Only one "mode" will be used; the first of these supplied will take
|
57
|
+
# precedence:
|
58
|
+
#
|
59
|
+
# 1. An already-connected {Sequel::Database} object
|
60
|
+
# 2. A connection string
|
61
|
+
# 3. A connection options hash
|
62
|
+
#
|
63
|
+
# While Checkpoint serves as a singleton, this will raise a DatabaseError
|
64
|
+
# if already connected. Check `connected?` if you are unsure.
|
65
|
+
#
|
66
|
+
# @see {Sequel.connect}
|
67
|
+
# @param [Hash] config Optional connection config
|
68
|
+
# @option config [String] :url A Sequel database URL
|
69
|
+
# @option config [Hash] :opts A set of connection options
|
70
|
+
# @option config [Sequel::Database] :db An already-connected database;
|
71
|
+
# @return [Sequel::Database] The initialized database connection
|
72
|
+
def connect!(config = {})
|
73
|
+
raise DatabaseError, ALREADY_CONNECTED if connected?
|
74
|
+
merge_config!(config)
|
75
|
+
raise DatabaseError, MISSING_CONFIG if self.config.db.nil? && conn_opts.empty?
|
76
|
+
|
77
|
+
# We splat here because we might give one or two arguments depending
|
78
|
+
# on whether we have a string or not; to add our logger regardless.
|
79
|
+
@db = self.config.db || Sequel.connect(*conn_opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Run any pending migrations.
|
83
|
+
# This will connect with the current config if not already connected.
|
84
|
+
def migrate!
|
85
|
+
connect! unless connected?
|
86
|
+
Sequel.extension :migration
|
87
|
+
Sequel::Migrator.run(db, File.join(__dir__, '../../db/migrations'), table: schema_table)
|
88
|
+
end
|
89
|
+
|
90
|
+
def schema_table
|
91
|
+
:checkpoint_schema
|
92
|
+
end
|
93
|
+
|
94
|
+
def schema_file
|
95
|
+
'db/checkpoint.yml'
|
96
|
+
end
|
97
|
+
|
98
|
+
def dump_schema!
|
99
|
+
connect! unless connected?
|
100
|
+
version = db[schema_table].first.to_yaml
|
101
|
+
File.write(schema_file, SCHEMA_HEADER + version)
|
102
|
+
end
|
103
|
+
|
104
|
+
def load_schema!
|
105
|
+
connect! unless connected?
|
106
|
+
version = YAML.load_file(schema_file)[:version]
|
107
|
+
db[schema_table].delete
|
108
|
+
db[schema_table].insert(version: version)
|
109
|
+
end
|
110
|
+
|
111
|
+
def model_files
|
112
|
+
[
|
113
|
+
'db/permit'
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Merge url, opts, or db settings from a hash into our config
|
118
|
+
def merge_config!(config = {})
|
119
|
+
self.config.url = config[:url] if config.key?(:url)
|
120
|
+
self.config.opts = config[:opts] if config.key?(:opts)
|
121
|
+
self.config.db = config[:db] if config.key?(:db)
|
122
|
+
end
|
123
|
+
|
124
|
+
def conn_opts
|
125
|
+
log = { logger: Logger.new('db/checkpoint.log') }
|
126
|
+
url = config.url
|
127
|
+
opts = config.opts
|
128
|
+
if url
|
129
|
+
[url, log]
|
130
|
+
elsif opts
|
131
|
+
[log.merge(opts)]
|
132
|
+
else
|
133
|
+
[]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def config
|
138
|
+
@config ||= OpenStruct.new(
|
139
|
+
url: ENV['CHECKPOINT_DATABASE_URL'] || ENV['DATABASE_URL']
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def connected?
|
144
|
+
!@db.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
# The Checkpoint database
|
148
|
+
# @return [Sequel::Database] The connected database; be sure to call initialize! first.
|
149
|
+
def db
|
150
|
+
raise DatabaseError, CONNECTION_ERROR unless connected?
|
151
|
+
@db
|
152
|
+
end
|
153
|
+
|
154
|
+
# Forward the Sequel::Database []-syntax down to db for convenience.
|
155
|
+
# Everything else must be called on db directly, but this is nice sugar.
|
156
|
+
def [](*args)
|
157
|
+
db[*args]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|