From 488100997eba0d6382804983ba112c886ebe35c4 Mon Sep 17 00:00:00 2001 From: Martin Norling Date: Thu, 2 Feb 2023 14:14:05 +0100 Subject: [PATCH 01/19] Add ega-fields to config --- compose.yml | 4 +++- config.yaml | 2 ++ helpers/helpers.go | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index e3cf982..1194bfc 100644 --- a/compose.yml +++ b/compose.yml @@ -21,7 +21,9 @@ services: environment: - LOG_LEVEL=debug - GLOBAL_CRYPT4GHKEY=/keys/c4gh.sec.pem - - GLOBAL_EGAUSER=test@sda.dev + - GLOBAL_EGAUSER=sda + - GLOBAL_EGAPASSWORD=pass + - GLOBAL_EGAURL=http://ega.dev - GLOBAL_EXPIRATIONDAYS=14 - GLOBAL_ISS=https://login.sda.dev - GLOBAL_JWTKEY=/keys/jwt.key diff --git a/config.yaml b/config.yaml index ba929f6..f062fb6 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,8 @@ global: crypt4ghKey: "" egaUser: "" + egaPassword: "" + egaUrl: "" expirationDays: 14 iss: "" jwtKey: "" diff --git a/helpers/helpers.go b/helpers/helpers.go index 46c260c..8291407 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -28,6 +28,8 @@ type Conf struct { Crypt4ghKeyPath string Crypt4ghKey string EgaUser string + EgaPassword string + EgaUrl string ExpirationDays int Iss string JwtKeyPath string @@ -96,6 +98,8 @@ func NewConf(conf *Conf) (err error) { conf.Password = viper.GetString("global.uppmaxPassword") conf.S3URL = viper.GetString("global.s3url") conf.EgaUser = viper.GetString("global.egaUser") + conf.EgaPassword = viper.GetString("global.egaPassword") + conf.EgaUrl = viper.GetString("global.egaUrl") conf.Crypt4ghKeyPath = viper.GetString("global.crypt4ghKey") if !viper.IsSet("global.expirationDays") { From 8c03f24766209d39818beab164bfe9a727f8e5a7 Mon Sep 17 00:00:00 2001 From: Martin Norling Date: Thu, 2 Feb 2023 14:15:03 +0100 Subject: [PATCH 02/19] Add basic ega-check function --- token/ega.go | 89 +++++++++++++++++++++++++++++++++++++++++++++ token/token.go | 49 ++++++++----------------- token/token_test.go | 8 ++-- 3 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 token/ega.go diff --git a/token/ega.go b/token/ega.go new file mode 100644 index 0000000..5e37a89 --- /dev/null +++ b/token/ega.go @@ -0,0 +1,89 @@ +package token + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/NBISweden/sda-uppmax-integration/helpers" + log "github.com/sirupsen/logrus" +) + +type EgaHeader struct { + ApiVersion string `json:"apiVersion"` + Code int `json:"code"` + Service string `json:"service"` + DeveloperMessage string `json:"developerMessage"` + UserMessage string `json:"userMessage"` + ErrorCode int `json:"errorCode"` + DocLink string `json:"docLink"` +} + +type EgaUserResult struct { + Username string `json:"username"` + SshPublicKey string `json:"sshPublicKey"` + PasswordHash string `json:"passwordHash"` + Uid int `json:"uid"` + Gecos string `json:"gecos"` +} + +type EgaResponse struct { + NumTotalResults int `json:"numTotalResults"` + ResultType string `json:"resultType"` + Result []EgaUserResult `json:"result"` +} + +type EgaReply struct { + Header EgaHeader `json:"header"` + Response EgaResponse `json:"response"` +} + +// getEGABoxAccount checks that a given `username` is a valid EGA account, and +// returns the first username for that account. +func getEGABoxAccount(username string) (egaUsername string, err error) { + + egaUser := helpers.Config.EgaUser + egaPass := helpers.Config.EgaPassword + egaUrl := helpers.Config.EgaUrl + + url := fmt.Sprintf("%v/%v?idType=username", egaUrl, username) + + client := &http.Client{ + Timeout: 5 * time.Second, + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + + return "", err + } + req.SetBasicAuth(egaUser, egaPass) + resp, err := client.Do(req) + if err != nil { + + return "", err + } + + if resp.StatusCode != 200 { + + message, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + return "", fmt.Errorf("got %v from EGA", message) + } + + var reply EgaReply + json.NewDecoder(resp.Body).Decode(&reply) + defer resp.Body.Close() + log.Debugf("reply: %v", reply) + if len(reply.Response.Result) == 0 { + return "", nil + } + egaUsername = reply.Response.Result[0].Username + + return egaUsername, nil +} diff --git a/token/token.go b/token/token.go index 0e14415..cda0409 100644 --- a/token/token.go +++ b/token/token.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "strings" "time" @@ -17,12 +16,12 @@ import ( ) type tokenRequest struct { - Swamid string `json:"swamid"` + SwamID string `json:"swamid"` ProjectID string `json:"projectid"` } type tokenResponse struct { - Swamid string `json:"swamid"` + SwamID string `json:"swamid"` ProjectID string `json:"projectid"` RequestTime string `json:"request_time"` Expiration string `json:"expiration"` @@ -32,22 +31,12 @@ type tokenResponse struct { func readRequestBody(body io.ReadCloser) (tokenRequest tokenRequest, err error) { - reqBody, err := io.ReadAll(body) + err = json.NewDecoder(body).Decode(&tokenRequest) if err != nil { - log.Print("Error reading request body: ", err) - - return tokenRequest, fmt.Errorf("error reading request body") + return tokenRequest, err } - defer body.Close() - - err = json.Unmarshal(reqBody, &tokenRequest) - if err != nil { - log.Print("Error unmarshaling: ", err) - return tokenRequest, fmt.Errorf("error unmarshaling data") - } - - if tokenRequest.ProjectID == "" || tokenRequest.Swamid == "" { + if tokenRequest.ProjectID == "" || tokenRequest.SwamID == "" { return tokenRequest, fmt.Errorf("incomplete incoming data") } @@ -77,9 +66,14 @@ func createECToken(key *ecdsa.PrivateKey, username string) (string, error) { } func createS3Config(username string) (s3config string, expiration string, err error) { - s3config = "guess_mime_type = True \nhuman_readable_sizes = True\nuse_https = True\n" + - "multipart_chunk_size_mb = 50\n" + "check_ssl_certificate = True\n" + - "check_ssl_hostname = True\n" + "encoding = UTF-8\n" + "encrypt = False\n" + + s3config = "guess_mime_type = True\n" + + "human_readable_sizes = True\n" + + "use_https = True\n" + + "multipart_chunk_size_mb = 50\n" + + "check_ssl_certificate = True\n" + + "check_ssl_hostname = True\n" + + "encoding = UTF-8\n" + + "encrypt = False\n" + "socket_timeout = 30\n" token, err := createECToken(helpers.Config.JwtParsedKey, username) @@ -101,7 +95,7 @@ func createS3Config(username string) (s3config string, expiration string, err er func createResponse(tokenRequest tokenRequest, username string) (tokenResponse tokenResponse, err error) { tokenResponse.RequestTime = time.Now().Format("01-02-2006 15:04:05") - tokenResponse.Swamid = tokenRequest.Swamid + tokenResponse.SwamID = tokenRequest.SwamID tokenResponse.ProjectID = tokenRequest.ProjectID tokenResponse.Crypt4ghKey = helpers.Config.Crypt4ghKey @@ -113,19 +107,6 @@ func createResponse(tokenRequest tokenRequest, username string) (tokenResponse t return tokenResponse, err } -// getEGABoxAccount checks whether the access for the specified swamID and project is granted -// and returns the respective ega-box account -func getEGABoxAccount(projectID string, swamID string) (username string, err error) { - - username = helpers.Config.EgaUser - - if err != nil { - return "", err - } - - return username, nil -} - // GetToken returns the information require for uploading data to the S3 backend, // including the token func GetToken(w http.ResponseWriter, r *http.Request) { @@ -142,7 +123,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } // Check specified swam_id against project_id - username, err := getEGABoxAccount(tokenRequest.ProjectID, tokenRequest.Swamid) + username, err := getEGABoxAccount(tokenRequest.SwamID) if err != nil { currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) diff --git a/token/token_test.go b/token/token_test.go index cfafbc8..1ddefd3 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -45,7 +45,7 @@ func (suite *TestSuite) SetupTest() { func (suite *TestSuite) TestNewConf() { expectedToken := tokenRequest{ - Swamid: "", + SwamID: "", ProjectID: "", } @@ -66,7 +66,7 @@ func (suite *TestSuite) TestNewConf() { _, err = readRequestBody(r) - assert.EqualError(suite.T(), err, "error unmarshaling data") + assert.EqualError(suite.T(), err, "unexpected EOF") r = io.NopCloser(strings.NewReader(`{ "swami": "", @@ -126,7 +126,7 @@ func (suite *TestSuite) TestCreateResponse() { requestBody := &tokenRequest{ ProjectID: "someproject", - Swamid: "someswam", + SwamID: "someswam", } confData := `global: @@ -153,7 +153,7 @@ func (suite *TestSuite) TestCreateResponse() { // Check that the base64 encoded key in the response is the expected one assert.Equal(suite.T(), "LS0tLS1CRUdJTiBDUllQVDRHSCBQVUJMSUMgS0VZLS0tLS0KdlNvbWUrYXNkL2FwdWJsaWNLZXkKLS0tLS1FTkQgQ1JZUFQ0R0ggUFVCTElDIEtFWS0tLS0t", responseBody.Crypt4ghKey) assert.Equal(suite.T(), requestBody.ProjectID, responseBody.ProjectID) - assert.Equal(suite.T(), requestBody.Swamid, responseBody.Swamid) + assert.Equal(suite.T(), requestBody.SwamID, responseBody.SwamID) defer os.Remove(configName) } From 1ab5133e85bb2a8d52007cf17737833a15d90d0f Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 10:16:57 +0100 Subject: [PATCH 03/19] Update names for ega to match linter --- token/ega.go | 43 ++++++++++++++++++++++++------------------- token/token_test.go | 4 ++-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/token/ega.go b/token/ega.go index 5e37a89..e8e181d 100644 --- a/token/ega.go +++ b/token/ega.go @@ -3,7 +3,7 @@ package token import ( "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -12,7 +12,7 @@ import ( ) type EgaHeader struct { - ApiVersion string `json:"apiVersion"` + APIVersion string `json:"apiVersion"` Code int `json:"code"` Service string `json:"service"` DeveloperMessage string `json:"developerMessage"` @@ -23,9 +23,9 @@ type EgaHeader struct { type EgaUserResult struct { Username string `json:"username"` - SshPublicKey string `json:"sshPublicKey"` + SSHPublicKey string `json:"sshPublicKey"` PasswordHash string `json:"passwordHash"` - Uid int `json:"uid"` + UID int `json:"uid"` Gecos string `json:"gecos"` } @@ -40,15 +40,15 @@ type EgaReply struct { Response EgaResponse `json:"response"` } -// getEGABoxAccount checks that a given `username` is a valid EGA account, and -// returns the first username for that account. -func getEGABoxAccount(username string) (egaUsername string, err error) { +// verifyEGABoxAccount checks that a given `username` is a valid EGA account, and +// returns error if the user does not exist. +func verifyEGABoxAccount(username string) (err error) { - egaUser := helpers.Config.EgaUser + egaUser := helpers.Config.EgaUsername egaPass := helpers.Config.EgaPassword - egaUrl := helpers.Config.EgaUrl + egaURL := helpers.Config.EgaURL - url := fmt.Sprintf("%v/%v?idType=username", egaUrl, username) + url := fmt.Sprintf("%v/%v?idType=username", egaURL, username) client := &http.Client{ Timeout: 5 * time.Second, @@ -56,34 +56,39 @@ func getEGABoxAccount(username string) (egaUsername string, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - return "", err + return err } req.SetBasicAuth(egaUser, egaPass) resp, err := client.Do(req) if err != nil { - return "", err + return err } if resp.StatusCode != 200 { - message, err := ioutil.ReadAll(resp.Body) + message, err := io.ReadAll(resp.Body) if err != nil { - return "", err + return err } defer resp.Body.Close() - return "", fmt.Errorf("got %v from EGA", message) + return fmt.Errorf("got %v from EGA", message) } var reply EgaReply - json.NewDecoder(resp.Body).Decode(&reply) + err = json.NewDecoder(resp.Body).Decode(&reply) + if err != nil { + + return err + } + defer resp.Body.Close() log.Debugf("reply: %v", reply) if len(reply.Response.Result) == 0 { - return "", nil + + return nil } - egaUsername = reply.Response.Result[0].Username - return egaUsername, nil + return nil } diff --git a/token/token_test.go b/token/token_test.go index 1ddefd3..fe2ab2f 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -99,7 +99,7 @@ func (suite *TestSuite) TestCreateECToken() { err = helpers.NewConf(&helpers.Config) assert.NoError(suite.T(), err) - tokenString, err := createECToken(helpers.Config.JwtParsedKey, helpers.Config.EgaUser) + tokenString, err := createECToken(helpers.Config.JwtParsedKey, helpers.Config.EgaUsername) assert.NoError(suite.T(), err) // Parse token to make sure it contains the correct information @@ -109,7 +109,7 @@ func (suite *TestSuite) TestCreateECToken() { // Check that token includes the correct information assert.Equal(suite.T(), helpers.Config.Username, claims["pilot"]) assert.Equal(suite.T(), helpers.Config.Iss, claims["iss"]) - assert.Equal(suite.T(), helpers.Config.EgaUser, claims["sub"]) + assert.Equal(suite.T(), helpers.Config.EgaUsername, claims["sub"]) s3config, _, err := createS3Config("someuser") From 1fe5eb7bb51f5ed6afe9f3b620a8fe5ee7cf2c82 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 10:17:27 +0100 Subject: [PATCH 04/19] Add implementation for SUPR account verification --- token/supr.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 token/supr.go diff --git a/token/supr.go b/token/supr.go new file mode 100644 index 0000000..6f5da37 --- /dev/null +++ b/token/supr.go @@ -0,0 +1,162 @@ +package token + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/NBISweden/sda-uppmax-integration/helpers" + log "github.com/sirupsen/logrus" +) + +type SuprResponse struct { + Matches []Match `json:"matches"` + Began string `json:"began"` +} + +type Match struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Title string `json:"title"` + DirectoryName string `json:"directory_name"` + DirectoryNameType string `json:"directory_name_type"` + NgiProjectName string `json:"ngi_project_name"` + Abstract string `json:"abstract"` + Webpage string `json:"webpage"` + Affiliation string `json:"affiliation"` + Classification1 string `json:"classification1"` + Classification2 string `json:"classification2"` + Classification3 string `json:"classification3"` + ManagedInSupr bool `json:"managed_in_supr"` + APIOpaqueData string `json:"api_opaque_data"` + NgiSensitiveData bool `json:"ngi_sensitive_data"` + NgiReady bool `json:"ngi_ready"` + NgiDeliveryStatus string `json:"ngi_delivery_status"` + ContinuationName string `json:"continuation_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Pi Pi `json:"pi"` + Members []Member `json:"members"` + LinksOutgoing []interface{} `json:"links_outgoing"` + LinksIncoming []interface{} `json:"links_incoming"` + Resourceprojects []ResourceProject `json:"resourceprojects"` + Modified string `json:"modified"` +} + +type Pi struct { + ID int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` +} + +type Member struct { + ID int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` +} + +type ResourceProject struct { + ID int `json:"id"` + Allocated int `json:"allocated"` + Resource Resource `json:"resource"` + DecommissioningState string `json:"decommissioning_state"` + Allocations []Allocation `json:"allocations"` +} + +type SuprHeader struct { + APIVersion string `json:"apiVersion"` + Code int `json:"code"` + Service string `json:"service"` + DeveloperMessage string `json:"developerMessage"` + UserMessage string `json:"userMessage"` + ErrorCode int `json:"errorCode"` + DocLink string `json:"docLink"` +} + +type SuprUserResult struct { + Username string `json:"username"` + SSHPublicKey string `json:"sshPublicKey"` + PasswordHash string `json:"passwordHash"` + UID int `json:"uid"` + Gecos string `json:"gecos"` +} + +type Resource struct { + ID int `json:"id"` + Name string `json:"name"` + CapacityUnit string `json:"capacity_unit"` + CapacityUnit2 string `json:"capacity_unit_2"` + Centre Center `json:"centre"` +} + +type Allocation struct { + ID int `json:"id"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Allocated int `json:"allocated"` +} + +type Center struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// verifyProjectAccount checks that the given `email` is actually +// the PI of the given `project_id` and returns error otherwise +func verifyProjectAccount(username string, projectID string) (err error) { + + suprUser := helpers.Config.SuprUsername + suprPass := helpers.Config.SuprPassword + suprURL := helpers.Config.SuprURL + + url := fmt.Sprintf("%v?name=%v", suprURL, projectID) + + client := &http.Client{ + Timeout: 5 * time.Second, + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + + return err + } + req.SetBasicAuth(suprUser, suprPass) + resp, err := client.Do(req) + if err != nil { + + return err + } + + if resp.StatusCode != 200 { + + message, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + return fmt.Errorf("got %v from SUPR", message) + } + + var response SuprResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + + return err + } + + defer resp.Body.Close() + log.Debugf("reply: %v", response) + + if response.Matches[0].Pi.Email != username { + log.Infof("Email %v does not exist for SUPR project %v", username, projectID) + + return fmt.Errorf("email is different than PI in requested project") + } + + return nil +} From 816a2ba0753ddaa9752af6d8360216e00f973cba Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 10:17:55 +0100 Subject: [PATCH 05/19] Add SUPR account verification --- token/token.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/token/token.go b/token/token.go index cda0409..dd1a813 100644 --- a/token/token.go +++ b/token/token.go @@ -123,7 +123,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } // Check specified swam_id against project_id - username, err := getEGABoxAccount(tokenRequest.SwamID) + err = verifyEGABoxAccount(tokenRequest.SwamID) if err != nil { currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) @@ -133,8 +133,17 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } + err = verifyProjectAccount(tokenRequest.SwamID, tokenRequest.ProjectID) + if err != nil { + currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, string(currentError)) + + return + } + // Create token for user corresponding to specified swam_id - resp, err := createResponse(tokenRequest, username) + resp, err := createResponse(tokenRequest, tokenRequest.SwamID) if err != nil { currentError := helpers.CreateErrorResponse("Unable to create token for specified project") w.WriteHeader(http.StatusInternalServerError) From 5974f1130ff5085b3e6b34e4a7a77f164a337b19 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 10:18:13 +0100 Subject: [PATCH 06/19] Add SUPR integration configuration --- helpers/helpers.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/helpers/helpers.go b/helpers/helpers.go index 8291407..0d2ef4c 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -27,9 +27,9 @@ var Config Conf type Conf struct { Crypt4ghKeyPath string Crypt4ghKey string - EgaUser string + EgaUsername string EgaPassword string - EgaUrl string + EgaURL string ExpirationDays int Iss string JwtKeyPath string @@ -37,6 +37,9 @@ type Conf struct { S3URL string Username string Password string + SuprUsername string + SuprPassword string + SuprURL string } // NewConf reads the configuration from the config.yaml file @@ -65,7 +68,8 @@ func NewConf(conf *Conf) (err error) { } requiredConfVars := []string{ - "global.iss", "global.crypt4ghKey", "global.uppmaxUsername", "global.uppmaxPassword", "global.s3url", "global.egaUser", "global.jwtKey", + "global.iss", "global.crypt4ghKey", "global.uppmaxUsername", "global.uppmaxPassword", "global.s3url", "global.jwtKey", + "global.suprUsername", "global.suprPassword", "global.suprUrl", "global.egaUsername", "global.egaPassword", "global.egaUrl", } for _, s := range requiredConfVars { @@ -97,10 +101,13 @@ func NewConf(conf *Conf) (err error) { conf.Username = viper.GetString("global.uppmaxUsername") conf.Password = viper.GetString("global.uppmaxPassword") conf.S3URL = viper.GetString("global.s3url") - conf.EgaUser = viper.GetString("global.egaUser") + conf.EgaUsername = viper.GetString("global.egaUsername") conf.EgaPassword = viper.GetString("global.egaPassword") - conf.EgaUrl = viper.GetString("global.egaUrl") + conf.EgaURL = viper.GetString("global.egaURL") conf.Crypt4ghKeyPath = viper.GetString("global.crypt4ghKey") + conf.SuprPassword = viper.GetString("global.suprPassword") + conf.SuprURL = viper.GetString("global.suprURL") + conf.SuprUsername = viper.GetString("global.suprUsername") if !viper.IsSet("global.expirationDays") { conf.ExpirationDays = 14 From 59ebbfbbcb733a712770095261903de3662b13de Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 10:18:32 +0100 Subject: [PATCH 07/19] Add SUPR integration fields in config --- config.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index f062fb6..caad7fb 100644 --- a/config.yaml +++ b/config.yaml @@ -1,11 +1,14 @@ global: crypt4ghKey: "" - egaUser: "" + egaUsername: "" egaPassword: "" - egaUrl: "" + egaURL: "" expirationDays: 14 iss: "" jwtKey: "" + suprUsername: "" + suprPassword: "" + suprURL: "" s3url: "" uppmaxUsername: "" uppmaxPassword: "" From 00258f51ae2ae991d7fb2509ba1a2951fd668e35 Mon Sep 17 00:00:00 2001 From: Martin Norling Date: Fri, 3 Feb 2023 12:46:17 +0100 Subject: [PATCH 08/19] update test configs --- helpers/helpers_test.go | 27 ++++++++++++++++++--------- token/token_test.go | 26 ++++++++++++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go index f061439..28a33d3 100644 --- a/helpers/helpers_test.go +++ b/helpers/helpers_test.go @@ -48,15 +48,19 @@ func (suite *TestSuite) TestCreateErrorResponse() { func (suite *TestSuite) TestNewConf() { confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "http://ega.dev" + expirationDays: 14 iss: "https://some.url" jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "http://supr.dev" + s3url: "some.s3.url" uppmaxUsername: "user" uppmaxPassword: "password" - s3url: "some.s3.url" - expirationDays: 14 - egaUser: "some-user" - crypt4ghKey: "` + suite.Crypt4ghKeyPath + `" - ` configName := "config.yaml" err := os.WriteFile(configName, []byte(confData), 0600) @@ -95,14 +99,19 @@ func (suite *TestSuite) TestNewConfMissingValue() { func (suite *TestSuite) TestNewConfMissingKey() { confData := `global: + crypt4ghKey: "` + suite.Crypt4ghKeyPath + `" + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "http://ega.dev" + expirationDays: 14 iss: "https://some.url" jwtKey: "some/path" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "http://supr.dev" + s3url: "some.s3.url" uppmaxUsername: "user" uppmaxPassword: "password" - s3url: "some.s3.url" - expirationDays: 14 - egaUser: "some-user" - crypt4ghKey: "` + suite.Crypt4ghKeyPath + `" ` configName := "config.yaml" err := os.WriteFile(configName, []byte(confData), 0600) diff --git a/token/token_test.go b/token/token_test.go index fe2ab2f..d657cbf 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -81,14 +81,19 @@ func (suite *TestSuite) TestNewConf() { func (suite *TestSuite) TestCreateECToken() { confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "http://ega.dev" + expirationDays: 14 iss: "https://some.url" jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "http://supr.dev" + s3url: "some.s3.url" uppmaxUsername: "user" uppmaxPassword: "password" - s3url: "some.s3.url" - expirationDays: 14 - egaUser: "some-user" - crypt4ghKey: "` + suite.Crypt4ghKeyPath + `" ` configName := "config.yaml" err := os.WriteFile(configName, []byte(confData), 0600) @@ -130,14 +135,19 @@ func (suite *TestSuite) TestCreateResponse() { } confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "http://ega.dev" + expirationDays: 14 iss: "https://some.url" jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "http://supr.dev" + s3url: "some.s3.url" uppmaxUsername: "user" uppmaxPassword: "password" - s3url: "some.s3.url" - expirationDays: 14 - egaUser: "some-user" - crypt4ghKey: "` + suite.Crypt4ghKeyPath + `" ` configName := "config.yaml" err := os.WriteFile(configName, []byte(confData), 0600) From 107ea67db5bbc12e122917971b48a9b1e8e41188 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:22:50 +0100 Subject: [PATCH 09/19] Add info messages regarding verification --- token/token.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/token/token.go b/token/token.go index dd1a813..1b4eab4 100644 --- a/token/token.go +++ b/token/token.go @@ -12,6 +12,7 @@ import ( b64 "encoding/base64" "github.com/NBISweden/sda-uppmax-integration/helpers" + "github.com/apex/log" "github.com/golang-jwt/jwt" ) @@ -125,6 +126,7 @@ func GetToken(w http.ResponseWriter, r *http.Request) { // Check specified swam_id against project_id err = verifyEGABoxAccount(tokenRequest.SwamID) if err != nil { + log.Infof("%v is not a valid ega account", tokenRequest.SwamID) currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, string(currentError)) @@ -132,15 +134,19 @@ func GetToken(w http.ResponseWriter, r *http.Request) { return } + log.Infof("%v is verified as existing ega account", tokenRequest.SwamID) err = verifyProjectAccount(tokenRequest.SwamID, tokenRequest.ProjectID) if err != nil { + + log.Infof("%v is not the PI of SUPR project %v", tokenRequest.SwamID, tokenRequest.ProjectID) currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, string(currentError)) return } + log.Infof("%v verified as the PI of SUPR project %v", tokenRequest.SwamID, tokenRequest.ProjectID) // Create token for user corresponding to specified swam_id resp, err := createResponse(tokenRequest, tokenRequest.SwamID) From fbf298f5b891c25555af99fa9cb51b20db4405eb Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:24:11 +0100 Subject: [PATCH 10/19] Update log package to logrus --- token/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token/token.go b/token/token.go index 1b4eab4..65e429c 100644 --- a/token/token.go +++ b/token/token.go @@ -12,8 +12,8 @@ import ( b64 "encoding/base64" "github.com/NBISweden/sda-uppmax-integration/helpers" - "github.com/apex/log" "github.com/golang-jwt/jwt" + log "github.com/sirupsen/logrus" ) type tokenRequest struct { From e10d2c88afbcd7f2424bcc9c010655c7d2404eb2 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:33:34 +0100 Subject: [PATCH 11/19] Added new secrets to deployment --- .../templates/deployment.yaml | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/charts/uppmax-integration/templates/deployment.yaml b/charts/uppmax-integration/templates/deployment.yaml index afd2476..9cb4325 100644 --- a/charts/uppmax-integration/templates/deployment.yaml +++ b/charts/uppmax-integration/templates/deployment.yaml @@ -39,11 +39,21 @@ spec: env: - name: GLOBAL_CRYPT4GHKEY value: /secrets/{{ .Values.global.crypt4ghKey }} - - name: GLOBAL_EGAUSER + - name: GLOBAL_EGAUSERNAME valueFrom: secretKeyRef: name: {{ include "uppmax-integration.name" . }}-secret - key: egaUser + key: egaUsername + - name: GLOBAL_EGAPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "uppmax-integration.name" . }}-secret + key: egaPassword + - name: GLOBAL_EGAURL + valueFrom: + secretKeyRef: + name: {{ include "uppmax-integration.name" . }}-secret + key: egaURL - name: GLOBAL_EXPIRATIONDAYS value: {{ .Values.global.expirationDays | quote }} - name: GLOBAL_ISS @@ -62,6 +72,21 @@ spec: secretKeyRef: name: {{ include "uppmax-integration.name" . }}-secret key: uppmaxPassword + - name: GLOBAL_SUPRUSERNAME + valueFrom: + secretKeyRef: + name: {{ include "uppmax-integration.name" . }}-secret + key: suprUsername + - name: GLOBAL_SUPRPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "uppmax-integration.name" . }}-secret + key: suprPassword + - name: GLOBAL_SUPRURL + valueFrom: + secretKeyRef: + name: {{ include "uppmax-integration.name" . }}-secret + key: suprURL securityContext: allowPrivilegeEscalation: false volumeMounts: From 01a9cf0bf31709742b16ed1925b2266761eac2c0 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:34:14 +0100 Subject: [PATCH 12/19] Added new secrets to secrets file --- charts/uppmax-integration/templates/secrets.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/charts/uppmax-integration/templates/secrets.yaml b/charts/uppmax-integration/templates/secrets.yaml index 3cdd476..fda34f2 100644 --- a/charts/uppmax-integration/templates/secrets.yaml +++ b/charts/uppmax-integration/templates/secrets.yaml @@ -6,6 +6,11 @@ type: Opaque stringData: uppmaxUsername: {{ .Values.global.uppmaxUsername | quote }} uppmaxPassword: {{ .Values.global.uppmaxPassword | quote }} - egaUser: {{ .Values.global.egaUser | quote }} + egaUsername: {{ .Values.global.egaUsername | quote }} + egaPassword: {{ .Values.global.egaPassword | quote }} + egaURL: {{ .Values.global.egaURL | quote }} + suprUsername: {{ .Values.global.suprUsername | quote }} + suprPassword: {{ .Values.global.suprPassword | quote }} + suprURL: {{ .Values.global.suprURL | quote }} crypt4ghKey: {{ .Values.global.crypt4ghKey }} \ No newline at end of file From 484cd80c02c63a9cf8930619c43b2492ac51053d Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:34:40 +0100 Subject: [PATCH 13/19] Update values file with new secrets --- charts/uppmax-integration/values.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/charts/uppmax-integration/values.yaml b/charts/uppmax-integration/values.yaml index a04162d..6d07171 100644 --- a/charts/uppmax-integration/values.yaml +++ b/charts/uppmax-integration/values.yaml @@ -11,7 +11,12 @@ global: uppmaxPassword: "" s3url: "" expirationDays: "25" - egaUser: "" + egaUsername: "" + egaPassword: "" + egaURL: "" + suprUsername: "" + suprPassword: "" + suprURL: "" crypt4ghKey: "" tls: enabled: false @@ -26,7 +31,7 @@ image: repository: harbor.nbis.se/uppmax/integration pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. - tag: "latest" + tag: "0.1.1" imagePullSecrets: [] nameOverride: "" From 8e84f28b38e3e91f3601e9e99d47ce82e0dae403 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 14:35:00 +0100 Subject: [PATCH 14/19] Update charts version --- charts/uppmax-integration/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/uppmax-integration/Chart.yaml b/charts/uppmax-integration/Chart.yaml index f6f9c90..32c0429 100644 --- a/charts/uppmax-integration/Chart.yaml +++ b/charts/uppmax-integration/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.1 +version: 0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From 7d14bbc066e7917032d5b738452b40cc4c010e4e Mon Sep 17 00:00:00 2001 From: Martin Norling Date: Fri, 3 Feb 2023 14:39:06 +0100 Subject: [PATCH 15/19] sanitize tokenRequest values --- token/token.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/token/token.go b/token/token.go index 65e429c..9bef7f0 100644 --- a/token/token.go +++ b/token/token.go @@ -113,6 +113,14 @@ func createResponse(tokenRequest tokenRequest, username string) (tokenResponse t func GetToken(w http.ResponseWriter, r *http.Request) { tokenRequest, err := readRequestBody(r.Body) + + // sanitize inputs just in case (and to make CodeQL happy) + swamID := strings.ReplaceAll(tokenRequest.SwamID, "\n", "") + swamID = strings.ReplaceAll(swamID, "\r", "") + + projectID := strings.ReplaceAll(tokenRequest.ProjectID, "\n", "") + projectID = strings.ReplaceAll(projectID, "\r", "") + w.Header().Set("Content-Type", "application/json; charset=UTF-8") if err != nil { currentError := helpers.CreateErrorResponse("Error reading request body - " + err.Error()) @@ -124,9 +132,9 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } // Check specified swam_id against project_id - err = verifyEGABoxAccount(tokenRequest.SwamID) + err = verifyEGABoxAccount(swamID) if err != nil { - log.Infof("%v is not a valid ega account", tokenRequest.SwamID) + log.Infof("%v is not a valid ega account", swamID) currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, string(currentError)) @@ -134,22 +142,22 @@ func GetToken(w http.ResponseWriter, r *http.Request) { return } - log.Infof("%v is verified as existing ega account", tokenRequest.SwamID) + log.Infof("%v is verified as existing ega account", swamID) - err = verifyProjectAccount(tokenRequest.SwamID, tokenRequest.ProjectID) + err = verifyProjectAccount(swamID, projectID) if err != nil { - log.Infof("%v is not the PI of SUPR project %v", tokenRequest.SwamID, tokenRequest.ProjectID) + log.Infof("%v is not the PI of SUPR project %v", swamID, projectID) currentError := helpers.CreateErrorResponse("Unauthorized to access specified project") w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, string(currentError)) return } - log.Infof("%v verified as the PI of SUPR project %v", tokenRequest.SwamID, tokenRequest.ProjectID) + log.Infof("%v verified as the PI of SUPR project %v", swamID, projectID) // Create token for user corresponding to specified swam_id - resp, err := createResponse(tokenRequest, tokenRequest.SwamID) + resp, err := createResponse(tokenRequest, swamID) if err != nil { currentError := helpers.CreateErrorResponse("Unable to create token for specified project") w.WriteHeader(http.StatusInternalServerError) From 7262cb26298be8246e575f00c57f464b96af2114 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 17:16:09 +0100 Subject: [PATCH 16/19] Update secrets grouping --- charts/uppmax-integration/templates/secrets.yaml | 12 ++++++------ charts/uppmax-integration/values.yaml | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/charts/uppmax-integration/templates/secrets.yaml b/charts/uppmax-integration/templates/secrets.yaml index fda34f2..d7f3d97 100644 --- a/charts/uppmax-integration/templates/secrets.yaml +++ b/charts/uppmax-integration/templates/secrets.yaml @@ -6,11 +6,11 @@ type: Opaque stringData: uppmaxUsername: {{ .Values.global.uppmaxUsername | quote }} uppmaxPassword: {{ .Values.global.uppmaxPassword | quote }} - egaUsername: {{ .Values.global.egaUsername | quote }} - egaPassword: {{ .Values.global.egaPassword | quote }} - egaURL: {{ .Values.global.egaURL | quote }} - suprUsername: {{ .Values.global.suprUsername | quote }} - suprPassword: {{ .Values.global.suprPassword | quote }} - suprURL: {{ .Values.global.suprURL | quote }} + egaUsername: {{ .Values.global.ega.username | quote }} + egaPassword: {{ .Values.global.ega.password | quote }} + egaURL: {{ .Values.global.ega.URL | quote }} + suprUsername: {{ .Values.global.supr.username | quote }} + suprPassword: {{ .Values.global.supr.password | quote }} + suprURL: {{ .Values.global.supr.URL | quote }} crypt4ghKey: {{ .Values.global.crypt4ghKey }} \ No newline at end of file diff --git a/charts/uppmax-integration/values.yaml b/charts/uppmax-integration/values.yaml index 6d07171..6495021 100644 --- a/charts/uppmax-integration/values.yaml +++ b/charts/uppmax-integration/values.yaml @@ -11,12 +11,14 @@ global: uppmaxPassword: "" s3url: "" expirationDays: "25" - egaUsername: "" - egaPassword: "" - egaURL: "" - suprUsername: "" - suprPassword: "" - suprURL: "" + ega: + username: "" + password: "" + URL: "" + supr: + username: "" + password: "" + URL: "" crypt4ghKey: "" tls: enabled: false From a63257118a2bdea3c438c215cad9df47581f6250 Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Fri, 3 Feb 2023 17:16:58 +0100 Subject: [PATCH 17/19] Return unamed variables on functions --- helpers/helpers.go | 10 ++++++---- token/ega.go | 2 +- token/supr.go | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/helpers/helpers.go b/helpers/helpers.go index 0d2ef4c..dfcb735 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -43,7 +43,7 @@ type Conf struct { } // NewConf reads the configuration from the config.yaml file -func NewConf(conf *Conf) (err error) { +func NewConf(conf *Conf) error { viper.SetConfigName("config") viper.AddConfigPath(".") viper.AutomaticEnv() @@ -114,10 +114,12 @@ func NewConf(conf *Conf) (err error) { } else { conf.ExpirationDays = viper.GetInt("global.expirationDays") } - conf.JwtParsedKey, err = parsePrivateECKey(conf.JwtKeyPath) + JwtParsedKey, err := parsePrivateECKey(conf.JwtKeyPath) if err != nil { return fmt.Errorf("could not parse ec key: %v", err) } + conf.JwtParsedKey = JwtParsedKey + // Parse crypt4gh key and store it as base64 encoded keyBytes, err := os.ReadFile(conf.Crypt4ghKeyPath) if err != nil { @@ -135,10 +137,10 @@ type errorStruct struct { } // CreateErrorResponse returns a JSON structure containing the error passed in the function -func CreateErrorResponse(errorMessage string) (errorBytes []byte) { +func CreateErrorResponse(errorMessage string) []byte { currentError := errorStruct{} currentError.ErrorStruct.Message = errorMessage - errorBytes, _ = json.Marshal(currentError) + errorBytes, _ := json.Marshal(currentError) return errorBytes } diff --git a/token/ega.go b/token/ega.go index e8e181d..bd77a5c 100644 --- a/token/ega.go +++ b/token/ega.go @@ -42,7 +42,7 @@ type EgaReply struct { // verifyEGABoxAccount checks that a given `username` is a valid EGA account, and // returns error if the user does not exist. -func verifyEGABoxAccount(username string) (err error) { +func verifyEGABoxAccount(username string) error { egaUser := helpers.Config.EgaUsername egaPass := helpers.Config.EgaPassword diff --git a/token/supr.go b/token/supr.go index 6f5da37..5d67e24 100644 --- a/token/supr.go +++ b/token/supr.go @@ -108,7 +108,7 @@ type Center struct { // verifyProjectAccount checks that the given `email` is actually // the PI of the given `project_id` and returns error otherwise -func verifyProjectAccount(username string, projectID string) (err error) { +func verifyProjectAccount(username string, projectID string) error { suprUser := helpers.Config.SuprUsername suprPass := helpers.Config.SuprPassword From 3d3b77edb158f28e5affebb4ed11f256e1dd43dd Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Tue, 7 Feb 2023 18:07:37 +0100 Subject: [PATCH 18/19] Add tests for ega and supr integration --- token/token_test.go | 155 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/token/token_test.go b/token/token_test.go index d657cbf..f239751 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -2,8 +2,11 @@ package token import ( b64 "encoding/base64" + "fmt" "io" "log" + "net/http" + "net/http/httptest" "os" "strings" "testing" @@ -59,6 +62,7 @@ func (suite *TestSuite) TestNewConf() { assert.NoError(suite.T(), err) assert.Equal(suite.T(), expectedToken, tokenRequest) + // Request body is not correct - missing closing bracket r = io.NopCloser(strings.NewReader(`{ "swami": "", "projectid": "" @@ -66,6 +70,7 @@ func (suite *TestSuite) TestNewConf() { _, err = readRequestBody(r) + // Request body is not correct - expected swamid instead of swami assert.EqualError(suite.T(), err, "unexpected EOF") r = io.NopCloser(strings.NewReader(`{ @@ -167,3 +172,153 @@ func (suite *TestSuite) TestCreateResponse() { defer os.Remove(configName) } + +// TestSuccessfulVerifications uses 2 mock servers for EGA and SUPR, which return StatusOK +// and sample responses from the two endpoints, containing the same user as in the configuration +func (suite *TestSuite) TestSuccessfulVerifications() { + ega := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "{ \"header\": { \"apiVersion\": \"v1\", \"code\": 200, \"service\": \"users\", \"developerMessage\": null, \"userMessage\": \"OK\", \"errorCode\": 0, \"docLink\": \"https://ega-archive.org\" }, \"response\": { \"numTotalResults\": 1, \"resultType\": \"LocalEgaUser\", \"result\": [ { \"username\": \"some.user@nbis.se\", \"sshPublicKey\": null, \"passwordHash\": \"somePasswordHash\", \"uid\": 1234, \"gecos\": null } ] }}") + })) + defer ega.Close() + + supr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "{\"matches\": [{\"id\": 1234, \"type\": \"Project\", \"name\": \"project-name\", \"title\": \"Test project\", \"directory_name\": \"\", \"directory_name_type\": \"\", \"ngi_project_name\": \"ngi-project-name\", \"abstract\": \"\", \"webpage\": \"\", \"affiliation\": \"Affiliate\", \"classification1\": \"\", \"classification2\": \"\", \"classification3\": \"\", \"managed_in_supr\": true, \"api_opaque_data\": \"\", \"ngi_sensitive_data\": true, \"ngi_ready\": false, \"ngi_delivery_status\": \"\", \"continuation_name\": \"\", \"start_date\": \"2022-09-19\", \"end_date\": \"2022-12-31\", \"pi\": {\"id\": 123, \"first_name\": \"Name\", \"last_name\": \"Lastname\", \"email\": \"some.user@nbis.se\"}, \"members\": [{\"id\": 175, \"first_name\": \"Name\", \"last_name\": \"Lastname\", \"email\": \"some.user@nbis.se\"}], \"links_outgoing\": [], \"links_incoming\": [], \"resourceprojects\": [{\"id\": 123, \"allocated\": 1000, \"resource\": {\"id\": 123, \"name\": \"Grus\", \"capacity_unit\": \"GiB\", \"capacity_unit_2\": \"\", \"centre\": {\"id\": 123, \"name\": \"UPPMAX\"}}, \"decommissioning_state\": \"N/A\", \"allocations\": [{\"id\": 123, \"start_date\": \"2022-09-19\", \"end_date\": \"2022-12-31\", \"allocated\": 1000}]}], \"modified\": \"2022-09-19 14:50:39\"}], \"began\": \"2023-02-06 13:04:31\"}") + })) + defer supr.Close() + + requestBody := &tokenRequest{ + ProjectID: "someproject", + SwamID: "some.user@nbis.se", + } + + confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "` + ega.URL + `" + expirationDays: 14 + iss: "https://some.url" + jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "` + supr.URL + `" + s3url: "some.s3.url" + uppmaxUsername: "user" + uppmaxPassword: "password" +` + configName := "config.yaml" + err := os.WriteFile(configName, []byte(confData), 0600) + if err != nil { + log.Printf("failed to write temp config file, %v", err) + } + + err = helpers.NewConf(&helpers.Config) + assert.NoError(suite.T(), err) + + err = verifyEGABoxAccount(requestBody.SwamID) + assert.NoError(suite.T(), err) + + err = verifyProjectAccount(requestBody.SwamID, requestBody.ProjectID) + assert.NoError(suite.T(), err) + +} + +// TestFailedVerifications uses 2 mock servers for EGA and SUPR, which return StatusNotFound +func (suite *TestSuite) TestFailedVerifications() { + ega := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer ega.Close() + + supr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer supr.Close() + + requestBody := &tokenRequest{ + ProjectID: "someproject", + SwamID: "some.user@nbis.se", + } + + confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "` + ega.URL + `" + expirationDays: 14 + iss: "https://some.url" + jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "` + supr.URL + `" + s3url: "some.s3.url" + uppmaxUsername: "user" + uppmaxPassword: "password" +` + configName := "config.yaml" + err := os.WriteFile(configName, []byte(confData), 0600) + if err != nil { + log.Printf("failed to write temp config file, %v", err) + } + + err = helpers.NewConf(&helpers.Config) + assert.NoError(suite.T(), err) + + err = verifyEGABoxAccount(requestBody.SwamID) + log.Print(err) + assert.Equal(suite.T(), fmt.Errorf("got [] from EGA"), err) + + err = verifyProjectAccount(requestBody.SwamID, requestBody.ProjectID) + assert.Equal(suite.T(), fmt.Errorf("got [] from SUPR"), err) +} + +// TestWrongSuprUser tests the case where SUPR returns a project with a user +// different than the one the request was made for +func (suite *TestSuite) TestWrongSuprUser() { + ega := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "{ \"header\": { \"apiVersion\": \"v1\", \"code\": 200, \"service\": \"users\", \"developerMessage\": null, \"userMessage\": \"OK\", \"errorCode\": 0, \"docLink\": \"https://ega-archive.org\" }, \"response\": { \"numTotalResults\": 1, \"resultType\": \"LocalEgaUser\", \"result\": [ { \"username\": \"some.user@nbis.se\", \"sshPublicKey\": null, \"passwordHash\": \"somePasswordHash\", \"uid\": 1234, \"gecos\": null } ] }}") + })) + defer ega.Close() + + supr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "{\"matches\": [{\"id\": 1234, \"type\": \"Project\", \"name\": \"project-name\", \"title\": \"Test project\", \"directory_name\": \"\", \"directory_name_type\": \"\", \"ngi_project_name\": \"ngi-project-name\", \"abstract\": \"\", \"webpage\": \"\", \"affiliation\": \"Affiliate\", \"classification1\": \"\", \"classification2\": \"\", \"classification3\": \"\", \"managed_in_supr\": true, \"api_opaque_data\": \"\", \"ngi_sensitive_data\": true, \"ngi_ready\": false, \"ngi_delivery_status\": \"\", \"continuation_name\": \"\", \"start_date\": \"2022-09-19\", \"end_date\": \"2022-12-31\", \"pi\": {\"id\": 123, \"first_name\": \"Name\", \"last_name\": \"Lastname\", \"email\": \"some.other.user@nbis.se\"}, \"members\": [{\"id\": 175, \"first_name\": \"Name\", \"last_name\": \"Lastname\", \"email\": \"some.user@nbis.se\"}], \"links_outgoing\": [], \"links_incoming\": [], \"resourceprojects\": [{\"id\": 123, \"allocated\": 1000, \"resource\": {\"id\": 123, \"name\": \"Grus\", \"capacity_unit\": \"GiB\", \"capacity_unit_2\": \"\", \"centre\": {\"id\": 123, \"name\": \"UPPMAX\"}}, \"decommissioning_state\": \"N/A\", \"allocations\": [{\"id\": 123, \"start_date\": \"2022-09-19\", \"end_date\": \"2022-12-31\", \"allocated\": 1000}]}], \"modified\": \"2022-09-19 14:50:39\"}], \"began\": \"2023-02-06 13:04:31\"}") + })) + defer supr.Close() + + requestBody := &tokenRequest{ + ProjectID: "someproject", + SwamID: "some.user@nbis.se", + } + + confData := `global: + crypt4ghKey: ` + suite.Crypt4ghKeyPath + ` + egaUsername: "some-user" + egaPassword: "some-pass" + egaURL: "` + ega.URL + `" + expirationDays: 14 + iss: "https://some.url" + jwtKey: "` + suite.PrivateKeyPath + `" + suprUsername: "some-user" + suprPassword: "some-pass" + suprURL: "` + supr.URL + `" + s3url: "some.s3.url" + uppmaxUsername: "user" + uppmaxPassword: "password" +` + configName := "config.yaml" + err := os.WriteFile(configName, []byte(confData), 0600) + if err != nil { + log.Printf("failed to write temp config file, %v", err) + } + + err = helpers.NewConf(&helpers.Config) + assert.NoError(suite.T(), err) + + err = verifyProjectAccount(requestBody.SwamID, requestBody.ProjectID) + assert.Equal(suite.T(), fmt.Errorf("email is different than PI in requested project"), err) + +} From 25e8add55846e2a8abbaf35af7deb3605626269e Mon Sep 17 00:00:00 2001 From: dbampalikis Date: Tue, 7 Feb 2023 18:14:46 +0100 Subject: [PATCH 19/19] Update README to reflect changes for external verification --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5f6a304..f68e80f 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,20 @@ The following configuration is required to run the service | Variable | Description | Example | | ------------ | :----------: | ------: | | crypt4ghKey | Path to public key | `../sda_crypt4gh.pub` | +| egaUsername | The username for the EGA external service | `some_ega_username` | +| egaPassword | The password for the EGA external service | `some_ega_password` | +| egaURL | The url for the EGA external service | `https://ega.url` | | expirationDays | Token validity duration in days | 14 | | iss | JWT issuer | `https://issuer.example.com` | | jwtKey | Path to private key | `../my_key.pub` | +| suprUsername | The username for the SUPR external service | `some_supr_username` | +| suprPassword | The password for the SUPR external service | `some_supr_password` | +| suprURL | The url for the SUPR external service | `https://supr.url` | | s3url | The URL to the s3Inbox | `s3.example.com` | | uppmaxUsername | Username for token requester | `some_username` | | uppmaxPassword | Password for token requester | `some_password` | + ## How to deploy To deploy the service without using vault (e.g. using minikube) in the `lega` namespace, build and push the image using ```sh