From f1ad61e7a130aa5cc9a42e3b073c59814ae7dae0 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 5 Aug 2024 09:38:48 -0700 Subject: [PATCH 1/9] Adding machine-readable transfer task description and other metadata fields. --- frictionless/frictionless.go | 37 ++++++++++++++++++++++++ services/prototype.go | 9 ++++-- services/transfer_service.go | 3 ++ tasks/tasks.go | 55 +++++++++++++++++++++++++++--------- tasks/tasks_test.go | 23 +++++++++++---- 5 files changed, 107 insertions(+), 20 deletions(-) diff --git a/frictionless/frictionless.go b/frictionless/frictionless.go index 1d33718d..c53ff5d7 100644 --- a/frictionless/frictionless.go +++ b/frictionless/frictionless.go @@ -40,6 +40,27 @@ type DataPackage struct { Licenses []DataLicense `json:"licenses,omitempty"` // a list identifying the sources for this resource (optional) Sources []DataSource `json:"sources,omitempty"` + // a timestamp indicated when the package was created + Created string `json:"created,omitempty"` + // the profile of this descriptor per the DataPackage profiles specification + // (https://specs.frictionlessdata.io/profiles/#language) + Profile string `json:"profile,omitempty"` + // a title or one sentence description for the data package + Title string `json:"title,omitempty"` + // a Markdown description of the data package + Description string `json:"description,omitempty"` + // a URL for a web address related to the data package + Homepage string `json:"homepage,omitempty"` + // a version string identifying the version of the data package, conforming to + // semantic versioning if relevant + Version string `json:"version,omitempty"` + // an array of string keywords to assist users searching for the data package + // in catalogs + Keywords []string `json:"keywords,omitempty"` + // list of contributors to the data package + Contributors []Contributor `json:"contributors,omitempty"` + // an image to use for this data package (URL or POSIX path) + Image string `json:"image,omitempty"` } // a Frictionless data resource describing a file in a search @@ -107,3 +128,19 @@ type DataLicense struct { // the descriptive title of the license (optional) Title string `json:"title,omitempty"` } + +// information about a contributor to a DataPackage +type Contributor struct { + // name/title of the contributor (name for person, name/title of organization) + Title string `json:"title"` + // the contributor's email address + Email string `json:"email"` + // a fully qualified http URL pointing to a relevant location online for the + // contributor + Path string `json:"path"` + // the role of the contributor ("author", "publisher", "maintainer", + // "wrangler", "contributor") + Role string `json:"role"` + // a string describing the contributor's organization + Organization string `json:"organization"` +} diff --git a/services/prototype.go b/services/prototype.go index e9b9ec48..f0445faa 100644 --- a/services/prototype.go +++ b/services/prototype.go @@ -458,8 +458,13 @@ func (service *prototype) createTransfer(ctx context.Context, return nil, err } - taskId, err := tasks.Create(orcid, input.Body.Source, - input.Body.Destination, input.Body.FileIds) + taskId, err := tasks.Create(tasks.Specification{ + Orcid: orcid, + Source: input.Body.Source, + Destination: input.Body.Destination, + FileIds: input.Body.FileIds, + Description: input.Body.Description, + }) if err != nil { return nil, err } diff --git a/services/transfer_service.go b/services/transfer_service.go index eb142cbc..d32d4847 100644 --- a/services/transfer_service.go +++ b/services/transfer_service.go @@ -59,6 +59,9 @@ type TransferRequest struct { FileIds []string `json:"file_ids" example:"[\"fileid1\", \"fileid2\"]" doc:"source-specific identifiers for files to be transferred"` // name of destination database Destination string `json:"destination" example:"kbase" doc:"destination database identifier"` + // a Markdown description of the transfer request (can contain e.g. machine- + // readable instructions for processing a payload at the desination site) + Description string `json:"description" example:"# title\n* type: assembly\n" doc:"Markdown task description, possibly with machine-readable instructions for processing payload at destination"` } // a response for a file transfer request (POST) diff --git a/tasks/tasks.go b/tasks/tasks.go index 46485a00..51844f01 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -64,14 +64,33 @@ const ( TransferStatusSucceeded = endpoints.TransferStatusSucceeded ) +// this type holds a specification used to create a valid transfer task +type Specification struct { + // the ORCID for the user requesting the task + Orcid string + // the name of source database from which files are transferred (as specified + // in the DTS config file) + Source string + // the name of destination database from which files are transferred (as + // specified in the DTS config file) + Destination string + // an array of identifiers for files to be transferred from Source to + // Destination + FileIds []string + // a Markdown description of the transfer task (can contain machine-readable + // instructions for processing the payload at its destination) + Description string +} + // this type holds multiple (possibly null) UUIDs corresponding to different -// portions of a file transfer +// states in the file transfer lifecycle type taskType struct { Id uuid.UUID // task identifier DestinationFolder string // folder path to which files are transferred Orcid string // Orcid ID for user requesting transfer Source, Destination string // names of source and destination databases FileIds []string // IDs of files within Source + Description string // Markdown description of the task Resources []DataResource // Frictionless DataResources for files PayloadSize float64 // Size of payload (gigabytes) Canceled bool // set if a cancellation request has been made @@ -248,6 +267,19 @@ func (task *taskType) checkStaging() error { return nil } +// creates a DataPackage that serves as the transfer manifest +func (task *taskType) createManifest() frictionless.DataPackage { + manifest := DataPackage{ + Name: "manifest", + Resources: make([]DataResource, len(task.Resources)), + Created: time.Now().Format(time.RFC3339), + Profile: "data-package", + Keywords: []string{"dts", "manifest"}, + } + copy(manifest.Resources, task.Resources) + return manifest +} + // checks whether files for a task are finished transferring and, if so, // initiates the generation of the file manifest func (task *taskType) checkTransfer() error { @@ -273,11 +305,7 @@ func (task *taskType) checkTransfer() error { return err } // generate a manifest for the transfer - manifest := DataPackage{ - Name: "manifest", - Resources: make([]DataResource, len(task.Resources)), - } - copy(manifest.Resources, task.Resources) + manifest := task.createManifest() // write the manifest to disk and begin transferring it to the // destination endpoint @@ -751,26 +779,27 @@ func Running() bool { // ID to the manager's set, returning a UUID for the task. The task is defined // by specifying the names of the source and destination databases and a set of // file IDs associated with the source. -func Create(orcid, source, destination string, fileIDs []string) (uuid.UUID, error) { +func Create(spec Specification) (uuid.UUID, error) { var taskId uuid.UUID // verify that we can fetch the task's source and destination databases // without incident - _, err := databases.NewDatabase(orcid, source) + _, err := databases.NewDatabase(spec.Orcid, spec.Source) if err != nil { return taskId, err } - _, err = databases.NewDatabase(orcid, destination) + _, err = databases.NewDatabase(spec.Orcid, spec.Destination) if err != nil { return taskId, err } // create a new task and send it along for processing taskChannels.CreateTask <- taskType{ - Orcid: orcid, - Source: source, - Destination: destination, - FileIds: fileIDs, + Orcid: spec.Orcid, + Source: spec.Source, + Destination: spec.Destination, + FileIds: spec.FileIds, + Description: spec.Description, } select { case taskId = <-taskChannels.ReturnTaskId: diff --git a/tasks/tasks_test.go b/tasks/tasks_test.go index 1147facd..141e061a 100644 --- a/tasks/tasks_test.go +++ b/tasks/tasks_test.go @@ -179,7 +179,12 @@ func (t *SerialTests) TestCreateTask() { // queue up a transfer task between two phony databases orcid := "1234-5678-9012-3456" - taskId, err := Create(orcid, "test-source", "test-destination", []string{"file1", "file2"}) + taskId, err := Create(Specification{ + Orcid: orcid, + Source: "test-source", + Destination: "test-destination", + FileIds: []string{"file1", "file2"}, + }) assert.Nil(err) assert.True(taskId != uuid.UUID{}) @@ -234,8 +239,12 @@ func (t *SerialTests) TestCancelTask() { pollInterval := time.Duration(config.Service.PollInterval) * time.Millisecond // queue up a transfer task between two phony databases - orcid := "1234-5678-9012-3456" - taskId, err := Create(orcid, "test-source", "test-destination", []string{"file1", "file2"}) + taskId, err := Create(Specification{ + Orcid: "1234-5678-9012-3456", + Source: "test-source", + Destination: "test-destination", + FileIds: []string{"file1", "file2"}, + }) assert.Nil(err) assert.True(taskId != uuid.UUID{}) @@ -270,11 +279,15 @@ func (t *SerialTests) TestStopAndRestart() { // start up, add a bunch of tasks, then immediately close err := Start() assert.Nil(err) - orcid := "1234-5678-9012-3456" numTasks := 10 taskIds := make([]uuid.UUID, numTasks) for i := 0; i < numTasks; i++ { - taskId, _ := Create(orcid, "test-source", "test-destination", []string{"file1", "file2"}) + taskId, _ := Create(Specification{ + Orcid: "1234-5678-9012-3456", + Source: "test-source", + Destination: "test-destination", + FileIds: []string{"file1", "file2"}, + }) taskIds[i] = taskId } time.Sleep(100 * time.Millisecond) // let things settle From a4b0a575369ddd52ddee972356f3c18cc1fd9fc0 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 5 Aug 2024 10:08:23 -0700 Subject: [PATCH 2/9] Consolidated KBase Auth2 server user info. --- auth/kbase_auth_server.go | 50 ++++++++++++++++------------------ auth/kbase_auth_server_test.go | 22 ++++++--------- services/prototype.go | 15 ++++------ 3 files changed, 38 insertions(+), 49 deletions(-) diff --git a/auth/kbase_auth_server.go b/auth/kbase_auth_server.go index 969dcbd4..05b0a213 100644 --- a/auth/kbase_auth_server.go +++ b/auth/kbase_auth_server.go @@ -44,6 +44,22 @@ type KBaseAuthServer struct { AccessToken string } +// a record containing information about a user logged into the KBase Auth2 +// server +type KBaseUserInfo struct { + // KBase username + Username string `json:"user"` + // KBase user display name + DisplayName string `json:"display"` + // User email address + Email string `json:"email"` + // Identities with associated providers + Idents []struct { + Provider string `json:"provider"` + UserName string `json:"provusername"` + } `json:"idents"` +} + // here's how KBase represents errors in responses to API calls type kbaseAuthErrorResponse struct { HttpCode int `json:"httpcode"` @@ -55,15 +71,6 @@ type kbaseAuthErrorResponse struct { Time time.Duration `json:"time"` } -// here's what we use to fetch user information -type kbaseAuthUserInfo struct { - Username string `json:"user"` - Idents []struct { - Provider string `json:"provider"` - UserName string `json:"provusername"` - } `json:"idents"` -} - // here's a set of instances to the KBase auth server, mapped by OAuth2 // access token var instances map[string]*KBaseAuthServer @@ -87,7 +94,7 @@ func NewKBaseAuthServer(accessToken string) (*KBaseAuthServer, error) { } // verify that the access token works (i.e. that the user is logged in) - userInfo, err := server.getUserInfo() + userInfo, err := server.UserInfo() if err != nil { return nil, err } @@ -162,8 +169,9 @@ func (server *KBaseAuthServer) get(resource string) (*http.Response, error) { return client.Do(req) } -func (server *KBaseAuthServer) getUserInfo() (kbaseAuthUserInfo, error) { - var userInfo kbaseAuthUserInfo +// returns information for the current KBase user accessing the auth server +func (server *KBaseAuthServer) UserInfo() (KBaseUserInfo, error) { + var userInfo KBaseUserInfo resp, err := server.get("me") if err != nil { return userInfo, err @@ -183,20 +191,10 @@ func (server *KBaseAuthServer) getUserInfo() (kbaseAuthUserInfo, error) { return userInfo, err } -// returns the username for the current KBase user accessing the -// KBase auth server -func (server *KBaseAuthServer) Username() (string, error) { - userInfo, err := server.getUserInfo() - return userInfo.Username, err -} - -// returns the current KBase user's registered ORCID identifiers (and/or an error) -// a user can have 0, 1, or many associated ORCID identifiers -func (server *KBaseAuthServer) Orcids() ([]string, error) { - userInfo, err := server.getUserInfo() - if err != nil { - return nil, err - } +// returns the registered ORCID identifiers (and/or an error) within the given +// KBase user info record (a user can have 0, 1, or many associated ORCID +// identifiers) +func (userInfo *KBaseUserInfo) Orcids() ([]string, error) { if len(userInfo.Idents) < 1 { return nil, fmt.Errorf("No ORCID IDs associated with this user!") } diff --git a/auth/kbase_auth_server_test.go b/auth/kbase_auth_server_test.go index 2b3b50f7..3fd72383 100644 --- a/auth/kbase_auth_server_test.go +++ b/auth/kbase_auth_server_test.go @@ -44,27 +44,21 @@ func TestNewKBaseAuthServer(t *testing.T) { assert.Nil(err, "Authentication server constructor triggered an error") } -// tests whether the authentication server can return the username for the -// for the owner of the developer token -func TestUsername(t *testing.T) { +// tests whether the authentication server can return information for the +// user associated with the specified developer token +func TestUserInfo(t *testing.T) { assert := assert.New(t) devToken := os.Getenv("DTS_KBASE_DEV_TOKEN") server, _ := NewKBaseAuthServer(devToken) assert.NotNil(server) - username, err := server.Username() + userInfo, err := server.UserInfo() assert.Nil(err) - assert.True(len(username) > 0) -} -// tests whether the authentication server can return the proper credentials -// for the owner of the developer token -func TestOrchids(t *testing.T) { - assert := assert.New(t) - devToken := os.Getenv("DTS_KBASE_DEV_TOKEN") + assert.True(len(userInfo.Username) > 0) + assert.True(len(userInfo.Email) > 0) + orcid := os.Getenv("DTS_KBASE_TEST_ORCID") - assert.False(orcid == "") - server, _ := NewKBaseAuthServer(devToken) - orcids, err := server.Orcids() + orcids, err := userInfo.Orcids() assert.Nil(err) var foundOrcid bool for _, id := range orcids { diff --git a/services/prototype.go b/services/prototype.go index f0445faa..aa31b2a1 100644 --- a/services/prototype.go +++ b/services/prototype.go @@ -70,18 +70,15 @@ func authorize(authorizationHeader string) (string, error) { // check the access token against the KBase auth server // and fetch the first ORCID associated with it authServer, err := auth.NewKBaseAuthServer(accessToken) - var orcid string - var orcids []string - if err == nil { - orcids, err = authServer.Orcids() - if err == nil { - orcid = orcids[0] - } + if err != nil { + return "", huma.Error401Unauthorized(err.Error()) } + userInfo, err := authServer.UserInfo() + orcids, err := userInfo.Orcids() if err != nil { - return orcid, huma.Error401Unauthorized(err.Error()) + return "", huma.Error401Unauthorized(err.Error()) } - return orcid, nil + return orcids[0], nil // return the first ORCID encountered } type ServiceInfoOutput struct { From 2093c2aed71aad1520740cbf3239b9c851ea3352 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 5 Aug 2024 10:42:40 -0700 Subject: [PATCH 3/9] Reorganizing user data and propagating to task level for manifest generation. --- auth/kbase_auth_server.go | 48 +++++++++++++++++++++++----------- auth/kbase_auth_server_test.go | 13 +-------- services/prototype.go | 29 ++++++++++---------- tasks/tasks.go | 39 ++++++++++++++++----------- tasks/tasks_test.go | 17 +++++++++--- 5 files changed, 85 insertions(+), 61 deletions(-) diff --git a/auth/kbase_auth_server.go b/auth/kbase_auth_server.go index 05b0a213..ccf24a3c 100644 --- a/auth/kbase_auth_server.go +++ b/auth/kbase_auth_server.go @@ -46,11 +46,11 @@ type KBaseAuthServer struct { // a record containing information about a user logged into the KBase Auth2 // server -type KBaseUserInfo struct { +type kbaseUserInfo struct { // KBase username Username string `json:"user"` // KBase user display name - DisplayName string `json:"display"` + Display string `json:"display"` // User email address Email string `json:"email"` // Identities with associated providers @@ -94,7 +94,7 @@ func NewKBaseAuthServer(accessToken string) (*KBaseAuthServer, error) { } // verify that the access token works (i.e. that the user is logged in) - userInfo, err := server.UserInfo() + userInfo, err := server.kbaseUserInfo() if err != nil { return nil, err } @@ -170,8 +170,8 @@ func (server *KBaseAuthServer) get(resource string) (*http.Response, error) { } // returns information for the current KBase user accessing the auth server -func (server *KBaseAuthServer) UserInfo() (KBaseUserInfo, error) { - var userInfo KBaseUserInfo +func (server *KBaseAuthServer) kbaseUserInfo() (kbaseUserInfo, error) { + var userInfo kbaseUserInfo resp, err := server.get("me") if err != nil { return userInfo, err @@ -188,21 +188,39 @@ func (server *KBaseAuthServer) UserInfo() (KBaseUserInfo, error) { return userInfo, err } err = json.Unmarshal(body, &userInfo) - return userInfo, err -} -// returns the registered ORCID identifiers (and/or an error) within the given -// KBase user info record (a user can have 0, 1, or many associated ORCID -// identifiers) -func (userInfo *KBaseUserInfo) Orcids() ([]string, error) { + // make sure we have at least one ORCID for this user if len(userInfo.Idents) < 1 { - return nil, fmt.Errorf("No ORCID IDs associated with this user!") + return userInfo, fmt.Errorf("KBase Auth2: No providers associated with this user!") } - orcidIds := make([]string, 0) + foundOrcid := false for _, pid := range userInfo.Idents { if pid.Provider == "OrcID" { - orcidIds = append(orcidIds, pid.UserName) + foundOrcid = true + break + } + } + if !foundOrcid { + return userInfo, fmt.Errorf("KBase Auth2: No ORCID IDs associated with this user!") + } + return userInfo, err +} + +// returns a normalized user info record for the current KBase user +func (server *KBaseAuthServer) UserInfo() (UserInfo, error) { + kbUserInfo, err := server.kbaseUserInfo() + if err != nil { + return UserInfo{}, err + } + userInfo := UserInfo{ + Name: kbUserInfo.Display, + Username: kbUserInfo.Username, + Email: kbUserInfo.Email, + } + for _, pid := range kbUserInfo.Idents { + if pid.Provider == "OrcID" { + userInfo.Orcid = pid.UserName } } - return orcidIds, nil + return userInfo, nil } diff --git a/auth/kbase_auth_server_test.go b/auth/kbase_auth_server_test.go index 3fd72383..91f9f25f 100644 --- a/auth/kbase_auth_server_test.go +++ b/auth/kbase_auth_server_test.go @@ -56,16 +56,5 @@ func TestUserInfo(t *testing.T) { assert.True(len(userInfo.Username) > 0) assert.True(len(userInfo.Email) > 0) - - orcid := os.Getenv("DTS_KBASE_TEST_ORCID") - orcids, err := userInfo.Orcids() - assert.Nil(err) - var foundOrcid bool - for _, id := range orcids { - if orcid == id { - foundOrcid = true - break - } - } - assert.True(foundOrcid) + assert.Equal(os.Getenv("DTS_KBASE_TEST_ORCID"), userInfo.Orcid) } diff --git a/services/prototype.go b/services/prototype.go index aa31b2a1..189f0301 100644 --- a/services/prototype.go +++ b/services/prototype.go @@ -56,14 +56,14 @@ type prototype struct { // authorize clients for the DTS, returning the client's ORCID ID and an error // describing any issue encountered -func authorize(authorizationHeader string) (string, error) { +func authorize(authorizationHeader string) (auth.UserInfo, error) { if !strings.Contains(authorizationHeader, "Bearer") { - return "", fmt.Errorf("Invalid authorization header") + return auth.UserInfo{}, fmt.Errorf("Invalid authorization header") } b64Token := authorizationHeader[len("Bearer "):] accessTokenBytes, err := base64.StdEncoding.DecodeString(b64Token) if err != nil { - return "", huma.Error401Unauthorized(err.Error()) + return auth.UserInfo{}, huma.Error401Unauthorized(err.Error()) } accessToken := strings.TrimSpace(string(accessTokenBytes)) @@ -71,14 +71,13 @@ func authorize(authorizationHeader string) (string, error) { // and fetch the first ORCID associated with it authServer, err := auth.NewKBaseAuthServer(accessToken) if err != nil { - return "", huma.Error401Unauthorized(err.Error()) + return auth.UserInfo{}, huma.Error401Unauthorized(err.Error()) } userInfo, err := authServer.UserInfo() - orcids, err := userInfo.Orcids() if err != nil { - return "", huma.Error401Unauthorized(err.Error()) + return userInfo, huma.Error401Unauthorized(err.Error()) } - return orcids[0], nil // return the first ORCID encountered + return userInfo, nil // return the first ORCID encountered } type ServiceInfoOutput struct { @@ -249,7 +248,7 @@ func (service *prototype) getDatabaseSearchParameters(ctx context.Context, Database string `path:"db" example:"jdp" doc:"the abbreviated name of a database"` }) (*SearchParametersOutput, error) { - orcid, err := authorize(input.Authorization) + userInfo, err := authorize(input.Authorization) if err != nil { return nil, err } @@ -259,7 +258,7 @@ func (service *prototype) getDatabaseSearchParameters(ctx context.Context, if !ok { return nil, fmt.Errorf("Database %s not found", input.Database) } - db, err := databases.NewDatabase(orcid, input.Database) + db, err := databases.NewDatabase(userInfo.Orcid, input.Database) if err != nil { return nil, err } @@ -294,7 +293,7 @@ func searchDatabase(ctx context.Context, input *SearchDatabaseInput, specific map[string]json.RawMessage) (*SearchResultsOutput, error) { - orcid, err := authorize(input.Authorization) + userInfo, err := authorize(input.Authorization) if err != nil { return nil, err } @@ -319,7 +318,7 @@ func searchDatabase(ctx context.Context, } slog.Info(fmt.Sprintf("Searching database %s for files...", input.Database)) - db, err := databases.NewDatabase(orcid, input.Database) + db, err := databases.NewDatabase(userInfo.Orcid, input.Database) if err != nil { return nil, err } @@ -400,7 +399,7 @@ func (service *prototype) fetchFileMetadata(ctx context.Context, Limit int `json:"limit" query:"limit" example:"50" doc:"Limits the number of metadata records returned"` }) (*FileMetadataOutput, error) { - orcid, err := authorize(input.Authorization) + userInfo, err := authorize(input.Authorization) if err != nil { return nil, err } @@ -419,7 +418,7 @@ func (service *prototype) fetchFileMetadata(ctx context.Context, slog.Info(fmt.Sprintf("Fetching file metadata for %d files in database %s...", len(ids), input.Database)) - db, err := databases.NewDatabase(orcid, input.Database) + db, err := databases.NewDatabase(userInfo.Orcid, input.Database) if err != nil { return nil, err } @@ -450,13 +449,13 @@ func (service *prototype) createTransfer(ctx context.Context, ContentType string `header:"Content-Type" doc:"Content-Type header (must be application/json)"` }) (*TransferOutput, error) { - orcid, err := authorize(input.Authorization) + userInfo, err := authorize(input.Authorization) if err != nil { return nil, err } taskId, err := tasks.Create(tasks.Specification{ - Orcid: orcid, + UserInfo: userInfo, Source: input.Body.Source, Destination: input.Body.Destination, FileIds: input.Body.FileIds, diff --git a/tasks/tasks.go b/tasks/tasks.go index 51844f01..f01f8f4a 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -35,6 +35,7 @@ import ( "github.com/google/uuid" + "github.com/kbase/dts/auth" "github.com/kbase/dts/config" "github.com/kbase/dts/databases" "github.com/kbase/dts/databases/jdp" @@ -66,8 +67,8 @@ const ( // this type holds a specification used to create a valid transfer task type Specification struct { - // the ORCID for the user requesting the task - Orcid string + // information about the user requesting the task + UserInfo auth.UserInfo // the name of source database from which files are transferred (as specified // in the DTS config file) Source string @@ -87,7 +88,7 @@ type Specification struct { type taskType struct { Id uuid.UUID // task identifier DestinationFolder string // folder path to which files are transferred - Orcid string // Orcid ID for user requesting transfer + UserInfo auth.UserInfo // info about user requesting transfer Source, Destination string // names of source and destination databases FileIds []string // IDs of files within Source Description string // Markdown description of the task @@ -122,7 +123,7 @@ func payloadSize(resources []DataResource) float64 { // starts a task going, initiating staging if needed func (task *taskType) start() error { - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } @@ -172,7 +173,7 @@ func (task *taskType) start() error { func (task *taskType) checkCancellation() error { if task.Transfer.Valid { // the task's status is the same as its transfer status - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } @@ -195,17 +196,17 @@ func (task *taskType) checkCancellation() error { // initiates a file transfer on a set of staged files func (task *taskType) beginTransfer() error { - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } - destination, err := databases.NewDatabase(task.Orcid, task.Destination) + destination, err := databases.NewDatabase(task.UserInfo.Orcid, task.Destination) if err != nil { return err } // construct the source/destination file paths - username, err := destination.LocalUser(task.Orcid) + username, err := destination.LocalUser(task.UserInfo.Orcid) if err != nil { return err } @@ -246,7 +247,7 @@ func (task *taskType) beginTransfer() error { // checks whether files for a task are finished staging and, if so, // initiates the transfer process func (task *taskType) checkStaging() error { - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } @@ -275,6 +276,14 @@ func (task *taskType) createManifest() frictionless.DataPackage { Created: time.Now().Format(time.RFC3339), Profile: "data-package", Keywords: []string{"dts", "manifest"}, + Contributors: []frictionless.Contributor{ + { + Title: task.UserInfo.Name, + Email: task.UserInfo.Email, + Role: "author", + Organization: task.UserInfo.Organization, + }, + }, } copy(manifest.Resources, task.Resources) return manifest @@ -284,7 +293,7 @@ func (task *taskType) createManifest() frictionless.DataPackage { // initiates the generation of the file manifest func (task *taskType) checkTransfer() error { // has the data transfer completed? - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } @@ -331,7 +340,7 @@ func (task *taskType) checkTransfer() error { } // construct the source/destination file manifest paths - destination, err := databases.NewDatabase(task.Orcid, task.Destination) + destination, err := databases.NewDatabase(task.UserInfo.Orcid, task.Destination) if err != nil { return err } @@ -414,7 +423,7 @@ func (task *taskType) Cancel() error { if task.Transfer.Valid { // we're transferring // fetch the source endpoint var endpoint endpoints.Endpoint - source, err := databases.NewDatabase(task.Orcid, task.Source) + source, err := databases.NewDatabase(task.UserInfo.Orcid, task.Source) if err != nil { return err } @@ -784,18 +793,18 @@ func Create(spec Specification) (uuid.UUID, error) { // verify that we can fetch the task's source and destination databases // without incident - _, err := databases.NewDatabase(spec.Orcid, spec.Source) + _, err := databases.NewDatabase(spec.UserInfo.Orcid, spec.Source) if err != nil { return taskId, err } - _, err = databases.NewDatabase(spec.Orcid, spec.Destination) + _, err = databases.NewDatabase(spec.UserInfo.Orcid, spec.Destination) if err != nil { return taskId, err } // create a new task and send it along for processing taskChannels.CreateTask <- taskType{ - Orcid: spec.Orcid, + UserInfo: spec.UserInfo, Source: spec.Source, Destination: spec.Destination, FileIds: spec.FileIds, diff --git a/tasks/tasks_test.go b/tasks/tasks_test.go index 141e061a..129b4e88 100644 --- a/tasks/tasks_test.go +++ b/tasks/tasks_test.go @@ -34,6 +34,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/kbase/dts/auth" "github.com/kbase/dts/config" "github.com/kbase/dts/dtstest" ) @@ -178,9 +179,11 @@ func (t *SerialTests) TestCreateTask() { deleteAfter := time.Duration(config.Service.DeleteAfter) * time.Second // queue up a transfer task between two phony databases - orcid := "1234-5678-9012-3456" taskId, err := Create(Specification{ - Orcid: orcid, + UserInfo: auth.UserInfo{ + Name: "Joe-bob", + Orcid: "1234-5678-9012-3456", + }, Source: "test-source", Destination: "test-destination", FileIds: []string{"file1", "file2"}, @@ -240,7 +243,10 @@ func (t *SerialTests) TestCancelTask() { // queue up a transfer task between two phony databases taskId, err := Create(Specification{ - Orcid: "1234-5678-9012-3456", + UserInfo: auth.UserInfo{ + Name: "Joe-bob", + Orcid: "1234-5678-9012-3456", + }, Source: "test-source", Destination: "test-destination", FileIds: []string{"file1", "file2"}, @@ -283,7 +289,10 @@ func (t *SerialTests) TestStopAndRestart() { taskIds := make([]uuid.UUID, numTasks) for i := 0; i < numTasks; i++ { taskId, _ := Create(Specification{ - Orcid: "1234-5678-9012-3456", + UserInfo: auth.UserInfo{ + Name: "Joe-bob", + Orcid: "1234-5678-9012-3456", + }, Source: "test-source", Destination: "test-destination", FileIds: []string{"file1", "file2"}, From ef2158c844d13a0bcd083c21067b0772d1b0d680 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 5 Aug 2024 10:47:50 -0700 Subject: [PATCH 4/9] Added an omitted source file. --- auth/user_info.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 auth/user_info.go diff --git a/auth/user_info.go b/auth/user_info.go new file mode 100644 index 00000000..96808cc9 --- /dev/null +++ b/auth/user_info.go @@ -0,0 +1,36 @@ +// Copyright (c) 2023 The KBase Project and its Contributors +// Copyright (c) 2023 Cohere Consulting, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package auth + +// a record containing information about a DTS user +type UserInfo struct { + // user's name (human-readable and display-friendly) + Name string + // username used to access DTS + Username string + // user's email address + Email string + // ORCID identifier associated with this user + Orcid string + // organization with which this user is affiliated + Organization string +} From 0bc9141636030c8e940f45f02b42095e1f9ff077 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Thu, 8 Aug 2024 08:46:26 -0700 Subject: [PATCH 5/9] Updated some out-of-date comments. --- services/prototype.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/prototype.go b/services/prototype.go index 189f0301..cd1d485b 100644 --- a/services/prototype.go +++ b/services/prototype.go @@ -54,8 +54,9 @@ type prototype struct { Server *http.Server } -// authorize clients for the DTS, returning the client's ORCID ID and an error -// describing any issue encountered +// authorize clients for the DTS, returning information about the user +// corresponding to the token in the header (or an error describing any issue +// encountered) func authorize(authorizationHeader string) (auth.UserInfo, error) { if !strings.Contains(authorizationHeader, "Bearer") { return auth.UserInfo{}, fmt.Errorf("Invalid authorization header") @@ -68,7 +69,7 @@ func authorize(authorizationHeader string) (auth.UserInfo, error) { accessToken := strings.TrimSpace(string(accessTokenBytes)) // check the access token against the KBase auth server - // and fetch the first ORCID associated with it + // and return info about the corresponding user authServer, err := auth.NewKBaseAuthServer(accessToken) if err != nil { return auth.UserInfo{}, huma.Error401Unauthorized(err.Error()) @@ -77,7 +78,7 @@ func authorize(authorizationHeader string) (auth.UserInfo, error) { if err != nil { return userInfo, huma.Error401Unauthorized(err.Error()) } - return userInfo, nil // return the first ORCID encountered + return userInfo, nil } type ServiceInfoOutput struct { From 8412c853d537c1f3befd63b4f5269650f83dbac6 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 12 Aug 2024 08:51:55 -0700 Subject: [PATCH 6/9] Added JSON-sensible instructions field for machine-readable instructions. --- frictionless/frictionless.go | 2 ++ services/prototype.go | 11 +++++---- services/transfer_service.go | 8 +++--- tasks/tasks.go | 48 ++++++++++++++++++++---------------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/frictionless/frictionless.go b/frictionless/frictionless.go index c53ff5d7..f234cc12 100644 --- a/frictionless/frictionless.go +++ b/frictionless/frictionless.go @@ -61,6 +61,8 @@ type DataPackage struct { Contributors []Contributor `json:"contributors,omitempty"` // an image to use for this data package (URL or POSIX path) Image string `json:"image,omitempty"` + // a machine-readable set of instructions for processing + Instructions json.RawMessage `json:"instructions,omitempty"` } // a Frictionless data resource describing a file in a search diff --git a/services/prototype.go b/services/prototype.go index cd1d485b..24ba0bca 100644 --- a/services/prototype.go +++ b/services/prototype.go @@ -456,11 +456,12 @@ func (service *prototype) createTransfer(ctx context.Context, } taskId, err := tasks.Create(tasks.Specification{ - UserInfo: userInfo, - Source: input.Body.Source, - Destination: input.Body.Destination, - FileIds: input.Body.FileIds, - Description: input.Body.Description, + UserInfo: userInfo, + Source: input.Body.Source, + Destination: input.Body.Destination, + FileIds: input.Body.FileIds, + Description: input.Body.Description, + Instructions: input.Body.Instructions, }) if err != nil { return nil, err diff --git a/services/transfer_service.go b/services/transfer_service.go index d32d4847..c123c994 100644 --- a/services/transfer_service.go +++ b/services/transfer_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "encoding/json" "github.com/google/uuid" @@ -59,9 +60,10 @@ type TransferRequest struct { FileIds []string `json:"file_ids" example:"[\"fileid1\", \"fileid2\"]" doc:"source-specific identifiers for files to be transferred"` // name of destination database Destination string `json:"destination" example:"kbase" doc:"destination database identifier"` - // a Markdown description of the transfer request (can contain e.g. machine- - // readable instructions for processing a payload at the desination site) - Description string `json:"description" example:"# title\n* type: assembly\n" doc:"Markdown task description, possibly with machine-readable instructions for processing payload at destination"` + // a Markdown description of the transfer request + Description string `json:"description" example:"# title\n* type: assembly\n" doc:"Markdown task description"` + // machine-readable instructions for processing a payload at the destination site + Instructions json.RawMessage `json:"instructions" doc:"JSON object containing machine-readable instructions for processing payload at destination"` } // a response for a file transfer request (POST) diff --git a/tasks/tasks.go b/tasks/tasks.go index f01f8f4a..208734c6 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -78,28 +78,30 @@ type Specification struct { // an array of identifiers for files to be transferred from Source to // Destination FileIds []string - // a Markdown description of the transfer task (can contain machine-readable - // instructions for processing the payload at its destination) + // a Markdown description of the transfer task Description string + // machine-readable instructions for processing the payload at its destination + Instructions json.RawMessage } // this type holds multiple (possibly null) UUIDs corresponding to different // states in the file transfer lifecycle type taskType struct { - Id uuid.UUID // task identifier - DestinationFolder string // folder path to which files are transferred - UserInfo auth.UserInfo // info about user requesting transfer - Source, Destination string // names of source and destination databases - FileIds []string // IDs of files within Source - Description string // Markdown description of the task - Resources []DataResource // Frictionless DataResources for files - PayloadSize float64 // Size of payload (gigabytes) - Canceled bool // set if a cancellation request has been made - Staging, Transfer uuid.NullUUID // staging and file transfer UUIDs (if any) - Manifest uuid.NullUUID // manifest generation UUID (if any) - ManifestFile string // name of locally-created manifest file - Status TransferStatus // status of file transfer operation - CompletionTime time.Time // time at which the transfer completed + Id uuid.UUID // task identifier + DestinationFolder string // folder path to which files are transferred + UserInfo auth.UserInfo // info about user requesting transfer + Source, Destination string // names of source and destination databases + FileIds []string // IDs of files within Source + Description string // Markdown description of the task + Instructions json.RawMessage // machine-readable task processing instructions + Resources []DataResource // Frictionless DataResources for files + PayloadSize float64 // Size of payload (gigabytes) + Canceled bool // set if a cancellation request has been made + Staging, Transfer uuid.NullUUID // staging and file transfer UUIDs (if any) + Manifest uuid.NullUUID // manifest generation UUID (if any) + ManifestFile string // name of locally-created manifest file + Status TransferStatus // status of file transfer operation + CompletionTime time.Time // time at which the transfer completed } // This error type is returned when a payload is requested that is too large. @@ -284,8 +286,11 @@ func (task *taskType) createManifest() frictionless.DataPackage { Organization: task.UserInfo.Organization, }, }, + Description: task.Description, + Instructions: make(json.RawMessage, len(task.Instructions)), } copy(manifest.Resources, task.Resources) + copy(manifest.Instructions, task.Instructions) return manifest } @@ -804,11 +809,12 @@ func Create(spec Specification) (uuid.UUID, error) { // create a new task and send it along for processing taskChannels.CreateTask <- taskType{ - UserInfo: spec.UserInfo, - Source: spec.Source, - Destination: spec.Destination, - FileIds: spec.FileIds, - Description: spec.Description, + UserInfo: spec.UserInfo, + Source: spec.Source, + Destination: spec.Destination, + FileIds: spec.FileIds, + Description: spec.Description, + Instructions: spec.Instructions, } select { case taskId = <-taskChannels.ReturnTaskId: From e1388d7d436392a143d7b468a20a69f370707324 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Mon, 12 Aug 2024 10:05:17 -0700 Subject: [PATCH 7/9] Made description and instructions fields optional. --- services/transfer_service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/transfer_service.go b/services/transfer_service.go index c123c994..e12ca333 100644 --- a/services/transfer_service.go +++ b/services/transfer_service.go @@ -61,9 +61,9 @@ type TransferRequest struct { // name of destination database Destination string `json:"destination" example:"kbase" doc:"destination database identifier"` // a Markdown description of the transfer request - Description string `json:"description" example:"# title\n* type: assembly\n" doc:"Markdown task description"` + Description string `json:"description,omitempty" example:"# title\n* type: assembly\n" doc:"Markdown task description"` // machine-readable instructions for processing a payload at the destination site - Instructions json.RawMessage `json:"instructions" doc:"JSON object containing machine-readable instructions for processing payload at destination"` + Instructions json.RawMessage `json:"instructions,omitempty" doc:"JSON object containing machine-readable instructions for processing payload at destination"` } // a response for a file transfer request (POST) From 80e602d5101da436d5814a0e09f8effdd61fd622 Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Tue, 13 Aug 2024 08:41:06 -0700 Subject: [PATCH 8/9] Alphabetized some struct fields. --- frictionless/frictionless.go | 98 ++++++++++++++++++------------------ tasks/tasks.go | 50 +++++++++--------- 2 files changed, 75 insertions(+), 73 deletions(-) diff --git a/frictionless/frictionless.go b/frictionless/frictionless.go index f234cc12..90300e99 100644 --- a/frictionless/frictionless.go +++ b/frictionless/frictionless.go @@ -31,73 +31,73 @@ import ( // a Frictionless data package describing a set of related resources // (https://specs.frictionlessdata.io/data-package/) type DataPackage struct { - // the name of the data package - Name string `json:"name"` - // a list of resources that belong to the package - Resources []DataResource `json:"resources"` + // list of contributors to the data package + Contributors []Contributor `json:"contributors,omitempty"` + // a timestamp indicated when the package was created + Created string `json:"created,omitempty"` + // a Markdown description of the data package + Description string `json:"description,omitempty"` + // a URL for a web address related to the data package + Homepage string `json:"homepage,omitempty"` + // an image to use for this data package (URL or POSIX path) + Image string `json:"image,omitempty"` + // a machine-readable set of instructions for processing + Instructions json.RawMessage `json:"instructions,omitempty"` + // an array of string keywords to assist users searching for the data package + // in catalogs + Keywords []string `json:"keywords,omitempty"` // a list identifying the license or licenses under which this resource is // managed (optional) Licenses []DataLicense `json:"licenses,omitempty"` - // a list identifying the sources for this resource (optional) - Sources []DataSource `json:"sources,omitempty"` - // a timestamp indicated when the package was created - Created string `json:"created,omitempty"` + // the name of the data package + Name string `json:"name"` // the profile of this descriptor per the DataPackage profiles specification // (https://specs.frictionlessdata.io/profiles/#language) Profile string `json:"profile,omitempty"` + // a list of resources that belong to the package + Resources []DataResource `json:"resources"` + // a list identifying the sources for this resource (optional) + Sources []DataSource `json:"sources,omitempty"` // a title or one sentence description for the data package Title string `json:"title,omitempty"` - // a Markdown description of the data package - Description string `json:"description,omitempty"` - // a URL for a web address related to the data package - Homepage string `json:"homepage,omitempty"` // a version string identifying the version of the data package, conforming to // semantic versioning if relevant Version string `json:"version,omitempty"` - // an array of string keywords to assist users searching for the data package - // in catalogs - Keywords []string `json:"keywords,omitempty"` - // list of contributors to the data package - Contributors []Contributor `json:"contributors,omitempty"` - // an image to use for this data package (URL or POSIX path) - Image string `json:"image,omitempty"` - // a machine-readable set of instructions for processing - Instructions json.RawMessage `json:"instructions,omitempty"` } // a Frictionless data resource describing a file in a search // (https://specs.frictionlessdata.io/data-resource/) type DataResource struct { - // a unique identifier for the resource - Id string `json:"id"` - // the name of the resource's file, with any suffix stripped off - Name string `json:"name"` - // a relative path to the resource's file within a data package directory - Path string `json:"path"` - // a title or label for the resource (optional) - Title string `json:"title,omitempty"` + // the size of the resource's file in bytes + Bytes int `json:"bytes"` + // credit metadata associated with the resource (optional for now) + Credit credit.CreditMetadata `json:"credit,omitempty"` // a description of the resource (optional) Description string `json:"description,omitempty"` - // indicates the format of the resource's file, often used as an extension - Format string `json:"format"` - // the mediatype/mimetype of the resource (optional, e.g. "test/csv") - MediaType string `json:"media_type,omitempty"` // the character encoding for the resource's file (optional, default: UTF-8) Encoding string `json:"encoding,omitempty"` - // the size of the resource's file in bytes - Bytes int `json:"bytes"` + // any other fields requested e.g. by a search query (optional, raw JSON object) + Extra json.RawMessage `json:"extra,omitempty"` + // indicates the format of the resource's file, often used as an extension + Format string `json:"format"` // the hash for the resource's file (algorithms other than MD5 are indicated // with a prefix to the hash delimited by a colon) Hash string `json:"hash"` - // a list identifying the sources for this resource (optional) - Sources []DataSource `json:"sources,omitempty"` + // a unique identifier for the resource + Id string `json:"id"` // a list identifying the license or licenses under which this resource is // managed (optional) Licenses []DataLicense `json:"licenses,omitempty"` - // credit metadata associated with the resource (optional for now) - Credit credit.CreditMetadata `json:"credit,omitempty"` - // any other fields requested e.g. by a search query (optional, raw JSON object) - Extra json.RawMessage `json:"extra,omitempty"` + // the mediatype/mimetype of the resource (optional, e.g. "test/csv") + MediaType string `json:"media_type,omitempty"` + // the name of the resource's file, with any suffix stripped off + Name string `json:"name"` + // a relative path to the resource's file within a data package directory + Path string `json:"path"` + // a list identifying the sources for this resource (optional) + Sources []DataSource `json:"sources,omitempty"` + // a title or label for the resource (optional) + Title string `json:"title,omitempty"` } // call this to get a string containing the name of the hashing algorithm used @@ -113,12 +113,12 @@ func (res DataResource) HashAlgorithm() string { // information about the source of a DataResource type DataSource struct { - // a descriptive title for the source - Title string `json:"title"` - // a URI or relative path pointing to the source (optional) - Path string `json:"path,omitempty"` // an email address identifying a contact associated with the source (optional) Email string `json:"email,omitempty"` + // a URI or relative path pointing to the source (optional) + Path string `json:"path,omitempty"` + // a descriptive title for the source + Title string `json:"title"` } // information about a license associated with a DataResource @@ -133,16 +133,16 @@ type DataLicense struct { // information about a contributor to a DataPackage type Contributor struct { - // name/title of the contributor (name for person, name/title of organization) - Title string `json:"title"` // the contributor's email address Email string `json:"email"` + // a string describing the contributor's organization + Organization string `json:"organization"` // a fully qualified http URL pointing to a relevant location online for the // contributor Path string `json:"path"` // the role of the contributor ("author", "publisher", "maintainer", // "wrangler", "contributor") Role string `json:"role"` - // a string describing the contributor's organization - Organization string `json:"organization"` + // name/title of the contributor (name for person, name/title of organization) + Title string `json:"title"` } diff --git a/tasks/tasks.go b/tasks/tasks.go index 208734c6..e8ba5612 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -67,41 +67,43 @@ const ( // this type holds a specification used to create a valid transfer task type Specification struct { - // information about the user requesting the task - UserInfo auth.UserInfo - // the name of source database from which files are transferred (as specified - // in the DTS config file) - Source string + // a Markdown description of the transfer task + Description string // the name of destination database from which files are transferred (as // specified in the DTS config file) Destination string + // machine-readable instructions for processing the payload at its destination + Instructions json.RawMessage // an array of identifiers for files to be transferred from Source to // Destination FileIds []string - // a Markdown description of the transfer task - Description string - // machine-readable instructions for processing the payload at its destination - Instructions json.RawMessage + // the name of source database from which files are transferred (as specified + // in the DTS config file) + Source string + // information about the user requesting the task + UserInfo auth.UserInfo } // this type holds multiple (possibly null) UUIDs corresponding to different // states in the file transfer lifecycle type taskType struct { - Id uuid.UUID // task identifier - DestinationFolder string // folder path to which files are transferred - UserInfo auth.UserInfo // info about user requesting transfer - Source, Destination string // names of source and destination databases - FileIds []string // IDs of files within Source - Description string // Markdown description of the task - Instructions json.RawMessage // machine-readable task processing instructions - Resources []DataResource // Frictionless DataResources for files - PayloadSize float64 // Size of payload (gigabytes) - Canceled bool // set if a cancellation request has been made - Staging, Transfer uuid.NullUUID // staging and file transfer UUIDs (if any) - Manifest uuid.NullUUID // manifest generation UUID (if any) - ManifestFile string // name of locally-created manifest file - Status TransferStatus // status of file transfer operation - CompletionTime time.Time // time at which the transfer completed + Canceled bool // set if a cancellation request has been made + CompletionTime time.Time // time at which the transfer completed + Description string // Markdown description of the task + Destination string // name of destination database + DestinationFolder string // folder path to which files are transferred + FileIds []string // IDs of files within Source + Id uuid.UUID // task identifier + Instructions json.RawMessage // machine-readable task processing instructions + Manifest uuid.NullUUID // manifest generation UUID (if any) + ManifestFile string // name of locally-created manifest file + PayloadSize float64 // Size of payload (gigabytes) + Resources []DataResource // Frictionless DataResources for files + Source string // name of source database + Staging uuid.NullUUID // staging UUID (if any) + Status TransferStatus // status of file transfer operation + Transfer uuid.NullUUID // file transfer UUID (if any) + UserInfo auth.UserInfo // info about user requesting transfer } // This error type is returned when a payload is requested that is too large. From d528a9cc48c631e3d2fafcf3857d209daedebc8e Mon Sep 17 00:00:00 2001 From: "Jeffrey N. Johnson" Date: Tue, 13 Aug 2024 08:44:30 -0700 Subject: [PATCH 9/9] Added a test for an invalid KBase token that fails to create an auth server proxy. --- auth/kbase_auth_server_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/auth/kbase_auth_server_test.go b/auth/kbase_auth_server_test.go index 91f9f25f..47c7c216 100644 --- a/auth/kbase_auth_server_test.go +++ b/auth/kbase_auth_server_test.go @@ -44,6 +44,16 @@ func TestNewKBaseAuthServer(t *testing.T) { assert.Nil(err, "Authentication server constructor triggered an error") } +// tests whether an invalid KBase token prevents a proxy for the auth server +// from being constructed +func TestInvalidToken(t *testing.T) { + assert := assert.New(t) + devToken := "INVALID_KBASE_TOKEN" + server, err := NewKBaseAuthServer(devToken) + assert.Nil(server, "Authentication server created with invalid token") + assert.NotNil(err, "Invalid token for authentication server triggered no error") +} + // tests whether the authentication server can return information for the // user associated with the specified developer token func TestUserInfo(t *testing.T) {