mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-03 11:38:23 -05:00
Compare commits
10 Commits
fix-dev-do
...
dragonchas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8288abf63e | ||
|
|
b1bc63274f | ||
|
|
d76f528c00 | ||
|
|
c15bb0f99b | ||
|
|
11900601d2 | ||
|
|
16d600cffe | ||
|
|
3272b4862a | ||
|
|
29804e355b | ||
|
|
9c4f74d394 | ||
|
|
55a6f057e5 |
@@ -42,6 +42,8 @@ type Config struct {
|
||||
Metadata Metadata `yaml:"metadata_config"`
|
||||
|
||||
UserSoftDeleteRetentionTime time.Duration `yaml:"user_soft_delete_retention_time" env:"GRAPH_USER_SOFT_DELETE_RETENTION_TIME" desc:"The time after which a soft-deleted user is permanently deleted. If set to 0 (default), there is no soft delete retention time and users are deleted immediately after being soft-deleted. If set to a positive value, the user will be kept in the system for that duration before being permanently deleted." introductionVersion:"%%NEXT%%"`
|
||||
|
||||
Store Store `yaml:"store"`
|
||||
}
|
||||
|
||||
type Spaces struct {
|
||||
@@ -168,3 +170,11 @@ type Metadata struct {
|
||||
SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;GRAPH_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"`
|
||||
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"`
|
||||
}
|
||||
|
||||
// Store configures the store to use
|
||||
type Store struct {
|
||||
Nodes []string `yaml:"nodes" env:"OC_PERSISTENT_STORE_NODES;GRAPH_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store is configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
|
||||
Database string `yaml:"database" env:"GRAPH_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"1.0.0"`
|
||||
AuthUsername string `yaml:"username" env:"OC_PERSISTENT_STORE_AUTH_USERNAME;GRAPH_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
|
||||
AuthPassword string `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;GRAPH_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
@@ -131,6 +131,10 @@ func DefaultConfig() *config.Config {
|
||||
SystemUserIDP: "internal",
|
||||
},
|
||||
UserSoftDeleteRetentionTime: 0,
|
||||
Store: config.Store{
|
||||
Nodes: []string{"127.0.0.1:9233"},
|
||||
Database: "graph",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v4/client"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
@@ -67,6 +68,7 @@ type Graph struct {
|
||||
keycloakClient keycloak.Client
|
||||
historyClient ehsvc.EventHistoryService
|
||||
traceProvider trace.TracerProvider
|
||||
natskv nats.KeyValue
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
ldapv3 "github.com/go-ldap/ldap/v3"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/riandyrn/otelchi"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
@@ -153,6 +154,38 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
identity.IdentityCacheWithGroupsTTL(time.Duration(options.Config.Spaces.GroupsCacheTTL)),
|
||||
)
|
||||
|
||||
// Connect to NATS servers
|
||||
natsOptions := nats.Options{
|
||||
Servers: options.Config.Store.Nodes,
|
||||
}
|
||||
conn, err := natsOptions.Connect()
|
||||
if err != nil {
|
||||
return Graph{}, err
|
||||
}
|
||||
|
||||
js, err := conn.JetStream()
|
||||
if err != nil {
|
||||
return Graph{}, err
|
||||
}
|
||||
|
||||
kv, err := js.KeyValue(options.Config.Store.Database)
|
||||
if err != nil {
|
||||
if !errors.Is(err, nats.ErrBucketNotFound) {
|
||||
return Graph{}, fmt.Errorf("Failed to get bucket (%s): %w", options.Config.Store.Database, err)
|
||||
|
||||
}
|
||||
|
||||
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
|
||||
Bucket: options.Config.Store.Database,
|
||||
})
|
||||
if err != nil {
|
||||
return Graph{}, fmt.Errorf("Failed to create bucket (%s): %w", options.Config.Store.Database, err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return Graph{}, err
|
||||
}
|
||||
|
||||
baseGraphService := BaseGraphService{
|
||||
logger: &options.Logger,
|
||||
identityCache: identityCache,
|
||||
@@ -198,6 +231,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
historyClient: options.EventHistoryClient,
|
||||
traceProvider: options.TraceProvider,
|
||||
valueService: options.ValueService,
|
||||
natskv: kv,
|
||||
}
|
||||
|
||||
if err := setIdentityBackends(options, &svc); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -22,12 +23,14 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nats-io/nats.go"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
|
||||
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/userstate"
|
||||
ocsettingssvc "github.com/opencloud-eu/opencloud/services/settings/pkg/service/v0"
|
||||
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
@@ -642,7 +645,30 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if g.config.UserSoftDeleteRetentionTime > 0 && purgeUser && user.GetAccountEnabled() {
|
||||
us, err := g.getUserStateFromNatsKeyValue(r.Context(), userID)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("id", userID).Msg("could not get user state")
|
||||
us = userstate.UserState{
|
||||
UserId: userID,
|
||||
State: userstate.UserStateUnspecified,
|
||||
}
|
||||
}
|
||||
|
||||
if us.State == userstate.UserStateHardDeleted {
|
||||
logger.Debug().Str("id", userID).Msg("could not delete user: user already hard deleted")
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
if us.State == userstate.UserStateUnspecified {
|
||||
if user.GetAccountEnabled() {
|
||||
us.State = userstate.UserStateEnabled
|
||||
} else {
|
||||
us.State = userstate.UserStateSoftDeleted
|
||||
}
|
||||
}
|
||||
|
||||
if g.config.UserSoftDeleteRetentionTime > 0 && purgeUser && us.State == userstate.UserStateEnabled {
|
||||
logger.Debug().Msg("could not delete user: purgeUser is set but user is still enabled")
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user should be hard deleted, but is still enabled, please soft delete first")
|
||||
return
|
||||
@@ -684,7 +710,9 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
for _, sp := range lspr.GetStorageSpaces() {
|
||||
if !(sp.SpaceType == _spaceTypePersonal && sp.Owner.Id.OpaqueId == user.GetId()) {
|
||||
// if the spacetype equals _spaceTypePersonal and the owner id equals the user id
|
||||
// then we found the personal space of the user to be deleted
|
||||
if !(sp.GetSpaceType() == _spaceTypePersonal && sp.Owner.GetId().GetOpaqueId() == user.GetId()) {
|
||||
continue
|
||||
}
|
||||
// TODO: check if request contains a homespace and if, check if requesting user has the privilege to
|
||||
@@ -706,7 +734,7 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
// the space will if the system does not have a UserSoftDeleteRetentionTime configured, e.g. SoftDelete disabled
|
||||
if g.config.UserSoftDeleteRetentionTime == 0 || (purgeUser && !user.GetAccountEnabled()) {
|
||||
if g.config.UserSoftDeleteRetentionTime == 0 || (purgeUser && us.State == userstate.UserStateSoftDeleted) {
|
||||
purgeSpaceFlag := utils.AppendPlainToOpaque(nil, "purge", "")
|
||||
_, err := client.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
|
||||
Opaque: purgeSpaceFlag,
|
||||
@@ -725,24 +753,41 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if g.config.UserSoftDeleteRetentionTime == 0 || (purgeUser && !user.GetAccountEnabled()) {
|
||||
if (g.config.UserSoftDeleteRetentionTime > 0 && us.State == userstate.UserStateSoftDeleted && purgeUser) ||
|
||||
(g.config.UserSoftDeleteRetentionTime == 0) {
|
||||
logger.Debug().Str("id", user.GetId()).Msg("calling delete user on backend")
|
||||
err = g.identityBackend.DeleteUser(r.Context(), user.GetId())
|
||||
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("could not delete user: backend error")
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
us.State = userstate.UserStateHardDeleted
|
||||
err = g.setUserStateToNatsKeyValue(r.Context(), userID, us)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("id", userID).Msg("could not set user state")
|
||||
errorcode.RenderError(w, r, err)
|
||||
}
|
||||
} else {
|
||||
logger.Debug().Str("id", user.GetId()).Msg("calling soft delete user on backend")
|
||||
userUpdate := *libregraph.NewUserUpdate()
|
||||
userUpdate.AccountEnabled = libregraph.PtrBool(false)
|
||||
us.State = userstate.UserStateSoftDeleted
|
||||
us.RetentionPeriod = g.config.UserSoftDeleteRetentionTime
|
||||
us.Reason = "User soft deleted via Graph API" // TODO: this needs a proper implementation through the request
|
||||
us.TimeStamp = time.Now()
|
||||
err = g.setUserStateToNatsKeyValue(r.Context(), userID, us)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("id", userID).Msg("could not set user state")
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
g.identityBackend.UpdateUser(r.Context(), user.GetId(), userUpdate)
|
||||
}
|
||||
|
||||
if g.config.UserSoftDeleteRetentionTime == 0 ||
|
||||
(g.config.UserSoftDeleteRetentionTime > 0 && purgeUser && !user.GetAccountEnabled()) {
|
||||
(g.config.UserSoftDeleteRetentionTime > 0 && purgeUser && us.State == userstate.UserStateSoftDeleted) {
|
||||
e := events.UserDeleted{UserID: user.GetId()}
|
||||
e.Executant = currentUser.GetId()
|
||||
g.publishEvent(r.Context(), e)
|
||||
@@ -1103,3 +1148,69 @@ func (g Graph) searchOCMAcceptedUsers(ctx context.Context, odataReq *godata.GoDa
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// getUserStateFromNatsKeyValue gets the user state from the nats key value store.
|
||||
func (g Graph) getUserStateFromNatsKeyValue(ctx context.Context, userID string) (userstate.UserState, error) {
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
if g.natskv == nil {
|
||||
logger.Debug().Msg("nats connection or user state key value store not configured")
|
||||
return userstate.UserState{
|
||||
UserId: userID,
|
||||
State: userstate.UserStateUnspecified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
entry, err := g.natskv.Get(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||
logger.Debug().Str("userid", userID).Msg("no user state found in nats key value store")
|
||||
return userstate.UserState{
|
||||
UserId: userID,
|
||||
State: userstate.UserStateUnspecified,
|
||||
}, nil
|
||||
}
|
||||
logger.Error().Err(err).Str("userid", userID).Msg("error getting user state from nats key value store")
|
||||
return userstate.UserState{
|
||||
State: userstate.UserStateUnspecified,
|
||||
}, err
|
||||
}
|
||||
|
||||
userState := userstate.UserState{}
|
||||
if err := json.Unmarshal(entry.Value(), &userState); err != nil {
|
||||
logger.Error().Err(err).Str("userid", userID).Msg("error unmarshalling user state from nats key value store")
|
||||
return userstate.UserState{
|
||||
UserId: userID,
|
||||
State: userstate.UserStateUnspecified,
|
||||
}, err
|
||||
}
|
||||
|
||||
return userState, nil
|
||||
}
|
||||
|
||||
// setUserStateToNatsKeyValue sets the user state in the nats key value store.
|
||||
func (g Graph) setUserStateToNatsKeyValue(ctx context.Context, userID string, us userstate.UserState) error {
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
|
||||
if ok, err := userstate.IsValidUserState(&us); !ok {
|
||||
logger.Debug().Str("userid", userID).Msg("invalid user state")
|
||||
return fmt.Errorf("invalid user state: %w", err)
|
||||
}
|
||||
|
||||
if g.natskv == nil {
|
||||
logger.Debug().Msg("nats connection or user state key value store not configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := json.Marshal(us)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("userid", userID).Msg("error marshalling user state to nats key value store")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := g.natskv.Put(userID, data); err != nil {
|
||||
logger.Error().Err(err).Str("userid", userID).Msg("error putting user state to nats key value store")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
37
services/graph/pkg/userstate/userstate.go
Normal file
37
services/graph/pkg/userstate/userstate.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package userstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
UserStateUnspecified
|
||||
UserStateEnabled
|
||||
UserStateDisabled
|
||||
UserStateSoftDeleted
|
||||
UserStateHardDeleted
|
||||
)
|
||||
|
||||
// UserState represents the state of a user account.
|
||||
// Note: This does not reflect state changes, these need to be red from the audit logs.
|
||||
type UserState struct {
|
||||
UserId string `json:"userid"`
|
||||
State uint8 `json:"state"`
|
||||
TimeStamp time.Time `json:"timestamp,omitempty"`
|
||||
RetentionPeriod time.Duration `json:"retentionPeriod,omitempty"`
|
||||
Reason string `json:"reason,omitempty,omitempty"`
|
||||
}
|
||||
|
||||
func IsValidUserState(us *UserState) (bool, error) {
|
||||
if us.State == UserStateSoftDeleted {
|
||||
if us.RetentionPeriod <= 0 {
|
||||
return false, fmt.Errorf("retention period must be greater than 0 for soft deleted users")
|
||||
}
|
||||
if us.Reason == "" {
|
||||
return false, fmt.Errorf("reason must be provided for soft deleted users")
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -289,12 +289,13 @@ class GraphContext implements Context {
|
||||
*/
|
||||
public function adminDeletesUserUsingTheGraphApi(string $user, ?string $byUser = null): ResponseInterface {
|
||||
$credentials = $this->getAdminOrUserCredentials($byUser);
|
||||
return GraphHelper::deleteUser(
|
||||
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
|
||||
return GraphHelper::deleteUserByUserId(
|
||||
$this->featureContext->getBaseUrl(),
|
||||
$this->featureContext->getStepLineRef(),
|
||||
$credentials["username"],
|
||||
$credentials["password"],
|
||||
$user
|
||||
$userId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user