diff --git a/Dockerfile.grader b/Dockerfile.grader index 1740e76..27f2cf9 100644 --- a/Dockerfile.grader +++ b/Dockerfile.grader @@ -1,4 +1,4 @@ -FROM golang:1.20.1-bullseye +FROM golang:1.23.1-bullseye # Copy Repo COPY go.mod /delegatio/ COPY go.sum /delegatio/ @@ -8,9 +8,17 @@ RUN go mod download COPY ./ /delegatio -WORKDIR /delegatio/grader +COPY ./grader/gradeapi/graders/exercises /exercises + +WORKDIR /delegatio/grader/server RUN go build -o grader . -CMD /delegatio/grader/grader +FROM archlinux:latest +COPY --from=0 /exercises /exercises +COPY --from=0 /delegatio/grader/server/grader /delegatio/grader/server/grader +RUN pacman -Syy +RUN pacman -S python --noconfirm + +CMD /delegatio/grader/server/grader diff --git a/Dockerfile.ssh b/Dockerfile.ssh index 53b5715..3751d08 100644 --- a/Dockerfile.ssh +++ b/Dockerfile.ssh @@ -1,4 +1,4 @@ -FROM golang:1.20.1-bullseye +FROM golang:1.23.1-bullseye # Copy Repo COPY go.mod /delegatio/ COPY go.sum /delegatio/ @@ -11,6 +11,11 @@ COPY ./ /delegatio WORKDIR /delegatio/ssh RUN go build -o ssh . +FROM archlinux:latest +COPY --from=0 /delegatio/ssh/ssh /delegatio/ssh/ssh + +RUN pacman -Syy + CMD /delegatio/ssh/ssh diff --git a/agent/server/main.go b/agent/server/main.go index a664052..f7f7525 100644 --- a/agent/server/main.go +++ b/agent/server/main.go @@ -14,6 +14,14 @@ import ( "go.uber.org/zap" ) +/* + * main is run in every docker container to allow agents to communicate with it. + * It sets up the gRPC server and listens for incoming connections. + * The SSH agents uses the stream exec to forward its incomming requests + * + * The same binary is also used in the VM to allow bootstrapping to take place via + * CLI rpc calls. + */ func main() { var bindIP, bindPort string cfg := zap.NewDevelopmentConfig() diff --git a/agent/vmapi/file.go b/agent/vmapi/file.go index 1b15fed..ac8005e 100644 --- a/agent/vmapi/file.go +++ b/agent/vmapi/file.go @@ -18,6 +18,9 @@ import ( // WriteFile creates a file and writes output to it. func (a *API) WriteFile(_ context.Context, in *vmproto.WriteFileRequest) (*vmproto.WriteFileResponse, error) { a.logger.Info("request to write file", zap.String("path", in.Filepath), zap.String("name", in.Filename)) + if _, err := os.Stat(in.Filepath); os.IsNotExist(err) { + os.MkdirAll(in.Filepath, 0o700) // Create your file + } if err := os.WriteFile(filepath.Join(in.Filepath, in.Filename), in.Content, os.ModeAppend); err != nil { a.logger.Error("failed to write file", zap.String("path", in.Filepath), zap.String("name", in.Filename), zap.Error(err)) return nil, status.Errorf(codes.Internal, "file write failed exited with error code: %v", err) diff --git a/agent/vmapi/vmapi.go b/agent/vmapi/vmapi.go index c886914..8bcd2fe 100644 --- a/agent/vmapi/vmapi.go +++ b/agent/vmapi/vmapi.go @@ -25,6 +25,7 @@ import ( // VMAPI interface contains functions to access the agent. type VMAPI interface { CreateExecInPodgRPC(context.Context, string, *config.KubeExecConfig) error + WriteFileInPodgRPC(context.Context, string, *config.KubeFileWriteConfig) error } // API is the API. @@ -65,7 +66,29 @@ type Dialer interface { } // TODO: This code needs some refactoring / cleanup. +// CreateExecInPodgRPC creates a new exec in pod using gRPC connection to the endpoint agent. +func (a *API) WriteFileInPodgRPC(ctx context.Context, endpoint string, conf *config.KubeFileWriteConfig) error { + conn, err := a.dialInsecure(ctx, endpoint) + if err != nil { + return err + } + defer conn.Close() + client := vmproto.NewAPIClient(conn) + _, err = client.WriteFile(ctx, + &vmproto.WriteFileRequest{ + Filepath: conf.FilePath, + Filename: conf.FileName, + Content: conf.FileData, + }) + if err != nil { + a.logger.Error("failed to write file in pod", zap.Error(err), zap.String("FileName", conf.FileName), zap.String("FilePath", conf.FilePath)) + return err + } + a.logger.Debug("file written in pod", zap.String("FileName", conf.FileName), zap.String("FilePath", conf.FilePath)) + return nil +} +// TODO: This code needs some refactoring / cleanup. // CreateExecInPodgRPC creates a new exec in pod using gRPC connection to the endpoint agent. func (a *API) CreateExecInPodgRPC(ctx context.Context, endpoint string, conf *config.KubeExecConfig) error { conn, err := a.dialInsecure(ctx, endpoint) diff --git a/cli/infrastructure/qemu/wrapper.go b/cli/infrastructure/qemu/wrapper.go index e8a344e..103df69 100644 --- a/cli/infrastructure/qemu/wrapper.go +++ b/cli/infrastructure/qemu/wrapper.go @@ -6,7 +6,6 @@ package qemu import ( "errors" - "fmt" "libvirt.org/go/libvirt" ) @@ -59,15 +58,6 @@ func (l *connectionWrapper) LookupDomainByName(id string) (domain, error) { } func (l *connectionWrapper) LookupStoragePoolByTargetPath(path string) (storagePool, error) { - pools, err := l.conn.ListStoragePools() - if err != nil { - fmt.Println("error listing pools") - return nil, err - } - for _, pool := range pools { - fmt.Println("pool path", pool) - } - pool, err := l.conn.LookupStoragePoolByTargetPath(path) if err != nil { return nil, err diff --git a/cli/installer/installer.go b/cli/installer/installer.go index 4b7e92f..2c81e17 100644 --- a/cli/installer/installer.go +++ b/cli/installer/installer.go @@ -130,18 +130,24 @@ func (k *installer) initalizeChallenges(ctx context.Context, userConfig *config. } stWrapper := storewrapper.StoreWrapper{Store: k.client.SharedStore} - for namespace := range userConfig.Challenges { + for namespace := range userConfig.Containers { if err := stWrapper.PutChallengeData(namespace, nil); err != nil { return err } k.logger.Info("added challenge to store", zap.String("challenge", namespace)) } - for publicKey, realName := range userConfig.PubKeyToUser { - if err := stWrapper.PutPublicKeyData(publicKey, realName); err != nil { + for uuid, userData := range userConfig.UUIDToUser { + if err := stWrapper.PutDataIdxByUuid(uuid, userData); err != nil { return err } - k.logger.Info("added user to store", zap.String("publicKey", publicKey), zap.Any("userinfo", realName)) + k.logger.Info("added user to store", zap.String("uuid", uuid), zap.Any("userinfo", userData)) + } + for pubkey, userData := range userConfig.PubKeyToUser { + if err := stWrapper.PutDataIdxByPubKey(pubkey, userData); err != nil { + return err + } + k.logger.Info("added user to store", zap.String("pubkey", pubkey), zap.Any("userinfo", userData)) } return nil } @@ -191,7 +197,16 @@ func (k *installer) initializeGrader(ctx context.Context) error { return err } k.logger.Info("create namespace", zap.String("namespace", config.GraderNamespaceName)) - + if err := k.createConfigMapAndPutData(ctx, config.GraderNamespaceName, "etcd-credentials", k.sshData); err != nil { + k.logger.With(zap.Error(err)).Error("failed to createConfigMapAndPutData") + return err + } + if err := k.client.CreateServiceAccount(ctx, config.GraderNamespaceName, config.GraderServiceAccountName); err != nil { + return err + } + if err := k.client.CreateClusterRoleBinding(ctx, config.GraderNamespaceName, config.GraderServiceAccountName); err != nil { + return err + } if err := k.client.CreateGraderDeployment(ctx, config.GraderNamespaceName, "grader", int32(config.ClusterConfiguration.NumberOfWorkers)); err != nil { return err } @@ -199,6 +214,7 @@ func (k *installer) initializeGrader(ctx context.Context) error { return err } // Not needed as long as we run on-prem + // Probably not needed at all? Since we access the gracer tthrough the ClusterServiceName? /* if err := k.client.CreateIngress(ctx, graderNamespaceName); err != nil { return err } */ diff --git a/container/challenges/testing/Dockerfile.archlinux b/container/challenges/testing/Dockerfile.archlinux index be229b8..89db130 100644 --- a/container/challenges/testing/Dockerfile.archlinux +++ b/container/challenges/testing/Dockerfile.archlinux @@ -11,14 +11,14 @@ COPY ./ /delegatio WORKDIR /delegatio/agent/server RUN go build -o agent . -WORKDIR /delegatio/agent/user +WORKDIR /delegatio/grader/user RUN go build -o agent-user . FROM archlinux:latest COPY --from=0 /delegatio/agent/server/agent / -COPY --from=0 /delegatio/agent/user/agent-user / +COPY --from=0 /delegatio/grader/user/agent-user / RUN pacman -Syy #RUN pacman -Sy --noconfirm archlinux-keyring #RUN pacman-key --refresh-keys diff --git a/go.mod b/go.mod index 7b32bf8..3b407c5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df github.com/creack/pty v1.1.21 github.com/docker/docker v27.1.1+incompatible + github.com/go-ldap/ldap/v3 v3.4.8 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 @@ -32,6 +33,7 @@ require ( require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -64,6 +66,7 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect diff --git a/go.sum b/go.sum index e149144..2a83c09 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -27,6 +29,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= @@ -124,12 +128,16 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= +github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -207,6 +215,8 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= @@ -223,6 +233,9 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -233,6 +246,18 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -468,6 +493,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -478,6 +506,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -488,11 +517,17 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -505,6 +540,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -527,18 +563,29 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -553,6 +600,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/grader/gradeAPI/gradeapi.go b/grader/gradeapi/gradeapi.go similarity index 53% rename from grader/gradeAPI/gradeapi.go rename to grader/gradeapi/gradeapi.go index bb10163..b074032 100644 --- a/grader/gradeAPI/gradeapi.go +++ b/grader/gradeapi/gradeapi.go @@ -11,8 +11,11 @@ import ( "os" "path" - "github.com/benschlueter/delegatio/grader/gradeAPI/gradeproto" + "github.com/benschlueter/delegatio/grader/gradeapi/gradeproto" + "github.com/benschlueter/delegatio/grader/gradeapi/graders" "github.com/benschlueter/delegatio/internal/config" + "github.com/benschlueter/delegatio/internal/store" + "github.com/benschlueter/delegatio/ssh/kubernetes" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -20,17 +23,37 @@ import ( // API is the API. type API struct { + client kubernetes.K8sAPI logger *zap.Logger dialer Dialer + grader Graders + store store.Store gradeproto.UnimplementedAPIServer } // New creates a new API. -func New(logger *zap.Logger, dialer Dialer) *API { +func New(logger *zap.Logger, dialer Dialer) (*API, error) { + grader, err := graders.NewGraders(logger.Named("graders")) + if err != nil { + return nil, err + } + client, err := kubernetes.NewK8sAPIWrapper(logger.Named("k8sAPI")) + if err != nil { + logger.With(zap.Error(err)).DPanic("failed to create k8s client") + } + + store, err := client.GetStore() + if err != nil { + logger.With(zap.Error(err)).DPanic("connecting to etcd") + } + return &API{ + client: client, logger: logger, dialer: dialer, - } + grader: grader, + store: store, + }, nil } // Dialer is a dialer. @@ -52,8 +75,23 @@ func (a *API) grpcWithDialer() grpc.DialOption { }) } +func (a *API) fileNameToBytes(fileName string) ([]byte, error) { + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + + file.Seek(0, 0) + fileInfo, _ := file.Stat() + fileSize := fileInfo.Size() + bytes := make([]byte, fileSize) + file.Read(bytes) + return bytes, nil +} + // SendGradingRequest sends a grading request to the grader service. -func (a *API) SendGradingRequest(ctx context.Context) (int, error) { +func (a *API) SendGradingRequest(ctx context.Context, fileName string) (int, error) { f, err := os.CreateTemp("/tmp", "gradingRequest-") if err != nil { return 0, err @@ -65,9 +103,13 @@ func (a *API) SendGradingRequest(ctx context.Context) (int, error) { return 0, err } - _, fileName := path.Split(f.Name()) + _, nonceName := path.Split(f.Name()) + a.logger.Info("create nonce file", zap.String("file", nonceName)) - a.logger.Info("create nonce file", zap.String("file", fileName)) + fileBytes, err := a.fileNameToBytes(fileName) + if err != nil { + a.logger.Error("failed to read file", zap.String("file", fileName), zap.Error(err)) + } conn, err := a.dialInsecure(ctx, fmt.Sprintf("grader-service.%s.svc.cluster.local:%d", config.GraderNamespaceName, config.GradeAPIport)) if err != nil { @@ -75,8 +117,9 @@ func (a *API) SendGradingRequest(ctx context.Context) (int, error) { } client := gradeproto.NewAPIClient(conn) resp, err := client.RequestGrading(ctx, &gradeproto.RequestGradingRequest{ - Id: 1, - Nonce: fileName, + Id: 1, + Nonce: nonceName, + Solution: fileBytes, }) if err != nil { return 0, err diff --git a/grader/gradeAPI/gradeproto/Dockerfile.gen-proto b/grader/gradeapi/gradeproto/Dockerfile.gen-proto similarity index 90% rename from grader/gradeAPI/gradeproto/Dockerfile.gen-proto rename to grader/gradeapi/gradeproto/Dockerfile.gen-proto index ea4456a..3add083 100644 --- a/grader/gradeAPI/gradeproto/Dockerfile.gen-proto +++ b/grader/gradeapi/gradeproto/Dockerfile.gen-proto @@ -23,9 +23,9 @@ RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v${GEN_GO_VER} && \ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GEN_GO_GRPC_VER} # Generate code for every existing proto file -WORKDIR /gradeAPI -COPY gradeAPI/gradeproto/*.proto /gradeAPI +WORKDIR /gradeapi +COPY gradeapi/gradeproto/*.proto /gradeapi RUN protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto FROM scratch as export -COPY --from=build /gradeAPI/*.go gradeAPI/gradeproto/ +COPY --from=build /gradeapi/*.go gradeapi/gradeproto/ diff --git a/grader/gradeAPI/gradeproto/gradeapi.pb.go b/grader/gradeapi/gradeproto/gradeapi.pb.go similarity index 83% rename from grader/gradeAPI/gradeproto/gradeapi.pb.go rename to grader/gradeapi/gradeproto/gradeapi.pb.go index 760da7a..709e9a5 100644 --- a/grader/gradeAPI/gradeproto/gradeapi.pb.go +++ b/grader/gradeapi/gradeproto/gradeapi.pb.go @@ -96,7 +96,8 @@ type RequestGradingResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Points int32 `protobuf:"varint,1,opt,name=points,proto3" json:"points,omitempty"` + Points int32 `protobuf:"varint,1,opt,name=points,proto3" json:"points,omitempty"` + Log []byte `protobuf:"bytes,2,opt,name=log,proto3" json:"log,omitempty"` } func (x *RequestGradingResponse) Reset() { @@ -138,6 +139,13 @@ func (x *RequestGradingResponse) GetPoints() int32 { return 0 } +func (x *RequestGradingResponse) GetLog() []byte { + if x != nil { + return x.Log + } + return nil +} + var File_gradeapi_proto protoreflect.FileDescriptor var file_gradeapi_proto_rawDesc = []byte{ @@ -149,21 +157,22 @@ var file_gradeapi_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x22, 0x30, 0x0a, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x22, 0x42, 0x0a, 0x16, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x47, 0x72, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x32, - 0x5a, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x47, 0x72, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x2e, 0x67, 0x72, 0x61, 0x64, 0x65, - 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x47, 0x72, 0x61, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x72, 0x61, 0x64, - 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x47, 0x72, 0x61, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, 0x3c, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x65, 0x6e, 0x73, 0x63, 0x68, - 0x6c, 0x75, 0x65, 0x74, 0x65, 0x72, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, - 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x72, 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x41, 0x50, 0x49, - 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6c, 0x6f, + 0x67, 0x32, 0x5a, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x47, 0x72, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x2e, 0x67, 0x72, 0x61, + 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x47, 0x72, 0x61, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x72, + 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x47, 0x72, + 0x61, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, + 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x65, 0x6e, 0x73, + 0x63, 0x68, 0x6c, 0x75, 0x65, 0x74, 0x65, 0x72, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, + 0x69, 0x6f, 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x72, 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x41, + 0x50, 0x49, 0x2f, 0x67, 0x72, 0x61, 0x64, 0x65, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/grader/gradeAPI/gradeproto/gradeapi.proto b/grader/gradeapi/gradeproto/gradeapi.proto similarity index 95% rename from grader/gradeAPI/gradeproto/gradeapi.proto rename to grader/gradeapi/gradeproto/gradeapi.proto index 469baae..8e04ad1 100644 --- a/grader/gradeAPI/gradeproto/gradeapi.proto +++ b/grader/gradeapi/gradeproto/gradeapi.proto @@ -18,4 +18,5 @@ message RequestGradingRequest { message RequestGradingResponse { int32 points = 1; + bytes log = 2; } \ No newline at end of file diff --git a/grader/gradeAPI/gradeproto/gradeapi_grpc.pb.go b/grader/gradeapi/gradeproto/gradeapi_grpc.pb.go similarity index 100% rename from grader/gradeAPI/gradeproto/gradeapi_grpc.pb.go rename to grader/gradeapi/gradeproto/gradeapi_grpc.pb.go diff --git a/grader/gradeapi/graders.go b/grader/gradeapi/graders.go new file mode 100644 index 0000000..abb6b93 --- /dev/null +++ b/grader/gradeapi/graders.go @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package gradeapi + +import "context" + +// Graders interface contains functions to access the state Graders data. +type Graders interface { + GradeExerciseType1(ctx context.Context, solution []byte, id int) (int, []byte, error) +} diff --git a/grader/gradeapi/graders/exercises.go b/grader/gradeapi/graders/exercises.go new file mode 100644 index 0000000..b2b3678 --- /dev/null +++ b/grader/gradeapi/graders/exercises.go @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package graders + +import "context" + +// Exercises interface contains functions to access the state Exercises data. +type Exercises interface { + GetFiles(ctx context.Context) ([]byte, error) +} diff --git a/grader/gradeapi/graders/exercises/exercise.go b/grader/gradeapi/graders/exercises/exercise.go new file mode 100644 index 0000000..e3adde3 --- /dev/null +++ b/grader/gradeapi/graders/exercises/exercise.go @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package exercises diff --git a/grader/gradeapi/graders/graders.go b/grader/gradeapi/graders/graders.go new file mode 100644 index 0000000..7c96fc7 --- /dev/null +++ b/grader/gradeapi/graders/graders.go @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package graders + +import ( + "context" + "os" + "os/exec" + "time" + + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Graders is responsible for maintaining state information +// of the graders. Currently we do not need any state. +type Graders struct { + logger *zap.Logger + singleExecTimeout time.Duration + totalExecTimeout time.Duration +} + +// NewGraders creates and initializes a new Graders object. +func NewGraders(zapLogger *zap.Logger) (*Graders, error) { + c := &Graders{ + logger: zapLogger, + singleExecTimeout: time.Second, + totalExecTimeout: 15 * time.Second, + } + + return c, nil +} + +func (g *Graders) executeCommand(ctx context.Context, fileName string, arg ...string) ([]byte, error) { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(g.singleExecTimeout)) + defer cancel() + command := exec.CommandContext(ctx, fileName, arg...) + output, err := command.Output() + if err != nil { + return nil, err + } + return output, nil +} + +func (g *Graders) writeFileToDisk(_ context.Context, solution []byte) (*os.File, error) { + f, err := os.CreateTemp("/tmp", "request-") + if err != nil { + g.logger.Error("failed to create content file", zap.Error(err)) + return nil, status.Error(codes.Internal, "failed to create content file") + } + defer f.Close() + + if _, err := f.Write(solution); err != nil { + g.logger.Error("failed to write content file", zap.Error(err)) + return nil, err + } + // make executable + if err := f.Chmod(0o700); err != nil { + g.logger.Error("failed to chmod content file", zap.Error(err)) + return nil, err + } + if err := f.Sync(); err != nil { + g.logger.Error("failed to sync content file", zap.Error(err)) + return nil, err + } + return f, nil +} diff --git a/grader/gradeapi/graders/type1.go b/grader/gradeapi/graders/type1.go new file mode 100644 index 0000000..a3d69f3 --- /dev/null +++ b/grader/gradeapi/graders/type1.go @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package graders + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "go.uber.org/zap" +) + +/* + * Type 1 exercises take a program from the user and execute it on given input files. + * The input files are stored in exercises/exerciseX/ + */ +// TODO: Put input files in exercises/exerciseX/input and have a json/yaml file with the expected output + +func (g *Graders) GradeExerciseType1(ctx context.Context, solution []byte, id int) (int, []byte, error) { + g.logger.Info("grading exercise type 1", zap.Int("id", id)) + defer g.logger.Info("finished grading exercise type 1", zap.Int("id", id)) + file, err := g.writeFileToDisk(ctx, solution) + if err != nil { + return 0, nil, err + } + defer func() { + file.Close() + }() + // TODO: Use Namespaces to run the code in a sandboxed environment + + inputDir := filepath.Join("/exercises/", fmt.Sprintf("exercise%d", id)) + files, err := os.ReadDir(inputDir) + if err != nil { + return 0, nil, err + } + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(g.totalExecTimeout)) + defer cancel() + for _, f := range files { + if !f.IsDir() { + inputFilePath := filepath.Join(inputDir, f.Name()) + output, err := g.executeCommand(ctx, file.Name(), inputFilePath) + if err != nil { + g.logger.Error("failed to execute command", zap.String("command", file.Name()), zap.String("arg", inputFilePath), zap.Error(err), zap.Error(ctx.Err())) + return 0, nil, err + } + if !strings.Contains(string(output), f.Name()[:len(f.Name())-4]) { + g.logger.Info("output does not match expected output", zap.String("output", string(output)), zap.String("expected", f.Name()[:len(f.Name())-4])) + return 0, output, nil + } + } + } + + return 100, nil, nil +} diff --git a/grader/gradeAPI/request.go b/grader/gradeapi/request.go similarity index 59% rename from grader/gradeAPI/request.go rename to grader/gradeapi/request.go index 3d87c17..f2bff44 100644 --- a/grader/gradeAPI/request.go +++ b/grader/gradeapi/request.go @@ -7,18 +7,31 @@ package gradeapi import ( "context" - "github.com/benschlueter/delegatio/grader/gradeAPI/gradeproto" + "github.com/benschlueter/delegatio/grader/gradeapi/gradeproto" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +func (a *API) updatePointsUser(ctx context.Context, points int, user string) error { + a.logger.Info("updating points for user", zap.String("user", user), zap.Int("points", points)) + + return nil +} + // RequestGrading is the gRPC endpoint for requesting grading. func (a *API) RequestGrading(ctx context.Context, in *gradeproto.RequestGradingRequest) (*gradeproto.RequestGradingResponse, error) { var points int + var log []byte + var err error + /* + * How to authenticate the user? + * Either the user signs the request with a private key and the server verifies the signature + * Or the server does a reverse RPC and reads the contents of the nonce file / checks the existence + */ /* p, _ := peer.FromContext(ctx) requestEndpoint := p.Addr.String() */ - + // ToDO: use a unique ID from the USER if in.GetSubmit() { a.logger.Info("received grading request; verifying identity") a.logger.Debug("nonce check passed", zap.String("nonce", in.GetNonce())) @@ -31,12 +44,15 @@ func (a *API) RequestGrading(ctx context.Context, in *gradeproto.RequestGradingR switch id := in.GetId(); id { case 1: a.logger.Info("received grading request for id 1") - points = 100 + points, log, err = a.grader.GradeExerciseType1(ctx, in.GetSolution(), 1) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "failed to grade exercise one") + } case 2: a.logger.Info("received grading request for id 2") } - return &gradeproto.RequestGradingResponse{Points: int32(points)}, nil + return &gradeproto.RequestGradingResponse{Points: int32(points), Log: log}, nil } func (a *API) checkNonce(_ context.Context, _ string) error { diff --git a/grader/main.go b/grader/server/main.go similarity index 100% rename from grader/main.go rename to grader/server/main.go diff --git a/grader/run.go b/grader/server/run.go similarity index 86% rename from grader/run.go rename to grader/server/run.go index 8358eb8..913f529 100644 --- a/grader/run.go +++ b/grader/server/run.go @@ -10,8 +10,8 @@ import ( "net" "sync" - gradeapi "github.com/benschlueter/delegatio/grader/gradeAPI" - "github.com/benschlueter/delegatio/grader/gradeAPI/gradeproto" + "github.com/benschlueter/delegatio/grader/gradeapi" + "github.com/benschlueter/delegatio/grader/gradeapi/gradeproto" "github.com/benschlueter/delegatio/internal/config" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" @@ -27,7 +27,11 @@ func run(dialer gradeapi.Dialer, bindIP, bindPort string, zapLoggerCore *zap.Log defer func() { _ = zapLoggerCore.Sync() }() zapLoggerCore.Info("starting delegatio grader", zap.String("version", version), zap.String("commit", config.Commit)) - gapi := gradeapi.New(zapLoggerCore.Named("gradeapi"), dialer) + gapi, err := gradeapi.New(zapLoggerCore.Named("gradeapi"), dialer) + if err != nil { + zapLoggerCore.Fatal("failed to create gradeapi", zap.Error(err)) + } + zapLoggergRPC := zapLoggerCore.Named("gRPC") grpcServer := grpc.NewServer( diff --git a/agent/user/main.go b/grader/user/main.go similarity index 81% rename from agent/user/main.go rename to grader/user/main.go index 339d4b1..2c29f56 100644 --- a/agent/user/main.go +++ b/grader/user/main.go @@ -13,6 +13,10 @@ import ( "go.uber.org/zap" ) +/* + * This binary is also part of the user docker container and used to communicate with the + * grader API. It sends a grading request and receives the points. + */ func main() { cfg := zap.NewDevelopmentConfig() diff --git a/agent/user/run.go b/grader/user/run.go similarity index 64% rename from agent/user/run.go rename to grader/user/run.go index 38490c7..ec89426 100644 --- a/agent/user/run.go +++ b/grader/user/run.go @@ -8,8 +8,9 @@ package main import ( "context" + "os" - gradeapi "github.com/benschlueter/delegatio/grader/gradeAPI" + gradeapi "github.com/benschlueter/delegatio/grader/gradeapi" "github.com/benschlueter/delegatio/internal/config" "go.uber.org/zap" ) @@ -20,8 +21,15 @@ func run(dialer gradeapi.Dialer, zapLoggerCore *zap.Logger) { defer func() { _ = zapLoggerCore.Sync() }() zapLoggerCore.Info("starting delegatio agent", zap.String("version", version), zap.String("commit", config.Commit)) - api := gradeapi.New(zapLoggerCore, dialer) - points, err := api.SendGradingRequest(context.Background()) + if len(os.Args) != 2 { + zapLoggerCore.Fatal("usage: delegatio-agent ") + } + + api, err := gradeapi.New(zapLoggerCore, dialer) + if err != nil { + zapLoggerCore.Fatal("failed to create gradeapi", zap.Error(err)) + } + points, err := api.SendGradingRequest(context.Background(), os.Args[1]) if err != nil { zapLoggerCore.Fatal("failed to send grading request", zap.Error(err)) } diff --git a/internal/config/global.go b/internal/config/global.go index e2d33a8..ed0cc43 100644 --- a/internal/config/global.go +++ b/internal/config/global.go @@ -30,7 +30,7 @@ var ( ) const ( - + Version = "0.0.1" // DefaultIP is the default IP address to bind to. DefaultIP = "0.0.0.0" // PublicAPIport is the port where we can access the public API. @@ -39,8 +39,12 @@ const ( GradeAPIport = 9027 // DefaultTimeout for the API. DefaultTimeout = 2 * time.Minute - // AuthenticatedUserID key for a hash map, where the sha256 fingerprint of the public key is saved. - AuthenticatedUserID = "sha256Fingerprint" + // AuthenticatedUserID key for a hash map, where the uid is saved. + AuthenticatedUserID = "authenticated-uuid" + // AuthenticationType is the type of authentication used. (i.e. pw, pk) + AuthenticationType = "authType" + // AuthenticatedPrivKey is the private key used for authentication. + AuthenticatedPrivKey = "privateKey" // UserContainerImage is the image used for the challenge containers. UserContainerImage = "ghcr.io/benschlueter/delegatio/archimage:0.1" // SSHContainerImage is the image used for the ssh containers. @@ -61,19 +65,18 @@ const ( SSHNamespaceName = "ssh" // GraderNamespaceName is the namespace where the grader containers are running. GraderNamespaceName = "grader" + // GraderServiceAccountName is the name of the Kubernetes grader service account with cluster access. + GraderServiceAccountName = "development-grader" + // NameSpaceFilePath is the path to the file where the namespace is stored. + NameSpaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" ) // GetExampleConfig writes an example config to config.json. func GetExampleConfig() *UserConfiguration { globalConfig := UserConfiguration{ - Challenges: map[string]ChallengeInformation{ + // Currently not used + Containers: map[string]ContainerInformation{ "testchallenge1": {}, - "testchallenge2": {}, - "testchallenge3": {}, - }, - - PubKeyToUser: map[string]UserInformation{ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLYDO+DPlwJTKYU+S9Q1YkgC7lUJgfsq+V6VxmzdP+omp2EmEIEUsB8WFtr3kAgtAQntaCejJ9ITgoLimkoPs7bV1rA7BZZgRTL2sF+F5zJ1uXKNZz1BVeGGDDXHW5X5V/ZIlH5Bl4kNaAWGx/S5PIszkhyNXEkE6GHsSU4dz69rlutjSbwQRFLx8vjgdAxP9+jUbJMh9u5Dg1SrXiMYpzplJWFt/jI13dDlNTrhWW7790xhHur4fiQbhrVzru29BKNQtSywC+3eH2XKTzobK6h7ECS5X75ghemRIDPw32SHbQP7or1xI+MjFCrZsGyZr1L0yBFNkNAsztpWAqE2FZ": {RealName: "Benedict Schlueter"}, }, } diff --git a/internal/config/types.go b/internal/config/types.go index cde6d25..dc52eb8 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -17,17 +17,28 @@ type ClusterConfig struct { // UserConfiguration is the configuration for the user. type UserConfiguration struct { + UUIDToUser map[string]UserInformation `yaml:"uuidToUser" json:"uuidToUser"` PubKeyToUser map[string]UserInformation `yaml:"pubkeysToUser" json:"pubkeysToUser"` - Challenges map[string]ChallengeInformation `yaml:"challenges" json:"challenges"` + Containers map[string]ContainerInformation `yaml:"challenges" json:"challenges"` } // UserInformation holds the data for a user. type UserInformation struct { - RealName string + Username string + RealName string + Email string + LegiNumber string + Uuid string + Gender string + PrivKey []byte + PubKey []byte + Points map[string]int } -// ChallengeInformation holds the data for a challenge. -type ChallengeInformation struct{} +// ContainerInformation holds the data for a challenge. +type ContainerInformation struct { + ContainerName string +} // KubeExecConfig holds the configuration parsed to the execCommand function. type KubeExecConfig struct { @@ -39,6 +50,15 @@ type KubeExecConfig struct { Tty bool } +// KubeFileWriteConfig holds the data to write a file using the VMAPI. +type KubeFileWriteConfig struct { + UserIdentifier string + Namespace string + FileName string + FilePath string + FileData []byte +} + // KubeForwardConfig holds the configuration parsed to the forwardCommand function. type KubeForwardConfig struct { Namespace string @@ -49,11 +69,11 @@ type KubeForwardConfig struct { // KubeRessourceIdentifier holds the information to identify a kubernetes ressource. type KubeRessourceIdentifier struct { - Namespace string - UserIdentifier string - Challenge string - NodeName string - StorageClass string + UserIdentifier string + Namespace string + ContainerIdentifier string + NodeName string + StorageClass string } // EtcdCredentials contains the credentials for etcd. diff --git a/internal/k8sapi/deployment.go b/internal/k8sapi/deployment.go index 1460cd4..be0856e 100644 --- a/internal/k8sapi/deployment.go +++ b/internal/k8sapi/deployment.go @@ -120,6 +120,8 @@ func (k *Client) CreateGraderDeployment(ctx context.Context, namespace, deployme }, }, Spec: coreAPI.PodSpec{ + ServiceAccountName: config.GraderServiceAccountName, + AutomountServiceAccountToken: &automountServiceAccountToken, Containers: []coreAPI.Container{ { Name: deploymentName, diff --git a/internal/k8sapi/k8sapi.go b/internal/k8sapi/k8sapi.go index df09441..38dbc20 100644 --- a/internal/k8sapi/k8sapi.go +++ b/internal/k8sapi/k8sapi.go @@ -91,7 +91,7 @@ func (k *Client) GetStoreUserData() (data *config.UserConfiguration, err error) if err != nil { return nil, err } - data = &config.UserConfiguration{PubKeyToUser: userData, Challenges: challenges} + data = &config.UserConfiguration{UUIDToUser: userData, Containers: challenges} return data, nil } diff --git a/internal/k8sapi/rbac.go b/internal/k8sapi/rbac.go index 7a81c4e..8a486e2 100644 --- a/internal/k8sapi/rbac.go +++ b/internal/k8sapi/rbac.go @@ -8,15 +8,33 @@ import ( "context" "github.com/benschlueter/delegatio/internal/k8sapi/templates" + v1 "k8s.io/api/rbac/v1" metaAPI "k8s.io/apimachinery/pkg/apis/meta/v1" ) // CreateClusterRoleBinding creates a clusterRoleBinding. func (k *Client) CreateClusterRoleBinding(ctx context.Context, namespace, name string) error { - binding := templates.ClusterRoleBinding(namespace, name) - _, err := k.Client.RbacV1().ClusterRoleBindings().Create(ctx, binding, metaAPI.CreateOptions{}) + var binding *v1.ClusterRoleBinding + + binding, err := k.Client.RbacV1().ClusterRoleBindings().Get(ctx, "add-on-cluster-admin", metaAPI.GetOptions{}) if err != nil { - return err + binding = templates.ClusterRoleBinding(namespace, name) + _, err = k.Client.RbacV1().ClusterRoleBindings().Create(ctx, binding, metaAPI.CreateOptions{}) + if err != nil { + return err + } + } else { + binding.ResourceVersion = "" + binding.Subjects = append(binding.Subjects, v1.Subject{ + Kind: "ServiceAccount", + Name: name, + Namespace: namespace, + }) + _, err = k.Client.RbacV1().ClusterRoleBindings().Update(ctx, binding, metaAPI.UpdateOptions{}) + if err != nil { + return err + } } + return nil } diff --git a/internal/storewrapper/storewrapper.go b/internal/storewrapper/storewrapper.go index 4ed88d0..0236667 100644 --- a/internal/storewrapper/storewrapper.go +++ b/internal/storewrapper/storewrapper.go @@ -16,6 +16,7 @@ import ( const ( challengeLocationPrefix = "challenge-" publicKeyPrefix = "publickey-" + uuidKeyPrefix = "uuid-" privKeyLocation = "privkey-ssh" ) @@ -60,19 +61,19 @@ func (s StoreWrapper) ChallengeExists(challengeName string) (bool, error) { } // GetAllChallenges gets all challenge names. -func (s StoreWrapper) GetAllChallenges() (map[string]config.ChallengeInformation, error) { +func (s StoreWrapper) GetAllChallenges() (map[string]config.ContainerInformation, error) { chIterator, err := s.Store.Iterator(challengeLocationPrefix) if err != nil { return nil, err } - challenges := make(map[string]config.ChallengeInformation) + challenges := make(map[string]config.ContainerInformation) for chIterator.HasNext() { key, err := chIterator.GetNext() if err != nil { return nil, err } key = strings.TrimPrefix(key, challengeLocationPrefix) - var challenge config.ChallengeInformation + var challenge config.ContainerInformation if err := s.GetChallengeData(key, &challenge); err != nil { return nil, err } @@ -81,8 +82,8 @@ func (s StoreWrapper) GetAllChallenges() (map[string]config.ChallengeInformation return challenges, nil } -// PutPublicKeyData puts a publicKey and associated data of the key into the store. -func (s StoreWrapper) PutPublicKeyData(pubkey string, target any) error { +// PutDataIdxByPubKey puts a publicKey and associated data of the key into the store. +func (s StoreWrapper) PutDataIdxByPubKey(pubkey string, target any) error { publicKeyData, err := json.Marshal(target) if err != nil { return err @@ -90,6 +91,15 @@ func (s StoreWrapper) PutPublicKeyData(pubkey string, target any) error { return s.Store.Put(publicKeyPrefix+pubkey, publicKeyData) } +// PutDataIdxByUuid puts a uuid and associated data of the key into the store. +func (s StoreWrapper) PutDataIdxByUuid(uuid string, target any) error { + publicKeyData, err := json.Marshal(target) + if err != nil { + return err + } + return s.Store.Put(uuidKeyPrefix+uuid, publicKeyData) +} + // GetPublicKeyData gets data associated with the publicKey. func (s StoreWrapper) GetPublicKeyData(publickey string, target any) error { publicKeyData, err := s.Store.Get(publicKeyPrefix + publickey) @@ -99,10 +109,33 @@ func (s StoreWrapper) GetPublicKeyData(publickey string, target any) error { return json.Unmarshal(publicKeyData, target) } +// GetPublicKeyUuid gets data associated with the publicKey. +func (s StoreWrapper) GetPublicKeyUuid(uuid string, target any) error { + uuidData, err := s.Store.Get(uuidKeyPrefix + uuid) + if err != nil { + return err + } + return json.Unmarshal(uuidData, target) +} + // PublicKeyExists checks whether the publicKey is in the store. func (s StoreWrapper) PublicKeyExists(publicKey string) (bool, error) { + var perr *store.ValueUnsetError _, err := s.Store.Get(publicKeyPrefix + publicKey) - if errors.Is(err, &store.ValueUnsetError{}) { + if errors.As(err, &perr) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +// UuidExists checks whether the publicKey is in the store. +func (s StoreWrapper) UuidExists(uuid string) (bool, error) { + var perr *store.ValueUnsetError + _, err := s.Store.Get(uuidKeyPrefix + uuid) + if errors.As(err, &perr) { return false, nil } if err != nil { @@ -149,7 +182,7 @@ func (s StoreWrapper) GetAllKeys() (keys []string, err error) { return } -// PutPrivKey puts a privKey into the store. +// PutPrivKey puts a privKey into the store. func (s StoreWrapper) PutPrivKey(privkey []byte) error { return s.Store.Put(privKeyLocation, privkey) } diff --git a/ssh/connection/builder.go b/ssh/connection/builder.go index 011b0dd..43f51ff 100644 --- a/ssh/connection/builder.go +++ b/ssh/connection/builder.go @@ -5,6 +5,7 @@ package connection import ( + "context" "encoding/base64" "errors" "sync" @@ -79,10 +80,11 @@ func (s *Builder) Build() (*Handler, error) { } userK8SAPI := kubernetes.NewK8sAPIUserWrapper(s.k8sHelper, &config.KubeRessourceIdentifier{ + // Namespace will define the challenge / container we're using Namespace: config.UserNamespace, UserIdentifier: userID, // Currently unused, but required for later. - Challenge: s.connection.User(), + ContainerIdentifier: s.connection.User(), }) return &Handler{ @@ -97,9 +99,23 @@ func (s *Builder) Build() (*Handler, error) { newSessionHandler: newSession, newDirectTCPIPHandler: newDirectTCPIP, + writeFileToContainer: writeFileToContainer, }, nil } +func writeFileToContainer(ctx context.Context, conn *ssh.ServerConn, api kubernetes.K8sAPIUser) error { + if conn.Permissions.Extensions[config.AuthenticationType] != "pw" { + return nil + } + return api.WriteFileInPod(ctx, &config.KubeFileWriteConfig{ + Namespace: config.UserNamespace, + UserIdentifier: conn.Permissions.Extensions[config.AuthenticatedUserID], + FileName: "delegatio_priv_key", + FileData: []byte(conn.Permissions.Extensions[config.AuthenticatedPrivKey]), + FilePath: "/root/.ssh", + }) +} + func newSession(log *zap.Logger, channel ssh.Channel, requests <-chan *ssh.Request, api kubernetes.K8sAPIUser) (channels.Channel, error) { builder := channels.SessionBuilderSkeleton() builder.SetRequests(requests) diff --git a/ssh/connection/builder_test.go b/ssh/connection/builder_test.go index 83b239d..64e8d23 100644 --- a/ssh/connection/builder_test.go +++ b/ssh/connection/builder_test.go @@ -329,3 +329,7 @@ func (k *stubK8sHelper) ExecuteCommandInPod(context.Context, *config.KubeExecCon func (k *stubK8sHelper) CreateAndWaitForRessources(context.Context, *config.KubeRessourceIdentifier) error { return nil } + +func (k *stubK8sHelper) WriteFileInPod(context.Context, *config.KubeFileWriteConfig) error { + return nil +} diff --git a/ssh/connection/channels/callback_test.go b/ssh/connection/channels/callback_test.go index 97d5fdf..cd77215 100644 --- a/ssh/connection/channels/callback_test.go +++ b/ssh/connection/channels/callback_test.go @@ -290,6 +290,7 @@ type stubK8sAPIWrapper struct { CreateAndWaitForRessourcesErr error execFunc func(ctx context.Context, kec *config.KubeExecConfig) error forwardFunc func(ctx context.Context, kec *config.KubeForwardConfig) error + writeFunc func(ctx context.Context, kec *config.KubeFileWriteConfig) error } func (k *stubK8sAPIWrapper) CreateAndWaitForRessources(_ context.Context, _ *config.KubeRessourceIdentifier) error { @@ -303,3 +304,7 @@ func (k *stubK8sAPIWrapper) ExecuteCommandInPod(ctx context.Context, conf *confi func (k *stubK8sAPIWrapper) CreatePodPortForward(ctx context.Context, conf *config.KubeForwardConfig) error { return k.forwardFunc(ctx, conf) } + +func (k *stubK8sAPIWrapper) WriteFileInPod(ctx context.Context, conf *config.KubeFileWriteConfig) error { + return k.writeFunc(ctx, conf) +} diff --git a/ssh/connection/connection.go b/ssh/connection/connection.go index e625ce5..91ed1df 100644 --- a/ssh/connection/connection.go +++ b/ssh/connection/connection.go @@ -29,6 +29,7 @@ type Handler struct { keepAliveInterval time.Duration newDirectTCPIPHandler func(*zap.Logger, ssh.Channel, <-chan *ssh.Request, kubernetes.K8sAPIUser, *payload.ForwardTCPChannelOpen) (channels.Channel, error) newSessionHandler func(*zap.Logger, ssh.Channel, <-chan *ssh.Request, kubernetes.K8sAPIUser) (channels.Channel, error) + writeFileToContainer func(context.Context, *ssh.ServerConn, kubernetes.K8sAPIUser) error // Also needed by channel handlers kubernetes.K8sAPIUser } @@ -37,6 +38,7 @@ type Handler struct { func (c *Handler) HandleGlobalConnection(ctx context.Context) { // if the connection is dead terminate it. defer func() { + // fails because channel close already closes the connection??? if err := c.connection.Close(); err != nil { c.log.Error("failed to close connection", zap.Error(err)) } @@ -66,6 +68,8 @@ func (c *Handler) HandleGlobalConnection(ctx context.Context) { return } c.log.Info("ressources are ready, serving channels") + + c.writeFileToContainer(ctx, c.connection, c.K8sAPIUser) // handle channel requests c.handleChannels(ctx) c.log.Info("closed handleGlobalConnection gracefully") diff --git a/ssh/connection/connection_test.go b/ssh/connection/connection_test.go index db242fb..4d92ae1 100644 --- a/ssh/connection/connection_test.go +++ b/ssh/connection/connection_test.go @@ -179,6 +179,7 @@ func TestHandleChannel(t *testing.T) { newSessionHandler: tc.sessionHandlerFunc, newDirectTCPIPHandler: tc.directtcpIPHandlerFunc, wg: &sync.WaitGroup{}, + writeFileToContainer: func(_ context.Context, _ *ssh.ServerConn, _ kubernetes.K8sAPIUser) error { return nil }, } ctx, cancel := context.WithCancel(context.Background()) @@ -246,9 +247,10 @@ func TestHandleChannels(t *testing.T) { channelChan := make(chan ssh.NewChannel) handler := Handler{ - log: observedLogger, - wg: &sync.WaitGroup{}, - channel: channelChan, + log: observedLogger, + wg: &sync.WaitGroup{}, + channel: channelChan, + writeFileToContainer: func(_ context.Context, _ *ssh.ServerConn, _ kubernetes.K8sAPIUser) error { return nil }, } ctx, cancel := context.WithCancel(context.Background()) @@ -331,10 +333,11 @@ func TestHandleGlobalConnection(t *testing.T) { channelChan := make(chan ssh.NewChannel) handler := Handler{ - log: observedLogger, - wg: &sync.WaitGroup{}, - keepAliveInterval: time.Second, - channel: channelChan, + log: observedLogger, + wg: &sync.WaitGroup{}, + keepAliveInterval: time.Second, + channel: channelChan, + writeFileToContainer: func(_ context.Context, _ *ssh.ServerConn, _ kubernetes.K8sAPIUser) error { return nil }, K8sAPIUser: &kubernetes.K8sAPIUserWrapper{ K8sAPI: &stubK8sAPIWrapper{ CreateAndWaitForRessourcesErr: tc.createWfuncErr, @@ -412,7 +415,13 @@ func TestKeepAlive(t *testing.T) { assert := assert.New(t) observedZapCore, observedLogs := observer.New(zap.DebugLevel) observedLogger := zap.New(observedZapCore) - handler := Handler{keepAliveInterval: tc.interval, log: observedLogger} + handler := Handler{ + keepAliveInterval: tc.interval, + log: observedLogger, + writeFileToContainer: func(ctx context.Context, sc *ssh.ServerConn, ka kubernetes.K8sAPIUser) error { + return nil + }, + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -467,7 +476,11 @@ func TestGlobalRequests(t *testing.T) { requests <- &ssh.Request{ WantReply: false, } - handler := Handler{log: observedLogger, globalRequests: requests} + handler := Handler{ + log: observedLogger, + globalRequests: requests, + writeFileToContainer: func(ctx context.Context, sc *ssh.ServerConn, ka kubernetes.K8sAPIUser) error { return nil }, + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -580,6 +593,7 @@ type stubK8sAPIWrapper struct { CreateAndWaitForRessourcesErr error ExecuteCommandInPodErr error CreatePodPortForwardErr error + WriteFileInPodErr error } func (k *stubK8sAPIWrapper) CreateAndWaitForRessources(_ context.Context, _ *config.KubeRessourceIdentifier) error { @@ -593,3 +607,7 @@ func (k *stubK8sAPIWrapper) ExecuteCommandInPod(_ context.Context, _ *config.Kub func (k *stubK8sAPIWrapper) CreatePodPortForward(_ context.Context, _ *config.KubeForwardConfig) error { return k.CreatePodPortForwardErr } + +func (k *stubK8sAPIWrapper) WriteFileInPod(_ context.Context, _ *config.KubeFileWriteConfig) error { + return k.WriteFileInPodErr +} diff --git a/ssh/kubernetes/api.go b/ssh/kubernetes/api.go index 683e51f..d1d6c9b 100644 --- a/ssh/kubernetes/api.go +++ b/ssh/kubernetes/api.go @@ -26,6 +26,7 @@ type K8sAPI interface { CreateAndWaitForRessources(context.Context, *config.KubeRessourceIdentifier) error ExecuteCommandInPod(context.Context, *config.KubeExecConfig) error CreatePodPortForward(context.Context, *config.KubeForwardConfig) error + WriteFileInPod(ctx context.Context, conf *config.KubeFileWriteConfig) error } // K8sAPIWrapper is the struct used to access kubernetes helpers. @@ -96,28 +97,50 @@ func (k *K8sAPIWrapper) ExecuteCommandInPod(ctx context.Context, conf *config.Ku return k.API.CreateExecInPodgRPC(ctx, net.JoinHostPort(pod.Status.PodIP, fmt.Sprint(config.AgentPort)), conf) } +func (k *K8sAPIWrapper) WriteFileInPod(ctx context.Context, conf *config.KubeFileWriteConfig) error { + service, err := k.Client.GetService(ctx, conf.Namespace, fmt.Sprintf("%s-service", conf.UserIdentifier)) + if err != nil { + k.logger.Error("failed to get service", zap.Error(err)) + return err + } + k.logger.Info("cluster ip", zap.String("ip", service.Spec.ClusterIP)) + + pod, err := k.Client.GetPod(ctx, conf.Namespace, fmt.Sprintf("%s-statefulset-0", conf.UserIdentifier)) + if err != nil { + k.logger.Error("failed to get pod", zap.Error(err)) + return err + } + k.logger.Info("pod ip", zap.String("ip", pod.Status.PodIP)) + // TODO: there is a race condition, where the pod is ready, but we can't connect to the endpoint yet. + // Probably should do a vmapi.dial until it succeeds here. + return k.API.WriteFileInPodgRPC(ctx, net.JoinHostPort(pod.Status.PodIP, fmt.Sprint(config.AgentPort)), conf) +} + // CreatePodPortForward creates a port forward on the specified pod. func (k *K8sAPIWrapper) CreatePodPortForward(ctx context.Context, conf *config.KubeForwardConfig) error { return k.Client.CreatePodPortForward(ctx, conf.Namespace, conf.PodName, conf.Port, conf.Communication) } -// GetStore returns a store backed by kube etcd. +// GetStore returns a store backed by kube etcd. Its only supposed to used within a kubernetes pod. func (k *K8sAPIWrapper) GetStore() (store.Store, error) { var err error var ns string - _, present := os.LookupEnv("KUBECONFIG") - if !present { + if _, err := os.Stat(config.NameSpaceFilePath); errors.Is(err, os.ErrNotExist) { // ns is not ready when container spawns ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() ns, err = waitForNamespaceMount(ctx) if err != nil { - k.logger.Error("failed to get namespace, assuming default namespace \"ssh\"", zap.Error(err)) - ns = "ssh" + k.logger.Error("failed to get namespace after timeout", zap.Error(err)) + return nil, err } } else { // out of cluster mode currently assumes 'ssh' namespace - ns = "ssh" + if content, err := os.ReadFile(config.NameSpaceFilePath); err == nil { + ns = strings.TrimSpace(string(content)) + } else { + return nil, err + } } k.logger.Info("namespace", zap.String("namespace", ns)) configData, err := k.Client.GetConfigMapData(context.Background(), ns, "etcd-credentials") diff --git a/ssh/ldap/ldap.go b/ssh/ldap/ldap.go new file mode 100644 index 0000000..6438bb4 --- /dev/null +++ b/ssh/ldap/ldap.go @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package ldap + +import ( + "fmt" + + "github.com/benschlueter/delegatio/internal/config" + goLdap "github.com/go-ldap/ldap/v3" + "go.uber.org/zap" +) + +type Ldap struct { + log *zap.Logger + address string + dn string + attributes []string +} + +func NewLdap(logger *zap.Logger) *Ldap { + address := "ldaps://ldaps-rz-1.ethz.ch" + dn := "cn=%s,ou=users,ou=nethz,ou=id,ou=auth,o=ethz,c=ch" + // https://help.switch.ch/aai/support/documents/attributes/ + attributes := []string{ + "swissEduPersonMatriculationNumber", + "swissEduPersonOrganizationalMail", + "swissEduPersonGender", + "eduPersonAffiliation", + "surname", + "givenName", + "mail", + "uid", + } + + return &Ldap{ + address: address, + dn: dn, + attributes: attributes, + log: logger, + } +} + +func (l *Ldap) dial(username, password string) (*goLdap.Conn, error) { + // Connect to LDAP server + patchedDn := fmt.Sprintf(l.dn, username) + connection, err := goLdap.DialURL(l.address) + if err != nil { + l.log.Error("ldap dial", zap.Error(err)) + return nil, err + } + // Bind to LDAP server + l.log.Info("binding to ldap server", zap.String("dn", patchedDn)) + if err := connection.Bind(patchedDn, password); err != nil { + l.log.Error("ldap bind", zap.Error(err), zap.String("dn", patchedDn)) + return nil, err + } + return connection, nil +} + +func (l *Ldap) Search(username, password string) (*config.UserInformation, error) { + l.log.Info("searching for user", zap.String("username", username)) + connection, err := l.dial(username, password) + if err != nil { + return nil, err + } + defer connection.Close() + + patchedDn := fmt.Sprintf(l.dn, username) + + searchRequest := goLdap.NewSearchRequest( + patchedDn, + goLdap.ScopeBaseObject, + goLdap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(objectClass=*)"), + l.attributes, + nil, + ) + sr, err := connection.Search(searchRequest) + if err != nil { + l.log.Error("ldap search", zap.Error(err), zap.String("dn", l.dn)) + return nil, err + } + // Sanity check: make sure the user is unique + if len(sr.Entries) != 1 { + l.log.Error("ldap user isn't unique", zap.String("dn", l.dn)) + return nil, fmt.Errorf("user not unique %s", patchedDn) + } + + return &config.UserInformation{ + Username: username, + Uuid: username + "-" + sr.Entries[0].GetAttributeValue("swissEduPersonMatriculationNumber"), + LegiNumber: sr.Entries[0].GetAttributeValue("swissEduPersonMatriculationNumber"), + Email: sr.Entries[0].GetAttributeValue("swissEduPersonOrganizationalMail"), + RealName: sr.Entries[0].GetAttributeValue("givenName") + " " + sr.Entries[0].GetAttributeValue("surname"), + Gender: sr.Entries[0].GetAttributeValue("swissEduPersonGender"), + }, nil +} diff --git a/ssh/main.go b/ssh/main.go index 4d677cb..05deb14 100644 --- a/ssh/main.go +++ b/ssh/main.go @@ -14,6 +14,7 @@ import ( "github.com/benschlueter/delegatio/internal/config" "github.com/benschlueter/delegatio/internal/storewrapper" "github.com/benschlueter/delegatio/ssh/kubernetes" + "github.com/benschlueter/delegatio/ssh/ldap" "go.uber.org/zap" ) @@ -49,7 +50,9 @@ func main() { logger.With(zap.Error(err)).DPanic("gettign priv key for ssh server") } logger.Info("pulled private key from store") - server := NewServer(client, logger, store, privKey) + ldap := ldap.NewLdap(logger.Named("ldap")) + logger.Info("created ldap client") + server := NewServer(client, logger, store, privKey, ldap) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/ssh/server.go b/ssh/server.go index b4daa7f..d0d8688 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -12,25 +12,22 @@ import ( "log" "net" "runtime" - "strings" "sync" "sync/atomic" "time" + "github.com/benschlueter/delegatio/internal/config" "github.com/benschlueter/delegatio/internal/store" "github.com/benschlueter/delegatio/internal/storewrapper" "github.com/benschlueter/delegatio/ssh/connection" "github.com/benschlueter/delegatio/ssh/kubernetes" + "github.com/benschlueter/delegatio/ssh/ldap" + "github.com/benschlueter/delegatio/ssh/util" "go.uber.org/zap" "golang.org/x/crypto/ssh" ) -const ( - authenticatedUserID = "sha256Fingerprint" -) - // TODO: Add support for multiple users - // Server is a ssh server. type Server struct { log *zap.Logger @@ -38,11 +35,12 @@ type Server struct { handleConnWG *sync.WaitGroup currentConnections int64 backingStore store.Store + ldap *ldap.Ldap privateKey []byte } // NewServer returns a sshServer. -func NewServer(client kubernetes.K8sAPI, log *zap.Logger, storage store.Store, privKey []byte) *Server { +func NewServer(client kubernetes.K8sAPI, log *zap.Logger, storage store.Store, privKey []byte, ldap *ldap.Ldap) *Server { return &Server{ k8sHelper: client, log: log, @@ -50,6 +48,7 @@ func NewServer(client kubernetes.K8sAPI, log *zap.Logger, storage store.Store, p currentConnections: 0, backingStore: storage, privateKey: privKey, + ldap: ldap, } } @@ -58,22 +57,63 @@ func (s *Server) Start(ctx context.Context) { config := &ssh.ServerConfig{ // Function is called to determine if the user is allowed to connect with the ssh server PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - s.log.Info("publickeycallback called", zap.String("user", conn.User()), zap.Binary("session", conn.SessionID())) - if ok, err := s.data().ChallengeExists(conn.User()); err != nil || !ok { - return nil, fmt.Errorf("user %s not in database or internal store error %w", conn.User(), err) - } + var userData config.UserInformation encodeKey := base64.StdEncoding.EncodeToString(key.Marshal()) - compareKey := fmt.Sprintf("%s %s", key.Type(), encodeKey) - if ok, err := s.data().PublicKeyExists(compareKey); err != nil || !ok { - return nil, fmt.Errorf("pubkey %v not in database or internal store error %w", compareKey, err) + s.log.Debug("publickeycallback called", zap.String("user", conn.User()), zap.Binary("session", conn.SessionID()), zap.String("key", encodeKey)) + + err := s.data().GetPublicKeyData(string(ssh.MarshalAuthorizedKey(key)), &userData) + if err != nil { + s.log.Error("failed to obtain user data", zap.Error(err)) + return nil, fmt.Errorf("failed to obtain user data: %w", err) } return &ssh.Permissions{ Extensions: map[string]string{ - "authType": "pk", - authenticatedUserID: strings.ToLower(ssh.FingerprintSHA256(key)[7:47]), + config.AuthenticationType: "pk", + config.AuthenticatedUserID: userData.Uuid, }, }, nil }, + PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + s.log.Debug("passwordcallback called", zap.String("user", conn.User()), zap.Binary("session", conn.SessionID())) + userData, err := s.ldap.Search(conn.User(), string(password)) + if err != nil { + return nil, fmt.Errorf("ldap search for user %s failed: %w", conn.User(), err) + } + exists, err := s.data().UuidExists(userData.Uuid) + if err != nil { + s.log.Error("error checking if uuid exists; likely due to etcd", zap.Error(err)) + return nil, fmt.Errorf("error checking if uuid %s exists: %w", userData.Uuid, err) + } + // We assume that when an entry exists in the store it ALWAYS has a public/private key pair + if !exists { + privKey, pubKey, err := util.CreateSSHKeypair() + if err != nil { + return nil, fmt.Errorf("failed to create ssh keypair: %w", err) + } + userData.PrivKey = privKey + userData.PubKey = pubKey + err = s.data().PutDataIdxByUuid(userData.Uuid, userData) + if err != nil { + return nil, fmt.Errorf("failed to put data into store: %w", err) + } + err = s.data().PutDataIdxByPubKey(string(userData.PubKey), userData) + if err != nil { + return nil, fmt.Errorf("failed to put data into store: %w", err) + } + s.log.Debug("public key created and stored", zap.String("key", string(userData.PubKey))) + } + s.log.Debug("private key found", zap.String("key", string(userData.PrivKey))) + return &ssh.Permissions{ + Extensions: map[string]string{ + config.AuthenticationType: "pw", + config.AuthenticatedPrivKey: string(userData.PrivKey), + config.AuthenticatedUserID: userData.Uuid, + }, + }, nil + }, + BannerCallback: func(conn ssh.ConnMetadata) string { + return fmt.Sprintf("delegatio ssh server version %s\ncommit %s\n", config.Version, config.Commit) + }, } // routine currently leaks go s.periodicLogs(ctx) diff --git a/ssh/util/util.go b/ssh/util/util.go new file mode 100644 index 0000000..a5a866d --- /dev/null +++ b/ssh/util/util.go @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Benedict Schlueter + */ + +package util + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + + "golang.org/x/crypto/ssh" +) + +// CreateSSHKeypair creates a new SSH keypair and writes it to the specified paths. +func CreateSSHKeypair() ([]byte, []byte, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, err + } + privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + privKeyMem := pem.EncodeToMemory(privateKeyPEM) + pub, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, nil, err + } + return privKeyMem, ssh.MarshalAuthorizedKey(pub), nil +}