mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-08 05:17:49 +02:00
61ecc4c505
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
602 lines
18 KiB
Go
602 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/phishingclub/phishingclub/data"
|
|
"github.com/phishingclub/phishingclub/errs"
|
|
"github.com/phishingclub/phishingclub/model"
|
|
"github.com/phishingclub/phishingclub/repository"
|
|
"github.com/phishingclub/phishingclub/validate"
|
|
"github.com/phishingclub/phishingclub/vo"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Option is a service for Option
|
|
type Option struct {
|
|
Common
|
|
OptionRepository *repository.Option
|
|
DomainRepository *repository.Domain
|
|
}
|
|
|
|
// GetOption gets an option
|
|
func (o *Option) GetOption(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
key string,
|
|
) (*model.Option, error) {
|
|
ae := NewAuditEvent("Option.GetOption", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
opt, err := o.OptionRepository.GetByKey(
|
|
ctx,
|
|
key,
|
|
)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to get option with key",
|
|
"key", key,
|
|
"error", err,
|
|
)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
return opt, nil
|
|
}
|
|
|
|
// GetOption gets an option
|
|
func (o *Option) GetOptionWithoutAuth(
|
|
ctx context.Context,
|
|
key string,
|
|
) (*model.Option, error) {
|
|
opt, err := o.OptionRepository.GetByKey(
|
|
ctx,
|
|
key,
|
|
)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to get option with key",
|
|
"key", key,
|
|
"error", err,
|
|
)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
return opt, nil
|
|
}
|
|
|
|
// MaskSSOSecret masks the sso secret
|
|
func (o *Option) MaskSSOSecret(opt *model.Option) (*model.Option, error) {
|
|
a, err := model.NewSSOOptionFromJSON([]byte(opt.Value.String()))
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to read sso option", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
// mask the key
|
|
a.ClientSecret = *vo.NewOptionalString1024Must("")
|
|
b, err := a.ToJSON()
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to mask sso secret", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
c, err := vo.NewOptionalString1MB(string(b))
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to mask secret option", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
opt.Value = *c
|
|
return opt, nil
|
|
}
|
|
|
|
// SetOptionByKey sets an option
|
|
func (o *Option) SetOptionByKey(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
option *model.Option,
|
|
) error {
|
|
ae := NewAuditEvent("Option.SetOptionByKey", session)
|
|
ae.Details["key"] = option.Key.String()
|
|
ae.Details["value"] = option.Value.String()
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return err
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
k := option.Key.String()
|
|
v := option.Value.String()
|
|
switch k {
|
|
case data.OptionKeyMaxFileUploadSizeMB:
|
|
if n, err := strconv.Atoi(v); err != nil || n <= 0 {
|
|
o.Logger.Debugw("invalid max file size",
|
|
"n", n,
|
|
"error", err,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid max"),
|
|
),
|
|
"max file size",
|
|
)
|
|
}
|
|
case data.OptionKeyRepeatOffenderMonths:
|
|
if n, err := strconv.Atoi(v); err != nil || n <= 0 {
|
|
o.Logger.Debugw("invalid repeat offender months",
|
|
"n", n,
|
|
"error", err,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid months"),
|
|
),
|
|
"repeat offender months",
|
|
)
|
|
}
|
|
case data.OptionKeyLogLevel:
|
|
// validate log level value
|
|
if v != "debug" && v != "info" && v != "warn" && v != "error" {
|
|
o.Logger.Debugw("invalid log level value",
|
|
"value", v,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid log level"),
|
|
),
|
|
"log level",
|
|
)
|
|
}
|
|
case data.OptionKeyDBLogLevel:
|
|
// validate db log level value
|
|
if v != "silent" && v != "info" && v != "warn" && v != "error" {
|
|
o.Logger.Debugw("invalid db log level value",
|
|
"value", v,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid db log level"),
|
|
),
|
|
"db log level",
|
|
)
|
|
}
|
|
case data.OptionKeyAdminSSOLogin:
|
|
// is allow listed
|
|
case data.OptionKeyDisplayMode:
|
|
// validate display mode value
|
|
if v != data.OptionValueDisplayModeWhitebox && v != data.OptionValueDisplayModeBlackbox {
|
|
o.Logger.Debugw("invalid display mode value",
|
|
"value", v,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid display mode"),
|
|
),
|
|
"display mode",
|
|
)
|
|
}
|
|
case data.OptionKeyAutoPruneOrphanedRecipients:
|
|
// stored as JSON — validate by parsing
|
|
if _, err := model.NewAutoPruneOptionFromJSON([]byte(v)); err != nil {
|
|
o.Logger.Debugw("invalid auto-prune option value", "value", v)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(errors.New("invalid value")),
|
|
"value",
|
|
)
|
|
}
|
|
case data.OptionKeyReportPDFEnabled:
|
|
if v != "true" && v != "false" {
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(errors.New("invalid value")),
|
|
"value",
|
|
)
|
|
}
|
|
case data.OptionKeyObfuscationTemplate:
|
|
// is allow listed
|
|
default:
|
|
o.Logger.Debugw("invalid settings key", "key", k)
|
|
return validate.WrapErrorWithField(
|
|
errs.NewValidationError(
|
|
errors.New("invalid option"),
|
|
),
|
|
"key",
|
|
)
|
|
}
|
|
// update options
|
|
err = o.OptionRepository.UpdateByKey(
|
|
ctx,
|
|
option,
|
|
)
|
|
if err != nil {
|
|
o.Logger.Errorw("failed to update option by key", "error", err)
|
|
return err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return nil
|
|
}
|
|
|
|
// GetAutoPruneOption returns the single auto-prune option row (requires global auth).
|
|
// Both the global flag and all per-company entries are embedded in the returned value.
|
|
func (o *Option) GetAutoPruneOption(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
) (*model.AutoPruneOption, error) {
|
|
ae := NewAuditEvent("Option.GetAutoPruneOption", session)
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return o.getAutoPruneOption(ctx)
|
|
}
|
|
|
|
// SetAutoPruneOption persists the full auto-prune option as a single JSON row
|
|
// (requires global auth). The caller supplies the complete AutoPruneOption
|
|
// value including any per-company entries.
|
|
func (o *Option) SetAutoPruneOption(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
autoPruneOpt *model.AutoPruneOption,
|
|
) error {
|
|
ae := NewAuditEvent("Option.SetAutoPruneOption", session)
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
if err := o.upsertAutoPruneOption(ctx, autoPruneOpt); err != nil {
|
|
return err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return nil
|
|
}
|
|
|
|
// GetCompanyAutoPruneOption returns whether the given company has opted in to auto-pruning
|
|
// by reading the single shared auto-prune option row (requires global auth).
|
|
func (o *Option) GetCompanyAutoPruneOption(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
companyID *uuid.UUID,
|
|
) (bool, error) {
|
|
ae := NewAuditEvent("Option.GetCompanyAutoPruneOption", session)
|
|
ae.Details["companyID"] = companyID.String()
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return false, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return false, errs.ErrAuthorizationFailed
|
|
}
|
|
opt, err := o.getAutoPruneOption(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return opt.IsCompanyEnabled(companyID), nil
|
|
}
|
|
|
|
// SetCompanyAutoPruneOption updates the per-company enabled flag within the
|
|
// single shared auto-prune option row (requires global auth).
|
|
func (o *Option) SetCompanyAutoPruneOption(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
companyID *uuid.UUID,
|
|
enabled bool,
|
|
) error {
|
|
ae := NewAuditEvent("Option.SetCompanyAutoPruneOption", session)
|
|
ae.Details["companyID"] = companyID.String()
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
// read-modify-write the single row
|
|
opt, err := o.getAutoPruneOption(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opt.SetCompanyEnabled(companyID, enabled)
|
|
if err := o.upsertAutoPruneOption(ctx, opt); err != nil {
|
|
return err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return nil
|
|
}
|
|
|
|
// GetAutoPruneOptionInternal returns the full auto-prune option without any
|
|
// authorization check. intended for internal/task-runner use only.
|
|
func (o *Option) GetAutoPruneOptionInternal(ctx context.Context) (*model.AutoPruneOption, error) {
|
|
return o.getAutoPruneOption(ctx)
|
|
}
|
|
|
|
// getAutoPruneOption reads the single auto-prune option row, returning the
|
|
// default (all-disabled) value when the row does not exist yet.
|
|
func (o *Option) getAutoPruneOption(ctx context.Context) (*model.AutoPruneOption, error) {
|
|
raw, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyAutoPruneOrphanedRecipients)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return model.NewAutoPruneOptionDefault(), nil
|
|
}
|
|
o.Logger.Errorw("failed to get auto-prune option", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
return model.NewAutoPruneOptionFromJSON([]byte(raw.Value.String()))
|
|
}
|
|
|
|
// upsertAutoPruneOption inserts or updates the single auto-prune option row.
|
|
func (o *Option) upsertAutoPruneOption(ctx context.Context, autoPruneOpt *model.AutoPruneOption) error {
|
|
opt, err := autoPruneOpt.ToOption()
|
|
if err != nil {
|
|
return errs.Wrap(err)
|
|
}
|
|
_, getErr := o.OptionRepository.GetByKey(ctx, data.OptionKeyAutoPruneOrphanedRecipients)
|
|
if getErr != nil {
|
|
if !errors.Is(getErr, gorm.ErrRecordNotFound) {
|
|
o.Logger.Errorw("failed to check auto-prune option existence", "error", getErr)
|
|
return errs.Wrap(getErr)
|
|
}
|
|
// row does not exist yet — insert
|
|
if _, insertErr := o.OptionRepository.Insert(ctx, opt); insertErr != nil {
|
|
o.Logger.Errorw("failed to insert auto-prune option", "error", insertErr)
|
|
return errs.Wrap(insertErr)
|
|
}
|
|
return nil
|
|
}
|
|
// row exists — update
|
|
if updateErr := o.OptionRepository.UpdateByKey(ctx, opt); updateErr != nil {
|
|
o.Logger.Errorw("failed to update auto-prune option", "error", updateErr)
|
|
return errs.Wrap(updateErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetObfuscationTemplate gets the obfuscation template from options or returns default
|
|
func (o *Option) GetObfuscationTemplate(ctx context.Context) (string, error) {
|
|
opt, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyObfuscationTemplate)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// return default template if not found
|
|
return data.OptionValueObfuscationTemplateDefault, nil
|
|
}
|
|
o.Logger.Errorw("failed to get obfuscation template option", "error", err)
|
|
return "", errs.Wrap(err)
|
|
}
|
|
template := opt.Value.String()
|
|
if template == "" {
|
|
// return default if empty
|
|
return data.OptionValueObfuscationTemplateDefault, nil
|
|
}
|
|
return template, nil
|
|
}
|
|
|
|
// GetScimDomainInternal returns the configured global SCIM domain without any
|
|
// authorization check. intended for the phishing-server host gate. returns an
|
|
// empty string when SCIM serving is not configured.
|
|
func (o *Option) GetScimDomainInternal(ctx context.Context) (string, error) {
|
|
opt, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyScimDomain)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", nil
|
|
}
|
|
o.Logger.Errorw("failed to get scim domain option", "error", err)
|
|
return "", errs.Wrap(err)
|
|
}
|
|
return opt.Value.String(), nil
|
|
}
|
|
|
|
// GetScimDomain returns the configured global SCIM domain (requires global auth).
|
|
func (o *Option) GetScimDomain(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
) (string, error) {
|
|
ae := NewAuditEvent("Option.GetScimDomain", session)
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return "", errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return "", errs.ErrAuthorizationFailed
|
|
}
|
|
return o.GetScimDomainInternal(ctx)
|
|
}
|
|
|
|
// SetScimDomain persists the global SCIM domain (requires global auth). An empty
|
|
// string disables SCIM serving. A non-empty value must be an existing global
|
|
// domain (one not tied to a company).
|
|
func (o *Option) SetScimDomain(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
domain string,
|
|
) error {
|
|
ae := NewAuditEvent("Option.SetScimDomain", session)
|
|
ae.Details["domain"] = domain
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
// a non-empty value must resolve to an existing global domain
|
|
if domain != "" {
|
|
nameVO, err := vo.NewString255(domain)
|
|
if err != nil {
|
|
return errs.NewValidationError(err)
|
|
}
|
|
d, err := o.DomainRepository.GetByName(ctx, nameVO, &repository.DomainOption{})
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errs.NewValidationError(errors.Errorf("domain %q does not exist", domain))
|
|
}
|
|
o.Logger.Errorw("failed to look up scim domain", "error", err)
|
|
return errs.Wrap(err)
|
|
}
|
|
// only global domains (not tied to a company) may be used for SCIM
|
|
if d.CompanyID.IsSpecified() && !d.CompanyID.IsNull() {
|
|
return errs.NewValidationError(errors.Errorf("domain %q is not a global domain", domain))
|
|
}
|
|
// AiTM proxy domains route traffic to a target site and must never serve
|
|
// the SCIM provisioning API
|
|
if d.ProxyID.IsSpecified() && !d.ProxyID.IsNull() {
|
|
return errs.NewValidationError(errors.Errorf("domain %q is a proxy domain and cannot be used for SCIM", domain))
|
|
}
|
|
}
|
|
if err := o.setScimDomainValue(ctx, domain); err != nil {
|
|
return err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return nil
|
|
}
|
|
|
|
// setScimDomainValue inserts or updates the single SCIM domain option row.
|
|
func (o *Option) setScimDomainValue(ctx context.Context, domain string) error {
|
|
valueVO, err := vo.NewOptionalString1MB(domain)
|
|
if err != nil {
|
|
return errs.NewValidationError(err)
|
|
}
|
|
opt := &model.Option{
|
|
Key: *vo.NewString127Must(data.OptionKeyScimDomain),
|
|
Value: *valueVO,
|
|
}
|
|
_, getErr := o.OptionRepository.GetByKey(ctx, data.OptionKeyScimDomain)
|
|
if getErr != nil {
|
|
if !errors.Is(getErr, gorm.ErrRecordNotFound) {
|
|
o.Logger.Errorw("failed to check scim domain option existence", "error", getErr)
|
|
return errs.Wrap(getErr)
|
|
}
|
|
if _, insertErr := o.OptionRepository.Insert(ctx, opt); insertErr != nil {
|
|
o.Logger.Errorw("failed to insert scim domain option", "error", insertErr)
|
|
return errs.Wrap(insertErr)
|
|
}
|
|
return nil
|
|
}
|
|
if updateErr := o.OptionRepository.UpdateByKey(ctx, opt); updateErr != nil {
|
|
o.Logger.Errorw("failed to update scim domain option", "error", updateErr)
|
|
return errs.Wrap(updateErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// upsertOptionValue inserts or updates a single option row by key.
|
|
func (o *Option) upsertOptionValue(ctx context.Context, key string, value string) error {
|
|
valueVO, err := vo.NewOptionalString1MB(value)
|
|
if err != nil {
|
|
return errs.NewValidationError(err)
|
|
}
|
|
opt := &model.Option{
|
|
Key: *vo.NewString127Must(key),
|
|
Value: *valueVO,
|
|
}
|
|
_, getErr := o.OptionRepository.GetByKey(ctx, key)
|
|
if getErr != nil {
|
|
if !errors.Is(getErr, gorm.ErrRecordNotFound) {
|
|
return errs.Wrap(getErr)
|
|
}
|
|
if _, insertErr := o.OptionRepository.Insert(ctx, opt); insertErr != nil {
|
|
return errs.Wrap(insertErr)
|
|
}
|
|
return nil
|
|
}
|
|
if updateErr := o.OptionRepository.UpdateByKey(ctx, opt); updateErr != nil {
|
|
return errs.Wrap(updateErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetScimSoftDeleteRetentionDaysInternal returns the retention window in days,
|
|
// defaulting when unset or invalid. No authorization check.
|
|
func (o *Option) GetScimSoftDeleteRetentionDaysInternal(ctx context.Context) (int, error) {
|
|
opt, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyScimSoftDeleteRetentionDays)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return data.OptionValueScimSoftDeleteRetentionDaysDefault, nil
|
|
}
|
|
return 0, errs.Wrap(err)
|
|
}
|
|
days, convErr := strconv.Atoi(opt.Value.String())
|
|
if convErr != nil || days < 0 {
|
|
return data.OptionValueScimSoftDeleteRetentionDaysDefault, nil
|
|
}
|
|
return days, nil
|
|
}
|
|
|
|
// GetScimSoftDeleteRetentionDays returns the retention window (requires global auth).
|
|
func (o *Option) GetScimSoftDeleteRetentionDays(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
) (int, error) {
|
|
ae := NewAuditEvent("Option.GetScimSoftDeleteRetentionDays", session)
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return 0, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return 0, errs.ErrAuthorizationFailed
|
|
}
|
|
return o.GetScimSoftDeleteRetentionDaysInternal(ctx)
|
|
}
|
|
|
|
// SetScimSoftDeleteRetentionDays persists the retention window (requires global auth).
|
|
func (o *Option) SetScimSoftDeleteRetentionDays(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
days int,
|
|
) error {
|
|
ae := NewAuditEvent("Option.SetScimSoftDeleteRetentionDays", session)
|
|
ae.Details["days"] = days
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
o.LogAuthError(err)
|
|
return errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
o.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
if days < 0 {
|
|
return errs.NewValidationError(errors.Errorf("retention days must be zero or positive"))
|
|
}
|
|
if err := o.upsertOptionValue(ctx, data.OptionKeyScimSoftDeleteRetentionDays, strconv.Itoa(days)); err != nil {
|
|
return err
|
|
}
|
|
o.AuditLogAuthorized(ae)
|
|
return nil
|
|
}
|