Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add GetSingleDrive handler #2978

Merged
merged 7 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/single-space-enpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Add endpoint to retrieve a single space

We added the endpoint ``/drives/{driveID}`` to get a single space by id from the server.

https://github.com/owncloud/ocis/pull/2978
137 changes: 106 additions & 31 deletions graph/pkg/service/v0/drives.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,46 +43,20 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
g.logger.Info().Msg("Calling GetDrives")
g.logger.Info().Interface("query", r.URL.Query()).Msg("Calling GetDrives")
ctx := r.Context()

client := g.GetGatewayClient()

permissions := make(map[string]struct{}, 1)
s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient)

_, err = s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{
PermissionId: settingsSvc.ListAllSpacesPermissionID,
})

// No error means the user has the permission
if err == nil {
permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{}
}
value, err := json.Marshal(permissions)
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
filters, err := generateCs3Filters(odataReq)
if err != nil {
g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error")
errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, err.Error())
return
}
res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{
"permissions": {
Decoder: "json",
Value: value,
},
}},
Filters: filters,
})
res, err := g.ListStorageSpacesWithFilters(ctx, filters)
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
Expand All @@ -91,7 +65,7 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &listResponse{})
micbar marked this conversation as resolved.
Show resolved Hide resolved
return
}
g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request")
g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
Expand All @@ -113,6 +87,75 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &listResponse{Value: files})
}

// GetSingleDrive does a lookup of a single space by spaceId
func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
driveID := chi.URLParam(r, "driveID")
if driveID == "" {
err := fmt.Errorf("no valid space id retrieved")
g.logger.Err(err)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}

g.logger.Info().Str("driveID", driveID).Msg("Calling GetSingleDrive")
ctx := r.Context()

filters := []*storageprovider.ListStorageSpacesRequest_Filter{
{
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID,
Term: &storageprovider.ListStorageSpacesRequest_Filter_Id{
Id: &storageprovider.StorageSpaceId{
OpaqueId: driveID,
},
},
},
}
res, err := g.ListStorageSpacesWithFilters(ctx, filters)
switch {
case err != nil:
g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr)
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
// the client is doing a lookup for a specific space, therefore we need to return
// not found to the caller
g.logger.Error().Str("driveID", driveID).Msg(fmt.Sprintf(NoSpaceFoundMessage, driveID))
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID))
return
}
g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}

wdu, err := url.Parse(g.config.Spaces.WebDavBase + g.config.Spaces.WebDavPath)
if err != nil {
g.logger.Error().Err(err).Msg("error parsing url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
spaces, err := g.formatDrives(ctx, wdu, res.StorageSpaces)
if err != nil {
g.logger.Error().Err(err).Msg("error encoding response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
switch num := len(spaces); {
case num == 0:
g.logger.Error().Str("driveID", driveID).Msg("no space found")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID))
return
case num == 1:
render.Status(r, http.StatusOK)
render.JSON(w, r, spaces[0])
default:
g.logger.Error().Int("number", num).Msg("expected to find a single space but found more")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "expected to find a single space but found more")
return
}
}

// CreateDrive creates a storage drive (space).
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
us, ok := ctxpkg.ContextGetUser(r.Context())
Expand Down Expand Up @@ -340,6 +383,38 @@ func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storag
return responses, nil
}

// ListStorageSpacesWithFilters List Storage Spaces using filters
func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*storageprovider.ListStorageSpacesRequest_Filter) (*storageprovider.ListStorageSpacesResponse, error) {
client := g.GetGatewayClient()

permissions := make(map[string]struct{}, 1)
s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient)

_, err := s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{
PermissionId: settingsSvc.ListAllSpacesPermissionID,
})

// No error means the user has the permission
if err == nil {
permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{}
}
value, err := json.Marshal(permissions)
if err != nil {
return nil, err
}

res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{
"permissions": {
Decoder: "json",
Value: value,
},
}},
Filters: filters,
})
return res, err
}

func cs3StorageSpaceToDrive(baseURL *url.URL, space *storageprovider.StorageSpace) (*libregraph.Drive, error) {
rootID := space.Root.StorageId + "!" + space.Root.OpaqueId
if space.Root.StorageId == space.Root.OpaqueId {
Expand Down
6 changes: 6 additions & 0 deletions graph/pkg/service/v0/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,9 @@ func (g Graph) GetHTTPClient() HTTPClient {
type listResponse struct {
Value interface{} `json:"value,omitempty"`
}

const (
NoSpaceFoundMessage = "space with id `%s` not found"
ListStorageSpacesTransportErr = "transport error sending list storage spaces grpc request"
ListStorageSpacesReturnsErr = "list storage spaces grpc request returns an errorcode in the response"
)
1 change: 1 addition & 0 deletions graph/pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func NewService(opts ...Option) Service {
r.Post("/", svc.CreateDrive)
r.Route("/{driveID}", func(r chi.Router) {
r.Patch("/", svc.UpdateDrive)
r.Get("/", svc.GetSingleDrive)
})
})
})
Expand Down
32 changes: 32 additions & 0 deletions tests/acceptance/features/apiSpaces/listSpaces.feature
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,35 @@ Feature: List and create spaces
| name | Project Venus |
| quota@@@total | 2000 |
| root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |

Scenario: A user can list his personal space via multiple endpoints
When user "Alice" lists all available spaces via the GraphApi with query "$filter=driveType eq 'personal'"
Then the json responded should contain a space "Alice Hansen" with these key and value pairs:
| key | value |
| driveType | personal |
| name | Alice Hansen |
| root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
When user "Alice" looks up the single space "Alice Hansen" via the GraphApi by using its id
Then the json responded should contain a space "Alice Hansen" with these key and value pairs:
| key | value |
| driveType | personal |
| name | Alice Hansen |
| root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |

Scenario: A user can list his created spaces via multiple endpoints
Given the administrator has given "Alice" the role "Admin" using the settings api
When user "Alice" creates a space "Project Venus" of type "project" with quota "2000" using the GraphApi
Then the HTTP status code should be "201"
And the json responded should contain a space "Project Venus" with these key and value pairs:
| key | value |
| driveType | project |
| name | Project Venus |
| quota@@@total | 2000 |
| root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
When user "Alice" looks up the single space "Project Venus" via the GraphApi by using its id
Then the json responded should contain a space "Project Venus" with these key and value pairs:
| key | value |
| driveType | project |
| name | Project Venus |
| quota@@@total | 2000 |
| root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
60 changes: 56 additions & 4 deletions tests/acceptance/features/bootstrap/SpacesContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public function setUpScenario(BeforeScenarioScope $scope): void {
}

/**
* Send Graph List Spaces Request
* Send Graph List My Spaces Request
*
* @param string $user
* @param string $password
Expand All @@ -245,7 +245,7 @@ public function setUpScenario(BeforeScenarioScope $scope): void {
*
* @throws GuzzleException
*/
public function listSpacesRequest(
public function listMySpacesRequest(
string $user,
string $password,
string $urlArguments = '',
Expand All @@ -258,6 +258,34 @@ public function listSpacesRequest(
return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body);
}

/**
* Send Graph List Single Space Request
*
* @param string $user
* @param string $password
* @param string $urlArguments
* @param string $xRequestId
* @param array $body
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
*/
public function listSingleSpaceRequest(
string $user,
string $password,
string $spaceId,
string $urlArguments = '',
string $xRequestId = '',
array $body = [],
array $headers = []
): ResponseInterface {
$fullUrl = $this->baseUrl . "/graph/v1.0/drives/" . $spaceId . "/" . $urlArguments;

return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body);
}

/**
* Send Graph Create Space Request
*
Expand Down Expand Up @@ -342,7 +370,7 @@ public function sendPutRequestToUrl(
*/
public function theUserListsAllHisAvailableSpacesUsingTheGraphApi(string $user): void {
$this->featureContext->setResponse(
$this->listSpacesRequest(
$this->listMySpacesRequest(
$user,
$this->featureContext->getPasswordForUser($user)
)
Expand All @@ -362,14 +390,38 @@ public function theUserListsAllHisAvailableSpacesUsingTheGraphApi(string $user):
*/
public function theUserListsAllHisAvailableSpacesUsingTheGraphApiWithFilter(string $user, string $query): void {
$this->featureContext->setResponse(
$this->listSpacesRequest(
$this->listMySpacesRequest(
$user,
$this->featureContext->getPasswordForUser($user),
"?". $query
)
);
}

/**
* @When /^user "([^"]*)" looks up the single space "([^"]*)" via the GraphApi by using its id$/
*
* @param string $user
* @param string $query
*
* @return void
*
* @throws GuzzleException
*/
public function theUserLooksUpTheSingleSpaceUsingTheGraphApiByUsingItsId(string $user, string $spaceName): void {
$space = $this->getSpaceByName($user, $spaceName);
Assert::assertIsArray($space);
Assert::assertNotEmpty($spaceId = $space["id"]);
Assert::assertNotEmpty($spaceWebDavUrl = $space["root"]["webDavUrl"]);
$this->featureContext->setResponse(
$this->listSingleSpaceRequest(
$user,
$this->featureContext->getPasswordForUser($user),
$spaceId
)
);
}

/**
* @When /^user "([^"]*)" creates a space "([^"]*)" of type "([^"]*)" with the default quota using the GraphApi$/
*
Expand Down