lex-github 0.2.5 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +45 -19
- data/README.md +155 -83
- data/lex-github.gemspec +2 -0
- data/lib/legion/extensions/github/app/actor/token_refresh.rb +68 -0
- data/lib/legion/extensions/github/app/actor/webhook_poller.rb +65 -0
- data/lib/legion/extensions/github/app/hooks/setup.rb +19 -0
- data/lib/legion/extensions/github/app/hooks/webhook.rb +19 -0
- data/lib/legion/extensions/github/app/runners/auth.rb +48 -0
- data/lib/legion/extensions/github/app/runners/credential_store.rb +46 -0
- data/lib/legion/extensions/github/app/runners/installations.rb +56 -0
- data/lib/legion/extensions/github/app/runners/manifest.rb +65 -0
- data/lib/legion/extensions/github/app/runners/webhooks.rb +118 -0
- data/lib/legion/extensions/github/app/transport/exchanges/app.rb +17 -0
- data/lib/legion/extensions/github/app/transport/messages/event.rb +18 -0
- data/lib/legion/extensions/github/app/transport/queues/auth.rb +18 -0
- data/lib/legion/extensions/github/app/transport/queues/webhooks.rb +18 -0
- data/lib/legion/extensions/github/cli/app.rb +57 -0
- data/lib/legion/extensions/github/cli/auth.rb +99 -0
- data/lib/legion/extensions/github/client.rb +24 -0
- data/lib/legion/extensions/github/errors.rb +44 -0
- data/lib/legion/extensions/github/helpers/browser_auth.rb +106 -0
- data/lib/legion/extensions/github/helpers/cache.rb +99 -0
- data/lib/legion/extensions/github/helpers/callback_server.rb +89 -0
- data/lib/legion/extensions/github/helpers/client.rb +292 -2
- data/lib/legion/extensions/github/helpers/scope_registry.rb +91 -0
- data/lib/legion/extensions/github/helpers/token_cache.rb +86 -0
- data/lib/legion/extensions/github/middleware/credential_fallback.rb +76 -0
- data/lib/legion/extensions/github/middleware/rate_limit.rb +40 -0
- data/lib/legion/extensions/github/middleware/scope_probe.rb +37 -0
- data/lib/legion/extensions/github/oauth/actor/token_refresh.rb +76 -0
- data/lib/legion/extensions/github/oauth/hooks/callback.rb +19 -0
- data/lib/legion/extensions/github/oauth/runners/auth.rb +111 -0
- data/lib/legion/extensions/github/oauth/transport/exchanges/oauth.rb +17 -0
- data/lib/legion/extensions/github/oauth/transport/queues/auth.rb +18 -0
- data/lib/legion/extensions/github/runners/actions.rb +100 -0
- data/lib/legion/extensions/github/runners/branches.rb +5 -3
- data/lib/legion/extensions/github/runners/checks.rb +84 -0
- data/lib/legion/extensions/github/runners/comments.rb +13 -7
- data/lib/legion/extensions/github/runners/commits.rb +11 -6
- data/lib/legion/extensions/github/runners/contents.rb +3 -1
- data/lib/legion/extensions/github/runners/deployments.rb +76 -0
- data/lib/legion/extensions/github/runners/gists.rb +9 -4
- data/lib/legion/extensions/github/runners/issues.rb +16 -9
- data/lib/legion/extensions/github/runners/labels.rb +16 -9
- data/lib/legion/extensions/github/runners/organizations.rb +10 -8
- data/lib/legion/extensions/github/runners/pull_requests.rb +24 -14
- data/lib/legion/extensions/github/runners/releases.rb +89 -0
- data/lib/legion/extensions/github/runners/repositories.rb +17 -10
- data/lib/legion/extensions/github/runners/repository_webhooks.rb +76 -0
- data/lib/legion/extensions/github/runners/search.rb +11 -8
- data/lib/legion/extensions/github/runners/users.rb +12 -8
- data/lib/legion/extensions/github/version.rb +1 -1
- data/lib/legion/extensions/github.rb +22 -0
- metadata +63 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'legion/extensions/github/helpers/client'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Github
|
|
10
|
+
module App
|
|
11
|
+
module Runners
|
|
12
|
+
module Auth
|
|
13
|
+
include Legion::Extensions::Github::Helpers::Client
|
|
14
|
+
|
|
15
|
+
def generate_jwt(app_id:, private_key:, **)
|
|
16
|
+
key = OpenSSL::PKey::RSA.new(private_key)
|
|
17
|
+
now = Time.now.to_i
|
|
18
|
+
payload = { iat: now - 60, exp: now + (10 * 60), iss: app_id.to_s }
|
|
19
|
+
token = JWT.encode(payload, key, 'RS256')
|
|
20
|
+
{ result: token }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_installation_token(jwt:, installation_id:, **)
|
|
24
|
+
conn = connection(token: jwt, **)
|
|
25
|
+
response = conn.post("/app/installations/#{installation_id}/access_tokens")
|
|
26
|
+
{ result: response.body }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_installations(jwt:, per_page: 30, page: 1, **)
|
|
30
|
+
conn = connection(token: jwt, **)
|
|
31
|
+
response = conn.get('/app/installations', per_page: per_page, page: page)
|
|
32
|
+
{ result: response.body }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get_installation(jwt:, installation_id:, **)
|
|
36
|
+
conn = connection(token: jwt, **)
|
|
37
|
+
response = conn.get("/app/installations/#{installation_id}")
|
|
38
|
+
{ result: response.body }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
42
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Github
|
|
8
|
+
module App
|
|
9
|
+
module Runners
|
|
10
|
+
module CredentialStore
|
|
11
|
+
def store_app_credentials(app_id:, private_key:, client_id:, client_secret:, webhook_secret:, **)
|
|
12
|
+
vault_set('github/app/app_id', app_id)
|
|
13
|
+
vault_set('github/app/private_key', private_key)
|
|
14
|
+
vault_set('github/app/client_id', client_id)
|
|
15
|
+
vault_set('github/app/client_secret', client_secret)
|
|
16
|
+
vault_set('github/app/webhook_secret', webhook_secret)
|
|
17
|
+
{ result: true }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def store_oauth_token(user:, access_token:, refresh_token:, expires_in: nil, scope: nil, **)
|
|
21
|
+
data = { 'access_token' => access_token, 'refresh_token' => refresh_token,
|
|
22
|
+
'expires_in' => expires_in, 'scope' => scope,
|
|
23
|
+
'stored_at' => Time.now.iso8601 }.compact
|
|
24
|
+
vault_set("github/oauth/#{user}/token", data)
|
|
25
|
+
# Also write to canonical delegated path so resolve_vault_delegated can discover the token
|
|
26
|
+
vault_set('github/oauth/delegated/token', data)
|
|
27
|
+
{ result: true }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_oauth_token(user:, **)
|
|
31
|
+
data = begin
|
|
32
|
+
vault_get("github/oauth/#{user}/token")
|
|
33
|
+
rescue StandardError => _e
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
{ result: data }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
40
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/github/helpers/client'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Github
|
|
8
|
+
module App
|
|
9
|
+
module Runners
|
|
10
|
+
module Installations
|
|
11
|
+
include Legion::Extensions::Github::Helpers::Client
|
|
12
|
+
|
|
13
|
+
def list_installations(jwt:, per_page: 30, page: 1, **)
|
|
14
|
+
conn = connection(token: jwt, **)
|
|
15
|
+
response = conn.get('/app/installations', per_page: per_page, page: page)
|
|
16
|
+
{ result: response.body }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_installation(jwt:, installation_id:, **)
|
|
20
|
+
conn = connection(token: jwt, **)
|
|
21
|
+
response = conn.get("/app/installations/#{installation_id}")
|
|
22
|
+
{ result: response.body }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def list_installation_repos(per_page: 30, page: 1, **)
|
|
26
|
+
response = connection(**).get('/installation/repositories',
|
|
27
|
+
per_page: per_page, page: page)
|
|
28
|
+
{ result: response.body }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def suspend_installation(jwt:, installation_id:, **)
|
|
32
|
+
conn = connection(token: jwt, **)
|
|
33
|
+
response = conn.put("/app/installations/#{installation_id}/suspended")
|
|
34
|
+
{ result: response.status == 204 }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def unsuspend_installation(jwt:, installation_id:, **)
|
|
38
|
+
conn = connection(token: jwt, **)
|
|
39
|
+
response = conn.delete("/app/installations/#{installation_id}/suspended")
|
|
40
|
+
{ result: response.status == 204 }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete_installation(jwt:, installation_id:, **)
|
|
44
|
+
conn = connection(token: jwt, **)
|
|
45
|
+
response = conn.delete("/app/installations/#{installation_id}")
|
|
46
|
+
{ result: response.status == 204 }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
50
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'legion/extensions/github/helpers/client'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Github
|
|
10
|
+
module App
|
|
11
|
+
module Runners
|
|
12
|
+
module Manifest
|
|
13
|
+
include Legion::Extensions::Github::Helpers::Client
|
|
14
|
+
|
|
15
|
+
DEFAULT_PERMISSIONS = {
|
|
16
|
+
contents: 'write', issues: 'write', pull_requests: 'write',
|
|
17
|
+
metadata: 'read', administration: 'write', members: 'read',
|
|
18
|
+
checks: 'write', statuses: 'write', actions: 'read',
|
|
19
|
+
workflows: 'write', webhooks: 'write', repository_hooks: 'write'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
DEFAULT_EVENTS = %w[
|
|
23
|
+
push pull_request pull_request_review issues issue_comment
|
|
24
|
+
create delete check_run check_suite status workflow_run
|
|
25
|
+
repository installation
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def generate_manifest(name:, url:, webhook_url:, callback_url:,
|
|
29
|
+
permissions: DEFAULT_PERMISSIONS, events: DEFAULT_EVENTS,
|
|
30
|
+
public: true, **)
|
|
31
|
+
manifest = {
|
|
32
|
+
name: name, url: url, public: public,
|
|
33
|
+
hook_attributes: { url: webhook_url, active: true },
|
|
34
|
+
setup_url: callback_url,
|
|
35
|
+
redirect_url: callback_url,
|
|
36
|
+
default_permissions: permissions,
|
|
37
|
+
default_events: events
|
|
38
|
+
}
|
|
39
|
+
{ result: manifest }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exchange_manifest_code(code:, **)
|
|
43
|
+
conn = connection(**)
|
|
44
|
+
response = conn.post("/app-manifests/#{code}/conversions")
|
|
45
|
+
{ result: response.body }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def manifest_url(manifest:, org: nil, **)
|
|
49
|
+
base = if org
|
|
50
|
+
"https://github.com/organizations/#{org}/settings/apps/new"
|
|
51
|
+
else
|
|
52
|
+
'https://github.com/settings/apps/new'
|
|
53
|
+
end
|
|
54
|
+
json_str = ::JSON.generate(manifest)
|
|
55
|
+
{ result: "#{base}?manifest=#{URI.encode_www_form_component(json_str)}" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
59
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'legion/extensions/github/helpers/client'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Github
|
|
10
|
+
module App
|
|
11
|
+
module Runners
|
|
12
|
+
module Webhooks
|
|
13
|
+
include Legion::Extensions::Github::Helpers::Client
|
|
14
|
+
|
|
15
|
+
def verify_signature(payload:, signature:, secret:, **)
|
|
16
|
+
return { result: false } if signature.nil? || signature.empty?
|
|
17
|
+
|
|
18
|
+
expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, payload)}"
|
|
19
|
+
# Use constant-time comparison to prevent timing side-channel attacks.
|
|
20
|
+
# Pad to equal length so fixed_length_secure_compare can be used safely.
|
|
21
|
+
result = expected.length == signature.length &&
|
|
22
|
+
OpenSSL.fixed_length_secure_compare(expected, signature)
|
|
23
|
+
{ result: result }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_event(payload:, event_type:, delivery_id:, **)
|
|
27
|
+
parsed = payload.is_a?(String) ? ::JSON.parse(payload) : payload
|
|
28
|
+
{ result: { event_type: event_type, delivery_id: delivery_id, payload: parsed } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def receive_event(payload:, signature:, secret:, event_type:, delivery_id:, **)
|
|
32
|
+
verified = verify_signature(payload: payload, signature: signature, secret: secret)[:result]
|
|
33
|
+
unless verified
|
|
34
|
+
return { result: { verified: false, event_type: event_type, delivery_id: delivery_id,
|
|
35
|
+
payload: nil } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
parsed = parse_event(payload: payload, event_type: event_type, delivery_id: delivery_id)[:result]
|
|
39
|
+
invalidate_scopes_for_event(event_type: event_type, payload: parsed[:payload])
|
|
40
|
+
{ result: parsed.merge(verified: true) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
SCOPE_INVALIDATION_EVENTS = %w[installation installation_repositories].freeze
|
|
44
|
+
|
|
45
|
+
def invalidate_scopes_for_event(event_type:, payload:, **)
|
|
46
|
+
return unless SCOPE_INVALIDATION_EVENTS.include?(event_type.to_s)
|
|
47
|
+
|
|
48
|
+
owner = payload&.dig('installation', 'account', 'login')
|
|
49
|
+
return unless owner
|
|
50
|
+
|
|
51
|
+
invalidate_all_scopes_for_owner(owner: owner)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def invalidate_all_scopes_for_owner(owner:)
|
|
55
|
+
known_fingerprints = resolve_known_fingerprints
|
|
56
|
+
known_fingerprints.each do |fp|
|
|
57
|
+
invalidate_scope(fingerprint: fp, owner: owner)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def resolve_known_fingerprints
|
|
64
|
+
fingerprints = []
|
|
65
|
+
|
|
66
|
+
# Delegated (OAuth user) — check Vault and settings without resolving tokens
|
|
67
|
+
fingerprints << credential_fingerprint(auth_type: :oauth_user, identifier: 'vault_delegated') if vault_delegated_configured?
|
|
68
|
+
fingerprints << credential_fingerprint(auth_type: :oauth_user, identifier: 'settings_delegated') if settings_delegated_configured?
|
|
69
|
+
|
|
70
|
+
# App installation — derive from app_id without generating installation tokens
|
|
71
|
+
if (vault_app_id = safe_vault_get('github/app/app_id'))
|
|
72
|
+
fingerprints << credential_fingerprint(auth_type: :app_installation, identifier: "vault_app_#{vault_app_id}")
|
|
73
|
+
end
|
|
74
|
+
if (settings_app_id = safe_settings_dig(:github, :app, :app_id))
|
|
75
|
+
fingerprints << credential_fingerprint(auth_type: :app_installation, identifier: "settings_app_#{settings_app_id}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# PAT
|
|
79
|
+
fingerprints << credential_fingerprint(auth_type: :pat, identifier: 'vault_pat') if safe_vault_get('github/token')
|
|
80
|
+
fingerprints << credential_fingerprint(auth_type: :pat, identifier: 'settings_pat') if safe_settings_dig(:github, :token)
|
|
81
|
+
|
|
82
|
+
# CLI and ENV
|
|
83
|
+
fingerprints << credential_fingerprint(auth_type: :cli, identifier: 'gh_cli')
|
|
84
|
+
fingerprints << credential_fingerprint(auth_type: :env, identifier: 'env')
|
|
85
|
+
|
|
86
|
+
fingerprints.uniq
|
|
87
|
+
rescue StandardError => _e
|
|
88
|
+
[]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def vault_delegated_configured?
|
|
92
|
+
defined?(Legion::Crypt) && safe_vault_get('github/oauth/delegated/token')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def settings_delegated_configured?
|
|
96
|
+
defined?(Legion::Settings) && safe_settings_dig(:github, :oauth, :access_token)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def safe_vault_get(path)
|
|
100
|
+
vault_get(path) if defined?(Legion::Crypt)
|
|
101
|
+
rescue StandardError => _e
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def safe_settings_dig(*keys)
|
|
106
|
+
Legion::Settings.dig(*keys) if defined?(Legion::Settings)
|
|
107
|
+
rescue StandardError => _e
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
112
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Github
|
|
6
|
+
module App
|
|
7
|
+
module Transport
|
|
8
|
+
module Exchanges
|
|
9
|
+
class App < Legion::Transport::Exchange
|
|
10
|
+
def exchange_name = 'lex.github.app'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Github
|
|
6
|
+
module App
|
|
7
|
+
module Transport
|
|
8
|
+
module Messages
|
|
9
|
+
class Event < Legion::Transport::Message
|
|
10
|
+
def routing_key = 'lex.github.app.runners.webhooks'
|
|
11
|
+
def exchange = Legion::Extensions::Github::App::Transport::Exchanges::App
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Github
|
|
6
|
+
module App
|
|
7
|
+
module Transport
|
|
8
|
+
module Queues
|
|
9
|
+
class Auth < Legion::Transport::Queue
|
|
10
|
+
def queue_name = 'lex.github.app.runners.auth'
|
|
11
|
+
def queue_options = { auto_delete: false }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Github
|
|
6
|
+
module App
|
|
7
|
+
module Transport
|
|
8
|
+
module Queues
|
|
9
|
+
class Webhooks < Legion::Transport::Queue
|
|
10
|
+
def queue_name = 'lex.github.app.runners.webhooks'
|
|
11
|
+
def queue_options = { auto_delete: false }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/github/helpers/client'
|
|
4
|
+
require 'legion/extensions/github/helpers/callback_server'
|
|
5
|
+
require 'legion/extensions/github/app/runners/manifest'
|
|
6
|
+
require 'legion/extensions/github/app/runners/credential_store'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Github
|
|
11
|
+
module CLI
|
|
12
|
+
module App
|
|
13
|
+
include Helpers::Client
|
|
14
|
+
include Github::App::Runners::Manifest
|
|
15
|
+
include Github::App::Runners::CredentialStore
|
|
16
|
+
|
|
17
|
+
def setup(name:, url:, webhook_url:, org: nil, callback_timeout: 300, **)
|
|
18
|
+
server = Helpers::CallbackServer.new
|
|
19
|
+
server.start
|
|
20
|
+
callback_url = server.redirect_uri
|
|
21
|
+
|
|
22
|
+
manifest = generate_manifest(
|
|
23
|
+
name: name, url: url,
|
|
24
|
+
webhook_url: webhook_url,
|
|
25
|
+
callback_url: callback_url
|
|
26
|
+
)[:result]
|
|
27
|
+
|
|
28
|
+
url_result = manifest_url(manifest: manifest, org: org)[:result]
|
|
29
|
+
|
|
30
|
+
{ result: { manifest_url: url_result, callback_port: server.port,
|
|
31
|
+
message: 'Open the manifest URL in your browser to create the GitHub App',
|
|
32
|
+
callback: server.wait_for_callback(timeout: callback_timeout) } }
|
|
33
|
+
ensure
|
|
34
|
+
server&.shutdown
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def complete_setup(code:, **)
|
|
38
|
+
result = exchange_manifest_code(code: code)[:result]
|
|
39
|
+
return { error: 'exchange_failed' } unless result&.dig('id')
|
|
40
|
+
|
|
41
|
+
if respond_to?(:store_app_credentials, true)
|
|
42
|
+
store_app_credentials(
|
|
43
|
+
app_id: result['id'].to_s,
|
|
44
|
+
private_key: result['pem'],
|
|
45
|
+
client_id: result['client_id'],
|
|
46
|
+
client_secret: result['client_secret'],
|
|
47
|
+
webhook_secret: result['webhook_secret']
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{ result: result }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/github/helpers/client'
|
|
4
|
+
require 'legion/extensions/github/helpers/browser_auth'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Github
|
|
9
|
+
module CLI
|
|
10
|
+
module Auth
|
|
11
|
+
include Helpers::Client
|
|
12
|
+
|
|
13
|
+
def login(client_id: nil, client_secret: nil, scopes: nil, **)
|
|
14
|
+
cid = client_id || settings_client_id
|
|
15
|
+
csec = client_secret || settings_client_secret
|
|
16
|
+
sc = scopes || settings_scopes
|
|
17
|
+
|
|
18
|
+
unless cid && csec
|
|
19
|
+
return { error: 'missing_config',
|
|
20
|
+
description: 'Set github.oauth.client_id or github.app.client_id and github.app.client_secret in settings or pass as arguments' }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
browser = Helpers::BrowserAuth.new(client_id: cid, client_secret: csec, scopes: sc)
|
|
24
|
+
result = browser.authenticate
|
|
25
|
+
|
|
26
|
+
if result[:result]&.dig('access_token') && respond_to?(:store_oauth_token, true)
|
|
27
|
+
user = begin
|
|
28
|
+
current_user(token: result[:result]['access_token'])
|
|
29
|
+
rescue StandardError => _e
|
|
30
|
+
'default'
|
|
31
|
+
end
|
|
32
|
+
store_oauth_token(
|
|
33
|
+
user: user,
|
|
34
|
+
access_token: result[:result]['access_token'],
|
|
35
|
+
refresh_token: result[:result]['refresh_token'],
|
|
36
|
+
expires_in: result[:result]['expires_in']
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def status(**)
|
|
44
|
+
cred = resolve_credential
|
|
45
|
+
return { result: { authenticated: false } } unless cred
|
|
46
|
+
|
|
47
|
+
user_info = {}
|
|
48
|
+
scopes = nil
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
response = connection(token: cred[:token]).get('/user')
|
|
52
|
+
user_info = response.body || {}
|
|
53
|
+
headers = response.respond_to?(:headers) ? response.headers : {}
|
|
54
|
+
scopes_header = headers['X-OAuth-Scopes'] || headers['x-oauth-scopes']
|
|
55
|
+
scopes = scopes_header&.split(',')&.map(&:strip)
|
|
56
|
+
rescue StandardError => _e
|
|
57
|
+
user_info = {}
|
|
58
|
+
scopes = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
{ result: { authenticated: true, auth_type: cred[:auth_type],
|
|
62
|
+
user: user_info['login'], scopes: scopes } }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def current_user(token:)
|
|
68
|
+
connection(token: token).get('/user').body['login']
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def settings_client_id
|
|
72
|
+
return nil unless defined?(Legion::Settings)
|
|
73
|
+
|
|
74
|
+
Legion::Settings.dig(:github, :oauth, :client_id) ||
|
|
75
|
+
Legion::Settings.dig(:github, :app, :client_id)
|
|
76
|
+
rescue StandardError => _e
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def settings_client_secret
|
|
81
|
+
return nil unless defined?(Legion::Settings)
|
|
82
|
+
|
|
83
|
+
Legion::Settings.dig(:github, :app, :client_secret)
|
|
84
|
+
rescue StandardError => _e
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def settings_scopes
|
|
89
|
+
return nil unless defined?(Legion::Settings)
|
|
90
|
+
|
|
91
|
+
Legion::Settings.dig(:github, :oauth, :scopes)
|
|
92
|
+
rescue StandardError => _e
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/github/helpers/client'
|
|
4
|
+
require 'legion/extensions/github/helpers/cache'
|
|
4
5
|
require 'legion/extensions/github/runners/repositories'
|
|
5
6
|
require 'legion/extensions/github/runners/issues'
|
|
6
7
|
require 'legion/extensions/github/runners/pull_requests'
|
|
@@ -13,12 +14,24 @@ require 'legion/extensions/github/runners/labels'
|
|
|
13
14
|
require 'legion/extensions/github/runners/comments'
|
|
14
15
|
require 'legion/extensions/github/runners/branches'
|
|
15
16
|
require 'legion/extensions/github/runners/contents'
|
|
17
|
+
require 'legion/extensions/github/runners/actions'
|
|
18
|
+
require 'legion/extensions/github/runners/checks'
|
|
19
|
+
require 'legion/extensions/github/runners/releases'
|
|
20
|
+
require 'legion/extensions/github/runners/deployments'
|
|
21
|
+
require 'legion/extensions/github/runners/repository_webhooks'
|
|
22
|
+
require 'legion/extensions/github/app/runners/auth'
|
|
23
|
+
require 'legion/extensions/github/app/runners/webhooks'
|
|
24
|
+
require 'legion/extensions/github/app/runners/manifest'
|
|
25
|
+
require 'legion/extensions/github/app/runners/installations'
|
|
26
|
+
require 'legion/extensions/github/app/runners/credential_store'
|
|
27
|
+
require 'legion/extensions/github/oauth/runners/auth'
|
|
16
28
|
|
|
17
29
|
module Legion
|
|
18
30
|
module Extensions
|
|
19
31
|
module Github
|
|
20
32
|
class Client
|
|
21
33
|
include Helpers::Client
|
|
34
|
+
include Helpers::Cache
|
|
22
35
|
include Runners::Repositories
|
|
23
36
|
include Runners::Issues
|
|
24
37
|
include Runners::PullRequests
|
|
@@ -31,6 +44,17 @@ module Legion
|
|
|
31
44
|
include Runners::Comments
|
|
32
45
|
include Runners::Branches
|
|
33
46
|
include Runners::Contents
|
|
47
|
+
include Runners::Actions
|
|
48
|
+
include Runners::Checks
|
|
49
|
+
include Runners::Releases
|
|
50
|
+
include Runners::Deployments
|
|
51
|
+
include Runners::RepositoryWebhooks
|
|
52
|
+
include App::Runners::Auth
|
|
53
|
+
include App::Runners::Webhooks
|
|
54
|
+
include App::Runners::Manifest
|
|
55
|
+
include App::Runners::Installations
|
|
56
|
+
include App::Runners::CredentialStore
|
|
57
|
+
include OAuth::Runners::Auth
|
|
34
58
|
|
|
35
59
|
attr_reader :opts
|
|
36
60
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Github
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
class RateLimitError < Error
|
|
9
|
+
attr_reader :reset_at, :credential_fingerprint
|
|
10
|
+
|
|
11
|
+
def initialize(message = 'GitHub API rate limit exceeded', reset_at: nil, credential_fingerprint: nil)
|
|
12
|
+
@reset_at = reset_at
|
|
13
|
+
@credential_fingerprint = credential_fingerprint
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class AuthorizationError < Error
|
|
19
|
+
attr_reader :owner, :repo, :attempted_sources
|
|
20
|
+
|
|
21
|
+
def initialize(message = 'No authorized credential available', owner: nil, repo: nil,
|
|
22
|
+
attempted_sources: [])
|
|
23
|
+
@owner = owner
|
|
24
|
+
@repo = repo
|
|
25
|
+
@attempted_sources = attempted_sources
|
|
26
|
+
super(message)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ScopeDeniedError < Error
|
|
31
|
+
attr_reader :owner, :repo, :credential_fingerprint, :auth_type
|
|
32
|
+
|
|
33
|
+
def initialize(message = 'Credential not authorized for this scope',
|
|
34
|
+
owner: nil, repo: nil, credential_fingerprint: nil, auth_type: nil)
|
|
35
|
+
@owner = owner
|
|
36
|
+
@repo = repo
|
|
37
|
+
@credential_fingerprint = credential_fingerprint
|
|
38
|
+
@auth_type = auth_type
|
|
39
|
+
super(message)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|