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 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 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: diff --git a/charts/uppmax-integration/templates/secrets.yaml b/charts/uppmax-integration/templates/secrets.yaml index 3cdd476..d7f3d97 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.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 a04162d..6495021 100644 --- a/charts/uppmax-integration/values.yaml +++ b/charts/uppmax-integration/values.yaml @@ -11,7 +11,14 @@ global: uppmaxPassword: "" s3url: "" expirationDays: "25" - egaUser: "" + ega: + username: "" + password: "" + URL: "" + supr: + username: "" + password: "" + URL: "" crypt4ghKey: "" tls: enabled: false @@ -26,7 +33,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: "" 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..caad7fb 100644 --- a/config.yaml +++ b/config.yaml @@ -1,9 +1,14 @@ global: crypt4ghKey: "" - egaUser: "" + egaUsername: "" + egaPassword: "" + egaURL: "" expirationDays: 14 iss: "" jwtKey: "" + suprUsername: "" + suprPassword: "" + suprURL: "" s3url: "" uppmaxUsername: "" uppmaxPassword: "" diff --git a/helpers/helpers.go b/helpers/helpers.go index 46c260c..dfcb735 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -27,7 +27,9 @@ var Config Conf type Conf struct { Crypt4ghKeyPath string Crypt4ghKey string - EgaUser string + EgaUsername string + EgaPassword string + EgaURL string ExpirationDays int Iss string JwtKeyPath string @@ -35,10 +37,13 @@ type Conf struct { S3URL string Username string Password string + SuprUsername string + SuprPassword string + SuprURL string } // 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() @@ -63,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 { @@ -95,18 +101,25 @@ 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.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 } 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 { @@ -124,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/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/ega.go b/token/ega.go new file mode 100644 index 0000000..bd77a5c --- /dev/null +++ b/token/ega.go @@ -0,0 +1,94 @@ +package token + +import ( + "encoding/json" + "fmt" + "io" + "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"` +} + +// verifyEGABoxAccount checks that a given `username` is a valid EGA account, and +// returns error if the user does not exist. +func verifyEGABoxAccount(username string) error { + + egaUser := helpers.Config.EgaUsername + 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 := io.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + return fmt.Errorf("got %v from EGA", message) + } + + var reply EgaReply + 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 +} diff --git a/token/supr.go b/token/supr.go new file mode 100644 index 0000000..5d67e24 --- /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) 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 +} diff --git a/token/token.go b/token/token.go index 0e14415..9bef7f0 100644 --- a/token/token.go +++ b/token/token.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "strings" "time" @@ -14,15 +13,16 @@ import ( "github.com/NBISweden/sda-uppmax-integration/helpers" "github.com/golang-jwt/jwt" + log "github.com/sirupsen/logrus" ) 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 +32,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 +67,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 +96,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,24 +108,19 @@ 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) { 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()) @@ -142,8 +132,9 @@ func GetToken(w http.ResponseWriter, r *http.Request) { } // Check specified swam_id against project_id - username, err := getEGABoxAccount(tokenRequest.ProjectID, tokenRequest.Swamid) + err = verifyEGABoxAccount(swamID) if err != nil { + 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)) @@ -151,9 +142,22 @@ func GetToken(w http.ResponseWriter, r *http.Request) { return } + log.Infof("%v is verified as existing ega account", swamID) + + err = verifyProjectAccount(swamID, projectID) + if err != nil { + + 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", swamID, projectID) // Create token for user corresponding to specified swam_id - resp, err := createResponse(tokenRequest, username) + resp, err := createResponse(tokenRequest, swamID) if err != nil { currentError := helpers.CreateErrorResponse("Unable to create token for specified project") w.WriteHeader(http.StatusInternalServerError) diff --git a/token/token_test.go b/token/token_test.go index cfafbc8..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" @@ -45,7 +48,7 @@ func (suite *TestSuite) SetupTest() { func (suite *TestSuite) TestNewConf() { expectedToken := tokenRequest{ - Swamid: "", + SwamID: "", ProjectID: "", } @@ -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,7 +70,8 @@ func (suite *TestSuite) TestNewConf() { _, err = readRequestBody(r) - assert.EqualError(suite.T(), err, "error unmarshaling data") + // Request body is not correct - expected swamid instead of swami + assert.EqualError(suite.T(), err, "unexpected EOF") r = io.NopCloser(strings.NewReader(`{ "swami": "", @@ -81,14 +86,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) @@ -99,7 +109,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 +119,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") @@ -126,18 +136,23 @@ func (suite *TestSuite) TestCreateResponse() { requestBody := &tokenRequest{ ProjectID: "someproject", - Swamid: "someswam", + SwamID: "someswam", } 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) @@ -153,7 +168,157 @@ 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) } + +// 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) + +}