roadforest 0.5 → 0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/file-management.rb +70 -58
- data/lib/roadforest/application.rb +9 -17
- data/lib/roadforest/application/dispatcher.rb +76 -9
- data/lib/roadforest/application/parameters.rb +9 -1
- data/lib/roadforest/application/path-provider.rb +30 -3
- data/lib/roadforest/application/route-adapter.rb +96 -14
- data/lib/roadforest/application/services-host.rb +21 -3
- data/lib/roadforest/augment/affordance.rb +82 -11
- data/lib/roadforest/augment/augmentation.rb +24 -6
- data/lib/roadforest/augment/augmenter.rb +12 -3
- data/lib/roadforest/authorization.rb +7 -229
- data/lib/roadforest/authorization/auth-entity.rb +26 -0
- data/lib/roadforest/authorization/authentication-chain.rb +79 -0
- data/lib/roadforest/authorization/default-authentication-store.rb +33 -0
- data/lib/roadforest/authorization/grant-builder.rb +23 -0
- data/lib/roadforest/authorization/grants-holder.rb +58 -0
- data/lib/roadforest/authorization/manager.rb +85 -0
- data/lib/roadforest/authorization/policy.rb +19 -0
- data/lib/roadforest/graph/access-manager.rb +25 -2
- data/lib/roadforest/graph/focus-list.rb +4 -0
- data/lib/roadforest/graph/graph-focus.rb +30 -13
- data/lib/roadforest/graph/nav-affordance-builder.rb +62 -0
- data/lib/roadforest/graph/normalization.rb +3 -3
- data/lib/roadforest/graph/path-vocabulary.rb +64 -0
- data/lib/roadforest/graph/post-focus.rb +5 -0
- data/lib/roadforest/graph/vocabulary.rb +4 -1
- data/lib/roadforest/http/adapters/excon.rb +4 -0
- data/lib/roadforest/http/graph-transfer.rb +17 -1
- data/lib/roadforest/http/keychain.rb +121 -33
- data/lib/roadforest/http/user-agent.rb +5 -3
- data/lib/roadforest/interface/application.rb +25 -8
- data/lib/roadforest/interface/rdf.rb +114 -15
- data/lib/roadforest/interface/utility.rb +3 -0
- data/lib/roadforest/interface/utility/backfill.rb +63 -0
- data/lib/roadforest/interface/utility/grant-list.rb +45 -0
- data/lib/roadforest/interface/utility/grant.rb +22 -0
- data/lib/roadforest/interfaces.rb +1 -0
- data/lib/roadforest/path-matcher.rb +471 -0
- data/lib/roadforest/remote-host.rb +159 -35
- data/lib/roadforest/resource/read-only.rb +23 -4
- data/lib/roadforest/server.rb +32 -3
- data/lib/roadforest/source-rigor/graph-store.rb +0 -2
- data/lib/roadforest/source-rigor/rigorous-access.rb +138 -21
- data/lib/roadforest/templates/affordance-property-values.haml +3 -0
- data/lib/roadforest/templates/rdfpost-curie.haml +1 -1
- data/lib/roadforest/test-support/matchers.rb +41 -12
- data/lib/roadforest/test-support/remote-host.rb +3 -3
- data/lib/roadforest/type-handlers/rdfa-writer/environment-decorator.rb +1 -1
- data/lib/roadforest/type-handlers/rdfa-writer/render-engine.rb +40 -27
- data/lib/roadforest/type-handlers/rdfa.rb +10 -3
- data/lib/roadforest/utility/class-registry.rb +44 -4
- data/spec/affordance-augmenter.rb +46 -19
- data/spec/affordances-flow.rb +46 -30
- data/spec/authorization.rb +16 -4
- data/spec/client.rb +22 -4
- data/spec/focus-list.rb +24 -0
- data/spec/full-integration.rb +8 -3
- data/spec/graph-store.rb +8 -0
- data/spec/keychain.rb +18 -14
- data/spec/rdf-normalization.rb +32 -6
- data/spec/update-focus.rb +36 -39
- metadata +19 -5
@@ -0,0 +1,26 @@
|
|
1
|
+
module RoadForest
|
2
|
+
module Authorization
|
3
|
+
class AuthEntity
|
4
|
+
def initialize
|
5
|
+
@authenticated = false
|
6
|
+
end
|
7
|
+
attr_accessor :username, :password, :token
|
8
|
+
|
9
|
+
def authenticated?
|
10
|
+
!!@authenticated
|
11
|
+
end
|
12
|
+
|
13
|
+
def authenticate_by_password(password)
|
14
|
+
@authenticated = (!password.nil? and password == @password)
|
15
|
+
end
|
16
|
+
|
17
|
+
def authenticate_by_token(token)
|
18
|
+
@authenticated = (!token.nil? and token == @token)
|
19
|
+
end
|
20
|
+
|
21
|
+
def authenticate!
|
22
|
+
@authenticated = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'roadforest/utility/class-registry'
|
3
|
+
module RoadForest
|
4
|
+
module Authorization
|
5
|
+
class AuthenticationChain
|
6
|
+
class Scheme
|
7
|
+
def self.registry_purpose; "authentication scheme"; end
|
8
|
+
extend Utility::ClassRegistry::Registrar
|
9
|
+
|
10
|
+
def self.register(name)
|
11
|
+
registrar.registry.add(name, self.new)
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticated_entity(credentials, store)
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Basic < Scheme
|
20
|
+
register "Basic"
|
21
|
+
|
22
|
+
def challenge(options)
|
23
|
+
"Basic realm=\"#{options.fetch(:realm, "Roadforest App")}\""
|
24
|
+
end
|
25
|
+
|
26
|
+
def authenticated_entity(credentials, store)
|
27
|
+
username, password = Base64.decode64(credentials).split(':',2)
|
28
|
+
|
29
|
+
entity = store.by_username(username)
|
30
|
+
entity.authenticate_by_password(password)
|
31
|
+
entity
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(store)
|
36
|
+
@store = store
|
37
|
+
end
|
38
|
+
attr_reader :store
|
39
|
+
|
40
|
+
def handler_for(scheme)
|
41
|
+
Scheme.get(scheme)
|
42
|
+
rescue
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def challenge(options)
|
47
|
+
(Scheme.registry.names.map do |scheme_name|
|
48
|
+
handler_for(scheme_name).challenge(options)
|
49
|
+
end).join(", ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_account(user,password,token)
|
53
|
+
@store.add_account(user,password,token)
|
54
|
+
end
|
55
|
+
|
56
|
+
def authenticate(request)
|
57
|
+
if request.respond_to?(:client_cert)
|
58
|
+
subject = request.client_cert.subject
|
59
|
+
name = subject.to_a.find{|entry| entry[0] == "CN"}[1]
|
60
|
+
entity = @store.by_username(name)
|
61
|
+
entity.authenticate!
|
62
|
+
return entity
|
63
|
+
end
|
64
|
+
|
65
|
+
header = request.headers["Authorization"]
|
66
|
+
return nil if header.nil?
|
67
|
+
scheme, credentials = header.split(/\s+/, 2)
|
68
|
+
|
69
|
+
handler = handler_for(scheme)
|
70
|
+
return nil if handler.nil?
|
71
|
+
|
72
|
+
entity = handler.authenticated_entity(credentials, store)
|
73
|
+
return nil if entity.nil?
|
74
|
+
return nil unless entity.authenticated?
|
75
|
+
return entity
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'roadforest/authorization/auth-entity'
|
2
|
+
module RoadForest
|
3
|
+
module Authorization
|
4
|
+
class DefaultAuthenticationStore
|
5
|
+
def initialize
|
6
|
+
@accounts = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_entity(account)
|
10
|
+
return nil if account.nil?
|
11
|
+
AuthEntity.new.tap do |entity|
|
12
|
+
entity.username = account[0]
|
13
|
+
entity.password = account[1]
|
14
|
+
entity.token = account[2]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_account(user, password, token)
|
19
|
+
@accounts << [user, password, token]
|
20
|
+
end
|
21
|
+
|
22
|
+
def by_username(username)
|
23
|
+
account = @accounts.find{|account| account[0] == username }
|
24
|
+
build_entity(account)
|
25
|
+
end
|
26
|
+
|
27
|
+
def by_token(token)
|
28
|
+
account = @accounts.find{|account| account[2] == token }
|
29
|
+
build_entity(account)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RoadForest
|
2
|
+
module Authorization
|
3
|
+
class GrantBuilder
|
4
|
+
def initialize(cache)
|
5
|
+
@cache = cache
|
6
|
+
@list = []
|
7
|
+
end
|
8
|
+
attr_reader :list
|
9
|
+
|
10
|
+
def add(name, params=nil)
|
11
|
+
canonical =
|
12
|
+
if params.nil?
|
13
|
+
[name]
|
14
|
+
else
|
15
|
+
[name, params.keys.sort.map do |key|
|
16
|
+
[key, params[key]]
|
17
|
+
end]
|
18
|
+
end
|
19
|
+
@list << @cache[canonical]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'roadforest/authorization/grant-builder'
|
2
|
+
module RoadForest
|
3
|
+
module Authorization
|
4
|
+
# Caches the obfuscated tokens used to identify permission grants
|
5
|
+
class GrantsHolder
|
6
|
+
def initialize(salt, hash_function)
|
7
|
+
digester = OpenSSL::HMAC.new(salt, hash_function)
|
8
|
+
@conceal = true
|
9
|
+
@grants_cache = Hash.new do |h, k| #XXX potential resource exhaustion here - only accumulate auth'd results
|
10
|
+
if conceal
|
11
|
+
digester.reset
|
12
|
+
digester << token_for(k)
|
13
|
+
h[k] = digester.hexdigest
|
14
|
+
else
|
15
|
+
token_for(k)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
attr_accessor :conceal
|
20
|
+
|
21
|
+
#For use in URIs, per RFC3986:
|
22
|
+
#Cannot use: ":/?#[]@!$&'()*+;="
|
23
|
+
#Percent encoding uses %
|
24
|
+
#Can use: ".,$^*_-|<>~`"
|
25
|
+
#Grants are of the form [:name, [:key, value]*]
|
26
|
+
def token_for(grant)
|
27
|
+
name, attrs = *grant
|
28
|
+
attrs = (attrs || []).map{|pair| group(pair, "_", "~")}
|
29
|
+
percent_encode(group([name] + attrs, ".", "-"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def group(list, sep, replace)
|
33
|
+
list.map{|part| part.to_s.gsub(sep, replace)}.join(sep)
|
34
|
+
end
|
35
|
+
|
36
|
+
PERCENT_ENCODINGS = Hash.new do |h,k|
|
37
|
+
h[k] = k.force_encoding("US-ASCII").getbyte(0).to_s(16)
|
38
|
+
end
|
39
|
+
|
40
|
+
def percent_encode(string)
|
41
|
+
string.gsub(%r|[\[\]:/?#@!$&'()*+;=]|) do |match|
|
42
|
+
PERCENT_ENCODINGS[match]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(key)
|
47
|
+
@grants_cache[key]
|
48
|
+
end
|
49
|
+
alias [] get
|
50
|
+
|
51
|
+
def build_grants
|
52
|
+
builder = GrantBuilder.new(self)
|
53
|
+
yield builder
|
54
|
+
return builder.list
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'roadforest/authorization/authentication-chain'
|
2
|
+
require 'roadforest/authorization/grants-holder'
|
3
|
+
require 'roadforest/authorization/default-authentication-store'
|
4
|
+
require 'roadforest/authorization/policy'
|
5
|
+
|
6
|
+
module RoadForest
|
7
|
+
module Authorization
|
8
|
+
# The root of the RoadForest authorization scheme.
|
9
|
+
|
10
|
+
# Resources describe a set of permissions that are allowed to access them,
|
11
|
+
# on a per-method case.
|
12
|
+
#
|
13
|
+
# An overall Policy object provides permission grants to authenticated
|
14
|
+
# entities (typically users, but could be e.g. applications acting on their
|
15
|
+
# behalf)
|
16
|
+
#
|
17
|
+
# The ultimate grant/refuse decision comes down to: is there a shared
|
18
|
+
# permission in the list required by the resource and those granted to the
|
19
|
+
# entity.
|
20
|
+
#
|
21
|
+
# Permissions have a name and an optional set of parameters, and can be
|
22
|
+
# referred to as such within the application on the server. They're stored
|
23
|
+
# as digests of those names, which should be safe to communicate to the
|
24
|
+
# user application, which can make interaction decisions based on the
|
25
|
+
# permissions presented.
|
26
|
+
#
|
27
|
+
# The default ServicesHost exposes a Manager as #authz
|
28
|
+
class Manager
|
29
|
+
attr_accessor :authenticator
|
30
|
+
attr_accessor :policy
|
31
|
+
attr_reader :grants
|
32
|
+
|
33
|
+
HASH_FUNCTION = "SHA256".freeze
|
34
|
+
|
35
|
+
def initialize(salt = nil, authenticator = nil, policy = nil)
|
36
|
+
#XXX consider launch-time randomized salt
|
37
|
+
@grants = GrantsHolder.new(salt || "roadforest-insecure", HASH_FUNCTION)
|
38
|
+
|
39
|
+
@store = DefaultAuthenticationStore.new
|
40
|
+
@authenticator = authenticator || AuthenticationChain.new(@store)
|
41
|
+
@policy = policy || Policy.new
|
42
|
+
@policy.grants_holder = @grants
|
43
|
+
end
|
44
|
+
|
45
|
+
def cleartext_grants!
|
46
|
+
@grants.conceal = false
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_grants(&block)
|
50
|
+
@grants.build_grants(&block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def challenge(options)
|
54
|
+
@authenticator.challenge(options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @returns [:public|:granted|:refused]
|
58
|
+
#
|
59
|
+
# :public means the request doesn't need authorization
|
60
|
+
# :granted means that it does need authz but the credentials passed are
|
61
|
+
# allowed to access the resource
|
62
|
+
# :refused means that the credentials passed are not allowed to access
|
63
|
+
# the resource
|
64
|
+
#
|
65
|
+
# TODO: Resource needs to add s-maxage=0 for :granted requests or public
|
66
|
+
# for :public requests to the CacheControl header
|
67
|
+
def authorization(request, required_grants)
|
68
|
+
entity = nil
|
69
|
+
if entity.nil?
|
70
|
+
entity = authenticator.authenticate(request)
|
71
|
+
end
|
72
|
+
|
73
|
+
return :refused if entity.nil?
|
74
|
+
|
75
|
+
available_grants = policy.grants_for(entity)
|
76
|
+
|
77
|
+
if required_grants.any?{|required| available_grants.include?(required)}
|
78
|
+
return :granted
|
79
|
+
else
|
80
|
+
return :refused
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RoadForest
|
2
|
+
module Authorization
|
3
|
+
# Responsible to assigning particular permission grants to entities. There
|
4
|
+
# should be one subclass of Policy per application, ideally.
|
5
|
+
class Policy
|
6
|
+
attr_accessor :grants_holder
|
7
|
+
|
8
|
+
def build_grants(&block)
|
9
|
+
grants_holder.build_grants(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def grants_for(entity)
|
13
|
+
build_grants do |builder|
|
14
|
+
builder.add(:admin)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -14,6 +14,9 @@ module RoadForest::Graph
|
|
14
14
|
@resource = normalize_context(resource)
|
15
15
|
end
|
16
16
|
|
17
|
+
def reset
|
18
|
+
end
|
19
|
+
|
17
20
|
def dup
|
18
21
|
other = self.class.allocate
|
19
22
|
other.resource = self.resource
|
@@ -29,6 +32,10 @@ module RoadForest::Graph
|
|
29
32
|
relevant_prefixes_for_graph(origin_graph)
|
30
33
|
end
|
31
34
|
|
35
|
+
def each_statement(&block)
|
36
|
+
origin_graph.each_statement(&block)
|
37
|
+
end
|
38
|
+
|
32
39
|
def each(&block)
|
33
40
|
origin_graph.each(&block)
|
34
41
|
end
|
@@ -53,14 +60,18 @@ module RoadForest::Graph
|
|
53
60
|
end
|
54
61
|
|
55
62
|
class WriteManager < ReadOnlyManager
|
63
|
+
include ::RDF::Writable
|
64
|
+
|
56
65
|
def insert(statement)
|
57
|
-
statement
|
66
|
+
destination_graph.insert(statement)
|
67
|
+
end
|
68
|
+
|
69
|
+
def insert_statement(statement)
|
58
70
|
destination_graph.insert(statement)
|
59
71
|
end
|
60
72
|
|
61
73
|
def delete(statement)
|
62
74
|
statement = RDF::Query::Pattern.from(statement)
|
63
|
-
statement.context = resource
|
64
75
|
destination_graph.delete(statement)
|
65
76
|
end
|
66
77
|
end
|
@@ -70,6 +81,11 @@ module RoadForest::Graph
|
|
70
81
|
|
71
82
|
alias destination_graph target_graph
|
72
83
|
|
84
|
+
def reset
|
85
|
+
@target_graph ||= ::RDF::Repository.new
|
86
|
+
@target_graph.clear
|
87
|
+
end
|
88
|
+
|
73
89
|
def dup
|
74
90
|
other = super
|
75
91
|
other.target_graph = self.target_graph
|
@@ -79,6 +95,13 @@ module RoadForest::Graph
|
|
79
95
|
def relevant_prefixes
|
80
96
|
super.merge(relevant_prefixes_for_graph(destination_graph))
|
81
97
|
end
|
98
|
+
|
99
|
+
def each_target
|
100
|
+
destination_graph.each_context do |context|
|
101
|
+
graph = ::RDF::Graph.new(context, :data => target_graph)
|
102
|
+
yield(context, graph)
|
103
|
+
end
|
104
|
+
end
|
82
105
|
end
|
83
106
|
|
84
107
|
class CopyManager < SplitManager
|
@@ -44,6 +44,10 @@ module RoadForest::Graph
|
|
44
44
|
alias << append
|
45
45
|
|
46
46
|
def append_node(subject=nil)
|
47
|
+
if empty? and self.subject == RDF.nil
|
48
|
+
raise "Attempted to append #{subject} to an empty list (remove the list and replace it)"
|
49
|
+
end
|
50
|
+
|
47
51
|
base_node.create_node(subject) do |node|
|
48
52
|
append(node.subject)
|
49
53
|
yield node if block_given?
|
@@ -45,15 +45,8 @@ module RoadForest::Graph
|
|
45
45
|
end
|
46
46
|
|
47
47
|
class GraphFocus
|
48
|
-
#XXX Any changes to this class heirarchy or to ContextFascade should start
|
49
|
-
#with a refactor like:
|
50
|
-
# Reduce this to the single-node API ([] []=)
|
51
|
-
# Change the ContextFascade into a family of classes (RO, RW, Update)
|
52
48
|
include Normalization
|
53
49
|
|
54
|
-
#attr_accessor :source_graph, :target_graph, :subject, :root_url,
|
55
|
-
#:source_rigor
|
56
|
-
|
57
50
|
attr_accessor :subject, :access_manager
|
58
51
|
|
59
52
|
alias rdf subject
|
@@ -63,6 +56,10 @@ module RoadForest::Graph
|
|
63
56
|
self.subject = subject unless subject.nil?
|
64
57
|
end
|
65
58
|
|
59
|
+
def reset
|
60
|
+
access_manager.reset
|
61
|
+
end
|
62
|
+
|
66
63
|
def root_url
|
67
64
|
@access_manager.resource
|
68
65
|
end
|
@@ -111,6 +108,14 @@ module RoadForest::Graph
|
|
111
108
|
return value
|
112
109
|
end
|
113
110
|
|
111
|
+
def <<(data)
|
112
|
+
case data
|
113
|
+
when Array
|
114
|
+
data = normalize_statement(*data)
|
115
|
+
end
|
116
|
+
access_manager << data
|
117
|
+
end
|
118
|
+
|
114
119
|
def delete(property, extra=nil)
|
115
120
|
access_manager.delete(:subject => subject, :predicate => normalize_property(property, extra))
|
116
121
|
end
|
@@ -148,11 +153,25 @@ module RoadForest::Graph
|
|
148
153
|
node
|
149
154
|
end
|
150
155
|
|
156
|
+
def empty_list(property, extra=nil)
|
157
|
+
unless extra.nil?
|
158
|
+
property = normalize_property([property, extra])
|
159
|
+
end
|
160
|
+
list
|
161
|
+
end
|
162
|
+
|
151
163
|
def add_list(property, extra=nil)
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
164
|
+
unless extra.nil?
|
165
|
+
property = normalize_property([property, extra])
|
166
|
+
end
|
167
|
+
if block_given?
|
168
|
+
list = add_node(property).as_list
|
169
|
+
yield list
|
170
|
+
else
|
171
|
+
list = FocusList.new
|
172
|
+
add(property, list.subject)
|
173
|
+
end
|
174
|
+
list
|
156
175
|
end
|
157
176
|
#### End of old GraphFocus
|
158
177
|
|
@@ -171,8 +190,6 @@ module RoadForest::Graph
|
|
171
190
|
return nil if value.nil?
|
172
191
|
if RDF::Literal === value
|
173
192
|
value.object
|
174
|
-
elsif value == RDF.nil
|
175
|
-
nil
|
176
193
|
else
|
177
194
|
wrap_node(value)
|
178
195
|
end
|