legion-data 1.7.3 → 1.8.0

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.pre-commit-config.yaml +29 -0
  3. data/AGENTS.md +66 -13
  4. data/CHANGELOG.md +29 -0
  5. data/CLAUDE.md +44 -307
  6. data/README.md +119 -7
  7. data/lib/legion/data/connection.rb +3 -1
  8. data/lib/legion/data/migrations/077_create_llm_conversations.rb +32 -0
  9. data/lib/legion/data/migrations/078_create_llm_messages.rb +33 -0
  10. data/lib/legion/data/migrations/079_create_llm_message_inference_requests.rb +47 -0
  11. data/lib/legion/data/migrations/080_create_llm_message_inference_responses.rb +39 -0
  12. data/lib/legion/data/migrations/081_add_llm_message_inference_foreign_keys.rb +17 -0
  13. data/lib/legion/data/migrations/082_create_llm_route_attempts.rb +31 -0
  14. data/lib/legion/data/migrations/083_create_llm_message_inference_metrics.rb +36 -0
  15. data/lib/legion/data/migrations/084_create_llm_tool_calls.rb +32 -0
  16. data/lib/legion/data/migrations/085_add_llm_message_tool_call_foreign_key.rb +15 -0
  17. data/lib/legion/data/migrations/086_create_llm_tool_call_attempts.rb +30 -0
  18. data/lib/legion/data/migrations/087_create_llm_conversation_compactions.rb +31 -0
  19. data/lib/legion/data/migrations/088_create_llm_policy_evaluations.rb +33 -0
  20. data/lib/legion/data/migrations/089_create_llm_security_events.rb +33 -0
  21. data/lib/legion/data/migrations/090_create_llm_registry_events.rb +23 -0
  22. data/lib/legion/data/migrations/091_create_portable_identity_providers.rb +35 -0
  23. data/lib/legion/data/migrations/092_create_portable_identity_principals.rb +25 -0
  24. data/lib/legion/data/migrations/093_create_portable_identities.rb +31 -0
  25. data/lib/legion/data/migrations/094_create_portable_identity_groups.rb +21 -0
  26. data/lib/legion/data/migrations/095_create_portable_identity_group_memberships.rb +25 -0
  27. data/lib/legion/data/migrations/096_create_portable_identity_audit_log.rb +26 -0
  28. data/lib/legion/data/migrations/097_add_llm_dispatch_fields.rb +16 -0
  29. data/lib/legion/data/model.rb +11 -1
  30. data/lib/legion/data/models/apollo/access_log.rb +17 -0
  31. data/lib/legion/data/models/apollo/entries.rb +22 -0
  32. data/lib/legion/data/models/apollo/expertise.rb +16 -0
  33. data/lib/legion/data/models/apollo/model_helpers.rb +17 -0
  34. data/lib/legion/data/models/apollo/operation.rb +16 -0
  35. data/lib/legion/data/models/apollo/relation.rb +18 -0
  36. data/lib/legion/data/models/function.rb +1 -0
  37. data/lib/legion/data/models/identity/audit_log.rb +20 -0
  38. data/lib/legion/data/models/identity/group.rb +28 -0
  39. data/lib/legion/data/models/identity/group_memberships.rb +28 -0
  40. data/lib/legion/data/models/identity/identity.rb +24 -0
  41. data/lib/legion/data/models/identity/model_helpers.rb +86 -0
  42. data/lib/legion/data/models/identity/principal.rb +37 -0
  43. data/lib/legion/data/models/identity/providers.rb +34 -0
  44. data/lib/legion/data/models/identity.rb +8 -0
  45. data/lib/legion/data/models/identity_group.rb +13 -0
  46. data/lib/legion/data/models/identity_provider.rb +8 -0
  47. data/lib/legion/data/models/llm/conversation.rb +25 -0
  48. data/lib/legion/data/models/llm/conversation_compaction.rb +22 -0
  49. data/lib/legion/data/models/llm/message.rb +105 -0
  50. data/lib/legion/data/models/llm/message_inference_metric.rb +46 -0
  51. data/lib/legion/data/models/llm/message_inference_request.rb +80 -0
  52. data/lib/legion/data/models/llm/message_inference_response.rb +23 -0
  53. data/lib/legion/data/models/llm/model_helpers.rb +18 -0
  54. data/lib/legion/data/models/llm/policy_evaluation.rb +20 -0
  55. data/lib/legion/data/models/llm/registry_event.rb +15 -0
  56. data/lib/legion/data/models/llm/route_attempt.rb +18 -0
  57. data/lib/legion/data/models/llm/security_event.rb +66 -0
  58. data/lib/legion/data/models/llm/tool_call.rb +21 -0
  59. data/lib/legion/data/models/llm/tool_call_attempt.rb +18 -0
  60. data/lib/legion/data/models/node.rb +2 -1
  61. data/lib/legion/data/models/principal.rb +13 -0
  62. data/lib/legion/data/models/rbac/cross_team_grants.rb +25 -0
  63. data/lib/legion/data/models/rbac/model_helpers.rb +25 -0
  64. data/lib/legion/data/models/rbac/role_assignments.rb +25 -0
  65. data/lib/legion/data/models/rbac/runner_grants.rb +23 -0
  66. data/lib/legion/data/models/relationship.rb +1 -0
  67. data/lib/legion/data/models/runner.rb +24 -2
  68. data/lib/legion/data/models/task.rb +4 -0
  69. data/lib/legion/data/version.rb +1 -1
  70. data/scripts/pre-commit-rubocop.sh +16 -0
  71. metadata +54 -1
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'identity/model_helpers'
4
+
3
5
  return unless Legion::Data::Connection.adapter == :postgres
4
6
 
5
7
  module Legion
6
8
  module Data
7
9
  module Model
8
10
  class IdentityProvider < Sequel::Model(:identity_providers)
11
+ include Identity::ModelHelpers
12
+
9
13
  one_to_many :identities, class: 'Legion::Data::Model::Identity'
10
14
 
15
+ def self.lookup_columns
16
+ %i[id uuid name]
17
+ end
18
+
11
19
  def parsed_capabilities
12
20
  Array(capabilities)
13
21
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class Conversation < Sequel::Model(:llm_conversations)
10
+ include ModelHelpers
11
+
12
+ one_to_many :messages
13
+ one_to_many :message_inference_requests
14
+ one_to_many :conversation_compactions
15
+ one_to_many :policy_evaluations
16
+ one_to_many :security_events
17
+
18
+ def security_incident_lineage
19
+ SecurityEvent.lineage_for_conversation(self)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class ConversationCompaction < Sequel::Model(:llm_conversation_compactions)
10
+ include ModelHelpers
11
+
12
+ many_to_one :conversation
13
+ many_to_one :triggered_by_message_inference_request,
14
+ class: 'Legion::Data::Models::LLM::MessageInferenceRequest',
15
+ key: :triggered_by_message_inference_request_id
16
+ many_to_one :replaces_message_from, class: 'Legion::Data::Models::LLM::Message', key: :replaces_message_from_id
17
+ many_to_one :replaces_message_to, class: 'Legion::Data::Models::LLM::Message', key: :replaces_message_to_id
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class Message < Sequel::Model(:llm_messages)
10
+ include ModelHelpers
11
+
12
+ many_to_one :conversation
13
+ many_to_one :parent_message, class: 'Legion::Data::Models::LLM::Message', key: :parent_message_id
14
+ many_to_one :message_inference_request
15
+ many_to_one :message_inference_response
16
+ many_to_one :tool_call
17
+
18
+ one_to_many :child_messages, class: 'Legion::Data::Models::LLM::Message', key: :parent_message_id
19
+ one_to_many :triggered_message_inference_requests,
20
+ class: 'Legion::Data::Models::LLM::MessageInferenceRequest',
21
+ key: :latest_message_id
22
+ one_to_many :message_inference_responses,
23
+ class: 'Legion::Data::Models::LLM::MessageInferenceResponse',
24
+ key: :response_message_id
25
+ one_to_many :requested_tool_calls, class: 'Legion::Data::Models::LLM::ToolCall',
26
+ key: :requested_by_message_id
27
+ one_to_many :result_tool_calls, class: 'Legion::Data::Models::LLM::ToolCall',
28
+ key: :result_message_id
29
+ one_to_many :compactions_from, class: 'Legion::Data::Models::LLM::ConversationCompaction',
30
+ key: :replaces_message_from_id
31
+ one_to_many :compactions_to, class: 'Legion::Data::Models::LLM::ConversationCompaction',
32
+ key: :replaces_message_to_id
33
+
34
+ class << self
35
+ def incident_flow_from(message_or_id)
36
+ message = message_or_id.is_a?(self) ? message_or_id : self[message_or_id]
37
+ message&.incident_flow
38
+ end
39
+ end
40
+
41
+ def incident_flow
42
+ requests = incident_flow_requests
43
+ responses = incident_flow_responses(requests)
44
+ route_attempts = RouteAttempt.where(message_inference_request_id: requests.map(&:id))
45
+ .order(:message_inference_request_id, :attempt_no, :id)
46
+ .all
47
+ tool_calls = incident_flow_tool_calls(responses)
48
+ tool_call_attempts = ToolCallAttempt.where(tool_call_id: tool_calls.map(&:id))
49
+ .order(:tool_call_id, :attempt_no, :id)
50
+ .all
51
+
52
+ {
53
+ message: self,
54
+ conversation: conversation,
55
+ requests: requests,
56
+ route_attempts: route_attempts,
57
+ responses: responses,
58
+ response_messages: responses.filter_map(&:response_message),
59
+ tool_calls: tool_calls,
60
+ tool_call_attempts: tool_call_attempts,
61
+ result_messages: incident_flow_result_messages(responses, tool_calls)
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ def incident_flow_requests
68
+ request_ids = []
69
+ request_ids << message_inference_request_id if message_inference_request_id
70
+ request_ids.concat(MessageInferenceRequest.where(latest_message_id: id).select_map(:id))
71
+ if message_inference_response_id && (linked_response = MessageInferenceResponse[message_inference_response_id])
72
+ request_ids << linked_response.message_inference_request_id
73
+ end
74
+ if tool_call_id && (linked_tool_call = ToolCall[tool_call_id])
75
+ request_ids << linked_tool_call.message_inference_response.message_inference_request_id
76
+ end
77
+
78
+ MessageInferenceRequest.where(id: request_ids.uniq).order(:id).all
79
+ end
80
+
81
+ def incident_flow_responses(requests)
82
+ request_ids = requests.map(&:id)
83
+ response_scope = MessageInferenceResponse.where(message_inference_request_id: request_ids)
84
+ response_scope = response_scope.or(id: message_inference_response_id) if message_inference_response_id
85
+ response_scope.order(:id).all
86
+ end
87
+
88
+ def incident_flow_tool_calls(responses)
89
+ response_ids = responses.map(&:id)
90
+ scope = ToolCall.where(message_inference_response_id: response_ids)
91
+ scope = scope.or(requested_by_message_id: id).or(result_message_id: id)
92
+ scope.order(:message_inference_response_id, :tool_call_index, :id).all
93
+ end
94
+
95
+ def incident_flow_result_messages(responses, tool_calls)
96
+ message_ids = responses.filter_map(&:response_message_id) + tool_calls.filter_map(&:result_message_id)
97
+ scope = Message.where(id: message_ids.uniq)
98
+ scope = scope.or(tool_call_id: tool_calls.map(&:id)) unless tool_calls.empty?
99
+ scope.order(:seq, :id).all
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class MessageInferenceMetric < Sequel::Model(:llm_message_inference_metrics)
10
+ include ModelHelpers
11
+
12
+ many_to_one :message_inference_request
13
+ many_to_one :message_inference_response
14
+
15
+ class << self
16
+ def finance_usage_by_cost_center_model_day(cost_center: nil, model_key: nil, from: nil, to: nil)
17
+ usage_day = Sequel.function(:date, :recorded_at)
18
+ scope = dataset
19
+ scope = scope.where(cost_center: cost_center) unless cost_center.nil?
20
+ scope = scope.where(model_key: model_key) unless model_key.nil?
21
+ scope = scope.where { recorded_at >= from } unless from.nil?
22
+ scope = scope.where { recorded_at < to } unless to.nil?
23
+
24
+ scope
25
+ .select(
26
+ :cost_center,
27
+ :model_key,
28
+ usage_day.as(:usage_day),
29
+ Sequel.function(:sum, :input_tokens).as(:input_tokens),
30
+ Sequel.function(:sum, :output_tokens).as(:output_tokens),
31
+ Sequel.function(:sum, :thinking_tokens).as(:thinking_tokens),
32
+ Sequel.function(:sum, :total_tokens).as(:total_tokens),
33
+ Sequel.function(:sum, :cost_usd).as(:cost_usd),
34
+ Sequel.function(:sum, :latency_ms).as(:latency_ms),
35
+ Sequel.function(:sum, :wall_clock_ms).as(:wall_clock_ms)
36
+ )
37
+ .group(:cost_center, :model_key, usage_day)
38
+ .order(:cost_center, :model_key, usage_day)
39
+ .map(&:values)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class MessageInferenceRequest < Sequel::Model(:llm_message_inference_requests)
10
+ include ModelHelpers
11
+
12
+ many_to_one :conversation
13
+ many_to_one :latest_message, class: 'Legion::Data::Models::LLM::Message', key: :latest_message_id
14
+ one_to_many :message_inference_responses
15
+ one_to_many :route_attempts
16
+ one_to_many :message_inference_metrics
17
+ one_to_many :conversation_compactions, key: :triggered_by_message_inference_request_id
18
+ one_to_many :policy_evaluations
19
+ one_to_many :security_events
20
+
21
+ class << self
22
+ def lookup(reference)
23
+ return reference if reference.is_a?(self)
24
+
25
+ value = reference.to_s
26
+ scope = where(uuid: value).or(request_ref: value)
27
+ scope = scope.or(id: value.to_i) if value.match?(/\A\d+\z/)
28
+ scope.first
29
+ end
30
+
31
+ def audit_lineage_for(reference)
32
+ lookup(reference)&.audit_lineage
33
+ end
34
+ end
35
+
36
+ def audit_lineage
37
+ responses = message_inference_responses_dataset.order(:id).all
38
+ response_ids = responses.map(&:id)
39
+ tool_calls = ToolCall.where(message_inference_response_id: response_ids).order(:tool_call_index, :id).all
40
+ tool_call_ids = tool_calls.map(&:id)
41
+
42
+ {
43
+ request: self,
44
+ request_id: id,
45
+ request_ref: request_ref,
46
+ conversation: conversation,
47
+ latest_message: latest_message,
48
+ caller_principal: caller_principal,
49
+ caller_identity: caller_identity,
50
+ route_attempts: route_attempts_dataset.order(:attempt_no, :id).all,
51
+ responses: responses,
52
+ response_messages: responses.filter_map(&:response_message),
53
+ metrics: message_inference_metrics_dataset.order(:recorded_at, :id).all,
54
+ policy_evaluations: policy_evaluations_dataset.order(:evaluated_at, :id).all,
55
+ security_events: security_events_dataset.order(:detected_at, :id).all,
56
+ tool_calls: tool_calls,
57
+ tool_call_attempts: ToolCallAttempt.where(tool_call_id: tool_call_ids).order(:tool_call_id, :attempt_no, :id).all
58
+ }
59
+ end
60
+
61
+ def request
62
+ self
63
+ end
64
+
65
+ def caller_principal
66
+ return nil unless caller_principal_id && defined?(Legion::Data::Model::Identity::Principal)
67
+
68
+ Legion::Data::Model::Identity::Principal.first(id: caller_principal_id)
69
+ end
70
+
71
+ def caller_identity
72
+ return nil unless caller_identity_id && defined?(Legion::Data::Model::Identity::Identity)
73
+
74
+ Legion::Data::Model::Identity::Identity.first(id: caller_identity_id)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class MessageInferenceResponse < Sequel::Model(:llm_message_inference_responses)
10
+ include ModelHelpers
11
+
12
+ many_to_one :message_inference_request
13
+ many_to_one :response_message, class: 'Legion::Data::Models::LLM::Message', key: :response_message_id
14
+ one_to_many :route_attempts
15
+ one_to_many :message_inference_metrics
16
+ one_to_many :tool_calls
17
+ one_to_many :policy_evaluations
18
+ one_to_many :security_events
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ module ModelHelpers
10
+ def before_create
11
+ self[:uuid] ||= SecureRandom.uuid if columns.include?(:uuid)
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class PolicyEvaluation < Sequel::Model(:llm_policy_evaluations)
10
+ include ModelHelpers
11
+
12
+ many_to_one :conversation
13
+ many_to_one :message_inference_request
14
+ many_to_one :message_inference_response
15
+ one_to_many :security_events
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class RegistryEvent < Sequel::Model(:llm_registry_events)
10
+ include ModelHelpers
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class RouteAttempt < Sequel::Model(:llm_route_attempts)
10
+ include ModelHelpers
11
+
12
+ many_to_one :message_inference_request
13
+ many_to_one :message_inference_response
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class SecurityEvent < Sequel::Model(:llm_security_events)
10
+ include ModelHelpers
11
+
12
+ many_to_one :conversation
13
+ many_to_one :message_inference_request
14
+ many_to_one :message_inference_response
15
+ many_to_one :tool_call
16
+ many_to_one :tool_call_attempt
17
+ many_to_one :policy_evaluation
18
+
19
+ class << self
20
+ def lineage_for_conversation(conversation_or_id)
21
+ conversation_id = conversation_or_id.respond_to?(:id) ? conversation_or_id.id : conversation_or_id
22
+ requests = MessageInferenceRequest.where(conversation_id: conversation_id).order(:id).all
23
+ request_ids = requests.map(&:id)
24
+ responses = MessageInferenceResponse.where(message_inference_request_id: request_ids).order(:id).all
25
+ response_ids = responses.map(&:id)
26
+ tool_calls = ToolCall.where(message_inference_response_id: response_ids).order(:tool_call_index, :id).all
27
+ tool_call_ids = tool_calls.map(&:id)
28
+
29
+ {
30
+ conversation: Conversation[conversation_id],
31
+ messages: Message.where(conversation_id: conversation_id).order(:seq, :id).all,
32
+ requests: requests,
33
+ route_attempts: RouteAttempt.where(message_inference_request_id: request_ids).order(:message_inference_request_id, :attempt_no,
34
+ :id).all,
35
+ responses: responses,
36
+ request_payload_hashes: requests.filter_map(&:request_content_hash),
37
+ response_payload_hashes: responses.filter_map(&:response_content_hash),
38
+ policy_evaluations: policy_evaluations_for(conversation_id, request_ids, response_ids),
39
+ security_events: security_events_for(conversation_id, request_ids, response_ids, tool_call_ids),
40
+ tool_calls: tool_calls,
41
+ tool_call_attempts: ToolCallAttempt.where(tool_call_id: tool_call_ids).order(:tool_call_id, :attempt_no, :id).all
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ def policy_evaluations_for(conversation_id, request_ids, response_ids)
48
+ scope = PolicyEvaluation.where(conversation_id: conversation_id)
49
+ scope = scope.or(message_inference_request_id: request_ids) unless request_ids.empty?
50
+ scope = scope.or(message_inference_response_id: response_ids) unless response_ids.empty?
51
+ scope.order(:evaluated_at, :id).all
52
+ end
53
+
54
+ def security_events_for(conversation_id, request_ids, response_ids, tool_call_ids)
55
+ scope = where(conversation_id: conversation_id)
56
+ scope = scope.or(message_inference_request_id: request_ids) unless request_ids.empty?
57
+ scope = scope.or(message_inference_response_id: response_ids) unless response_ids.empty?
58
+ scope = scope.or(tool_call_id: tool_call_ids) unless tool_call_ids.empty?
59
+ scope.order(:detected_at, :id).all
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class ToolCall < Sequel::Model(:llm_tool_calls)
10
+ include ModelHelpers
11
+
12
+ many_to_one :message_inference_response
13
+ many_to_one :requested_by_message, class: 'Legion::Data::Models::LLM::Message', key: :requested_by_message_id
14
+ many_to_one :result_message, class: 'Legion::Data::Models::LLM::Message', key: :result_message_id
15
+ one_to_many :tool_call_attempts
16
+ one_to_many :security_events
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Models
8
+ module LLM
9
+ class ToolCallAttempt < Sequel::Model(:llm_tool_call_attempts)
10
+ include ModelHelpers
11
+
12
+ many_to_one :tool_call
13
+ one_to_many :security_events
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,7 +8,8 @@ module Legion
8
8
  class Node < Sequel::Model
9
9
  include Legion::Logging::Helper
10
10
 
11
- # one_to_many :task_log
11
+ one_to_many :task_log
12
+ one_to_many :task_logs, class: 'Legion::Data::Model::TaskLog'
12
13
  many_to_one :principal, class: 'Legion::Data::Model::Principal'
13
14
 
14
15
  def parsed_metrics
@@ -1,13 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'identity/model_helpers'
4
+
3
5
  return unless Legion::Data::Connection.adapter == :postgres
4
6
 
5
7
  module Legion
6
8
  module Data
7
9
  module Model
8
10
  class Principal < Sequel::Model(:principals)
11
+ include Identity::ModelHelpers
12
+
9
13
  one_to_many :identities, class: 'Legion::Data::Model::Identity'
10
14
  one_to_many :group_memberships, class: 'Legion::Data::Model::IdentityGroupMembership'
15
+ many_to_many :groups,
16
+ class: 'Legion::Data::Model::IdentityGroup',
17
+ join_table: :identity_group_memberships,
18
+ left_key: :principal_id,
19
+ right_key: :group_id
20
+
21
+ def self.lookup_columns
22
+ %i[id uuid canonical_name employee_key employee_id]
23
+ end
11
24
 
12
25
  def active_groups
13
26
  group_memberships_dataset
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ module RBAC
9
+ class CrossTeamGrant < Sequel::Model(:rbac_cross_team_grants)
10
+ include ModelHelpers
11
+
12
+ def validate
13
+ super
14
+ errors.add(:source_team, 'cannot be empty') if source_team.nil? || source_team.empty?
15
+ errors.add(:target_team, 'cannot be empty') if target_team.nil? || target_team.empty?
16
+ errors.add(:source_team, 'cannot equal target_team') if source_team == target_team
17
+ errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
18
+ errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
19
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ module RBAC
7
+ module ModelHelpers
8
+ def expired?
9
+ return false if expires_at.nil?
10
+
11
+ expires_at < Time.now
12
+ end
13
+
14
+ def active?
15
+ !expired?
16
+ end
17
+
18
+ def actions_list
19
+ (actions || '').split(',').map(&:strip)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ module RBAC
9
+ class RoleAssignment < Sequel::Model(:rbac_role_assignments)
10
+ include ModelHelpers
11
+
12
+ VALID_PRINCIPAL_TYPES = %w[worker human].freeze
13
+
14
+ def validate
15
+ super
16
+ errors.add(:principal_type, 'must be worker or human') unless VALID_PRINCIPAL_TYPES.include?(principal_type)
17
+ errors.add(:principal_id, 'cannot be empty') if principal_id.nil? || principal_id.empty?
18
+ errors.add(:role, 'cannot be empty') if role.nil? || role.empty?
19
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helpers'
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ module RBAC
9
+ class RunnerGrant < Sequel::Model(:rbac_runner_grants)
10
+ include ModelHelpers
11
+
12
+ def validate
13
+ super
14
+ errors.add(:team, 'cannot be empty') if team.nil? || team.empty?
15
+ errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
16
+ errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
17
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -6,6 +6,7 @@ module Legion
6
6
  class Relationship < Sequel::Model
7
7
  many_to_one :trigger, class: 'Legion::Data::Model::Function'
8
8
  many_to_one :action, class: 'Legion::Data::Model::Function'
9
+ many_to_one :chain
9
10
  one_to_many :tasks
10
11
  end
11
12
  end