Add endpoint for listing permissions for a resource

This commit is contained in:
Benedikt Kulmann
2020-08-24 16:50:28 +02:00
parent e016ebdec8
commit 5fb1a8a2bb
14 changed files with 1060 additions and 452 deletions

1
go.mod
View File

@@ -34,6 +34,7 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
google.golang.org/protobuf v1.23.0
gotest.tools v2.2.0+incompatible
)
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

1
go.sum
View File

@@ -1352,6 +1352,7 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

File diff suppressed because it is too large Load Diff

View File

@@ -567,3 +567,77 @@ func (h *roleServiceHandler) AssignRoleToUser(ctx context.Context, in *AssignRol
func (h *roleServiceHandler) RemoveRoleFromUser(ctx context.Context, in *RemoveRoleFromUserRequest, out *empty.Empty) error {
return h.RoleServiceHandler.RemoveRoleFromUser(ctx, in, out)
}
// Api Endpoints for PermissionService service
func NewPermissionServiceEndpoints() []*api.Endpoint {
return []*api.Endpoint{
&api.Endpoint{
Name: "PermissionService.ListPermissionsByResource",
Path: []string{"/api/v0/settings/permissions-list-by-resource"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
},
}
}
// Client API for PermissionService service
type PermissionService interface {
ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, opts ...client.CallOption) (*ListPermissionsByResourceResponse, error)
}
type permissionService struct {
c client.Client
name string
}
func NewPermissionService(name string, c client.Client) PermissionService {
return &permissionService{
c: c,
name: name,
}
}
func (c *permissionService) ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, opts ...client.CallOption) (*ListPermissionsByResourceResponse, error) {
req := c.c.NewRequest(c.name, "PermissionService.ListPermissionsByResource", in)
out := new(ListPermissionsByResourceResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for PermissionService service
type PermissionServiceHandler interface {
ListPermissionsByResource(context.Context, *ListPermissionsByResourceRequest, *ListPermissionsByResourceResponse) error
}
func RegisterPermissionServiceHandler(s server.Server, hdlr PermissionServiceHandler, opts ...server.HandlerOption) error {
type permissionService interface {
ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, out *ListPermissionsByResourceResponse) error
}
type PermissionService struct {
permissionService
}
h := &permissionServiceHandler{hdlr}
opts = append(opts, api.WithEndpoint(&api.Endpoint{
Name: "PermissionService.ListPermissionsByResource",
Path: []string{"/api/v0/settings/permissions-list-by-resource"},
Method: []string{"POST"},
Body: "*",
Handler: "rpc",
}))
return s.Handle(s.NewHandler(&PermissionService{h}, opts...))
}
type permissionServiceHandler struct {
PermissionServiceHandler
}
func (h *permissionServiceHandler) ListPermissionsByResource(ctx context.Context, in *ListPermissionsByResourceRequest, out *ListPermissionsByResourceResponse) error {
return h.PermissionServiceHandler.ListPermissionsByResource(ctx, in, out)
}

View File

@@ -389,6 +389,48 @@ func RegisterRoleServiceWeb(r chi.Router, i RoleServiceHandler, middlewares ...f
r.MethodFunc("POST", "/api/v0/settings/assignments-remove", handler.RemoveRoleFromUser)
}
type webPermissionServiceHandler struct {
r chi.Router
h PermissionServiceHandler
}
func (h *webPermissionServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.r.ServeHTTP(w, r)
}
func (h *webPermissionServiceHandler) ListPermissionsByResource(w http.ResponseWriter, r *http.Request) {
req := &ListPermissionsByResourceRequest{}
resp := &ListPermissionsByResourceResponse{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusPreconditionFailed)
return
}
if err := h.h.ListPermissionsByResource(
r.Context(),
req,
resp,
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
render.Status(r, http.StatusCreated)
render.JSON(w, r, resp)
}
func RegisterPermissionServiceWeb(r chi.Router, i PermissionServiceHandler, middlewares ...func(http.Handler) http.Handler) {
handler := &webPermissionServiceHandler{
r: r,
h: i,
}
r.MethodFunc("POST", "/api/v0/settings/permissions-list-by-resource", handler.ListPermissionsByResource)
}
// SaveBundleRequestJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of SaveBundleRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
@@ -1253,6 +1295,78 @@ func (m *UserRoleAssignment) UnmarshalJSON(b []byte) error {
var _ json.Unmarshaler = (*UserRoleAssignment)(nil)
// ListPermissionsByResourceRequestJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of ListPermissionsByResourceRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceRequestJSONMarshaler = new(jsonpb.Marshaler)
// MarshalJSON satisfies the encoding/json Marshaler interface. This method
// uses the more correct jsonpb package to correctly marshal the message.
func (m *ListPermissionsByResourceRequest) MarshalJSON() ([]byte, error) {
if m == nil {
return json.Marshal(nil)
}
buf := &bytes.Buffer{}
if err := ListPermissionsByResourceRequestJSONMarshaler.Marshal(buf, m); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var _ json.Marshaler = (*ListPermissionsByResourceRequest)(nil)
// ListPermissionsByResourceRequestJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all
// instances of ListPermissionsByResourceRequest. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceRequestJSONUnmarshaler = new(jsonpb.Unmarshaler)
// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method
// uses the more correct jsonpb package to correctly unmarshal the message.
func (m *ListPermissionsByResourceRequest) UnmarshalJSON(b []byte) error {
return ListPermissionsByResourceRequestJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m)
}
var _ json.Unmarshaler = (*ListPermissionsByResourceRequest)(nil)
// ListPermissionsByResourceResponseJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of ListPermissionsByResourceResponse. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceResponseJSONMarshaler = new(jsonpb.Marshaler)
// MarshalJSON satisfies the encoding/json Marshaler interface. This method
// uses the more correct jsonpb package to correctly marshal the message.
func (m *ListPermissionsByResourceResponse) MarshalJSON() ([]byte, error) {
if m == nil {
return json.Marshal(nil)
}
buf := &bytes.Buffer{}
if err := ListPermissionsByResourceResponseJSONMarshaler.Marshal(buf, m); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var _ json.Marshaler = (*ListPermissionsByResourceResponse)(nil)
// ListPermissionsByResourceResponseJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all
// instances of ListPermissionsByResourceResponse. This struct is safe to replace or modify but
// should not be done so concurrently.
var ListPermissionsByResourceResponseJSONUnmarshaler = new(jsonpb.Unmarshaler)
// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method
// uses the more correct jsonpb package to correctly unmarshal the message.
func (m *ListPermissionsByResourceResponse) UnmarshalJSON(b []byte) error {
return ListPermissionsByResourceResponseJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m)
}
var _ json.Unmarshaler = (*ListPermissionsByResourceResponse)(nil)
// ResourceJSONMarshaler describes the default jsonpb.Marshaler used by all
// instances of Resource. This struct is safe to replace or modify but
// should not be done so concurrently.

View File

@@ -118,6 +118,15 @@ service RoleService {
}
}
service PermissionService {
rpc ListPermissionsByResource(ListPermissionsByResourceRequest) returns (ListPermissionsByResourceResponse) {
option (google.api.http) = {
post: "/api/v0/settings/permissions-list-by-resource",
body: "*"
};
}
}
// ---
// requests and responses for settings bundles
// ---
@@ -238,6 +247,19 @@ message UserRoleAssignment {
string role_id = 3;
}
// --
// requests and responses for permissions
// ---
message ListPermissionsByResourceRequest {
Resource resource = 1;
repeated string role_ids = 2;
}
message ListPermissionsByResourceResponse {
repeated Permission permissions = 1;
}
// ---
// resource payloads
// ---

View File

@@ -280,6 +280,38 @@
]
}
},
"/api/v0/settings/permissions-list-by-resource": {
"post": {
"operationId": "PermissionService_ListPermissionsByResource",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/protoListPermissionsByResourceResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/protoListPermissionsByResourceRequest"
}
}
],
"tags": [
"PermissionService"
]
}
},
"/api/v0/settings/roles-list": {
"post": {
"operationId": "RoleService_ListRoles",
@@ -668,6 +700,31 @@
}
}
},
"protoListPermissionsByResourceRequest": {
"type": "object",
"properties": {
"resource": {
"$ref": "#/definitions/protoResource"
},
"role_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"protoListPermissionsByResourceResponse": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/protoPermission"
}
}
}
},
"protoListRoleAssignmentsRequest": {
"type": "object",
"properties": {

View File

@@ -3,50 +3,53 @@ package svc
import "github.com/owncloud/ocis-settings/pkg/proto/v0"
func (g Service) hasPermission(
assignments []*proto.UserRoleAssignment,
roleIDs []string,
resource *proto.Resource,
operation proto.Permission_Operation,
operations []proto.Permission_Operation,
constraint proto.Permission_Constraint,
) bool {
for index := range assignments {
if g.isAllowedByRole(assignments[index], resource, operation, constraint) {
permissions, err := g.manager.ListPermissionsByResource(resource, roleIDs)
if err != nil {
g.logger.Debug().Err(err).
Str("resource-type", resource.Type.String()).
Str("resource-id", resource.Id).
Msg("permissions could not be loaded for resource")
return false
}
permissions = getFilteredPermissionsByOperations(permissions, operations)
return isConstraintFulfilled(permissions, constraint)
}
// filterPermissionsByOperations returns the subset of the given permissions, where at least one of the given operations is fulfilled.
func getFilteredPermissionsByOperations(permissions []*proto.Permission, operations []proto.Permission_Operation) []*proto.Permission {
var filteredPermissions []*proto.Permission
for _, permission := range permissions {
if isAnyOperationFulfilled(permission, operations) {
filteredPermissions = append(filteredPermissions, permission)
}
}
return filteredPermissions
}
// isAnyOperationFulfilled checks if the permissions is about any of the operations
func isAnyOperationFulfilled(permission *proto.Permission, operations []proto.Permission_Operation) bool {
for _, operation := range operations {
if operation == permission.Operation {
return true
}
}
return false
}
func (g Service) isAllowedByRole(
assignment *proto.UserRoleAssignment,
resource *proto.Resource,
operation proto.Permission_Operation,
constraint proto.Permission_Constraint,
) bool {
role, err := g.manager.ReadBundle(assignment.RoleId)
if err != nil {
g.logger.Err(err).Str("bundle", assignment.RoleId).Msg("Failed to fetch role")
return false
}
for _, setting := range role.Settings {
if _, ok := setting.Value.(*proto.Setting_PermissionValue); ok {
value := setting.Value.(*proto.Setting_PermissionValue).PermissionValue
if resource.Type == setting.Resource.Type &&
resource.Id == setting.Resource.Id &&
operation == value.Operation &&
isConstraintMatch(constraint, value.Constraint) {
return true
}
// isConstraintFulfilled checks if one of the permissions has the same or a parent of the constraint.
// this is only a comparison on ENUM level. More sophisticated checks cannot happen here...
func isConstraintFulfilled(permissions []*proto.Permission, constraint proto.Permission_Constraint) bool {
for _, permission := range permissions {
// comparing enum by order is not a feasible solution, because `SHARED` is not a superset of `OWN`.
if permission.Constraint == proto.Permission_CONSTRAINT_ALL {
return true
}
return permission.Constraint != proto.Permission_CONSTRAINT_UNKNOWN && permission.Constraint == constraint
}
return false
}
// isConstraintMatch checks if the `given` constraint is the same or a superset of the `required` constraint.
// this is only a comparison on ENUM level. this is not a check about the appropriate constraint for a resource.
func isConstraintMatch(given, required proto.Permission_Constraint) bool {
// comparing enum by order is not a feasible solution, because `SHARED` is not a superset of `OWN`.
if given == proto.Permission_CONSTRAINT_ALL {
return true
}
return given != proto.Permission_CONSTRAINT_UNKNOWN && given == required
}

View File

@@ -86,13 +86,7 @@ func (g Service) ListBundles(c context.Context, req *proto.ListBundlesRequest, r
if err != nil {
return merrors.NotFound("ocis-settings", "%s", err)
}
// fetch roles of the user
rolesResponse := &proto.ListRoleAssignmentsResponse{}
err = g.ListRoleAssignments(c, &proto.ListRoleAssignmentsRequest{AccountUuid: req.AccountUuid}, rolesResponse)
if err != nil {
return err
}
roleIDs := g.getRoleIDs(c, req.AccountUuid)
// filter settings in bundles that are allowed according to roles
var filteredBundles []*proto.Bundle
@@ -104,9 +98,9 @@ func (g Service) ListBundles(c context.Context, req *proto.ListBundlesRequest, r
Id: setting.Id,
}
if g.hasPermission(
rolesResponse.Assignments,
roleIDs,
settingResource,
proto.Permission_OPERATION_UPDATE,
[]proto.Permission_Operation{proto.Permission_OPERATION_READ},
proto.Permission_CONSTRAINT_OWN,
) {
filteredSettings = append(filteredSettings, setting)
@@ -277,6 +271,19 @@ func (g Service) RemoveRoleFromUser(c context.Context, req *proto.RemoveRoleFrom
return nil
}
// ListPermissionsByResource implements the PermissionServiceHandler interface
func (g Service) ListPermissionsByResource(c context.Context, req *proto.ListPermissionsByResourceRequest, res *proto.ListPermissionsByResourceResponse) error {
if validationError := validateListPermissionsByResource(req); validationError != nil {
return merrors.BadRequest("ocis-settings", "%s", validationError)
}
permissions, err := g.manager.ListPermissionsByResource(req.Resource, req.RoleIds)
if err != nil {
return merrors.BadRequest("ocis-settings", "%s", err)
}
res.Permissions = permissions
return nil
}
// cleanUpResource makes sure that the account uuid of the authenticated user is injected if needed.
func cleanUpResource(c context.Context, resource *proto.Resource) {
if resource != nil && resource.Type == proto.Resource_TYPE_USER {
@@ -295,6 +302,24 @@ func getValidatedAccountUUID(c context.Context, accountUUID string) string {
return accountUUID
}
// getRoleIDs loads the role assignments for the given accountUUID
// TODO: this should work on the context in the future, as roles are supposed to be sent within the context.
func (g Service) getRoleIDs(c context.Context, accountUUID string) []string {
// TODO: replace this with role ids from the context
// WIP PR: https://github.com/owncloud/ocis-proxy/pull/70
rolesResponse := &proto.ListRoleAssignmentsResponse{}
err := g.ListRoleAssignments(c, &proto.ListRoleAssignmentsRequest{AccountUuid: accountUUID}, rolesResponse)
if err != nil {
g.logger.Err(err).Str("accountUUID", accountUUID).Msg("failed to list role assignments")
return []string{}
}
var roleIDs []string
for _, assignment := range rolesResponse.Assignments {
roleIDs = append(roleIDs, assignment.RoleId)
}
return roleIDs
}
func (g Service) getValueWithIdentifier(value *proto.Value) (*proto.ValueWithIdentifier, error) {
bundle, err := g.manager.ReadBundle(value.BundleId)
if err != nil {

View File

@@ -122,6 +122,16 @@ func validateRemoveRoleFromUser(req *proto.RemoveRoleFromUserRequest) error {
)
}
func validateListPermissionsByResource(req *proto.ListPermissionsByResourceRequest) error {
if err := validateResource(req.Resource); err != nil {
return err
}
return validation.ValidateStruct(
req,
validation.Field(&req.RoleIds, validation.Each(requireAlphanumeric...)),
)
}
// validateResource is an internal helper for validating the content of a resource.
func validateResource(resource *proto.Resource) error {
if err := validation.Validate(&resource, validation.Required); err != nil {

View File

@@ -18,6 +18,7 @@ type Manager interface {
BundleManager
ValueManager
RoleAssignmentManager
PermissionManager
}
// BundleManager is a bundle service interface for abstraction of storage implementations
@@ -44,3 +45,8 @@ type RoleAssignmentManager interface {
WriteRoleAssignment(accountUUID, roleID string) (*proto.UserRoleAssignment, error)
RemoveRoleAssignment(assignmentID string) error
}
// PermissionManager is a permissions service interface for abstraction of storage implementations
type PermissionManager interface {
ListPermissionsByResource(resource *proto.Resource, roleIDs []string) ([]*proto.Permission, error)
}

View File

@@ -0,0 +1,34 @@
package store
import (
"github.com/owncloud/ocis-settings/pkg/proto/v0"
"github.com/owncloud/ocis-settings/pkg/util"
)
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
func (s Store) ListPermissionsByResource(resource *proto.Resource, roleIDs []string) ([]*proto.Permission, error) {
var records []*proto.Permission
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
records = append(records, extractPermissionsByResource(resource, role)...)
}
return records, nil
}
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
func extractPermissionsByResource(resource *proto.Resource, role *proto.Bundle) []*proto.Permission {
var permissions []*proto.Permission
for _, setting := range role.Settings {
if _, ok := setting.Value.(*proto.Setting_PermissionValue); ok {
value := setting.Value.(*proto.Setting_PermissionValue).PermissionValue
if util.IsResourceMatched(setting.Resource, resource) {
permissions = append(permissions, value)
}
}
}
return permissions
}

View File

@@ -0,0 +1,15 @@
package util
import "github.com/owncloud/ocis-settings/pkg/proto/v0"
const (
ResourceIdAll = "all"
)
// IsResourceMatched checks if the `example` resource is an exact match or a subset of `definition`
func IsResourceMatched(definition, example *proto.Resource) bool {
if definition.Type != example.Type {
return false
}
return definition.Id == ResourceIdAll || definition.Id == example.Id
}

View File

@@ -0,0 +1,90 @@
package util
import (
"github.com/owncloud/ocis-settings/pkg/proto/v0"
"gotest.tools/assert"
"testing"
)
func TestIsResourceMatched(t *testing.T) {
scenarios := []struct {
name string
definition *proto.Resource
example *proto.Resource
matched bool
}{
{
"same resource types without ids match",
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
true,
},
{
"different resource types without ids don't match",
&proto.Resource{
Type: proto.Resource_TYPE_SYSTEM,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
},
false,
},
{
"same resource types with different ids don't match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "marie",
},
false,
},
{
"same resource types with same ids match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
true,
},
{
"same resource types with definition = ALL and without id in example is a match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: ResourceIdAll,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
},
true,
},
{
"same resource types with definition.id = ALL and with some id in example is a match",
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: ResourceIdAll,
},
&proto.Resource{
Type: proto.Resource_TYPE_USER,
Id: "einstein",
},
true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
assert.Equal(t, scenario.matched, IsResourceMatched(scenario.definition, scenario.example))
})
}
}