diff --git a/.drone.star b/.drone.star index 532558c39..c5183010f 100644 --- a/.drone.star +++ b/.drone.star @@ -454,7 +454,6 @@ def localApiTests(ctx, storage = 'owncloud', suite = 'apiBugDemonstration', acco 'environment' : { 'TEST_SERVER_URL': 'https://ocis-server:9200', 'OCIS_REVA_DATA_ROOT': '%s' % ('/srv/app/tmp/ocis/owncloud/data/' if storage == 'owncloud' else ''), - 'DELETE_USER_DATA_CMD': '%s' % ('' if storage == 'owncloud' else 'rm -rf /srv/app/tmp/ocis/storage/users/nodes/root/* /srv/app/tmp/ocis/storage/users/nodes/*-*-*-*'), 'SKELETON_DIR': '/srv/app/tmp/testing/data/apiSkeleton', 'OCIS_SKELETON_STRATEGY': '%s' % ('copy' if storage == 'owncloud' else 'upload'), 'TEST_OCIS':'true', @@ -501,7 +500,6 @@ def coreApiTests(ctx, part_number = 1, number_of_parts = 1, storage = 'owncloud' 'environment' : { 'TEST_SERVER_URL': 'https://ocis-server:9200', 'OCIS_REVA_DATA_ROOT': '%s' % ('/srv/app/tmp/ocis/owncloud/data/' if storage == 'owncloud' else ''), - 'DELETE_USER_DATA_CMD': '%s' % ('' if storage == 'owncloud' else 'rm -rf /srv/app/tmp/ocis/storage/users/nodes/root/* /srv/app/tmp/ocis/storage/users/nodes/*-*-*-*'), 'SKELETON_DIR': '/srv/app/tmp/testing/data/apiSkeleton', 'OCIS_SKELETON_STRATEGY': '%s' % ('copy' if storage == 'owncloud' else 'upload'), 'TEST_OCIS':'true', diff --git a/changelog/unreleased/ocs-user-deprovisioning.md b/changelog/unreleased/ocs-user-deprovisioning.md new file mode 100644 index 000000000..9e5eb5535 --- /dev/null +++ b/changelog/unreleased/ocs-user-deprovisioning.md @@ -0,0 +1,15 @@ +Enhancement: User Deprovisioning for the OCS API + +Use the CS3 API and Reva to deprovision users completely. + +Two new environment variables introduced: +``` +OCS_IDM_ADDRESS +OCS_STORAGE_USERS_DRIVER +``` + +`OCS_IDM_ADDRESS` is also an alias for `OCIS_URL`; allows the OCS service to mint jwt tokens for the authenticated user that will be read by the reva authentication middleware. + +`OCS_STORAGE_USERS_DRIVER` determines how a user is deprovisioned. This kind of behavior is needed since every storage driver deals with deleting differently. + +https://github.com/owncloud/ocis/pull/1962 diff --git a/ocs/go.mod b/ocs/go.mod index 920a17f6a..c573fbdb8 100644 --- a/ocs/go.mod +++ b/ocs/go.mod @@ -22,12 +22,14 @@ require ( github.com/owncloud/ocis/proxy v0.0.0-20210412105747-9b95e9b1191b github.com/owncloud/ocis/settings v0.0.0-20210413063522-955bd60edf33 github.com/owncloud/ocis/store v0.0.0-20210413063522-955bd60edf33 + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.10.0 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/thejerf/suture/v4 v4.0.0 go.opencensus.io v0.23.0 google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea + google.golang.org/grpc v1.37.0 // indirect google.golang.org/protobuf v1.26.0 ) diff --git a/ocs/pkg/config/config.go b/ocs/pkg/config/config.go index 451701210..d43c06f9c 100644 --- a/ocs/pkg/config/config.go +++ b/ocs/pkg/config/config.go @@ -45,17 +45,26 @@ type TokenManager struct { JWTSecret string } +// IdentityManagement keeps track of the OIDC address. This is because Reva requisite of uniqueness for users +// is based in the combination of IDP hostname + UserID. For more information see: +// https://github.com/cs3org/reva/blob/4fd0229f13fae5bc9684556a82dbbd0eced65ef9/pkg/storage/utils/decomposedfs/node/node.go#L856-L865 +type IdentityManagement struct { + Address string +} + // Config combines all available configuration parts. type Config struct { - File string - Log Log - Debug Debug - HTTP HTTP - Tracing Tracing - TokenManager TokenManager - Service Service - AccountBackend string - RevaAddress string + File string + Log Log + Debug Debug + HTTP HTTP + Tracing Tracing + TokenManager TokenManager + Service Service + AccountBackend string + RevaAddress string + StorageUsersDriver string + IdentityManagement IdentityManagement Context context.Context Supervised bool diff --git a/ocs/pkg/flagset/flagset.go b/ocs/pkg/flagset/flagset.go index d1924af67..a5e69d7fc 100644 --- a/ocs/pkg/flagset/flagset.go +++ b/ocs/pkg/flagset/flagset.go @@ -165,6 +165,20 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { EnvVars: []string{"OCS_REVA_GATEWAY_ADDR"}, Destination: &cfg.RevaAddress, }, + &cli.StringFlag{ + Name: "idm-address", + Value: flags.OverrideDefaultString(cfg.IdentityManagement.Address, "https://localhost:9200"), + EnvVars: []string{"OCS_IDM_ADDRESS", "OCIS_URL"}, + Usage: "keeps track of the IDM Address. Needed because of Reva requisite of uniqueness for users", + Destination: &cfg.IdentityManagement.Address, + }, + &cli.StringFlag{ + Name: "users-driver", + Value: flags.OverrideDefaultString(cfg.StorageUsersDriver, "ocis"), + Usage: "storage driver for users mount: eg. local, eos, owncloud, ocis or s3", + EnvVars: []string{"OCS_STORAGE_USERS_DRIVER", "STORAGE_USERS_DRIVER"}, + Destination: &cfg.StorageUsersDriver, + }, } } diff --git a/ocs/pkg/service/v0/users.go b/ocs/pkg/service/v0/users.go index ac25d0541..298e62015 100644 --- a/ocs/pkg/service/v0/users.go +++ b/ocs/pkg/service/v0/users.go @@ -10,18 +10,27 @@ import ( "strings" "github.com/asim/go-micro/plugins/client/grpc/v3" + merrors "github.com/asim/go-micro/v3/errors" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + revauser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/token" + "github.com/cs3org/reva/pkg/token/manager/jwt" "github.com/cs3org/reva/pkg/user" "github.com/go-chi/chi" "github.com/go-chi/render" - "google.golang.org/genproto/protobuf/field_mask" - "google.golang.org/protobuf/types/known/fieldmaskpb" - - merrors "github.com/asim/go-micro/v3/errors" - cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0" "github.com/owncloud/ocis/ocs/pkg/service/v0/data" "github.com/owncloud/ocis/ocs/pkg/service/v0/response" storepb "github.com/owncloud/ocis/store/pkg/proto/v0" + "github.com/pkg/errors" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) // GetSelf returns the currently logged in user @@ -358,6 +367,100 @@ func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) { return } + if o.config.RevaAddress != "" && o.config.StorageUsersDriver != "owncloud" { + t, err := o.mintTokenForUser(r.Context(), account) + if err != nil { + mustNotFail(render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "error minting token").Error()))) + return + } + + ctx := metadata.AppendToOutgoingContext(r.Context(), token.TokenHeader, t) + + gwc, err := pool.GetGatewayServiceClient(o.config.RevaAddress) + if err != nil { + o.logger.Error().Err(err).Msg("error securing a connection to Reva gateway") + } + + homeResp, err := gwc.GetHome(ctx, &provider.GetHomeRequest{}) + if err != nil { + mustNotFail(render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not get home").Error()))) + return + } + + if homeResp.Status.Code != rpcv1beta1.Code_CODE_OK { + o.logger.Error(). + Str("stat_status_code", homeResp.Status.Code.String()). + Str("stat_message", homeResp.Status.Message). + Msg("DeleteUser: could not get user home: get failed") + return + } + + statResp, err := gwc.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: homeResp.Path, + }, + }, + }) + + if err != nil { + mustNotFail(render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not stat home").Error()))) + return + } + + if statResp.Status.Code != rpcv1beta1.Code_CODE_OK { + o.logger.Error(). + Str("stat_status_code", statResp.Status.Code.String()). + Str("stat_message", statResp.Status.Message). + Msg("DeleteUser: could not delete user home: stat failed") + return + } + + delReq := &provider.DeleteRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Id{ + Id: statResp.Info.Id, + }, + }, + } + + delResp, err := gwc.Delete(ctx, delReq) + if err != nil { + mustNotFail(render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not delete home").Error()))) + return + } + + if delResp.Status.Code != rpcv1beta1.Code_CODE_OK { + o.logger.Error(). + Str("stat_status_code", statResp.Status.Code.String()). + Str("stat_message", statResp.Status.Message). + Msg("DeleteUser: could not delete user home: delete failed") + return + } + + req := &gateway.PurgeRecycleRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: homeResp.Path, + }, + }, + } + + purgeRecycleResponse, err := gwc.PurgeRecycle(ctx, req) + if err != nil { + mustNotFail(render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not delete trash").Error()))) + return + } + + if purgeRecycleResponse.Status.Code != rpcv1beta1.Code_CODE_OK { + o.logger.Error(). + Str("stat_status_code", statResp.Status.Code.String()). + Str("stat_message", statResp.Status.Message). + Msg("DeleteUser: could not delete user trash: delete failed") + return + } + } + req := accounts.DeleteAccountRequest{ Id: account.Id, } @@ -378,6 +481,35 @@ func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) { mustNotFail(render.Render(w, r, response.DataRender(struct{}{}))) } +// TODO(refs) this to ocis-pkg ... we are minting tokens all over the place ... or use a service? ... like reva? +func (o Ocs) mintTokenForUser(ctx context.Context, account *accounts.Account) (string, error) { + tm, _ := jwt.New(map[string]interface{}{ + "secret": o.config.TokenManager.JWTSecret, + "expires": int64(60), + }) + + u := &revauser.User{ + Id: &revauser.UserId{ + OpaqueId: account.Id, + Idp: o.config.IdentityManagement.Address, + }, + Groups: []string{}, + Opaque: &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "uid": { + Decoder: "plain", + Value: []byte(strconv.FormatInt(account.UidNumber, 10)), + }, + "gid": { + Decoder: "plain", + Value: []byte(strconv.FormatInt(account.GidNumber, 10)), + }, + }, + }, + } + return tm.MintToken(ctx, u) +} + // EnableUser enables a user func (o Ocs) EnableUser(w http.ResponseWriter, r *http.Request) { userid := chi.URLParam(r, "userid") diff --git a/tests/acceptance/expected-failures-API-on-OCIS-storage.md b/tests/acceptance/expected-failures-API-on-OCIS-storage.md index 8c2d49989..b0c364f7b 100644 --- a/tests/acceptance/expected-failures-API-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-API-on-OCIS-storage.md @@ -1539,8 +1539,6 @@ special character username not valid - [apiProvisioning-v2/enableUser.feature:20](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/enableUser.feature#L20) - [apiProvisioning-v2/getUser.feature:34](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/getUser.feature#L34) - [apiProvisioning-v2/getUser.feature:35](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/getUser.feature#L35) -- [apiTrashbin/trashbinFilesFolders.feature:201](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinFilesFolders.feature#L201) -- [apiTrashbin/trashbinFilesFolders.feature:202](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinFilesFolders.feature#L202) - [apiTrashbin/trashbinFilesFolders.feature:246](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinFilesFolders.feature#L246) - [apiTrashbin/trashbinFilesFolders.feature:247](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinFilesFolders.feature#L247) - [apiTrashbin/trashbinFilesFolders.feature:248](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiTrashbin/trashbinFilesFolders.feature#L248)