GDPR compliant deletion of Users (#16859)

fixes #16697

Signed-off-by: Maksym Trofimenko <maksym@container-registry.com>
This commit is contained in:
Maksym Trofimenko 2022-06-16 14:28:15 +02:00 committed by GitHub
parent e9fca3de45
commit 9a3cb4a041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 92 additions and 0 deletions

View File

@ -187,6 +187,8 @@ const (
TraceOtelInsecure = "trace_otel_insecure"
TraceOtelTimeout = "trace_otel_timeout"
GDPRDeleteUser = "gdpr_delete_user"
// These variables are temporary solution for issue: https://github.com/goharbor/harbor/issues/16039
// When user disable the pull count/time/audit log, it will decrease the database access, especially in large concurrency pull scenarios.
// TODO: Once we have a complete solution, delete these variables.

View File

@ -16,6 +16,7 @@ package user
import (
"context"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/common"
commonmodels "github.com/goharbor/harbor/src/common/models"
@ -178,6 +179,13 @@ func (c *controller) Delete(ctx context.Context, id int) error {
return errors.UnknownError(err).WithMessage("delete user failed, user id: %v, cannot delete oidc user, error:%v", id, err)
}
}
gdprSetting, err := config.GDPRSetting(ctx)
if err != nil {
return errors.UnknownError(err).WithMessage("failed to load GDPR setting: %v", err)
}
if gdprSetting.DeleteUser {
return c.mgr.DeleteGDPR(ctx, id)
}
return c.mgr.Delete(ctx, id)
}

View File

@ -53,6 +53,7 @@ const (
// Put all config items do not belong a existing group into basic
BasicGroup = "basic"
TrivyGroup = "trivy"
GDPRGroup = "gdpr"
)
var (
@ -187,6 +188,8 @@ var (
{Name: common.CacheEnabled, Scope: SystemScope, Group: BasicGroup, EnvKey: "CACHE_ENABLED", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `Enable cache`},
{Name: common.CacheExpireHours, Scope: SystemScope, Group: BasicGroup, EnvKey: "CACHE_EXPIRE_HOURS", DefaultValue: "24", ItemType: &IntType{}, Editable: false, Description: `The expire hours for cache`},
{Name: common.GDPRDeleteUser, Scope: SystemScope, Group: GDPRGroup, EnvKey: "GDPR_DELETE_USER", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The flag indicates if a user should be deleted compliant with GDPR.`},
{Name: common.AuditLogForwardEndpoint, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_FORWARD_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false, Description: `The endpoint to forward the audit log.`},
{Name: common.SkipAuditLogDatabase, Scope: UserScope, Group: BasicGroup, EnvKey: "SKIP_LOG_AUDIT_DATABASE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip audit log in database`},
}

View File

@ -106,3 +106,7 @@ type GroupConf struct {
AdminDN string `json:"ldap_group_admin_dn,omitempty"`
MembershipAttribute string `json:"ldap_group_membership_attribute,omitempty"`
}
type GDPRSetting struct {
DeleteUser bool `json:"user_delete,omitempty"`
}

View File

@ -191,6 +191,16 @@ func OIDCSetting(ctx context.Context) (*cfgModels.OIDCSetting, error) {
}, nil
}
// GDPRSetting returns the setting of GDPR
func GDPRSetting(ctx context.Context) (*cfgModels.GDPRSetting, error) {
if err := DefaultMgr().Load(ctx); err != nil {
return nil, err
}
return &cfgModels.GDPRSetting{
DeleteUser: DefaultMgr().Get(ctx, common.GDPRDeleteUser).GetBool(),
}, nil
}
// NotificationEnable returns a bool to indicates if notification enabled in harbor
func NotificationEnable(ctx context.Context) bool {
return DefaultMgr().Get(ctx, common.NotificationEnable).GetBool()

View File

@ -24,6 +24,7 @@ import (
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/user/dao"
"github.com/goharbor/harbor/src/pkg/user/models"
"hash/crc32"
"strings"
)
@ -46,6 +47,8 @@ type Manager interface {
Create(ctx context.Context, user *commonmodels.User) (int, error)
// Delete deletes the user by updating user's delete flag and update the name and Email
Delete(ctx context.Context, id int) error
// DeleteGDPR deletes the user by updating user's delete flag and replace identifiable data with its crc
DeleteGDPR(ctx context.Context, id int) error
// SetSysAdminFlag sets the system admin flag of the user in local DB
SetSysAdminFlag(ctx context.Context, id int, admin bool) error
// UpdateProfile updates the user's profile
@ -102,6 +105,18 @@ func (m *manager) Delete(ctx context.Context, id int) error {
return m.dao.Update(ctx, u, "username", "email", "deleted")
}
func (m *manager) DeleteGDPR(ctx context.Context, id int) error {
u, err := m.Get(ctx, id)
if err != nil {
return err
}
u.Username = fmt.Sprintf("%s#%d", checkSum(u.Username), u.UserID)
u.Email = fmt.Sprintf("%s#%d", checkSum(u.Email), u.UserID)
u.Realname = fmt.Sprintf("%s#%d", checkSum(u.Realname), u.UserID)
u.Deleted = true
return m.dao.Update(ctx, u, "username", "email", "realname", "deleted")
}
func (m *manager) MatchLocalPassword(ctx context.Context, usernameOrEmail, password string) (*commonmodels.User, error) {
l, err := m.dao.List(ctx, q.New(q.KeyWords{"username_or_email": usernameOrEmail}))
if err != nil {
@ -202,3 +217,7 @@ func injectPasswd(u *commonmodels.User, password string) {
u.Salt = salt
u.PasswordVersion = utils.SHA256
}
func checkSum(str string) string {
return fmt.Sprintf("%08x", crc32.Checksum([]byte(str), crc32.IEEETable))
}

View File

@ -2,6 +2,7 @@ package user
import (
"context"
"fmt"
"testing"
"github.com/goharbor/harbor/src/common/models"
@ -47,6 +48,37 @@ func (m *mgrTestSuite) TestSetAdminFlag() {
m.dao.AssertExpectations(m.T())
}
func (m *mgrTestSuite) TestUserDeleteGDPR() {
existingUser := &models.User{
UserID: 123,
Username: "existing",
Email: "existing@mytest.com",
Realname: "RealName",
}
m.dao.On("List", mock.Anything, testifymock.MatchedBy(
func(query *q.Query) bool {
return query.Keywords["user_id"] == 123
})).Return(
[]*models.User{existingUser}, nil)
m.dao.On("Update", mock.Anything, testifymock.MatchedBy(
func(u *models.User) bool {
return u.UserID == 123 &&
u.Email == fmt.Sprintf("%s#%d", checkSum("existing@mytest.com"), existingUser.UserID) &&
u.Username == fmt.Sprintf("%s#%d", checkSum("existing"), existingUser.UserID) &&
u.Realname == fmt.Sprintf("%s#%d", checkSum("RealName"), existingUser.UserID) &&
u.Deleted == true
}),
"username",
"email",
"realname",
"deleted",
).Return(nil)
err := m.mgr.DeleteGDPR(context.Background(), 123)
m.Nil(err)
}
func (m *mgrTestSuite) TestOnboard() {
existingUser := &models.User{
UserID: 123,

View File

@ -74,6 +74,20 @@ func (_m *Manager) Delete(ctx context.Context, id int) error {
return r0
}
// DeleteGDPR provides a mock function with given fields: ctx, id
func (_m *Manager) DeleteGDPR(ctx context.Context, id int) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int) (*models.User, error) {
ret := _m.Called(ctx, id)