1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
# frozen_string_literal: true
module API
# Kubernetes Internal API
module Internal
class Kubernetes < ::API::Base
include Gitlab::Utils::StrongMemoize
before do
check_feature_enabled
authenticate_gitlab_kas_request!
end
helpers do
def authenticate_gitlab_kas_request!
render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers)
end
def agent_token
@agent_token ||= cluster_agent_token_from_authorization_token
end
def agent
@agent ||= agent_token.agent
end
def repo_type
Gitlab::GlRepository::PROJECT
end
def gitaly_info(project)
gitaly_features = Feature::Gitaly.server_feature_flags
Gitlab::GitalyClient.connection_data(project.repository_storage).merge(features: gitaly_features)
end
def gitaly_repository(project)
project.repository.gitaly_repository.to_h
end
def check_feature_enabled
not_found!('Internal API not found') unless Feature.enabled?(:kubernetes_agent_internal_api, type: :ops)
end
def check_agent_token
unauthorized! unless agent_token
::Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
end
def agent_has_access_to_project?(project)
Guest.can?(:download_code, project) || agent.has_access_to?(project)
end
def increment_unique_events
events = params[:unique_counters]&.slice(
:agent_users_using_ci_tunnel,
:k8s_api_proxy_requests_unique_users_via_ci_access, :k8s_api_proxy_requests_unique_agents_via_ci_access,
:k8s_api_proxy_requests_unique_users_via_user_access, :k8s_api_proxy_requests_unique_agents_via_user_access,
:flux_git_push_notified_unique_projects
)
events&.each do |event, entity_ids|
increment_unique_values(event, entity_ids)
end
end
def increment_count_events
events = params[:counters]&.slice(
:gitops_sync, :k8s_api_proxy_request, :flux_git_push_notifications_total,
:k8s_api_proxy_requests_via_ci_access, :k8s_api_proxy_requests_via_user_access
)
Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_event_counts(events)
end
def update_configuration(agent:, config:)
::Clusters::Agents::Authorizations::CiAccess::RefreshService.new(agent, config: config).execute
::Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: config).execute
end
end
namespace 'internal' do
namespace 'kubernetes' do
before do
check_agent_token
end
desc 'Gets agent info' do
detail 'Retrieves agent info for the given token'
end
route_setting :authentication, cluster_agent_token_allowed: true
get '/agent_info', feature_category: :deployment_management, urgency: :low do
project = agent.project
status 200
{
project_id: project.id,
agent_id: agent.id,
agent_name: agent.name,
gitaly_info: gitaly_info(project),
gitaly_repository: gitaly_repository(project),
default_branch: project.default_branch_or_main
}
end
desc 'Gets project info' do
detail 'Retrieves project info (if authorized)'
end
route_setting :authentication, cluster_agent_token_allowed: true
get '/project_info', feature_category: :deployment_management, urgency: :low do
project = find_project(params[:id])
not_found! unless agent_has_access_to_project?(project)
status 200
{
project_id: project.id,
gitaly_info: gitaly_info(project),
gitaly_repository: gitaly_repository(project),
default_branch: project.default_branch_or_main
}
end
desc 'Verify agent access to a project' do
detail 'Verifies if the agent (owning the token) is authorized to access the given project'
end
route_setting :authentication, cluster_agent_token_allowed: true
get '/verify_project_access', feature_category: :deployment_management, urgency: :low do
project = find_project(params[:id])
not_found! unless agent_has_access_to_project?(project)
status 204
end
end
namespace 'kubernetes/agent_configuration' do
desc 'POST agent configuration' do
detail 'Store configuration for an agent'
end
params do
requires :agent_id, type: Integer, desc: 'ID of the configured Agent'
requires :agent_config, type: JSON, desc: 'Configuration for the Agent'
end
post '/', feature_category: :deployment_management, urgency: :low do
agent = ::Clusters::Agent.find(params[:agent_id])
update_configuration(agent: agent, config: params[:agent_config])
no_content!
end
end
namespace 'kubernetes/authorize_proxy_user' do
desc 'Authorize a proxy user request'
params do
requires :agent_id, type: Integer, desc: 'ID of the agent accessed'
requires :access_type, type: String, values: ['session_cookie'], desc: 'The type of the access key being verified.'
requires :access_key, type: String, desc: 'The authentication secret for the given access type.'
given access_type: ->(val) { val == 'session_cookie' } do
requires :csrf_token, type: String, allow_blank: false, desc: 'CSRF token that must be checked when access_type is "session_cookie", to ensure the request originates from a GitLab browsing session.'
end
end
post '/', feature_category: :deployment_management do
# Load session
public_session_id_string =
begin
Gitlab::Kas::UserAccess.decrypt_public_session_id(params[:access_key])
rescue StandardError
bad_request!('Invalid access_key')
end
session_id = Rack::Session::SessionId.new(public_session_id_string)
session = ActiveSession.sessions_from_ids([session_id.private_id]).first
unauthorized!('Invalid session') unless session
# CSRF check
unless ::Gitlab::Kas::UserAccess.valid_authenticity_token?(session.symbolize_keys, params[:csrf_token])
unauthorized!('CSRF token does not match')
end
# Load user
user = Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
unauthorized!('Invalid user in session') unless user
# Load agent
agent = ::Clusters::Agent.find(params[:agent_id])
unauthorized!('Feature disabled for agent') unless ::Gitlab::Kas::UserAccess.enabled?
service_response = ::Clusters::Agents::AuthorizeProxyUserService.new(user, agent).execute
render_api_error!(service_response[:message], service_response[:reason]) unless service_response.success?
service_response.payload
end
end
namespace 'kubernetes/usage_metrics' do
desc 'POST usage metrics' do
detail 'Updates usage metrics for agent'
end
params do
optional :counters, type: Hash do
optional :gitops_sync, type: Integer, desc: 'The count to increment the gitops_sync metric by'
optional :k8s_api_proxy_request, type: Integer, desc: 'The count to increment the k8s_api_proxy_request metric by'
optional :flux_git_push_notifications_total, type: Integer, desc: 'The count to increment the flux_git_push_notifications_total metrics by'
optional :k8s_api_proxy_requests_via_ci_access, type: Integer, desc: 'The count to increment the k8s_api_proxy_requests_via_ci_access metric by'
optional :k8s_api_proxy_requests_via_user_access, type: Integer, desc: 'The count to increment the k8s_api_proxy_requests_via_user_access metric by'
end
optional :unique_counters, type: Hash do
optional :agent_users_using_ci_tunnel, type: Array[Integer], desc: 'An array of user ids that have interacted with CI Tunnel'
optional :k8s_api_proxy_requests_unique_users_via_ci_access, type: Array[Integer], desc: 'An array of users that have interacted with the CI tunnel via `ci_access`'
optional :k8s_api_proxy_requests_unique_agents_via_ci_access, type: Array[Integer], desc: 'An array of agents that have interacted with the CI tunnel via `ci_access`'
optional :k8s_api_proxy_requests_unique_users_via_user_access, type: Array[Integer], desc: 'An array of users that have interacted with the CI tunnel via `user_access`'
optional :k8s_api_proxy_requests_unique_agents_via_user_access, type: Array[Integer], desc: 'An array of agents that have interacted with the CI tunnel via `user_access`'
optional :flux_git_push_notified_unique_projects, type: Array[Integer], desc: 'An array of projects that have been notified to reconcile their Flux workloads'
end
end
post '/', feature_category: :deployment_management do
increment_count_events
increment_unique_events
no_content!
rescue ArgumentError => e
bad_request!(e.message)
end
end
end
end
end
end
API::Internal::Kubernetes.prepend_mod_with('API::Internal::Kubernetes')
|