Skip to content
Snippets Groups Projects
Commit ef11253d authored by Mikhail Mazurskiy's avatar Mikhail Mazurskiy
Browse files

Merge branch 'ash2k/ci-impersonation' into 'master'

Impersonation support in CI tunnel

See merge request !471
parents 36e66dfb c65e2343
No related branches found
No related tags found
1 merge request!471Impersonation support in CI tunnel
Pipeline #355534251 passed
Showing
with 1589 additions and 100 deletions
......@@ -39,6 +39,7 @@ multirun(
name = "extract_generated_proto",
commands = [
"//cmd/kas/kasapp:extract_generated",
"//internal/gitlab/api:extract_generated",
"//internal/module/agent_configuration/rpc:extract_generated",
"//internal/module/agent_tracker/rpc:extract_generated",
"//internal/module/agent_tracker:extract_generated",
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//build:build.bzl", "go_custom_test")
load("//build:proto.bzl", "go_proto_generate")
go_proto_generate(
src = "api.proto",
workspace_relative_target_directory = "internal/gitlab/api",
deps = [
"//pkg/agentcfg:proto",
],
)
go_library(
name = "api",
srcs = [
"api.pb.go",
"get_agent_info.go",
"get_allowed_agents.go",
"get_project_info.go",
......@@ -15,6 +25,10 @@ go_library(
deps = [
"//internal/api",
"//internal/gitlab",
"//pkg/agentcfg",
"@org_golang_google_protobuf//encoding/protojson",
"@org_golang_google_protobuf//reflect/protoreflect",
"@org_golang_google_protobuf//runtime/protoimpl",
],
)
......@@ -22,6 +36,7 @@ go_custom_test(
name = "api_test",
srcs = [
"get_agent_info_test.go",
"get_allowed_agents_test.go",
"get_project_info_test.go",
],
embed = [":api"],
......@@ -29,7 +44,9 @@ go_custom_test(
"//internal/gitlab",
"//internal/tool/testing/mock_gitlab",
"//internal/tool/testing/testhelpers",
"@com_github_google_go_cmp//cmp",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_golang_google_protobuf//testing/protocmp",
],
)
This diff is collapsed.
syntax = "proto3";
// If you make any changes make sure you run: make regenerate-proto
package gitlab.agent.gitlab.api;
option go_package = "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/gitlab/api";
import "pkg/agentcfg/agentcfg.proto";
// Configuration contains shared fields from agentcfg.CiAccessProjectCF and agentcfg.CiAccessGroupCF.
// It is used to parse response from the allowed_agents API endpoint.
// See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api.
message Configuration {
string default_namespace = 1 [json_name = "default_namespace"];
agentcfg.CiAccessAsCF access_as = 2 [json_name = "access_as"];
}
message AllowedAgent {
int64 id = 1 [json_name = "id"];
ConfigProject config_project = 2 [json_name = "config_project"];
Configuration configuration = 3 [json_name = "configuration"];
}
message ConfigProject {
int64 id = 1 [json_name = "id"];
}
message Pipeline {
int64 id = 1 [json_name = "id"];
}
message Project {
int64 id = 1 [json_name = "id"];
}
message Job {
int64 id = 1 [json_name = "id"];
}
message User {
int64 id = 1 [json_name = "id"];
string username = 2 [json_name = "username"];
}
message AllowedAgentsForJob {
repeated AllowedAgent allowed_agents = 1 [json_name = "allowed_agents"];
Job job = 2 [json_name = "job"];
Pipeline pipeline = 3 [json_name = "pipeline"];
Project project = 4 [json_name = "project"];
User user = 5 [json_name = "user"];
}
......@@ -4,48 +4,29 @@ import (
"context"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/gitlab"
"google.golang.org/protobuf/encoding/protojson"
)
const (
AllowedAgentsApiPath = "/api/v4/job/allowed_agents"
)
type AllowedAgent struct {
Id int64 `json:"id"`
ConfigProject ConfigProject `json:"config_project"`
}
type ConfigProject struct {
Id int64 `json:"id"`
}
type Pipeline struct {
Id int64 `json:"id"`
}
type Project struct {
Id int64 `json:"id"`
}
type Job struct {
Id int64 `json:"id"`
}
// AllowedAgentsForJobAlias ensures the protojson package is used for to/from JSON marshaling.
// See https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson.
type AllowedAgentsForJobAlias AllowedAgentsForJob
type User struct {
Id int64 `json:"id"`
Username string `json:"username"`
func (a *AllowedAgentsForJobAlias) MarshalJSON() ([]byte, error) {
typedA := (*AllowedAgentsForJob)(a)
return protojson.Marshal(typedA)
}
type AllowedAgentsForJob struct {
AllowedAgents []AllowedAgent `json:"allowed_agents"`
Job Job `json:"job"`
Pipeline Pipeline `json:"pipeline"`
Project Project `json:"project"`
User User `json:"user"`
func (a *AllowedAgentsForJobAlias) UnmarshalJSON(data []byte) error {
typedA := (*AllowedAgentsForJob)(a)
return protojson.Unmarshal(data, typedA)
}
func GetAllowedAgentsForJob(ctx context.Context, client gitlab.ClientInterface, jobToken string) (*AllowedAgentsForJob, error) {
ji := &AllowedAgentsForJob{}
ji := &AllowedAgentsForJobAlias{}
err := client.Do(ctx,
gitlab.WithPath(AllowedAgentsApiPath),
gitlab.WithJobToken(jobToken),
......@@ -54,5 +35,5 @@ func GetAllowedAgentsForJob(ctx context.Context, client gitlab.ClientInterface,
if err != nil {
return nil, err
}
return ji, nil
return (*AllowedAgentsForJob)(ji), nil
}
package api
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
)
var (
_ json.Marshaler = (*AllowedAgentsForJobAlias)(nil)
_ json.Unmarshaler = (*AllowedAgentsForJobAlias)(nil)
)
func TestGetAllowedAgents_JSON(t *testing.T) {
expected := &AllowedAgentsForJob{
AllowedAgents: []*AllowedAgent{
{
Id: 123,
ConfigProject: &ConfigProject{
Id: 234,
},
Configuration: &Configuration{
DefaultNamespace: "abc",
},
},
{
Id: 1,
ConfigProject: &ConfigProject{
Id: 2,
},
Configuration: &Configuration{
DefaultNamespace: "", // empty
},
},
},
Job: &Job{
Id: 32,
},
Pipeline: &Pipeline{
Id: 3232,
},
Project: &Project{
Id: 44,
},
User: &User{
Id: 2323,
Username: "abc",
},
}
data, err := json.Marshal(expected)
require.NoError(t, err)
actual := &AllowedAgentsForJob{}
err = json.Unmarshal(data, actual)
require.NoError(t, err)
assert.Empty(t, cmp.Diff(expected, actual, cmp.Transformer("AllowedAgentsForJob", transformAlias), protocmp.Transform()))
}
func transformAlias(val *AllowedAgentsForJobAlias) *AllowedAgentsForJob {
return (*AllowedAgentsForJob)(val)
}
......@@ -10,6 +10,7 @@ import "internal/tool/grpctool/grpctool.proto";
//import "github.com/envoyproxy/protoc-gen-validate/blob/master/validate/validate.proto";
import "validate/validate.proto";
// HeaderExtra is passed in grpctool.HttpRequest.extra.
message HeaderExtra {
// Name of the module that is making the request.
string module_name = 1 [(validate.rules).string.min_bytes = 1];
......
load("//build:build.bzl", "go_custom_test")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "agent",
srcs = [
"client.go",
"factory.go",
"module.go",
"server.go",
......@@ -18,5 +20,21 @@ go_library(
"//pkg/agentcfg",
"@io_k8s_apimachinery//pkg/runtime/schema",
"@io_k8s_client_go//rest",
"@io_k8s_client_go//transport",
],
)
go_custom_test(
name = "agent_test",
srcs = ["client_test.go"],
embed = [":agent"],
deps = [
"//internal/module/kubernetes_api/rpc",
"//internal/module/modagent",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@io_k8s_client_go//rest",
"@io_k8s_client_go//transport",
"@io_k8s_kubectl//pkg/cmd/testing",
],
)
package agent
import (
"io"
"net/http"
"strings"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/module/kubernetes_api/rpc"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
)
type impersonatingClient struct {
restConfig *rest.Config
}
func (c *impersonatingClient) Do(impConfig *rpc.ImpersonationConfig, r *http.Request) (*http.Response, error) {
var config *rest.Config
restImp := !isEmptyImpersonationConfig(c.restConfig.Impersonate)
cfgImp := !impConfig.IsEmpty()
reqImp := hasImpersonationHeaders(r)
switch {
case !restImp && !cfgImp && !reqImp:
// No impersonation
config = c.restConfig
case restImp && !cfgImp && !reqImp:
// Impersonation is configured in the rest config
config = c.restConfig
case !restImp && cfgImp && !reqImp:
// Impersonation is configured in the agent config
config = rest.CopyConfig(c.restConfig) // copy to avoid mutating a potentially shared config object
config.Impersonate.UserName = impConfig.Username
config.Impersonate.Groups = impConfig.Groups
// TODO Add uid when we upgrade to Kubernetes 1.22 libraries
config.Impersonate.Extra = impConfig.GetExtraAsMap()
case !restImp && !cfgImp && reqImp:
// Impersonation is configured in the HTTP request
config = c.restConfig
default:
// Nested impersonation support https://gitlab.com/gitlab-org/gitlab/-/issues/338664
return httpErrorResponse(http.StatusBadRequest, "Nested impersonation is not supported - agent is already configured to impersonate an identity"), nil
}
transportForConfig, err := rest.TransportFor(config)
if err != nil {
return nil, err
}
client := http.Client{
Transport: transportForConfig,
CheckRedirect: useLastResponse,
}
return client.Do(r)
}
func httpErrorResponse(statusCode int, text string) *http.Response {
return &http.Response{
Status: http.StatusText(statusCode),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{ // What http.Error() returns
"Content-Type": {"text/plain; charset=utf-8"},
"X-Content-Type-Options": {"nosniff"},
},
Body: io.NopCloser(strings.NewReader(text)),
}
}
func isEmptyImpersonationConfig(cfg rest.ImpersonationConfig) bool {
return cfg.UserName == "" && len(cfg.Groups) == 0 && len(cfg.Extra) == 0
}
func hasImpersonationHeaders(r *http.Request) bool {
for k := range r.Header {
if isImpersonationHeader(k) {
return true
}
}
return false
}
func isImpersonationHeader(header string) bool {
// header==transport.ImpersonateUidHeader: TODO add when we upgrade to Kubernetes 1.22 libraries
return header == transport.ImpersonateUserHeader || header == transport.ImpersonateGroupHeader || strings.HasPrefix(header, transport.ImpersonateUserExtraHeaderPrefix)
}
func useLastResponse(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
package agent
import (
"context"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/module/kubernetes_api/rpc"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/module/modagent"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
)
var (
_ httpClient = (*impersonatingClient)(nil)
_ modagent.Module = (*module)(nil)
_ modagent.Factory = (*Factory)(nil)
)
func TestClientImpersonation(t *testing.T) {
// TODO test uid with Kubernetes 1.22
restImpConfig := rest.ImpersonationConfig{
UserName: "ruser1",
Groups: []string{"rg1", "rg2"},
Extra: map[string][]string{
"rx": {"rx1", "rx2"},
},
}
impConfig := &rpc.ImpersonationConfig{
Username: "iuser1",
Groups: []string{"ig1", "ig2"},
Uid: "iuid",
Extra: []*rpc.ExtraKeyVal{
{
Key: "ix",
Val: []string{"ix1", "ix2"},
},
},
}
requestHeader := http.Header{}
requestHeader.Set(transport.ImpersonateUserHeader, "huser1")
requestHeader.Set(transport.ImpersonateGroupHeader, "hg1")
requestHeader.Add(transport.ImpersonateGroupHeader, "hg2")
requestHeader.Set(transport.ImpersonateUserExtraHeaderPrefix+"Hx", "hx1")
requestHeader.Add(transport.ImpersonateUserExtraHeaderPrefix+"Hx", "hx2")
tests := []struct {
name string
restImpConfig rest.ImpersonationConfig
impConfig *rpc.ImpersonationConfig
requestHeader http.Header
expectedRequestHeader http.Header
expectedStatus int
}{
{
name: "no impersonation",
impConfig: &rpc.ImpersonationConfig{},
expectedStatus: http.StatusOK,
},
{
name: "rest config",
restImpConfig: restImpConfig,
impConfig: &rpc.ImpersonationConfig{},
expectedRequestHeader: http.Header{
transport.ImpersonateUserHeader: {"ruser1"},
transport.ImpersonateGroupHeader: {"rg1", "rg2"},
transport.ImpersonateUserExtraHeaderPrefix + "Rx": {"rx1", "rx2"},
},
expectedStatus: http.StatusOK,
},
{
name: "rest config and impConfig",
restImpConfig: restImpConfig,
impConfig: impConfig,
expectedStatus: http.StatusBadRequest,
},
{
name: "rest config and requestHeader",
restImpConfig: restImpConfig,
requestHeader: requestHeader,
expectedStatus: http.StatusBadRequest,
},
{
name: "rest config and impConfig and requestHeader",
restImpConfig: restImpConfig,
impConfig: impConfig,
requestHeader: requestHeader,
expectedStatus: http.StatusBadRequest,
},
{
name: "impConfig and requestHeader",
impConfig: impConfig,
requestHeader: requestHeader,
expectedStatus: http.StatusBadRequest,
},
{
name: "requestHeader",
requestHeader: requestHeader,
expectedRequestHeader: http.Header{
transport.ImpersonateUserHeader: {"huser1"},
transport.ImpersonateGroupHeader: {"hg1", "hg2"},
transport.ImpersonateUserExtraHeaderPrefix + "Hx": {"hx1", "hx2"},
},
expectedStatus: http.StatusOK,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
config, err := cmdtesting.NewTestFactory().ToRESTConfig()
require.NoError(t, err)
config.Impersonate = tc.restImpConfig
rt := &testRoundTripper{
Response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("")),
},
}
config.Transport = rt
c := impersonatingClient{
restConfig: config,
}
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
require.NoError(t, err)
if tc.requestHeader != nil {
r.Header = tc.requestHeader
}
resp, err := c.Do(tc.impConfig, r)
require.NoError(t, err)
defer resp.Body.Close()
for k, v := range tc.expectedRequestHeader {
assert.Equal(t, v, rt.Request.Header[k], k)
}
assert.Equal(t, tc.expectedStatus, resp.StatusCode)
})
}
}
type testRoundTripper struct {
Request *http.Request
Response *http.Response
Err error
}
func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
rt.Request = req
return rt.Response, rt.Err
}
......@@ -2,7 +2,6 @@ package agent
import (
"fmt"
"net/http"
"net/url"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/module/kubernetes_api"
......@@ -20,22 +19,12 @@ func (f *Factory) New(config *modagent.Config) (modagent.Module, error) {
if err != nil {
return nil, err
}
transport, err := rest.TransportFor(restConfig)
if err != nil {
return nil, err
}
baseUrl, _, err := defaultServerUrlFor(restConfig)
if err != nil {
return nil, err
}
userAgent := fmt.Sprintf("%s/%s/%s", config.AgentName, config.AgentMeta.Version, config.AgentMeta.CommitId)
client := &http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
s := newServer(userAgent, client, baseUrl)
s := newServer(userAgent, &impersonatingClient{restConfig: restConfig}, baseUrl)
rpc.RegisterKubernetesApiServer(config.Server, s)
return &module{
api: config.Api,
......
......@@ -8,11 +8,6 @@ import (
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/pkg/agentcfg"
)
var (
_ modagent.Module = &module{}
_ modagent.Factory = &Factory{}
)
type module struct {
api modagent.Api
}
......
......@@ -14,7 +14,7 @@ import (
)
type httpClient interface {
Do(*http.Request) (*http.Response, error)
Do(*rpc.ImpersonationConfig, *http.Request) (*http.Response, error)
}
type server struct {
......@@ -35,10 +35,14 @@ func newServer(userAgent string, client httpClient, baseUrl *url.URL) *server {
if err != nil {
return nil, err
}
var headerExtra rpc.HeaderExtra
err = h.Extra.UnmarshalTo(&headerExtra)
if err != nil {
return nil, err
}
req.Header = h.Request.HttpHeader()
req.Header.Add("Via", via)
resp, err := client.Do(req)
resp, err := client.Do(headerExtra.ImpConfig, req)
if err != nil {
select {
case <-ctx.Done(): // assume request errored out because of context
......
......@@ -13,6 +13,7 @@ go_library(
name = "rpc",
srcs = [
"rpc.pb.go",
"rpc_extra.go",
"rpc_grpc.pb.go",
],
importpath = "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/module/kubernetes_api/rpc",
......
......@@ -8,6 +8,7 @@ package rpc
import (
reflect "reflect"
sync "sync"
grpctool "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/grpctool"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
......@@ -21,6 +22,179 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HeaderExtra struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ImpConfig *ImpersonationConfig `protobuf:"bytes,1,opt,name=imp_config,json=impConfig,proto3" json:"imp_config,omitempty"`
}
func (x *HeaderExtra) Reset() {
*x = HeaderExtra{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeaderExtra) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeaderExtra) ProtoMessage() {}
func (x *HeaderExtra) ProtoReflect() protoreflect.Message {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeaderExtra.ProtoReflect.Descriptor instead.
func (*HeaderExtra) Descriptor() ([]byte, []int) {
return file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescGZIP(), []int{0}
}
func (x *HeaderExtra) GetImpConfig() *ImpersonationConfig {
if x != nil {
return x.ImpConfig
}
return nil
}
type ImpersonationConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Groups []string `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"`
Uid string `protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"`
Extra []*ExtraKeyVal `protobuf:"bytes,4,rep,name=extra,proto3" json:"extra,omitempty"`
}
func (x *ImpersonationConfig) Reset() {
*x = ImpersonationConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ImpersonationConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ImpersonationConfig) ProtoMessage() {}
func (x *ImpersonationConfig) ProtoReflect() protoreflect.Message {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ImpersonationConfig.ProtoReflect.Descriptor instead.
func (*ImpersonationConfig) Descriptor() ([]byte, []int) {
return file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescGZIP(), []int{1}
}
func (x *ImpersonationConfig) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *ImpersonationConfig) GetGroups() []string {
if x != nil {
return x.Groups
}
return nil
}
func (x *ImpersonationConfig) GetUid() string {
if x != nil {
return x.Uid
}
return ""
}
func (x *ImpersonationConfig) GetExtra() []*ExtraKeyVal {
if x != nil {
return x.Extra
}
return nil
}
type ExtraKeyVal struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Val []string `protobuf:"bytes,2,rep,name=val,proto3" json:"val,omitempty"`
}
func (x *ExtraKeyVal) Reset() {
*x = ExtraKeyVal{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExtraKeyVal) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExtraKeyVal) ProtoMessage() {}
func (x *ExtraKeyVal) ProtoReflect() protoreflect.Message {
mi := &file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExtraKeyVal.ProtoReflect.Descriptor instead.
func (*ExtraKeyVal) Descriptor() ([]byte, []int) {
return file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescGZIP(), []int{2}
}
func (x *ExtraKeyVal) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *ExtraKeyVal) GetVal() []string {
if x != nil {
return x.Val
}
return nil
}
var File_internal_module_kubernetes_api_rpc_rpc_proto protoreflect.FileDescriptor
var file_internal_module_kubernetes_api_rpc_rpc_proto_rawDesc = []byte{
......@@ -31,34 +205,72 @@ var file_internal_module_kubernetes_api_rpc_rpc_proto_rawDesc = []byte{
0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x70, 0x63, 0x1a,
0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x6f, 0x6f, 0x6c, 0x2f, 0x67,
0x72, 0x70, 0x63, 0x74, 0x6f, 0x6f, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x74, 0x6f, 0x6f, 0x6c,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0x6d, 0x0a, 0x0d, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e,
0x65, 0x74, 0x65, 0x73, 0x41, 0x70, 0x69, 0x12, 0x5c, 0x0a, 0x0b, 0x4d, 0x61, 0x6b, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x74, 0x6f, 0x6f, 0x6c, 0x2e, 0x48,
0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x69, 0x74,
0x6c, 0x61, 0x62, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x74, 0x6f,
0x6f, 0x6c, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x5f, 0x5a, 0x5d, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x63,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x2d, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f,
0x76, 0x31, 0x34, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x6f, 0x64,
0x75, 0x6c, 0x65, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x5f, 0x61,
0x70, 0x69, 0x2f, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x62, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
0x45, 0x78, 0x74, 0x72, 0x61, 0x12, 0x53, 0x0a, 0x0a, 0x69, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x69, 0x74, 0x6c,
0x61, 0x62, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65,
0x74, 0x65, 0x73, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x70, 0x65,
0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
0x09, 0x69, 0x6d, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9f, 0x01, 0x0a, 0x13, 0x49,
0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16,
0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06,
0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x42, 0x0a, 0x05, 0x65, 0x78, 0x74, 0x72,
0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62,
0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65,
0x73, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x4b,
0x65, 0x79, 0x56, 0x61, 0x6c, 0x52, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x22, 0x31, 0x0a, 0x0b,
0x45, 0x78, 0x74, 0x72, 0x61, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x10, 0x0a,
0x03, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x32,
0x6d, 0x0a, 0x0d, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x41, 0x70, 0x69,
0x12, 0x5c, 0x0a, 0x0b, 0x4d, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x22, 0x2e, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x67,
0x72, 0x70, 0x63, 0x74, 0x6f, 0x6f, 0x6c, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x61, 0x67, 0x65,
0x6e, 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x74, 0x6f, 0x6f, 0x6c, 0x2e, 0x48, 0x74, 0x74, 0x70,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x5f,
0x5a, 0x5d, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74,
0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x2d,
0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x67, 0x69, 0x74, 0x6c,
0x61, 0x62, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x34, 0x2f, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x6b, 0x75, 0x62,
0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x70, 0x63, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescOnce sync.Once
file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescData = file_internal_module_kubernetes_api_rpc_rpc_proto_rawDesc
)
func file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescGZIP() []byte {
file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescOnce.Do(func() {
file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescData)
})
return file_internal_module_kubernetes_api_rpc_rpc_proto_rawDescData
}
var file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_internal_module_kubernetes_api_rpc_rpc_proto_goTypes = []interface{}{
(*grpctool.HttpRequest)(nil), // 0: gitlab.agent.grpctool.HttpRequest
(*grpctool.HttpResponse)(nil), // 1: gitlab.agent.grpctool.HttpResponse
(*HeaderExtra)(nil), // 0: gitlab.agent.kubernetes_api.rpc.HeaderExtra
(*ImpersonationConfig)(nil), // 1: gitlab.agent.kubernetes_api.rpc.ImpersonationConfig
(*ExtraKeyVal)(nil), // 2: gitlab.agent.kubernetes_api.rpc.ExtraKeyVal
(*grpctool.HttpRequest)(nil), // 3: gitlab.agent.grpctool.HttpRequest
(*grpctool.HttpResponse)(nil), // 4: gitlab.agent.grpctool.HttpResponse
}
var file_internal_module_kubernetes_api_rpc_rpc_proto_depIdxs = []int32{
0, // 0: gitlab.agent.kubernetes_api.rpc.KubernetesApi.MakeRequest:input_type -> gitlab.agent.grpctool.HttpRequest
1, // 1: gitlab.agent.kubernetes_api.rpc.KubernetesApi.MakeRequest:output_type -> gitlab.agent.grpctool.HttpResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
1, // 0: gitlab.agent.kubernetes_api.rpc.HeaderExtra.imp_config:type_name -> gitlab.agent.kubernetes_api.rpc.ImpersonationConfig
2, // 1: gitlab.agent.kubernetes_api.rpc.ImpersonationConfig.extra:type_name -> gitlab.agent.kubernetes_api.rpc.ExtraKeyVal
3, // 2: gitlab.agent.kubernetes_api.rpc.KubernetesApi.MakeRequest:input_type -> gitlab.agent.grpctool.HttpRequest
4, // 3: gitlab.agent.kubernetes_api.rpc.KubernetesApi.MakeRequest:output_type -> gitlab.agent.grpctool.HttpResponse
3, // [3:4] is the sub-list for method output_type
2, // [2:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_internal_module_kubernetes_api_rpc_rpc_proto_init() }
......@@ -66,18 +278,57 @@ func file_internal_module_kubernetes_api_rpc_rpc_proto_init() {
if File_internal_module_kubernetes_api_rpc_rpc_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeaderExtra); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ImpersonationConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExtraKeyVal); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_module_kubernetes_api_rpc_rpc_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_internal_module_kubernetes_api_rpc_rpc_proto_goTypes,
DependencyIndexes: file_internal_module_kubernetes_api_rpc_rpc_proto_depIdxs,
MessageInfos: file_internal_module_kubernetes_api_rpc_rpc_proto_msgTypes,
}.Build()
File_internal_module_kubernetes_api_rpc_rpc_proto = out.File
file_internal_module_kubernetes_api_rpc_rpc_proto_rawDesc = nil
......
......@@ -13,3 +13,22 @@ service KubernetesApi {
rpc MakeRequest (stream grpctool.HttpRequest) returns (stream grpctool.HttpResponse) {
}
}
// HeaderExtra is passed in grpctool.HttpRequest.extra.
message HeaderExtra {
ImpersonationConfig imp_config = 1;
}
// ImpersonationConfig is a representation of client-go rest.ImpersonationConfig.
// See https://github.com/kubernetes/client-go/blob/release-1.22/rest/config.go#L201-L210
message ImpersonationConfig {
string username = 1;
repeated string groups = 2;
string uid = 3;
repeated ExtraKeyVal extra = 4;
}
message ExtraKeyVal {
string key = 1;
repeated string val = 2;
}
package rpc
func (x *ImpersonationConfig) GetExtraAsMap() map[string][]string {
extra := x.GetExtra() // nil-safe
res := make(map[string][]string, len(extra))
for _, kv := range extra {
res[kv.Key] = kv.Val
}
return res
}
func (x *ImpersonationConfig) IsEmpty() bool {
if x == nil {
return true
}
return x.Username == "" && len(x.Groups) == 0 && x.Uid == "" && len(x.Extra) == 0
}
......@@ -25,10 +25,12 @@ go_library(
"//internal/tool/logz",
"//internal/tool/prototool",
"//internal/tool/tlstool",
"//pkg/agentcfg",
"//pkg/kascfg",
"@com_gitlab_gitlab_org_labkit//correlation",
"@org_golang_google_grpc//metadata",
"@org_golang_google_protobuf//reflect/protoreflect",
"@org_golang_google_protobuf//types/known/anypb",
"@org_golang_x_sync//errgroup",
"@org_uber_go_zap//:zap",
],
......@@ -54,6 +56,7 @@ go_custom_test(
"//internal/tool/testing/mock_modserver",
"//internal/tool/testing/mock_usage_metrics",
"//internal/tool/testing/testhelpers",
"//pkg/agentcfg",
"@com_github_golang_mock//gomock",
"@com_github_google_go_cmp//cmp",
"@com_github_stretchr_testify//assert",
......@@ -62,6 +65,7 @@ go_custom_test(
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//metadata",
"@org_golang_google_protobuf//proto",
"@org_golang_google_protobuf//types/known/anypb",
"@org_uber_go_zap//zaptest",
],
)
......@@ -22,11 +22,13 @@ import (
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/httpz"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/logz"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/prototool"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/pkg/agentcfg"
"gitlab.com/gitlab-org/labkit/correlation"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
)
const (
......@@ -141,12 +143,19 @@ func (p *kubernetesApiProxy) proxy(w http.ResponseWriter, r *http.Request) {
p.requestCount.Inc() // Count only authenticated and authorized requests
impConfig, err := constructImpersonationConfig(aa)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
p.api.HandleProcessingError(ctx, log, agentId, "Failed to construct impersonation config", err)
return
}
// urlPathPrefix is guaranteed to end with / by defaulting. That means / will be removed here.
// Put it back by -1 on length.
r.URL.Path = r.URL.Path[len(p.urlPathPrefix)-1:]
r.Header.Add(viaHeader, "gRPC/1.0 "+p.serverName)
headerWritten, errF := p.pipeStreams(ctx, log, w, r, agentId)
headerWritten, errF := p.pipeStreams(ctx, log, agentId, w, r, impConfig)
if errF != nil {
if headerWritten {
// HTTP status has been written already as part of the normal response flow.
......@@ -171,7 +180,7 @@ func (p *kubernetesApiProxy) getAllowedAgentsForJob(ctx context.Context, jobToke
return allowedForJob.(*gapi.AllowedAgentsForJob), nil
}
func (p *kubernetesApiProxy) pipeStreams(ctx context.Context, log *zap.Logger, w http.ResponseWriter, r *http.Request, agentId int64) (bool /* headerWritten */, errFunc) {
func (p *kubernetesApiProxy) pipeStreams(ctx context.Context, log *zap.Logger, agentId int64, w http.ResponseWriter, r *http.Request, impConfig *rpc.ImpersonationConfig) (bool, errFunc) {
g, ctx := errgroup.WithContext(ctx)
md := metadata.Pairs(modserver.RoutingAgentIdMetadataKey, strconv.FormatInt(agentId, 10))
mkClient, err := p.kubernetesApiClient.MakeRequest(metadata.NewOutgoingContext(ctx, md)) // must use context from errgroup
......@@ -180,7 +189,7 @@ func (p *kubernetesApiProxy) pipeStreams(ctx context.Context, log *zap.Logger, w
}
// Pipe client -> remote
g.Go(func() error {
errFuncRet := p.pipeClientToRemote(ctx, log, agentId, mkClient, r)
errFuncRet := p.pipeClientToRemote(ctx, log, agentId, mkClient, r, impConfig)
if errFuncRet != nil {
return errFuncRet
}
......@@ -190,7 +199,7 @@ func (p *kubernetesApiProxy) pipeStreams(ctx context.Context, log *zap.Logger, w
headerWritten := false
g.Go(func() error {
var errFuncRet errFunc
headerWritten, errFuncRet = p.pipeRemoteToClient(ctx, log, agentId, w, mkClient)
headerWritten, errFuncRet = p.pipeRemoteToClient(ctx, log, agentId, mkClient, w)
if errFuncRet != nil {
return errFuncRet
}
......@@ -203,7 +212,7 @@ func (p *kubernetesApiProxy) pipeStreams(ctx context.Context, log *zap.Logger, w
return false, nil
}
func (p *kubernetesApiProxy) pipeRemoteToClient(ctx context.Context, log *zap.Logger, agentId int64, w http.ResponseWriter, mkClient rpc.KubernetesApi_MakeRequestClient) (bool, errFunc) {
func (p *kubernetesApiProxy) pipeRemoteToClient(ctx context.Context, log *zap.Logger, agentId int64, mkClient rpc.KubernetesApi_MakeRequestClient, w http.ResponseWriter) (bool, errFunc) {
writeFailed := false
headerWritten := false
err := p.streamVisitor.Visit(mkClient,
......@@ -242,8 +251,15 @@ func (p *kubernetesApiProxy) pipeRemoteToClient(ctx context.Context, log *zap.Lo
return headerWritten, nil
}
func (p *kubernetesApiProxy) pipeClientToRemote(ctx context.Context, log *zap.Logger, agentId int64, mkClient rpc.KubernetesApi_MakeRequestClient, r *http.Request) errFunc {
err := mkClient.Send(&grpctool.HttpRequest{
func (p *kubernetesApiProxy) pipeClientToRemote(ctx context.Context, log *zap.Logger, agentId int64,
mkClient rpc.KubernetesApi_MakeRequestClient, r *http.Request, impConfig *rpc.ImpersonationConfig) errFunc {
extra, err := anypb.New(&rpc.HeaderExtra{
ImpConfig: impConfig,
})
if err != nil {
return p.handleProcessingError(ctx, log, agentId, "Proxy failed to marshal HttpRequestExtra proto", err)
}
err = mkClient.Send(&grpctool.HttpRequest{
Message: &grpctool.HttpRequest_Header_{
Header: &grpctool.HttpRequest_Header{
Request: &prototool.HttpRequest{
......@@ -252,6 +268,7 @@ func (p *kubernetesApiProxy) pipeClientToRemote(ctx context.Context, log *zap.Lo
UrlPath: r.URL.Path,
Query: prototool.UrlValuesToValuesMap(r.URL.Query()),
},
Extra: extra,
},
},
})
......@@ -302,7 +319,7 @@ func (p *kubernetesApiProxy) pipeClientToRemote(ctx context.Context, log *zap.Lo
func findAllowedAgent(agentId int64, agentsForJob *gapi.AllowedAgentsForJob) *gapi.AllowedAgent {
for _, aa := range agentsForJob.AllowedAgents {
if aa.Id == agentId {
return &aa
return aa
}
}
return nil
......@@ -378,6 +395,39 @@ func (p *kubernetesApiProxy) handleProcessingError(ctx context.Context, log *zap
return writeError(msg, err)
}
func constructImpersonationConfig(aa *gapi.AllowedAgent) (*rpc.ImpersonationConfig, error) {
as := aa.GetConfiguration().GetAccessAs().GetAs() // all these fields are optional, so handle nils.
if as == nil {
as = &agentcfg.CiAccessAsCF_Agent{} // default value
}
switch imp := as.(type) {
case *agentcfg.CiAccessAsCF_Impersonate:
i := imp.Impersonate
return &rpc.ImpersonationConfig{
Username: i.Username,
Groups: i.Groups,
Uid: i.Uid,
Extra: convertExtra(i.Extra),
}, nil
case *agentcfg.CiAccessAsCF_Agent:
return &rpc.ImpersonationConfig{}, nil
default:
// Normally this should never happen
return nil, fmt.Errorf("unexpected impersonation mode: %T", imp)
}
}
func convertExtra(in []*agentcfg.ExtraKeyValCF) []*rpc.ExtraKeyVal {
out := make([]*rpc.ExtraKeyVal, 0, len(in))
for _, kv := range in {
out = append(out, &rpc.ExtraKeyVal{
Key: kv.Key,
Val: kv.Val,
})
}
return out
}
func writeError(msg string, err error) errFunc {
return func(w http.ResponseWriter) {
// See https://tools.ietf.org/html/rfc7231#section-6.6.3
......
......@@ -29,10 +29,12 @@ import (
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/testing/mock_modserver"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/testing/mock_usage_metrics"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/internal/tool/testing/testhelpers"
"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v14/pkg/agentcfg"
"go.uber.org/zap/zaptest"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"k8s.io/apimachinery/pkg/util/wait"
)
......@@ -149,7 +151,7 @@ func TestProxy_AllowedAgentsError(t *testing.T) {
}
func TestProxy_NoExpectedUrlPathPrefix(t *testing.T) {
_, _, client, req, _ := setupProxyWithHandler(t, "/bla/", defaultGitLabHandler(t))
_, _, client, req, _ := setupProxyWithHandler(t, "/bla/", configGitLabHandler(t, nil))
req.URL.Path = requestPath
resp, err := client.Do(req)
require.NoError(t, err)
......@@ -168,16 +170,85 @@ func TestProxy_ForbiddenAgentId(t *testing.T) {
assert.Empty(t, string(readAll(t, resp.Body)))
}
func TestProxy_HappyPathWithoutUrlPrefix(t *testing.T) {
testProxyHappyPath(t, "/")
}
func TestProxy_HappyPathWithUrlPrefix(t *testing.T) {
testProxyHappyPath(t, "/bla/")
func TestProxy_HappyPath(t *testing.T) {
tests := []struct {
name string
urlPathPrefix string
config *gapi.Configuration
expectedExtra *rpc.HeaderExtra
}{
{
name: "no prefix",
urlPathPrefix: "/",
expectedExtra: &rpc.HeaderExtra{
ImpConfig: &rpc.ImpersonationConfig{},
},
},
{
name: "with prefix",
urlPathPrefix: "/bla/",
expectedExtra: &rpc.HeaderExtra{
ImpConfig: &rpc.ImpersonationConfig{},
},
},
{
name: "impersonate agent",
urlPathPrefix: "/",
config: &gapi.Configuration{
AccessAs: &agentcfg.CiAccessAsCF{
As: &agentcfg.CiAccessAsCF_Agent{
Agent: &agentcfg.CiAccessAsAgentCF{},
},
},
},
expectedExtra: &rpc.HeaderExtra{
ImpConfig: &rpc.ImpersonationConfig{},
},
},
{
name: "impersonate",
urlPathPrefix: "/",
config: &gapi.Configuration{
AccessAs: &agentcfg.CiAccessAsCF{
As: &agentcfg.CiAccessAsCF_Impersonate{
Impersonate: &agentcfg.CiAccessAsImpersonateCF{
Username: "user1",
Groups: []string{"g1", "g2"},
Uid: "uid",
Extra: []*agentcfg.ExtraKeyValCF{
{
Key: "k1",
Val: []string{"v1", "v2"},
},
},
},
},
},
},
expectedExtra: &rpc.HeaderExtra{
ImpConfig: &rpc.ImpersonationConfig{
Username: "user1",
Groups: []string{"g1", "g2"},
Uid: "uid",
Extra: []*rpc.ExtraKeyVal{
{
Key: "k1",
Val: []string{"v1", "v2"},
},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
testProxyHappyPath(t, tc.urlPathPrefix, tc.expectedExtra, configGitLabHandler(t, tc.config))
})
}
}
func testProxyHappyPath(t *testing.T, urlPathPrefix string) {
_, k8sClient, client, req, requestCount := setupProxyWithHandler(t, urlPathPrefix, defaultGitLabHandler(t))
func testProxyHappyPath(t *testing.T, urlPathPrefix string, expectedExtra *rpc.HeaderExtra, handler func(http.ResponseWriter, *http.Request)) {
_, k8sClient, client, req, requestCount := setupProxyWithHandler(t, urlPathPrefix, handler)
requestCount.EXPECT().Inc()
mrClient := mock_kubernetes_api.NewMockKubernetesApi_MakeRequestClient(gomock.NewController(t))
mrCall := k8sClient.EXPECT().
......@@ -186,6 +257,8 @@ func testProxyHappyPath(t *testing.T, urlPathPrefix string) {
requireCorrectOutgoingMeta(t, ctx)
return mrClient, nil
})
extra, err := anypb.New(expectedExtra)
require.NoError(t, err)
gomock.InOrder(append([]*gomock.Call{mrCall}, mockSendStream(t, mrClient,
&grpctool.HttpRequest{
Message: &grpctool.HttpRequest_Header_{
......@@ -216,6 +289,7 @@ func testProxyHappyPath(t *testing.T, urlPathPrefix string) {
},
},
},
Extra: extra,
},
},
},
......@@ -373,25 +447,38 @@ func assertToken(t *testing.T, r *http.Request) bool {
}
func setupProxy(t *testing.T) (*mock_modserver.MockApi, *mock_kubernetes_api.MockKubernetesApiClient, *http.Client, *http.Request, *mock_usage_metrics.MockCounter) {
return setupProxyWithHandler(t, "/", defaultGitLabHandler(t))
return setupProxyWithHandler(t, "/", configGitLabHandler(t, nil))
}
func defaultGitLabHandler(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
func configGitLabHandler(t *testing.T, config *gapi.Configuration) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !assertToken(t, r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
testhelpers.RespondWithJSON(t, w, &gapi.AllowedAgentsForJob{
AllowedAgents: []gapi.AllowedAgent{
testhelpers.RespondWithJSON(t, w, &gapi.AllowedAgentsForJobAlias{ // use alias to ensure proper JSON marshaling
AllowedAgents: []*gapi.AllowedAgent{
{
Id: testhelpers.AgentId,
ConfigProject: &gapi.ConfigProject{
Id: 5,
},
Configuration: config,
},
},
Job: gapi.Job{},
Pipeline: gapi.Pipeline{},
Project: gapi.Project{},
User: gapi.User{},
Job: &gapi.Job{
Id: 1,
},
Pipeline: &gapi.Pipeline{
Id: 2,
},
Project: &gapi.Project{
Id: 3,
},
User: &gapi.User{
Id: 3,
Username: "testuser",
},
})
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment