Skip to content

Commit

Permalink
feat: Persistence for service users
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurpitman committed Jan 17, 2025
1 parent f01c659 commit a928904
Show file tree
Hide file tree
Showing 17 changed files with 533 additions and 83 deletions.
30 changes: 23 additions & 7 deletions pkg/account/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,19 @@ func NewAccountManagementResources() *Resources {
}

type (
PolicyId = string
GroupId = string
UserId = string
PolicyLevel = any // either PolicyLevelAccount or PolicyLevelEnvironment is allowed
PolicyId = string
GroupId = string
UserId = string
ServiceUserId = string
PolicyLevel = any // either PolicyLevelAccount or PolicyLevelEnvironment is allowed

Resources struct {
Policies map[PolicyId]Policy
Groups map[GroupId]Group
Users map[UserId]User
Policies map[PolicyId]Policy
Groups map[GroupId]Group
Users map[UserId]User
ServiceUsers map[ServiceUserId]ServiceUser
}

Policy struct {
ID string
Name string
Expand All @@ -46,9 +49,11 @@ type (
Policy string
OriginObjectID string
}

PolicyLevelAccount struct {
Type string
}

PolicyLevelEnvironment struct {
Type string
Environment string
Expand All @@ -64,15 +69,18 @@ type (
ManagementZone []ManagementZone
OriginObjectID string
}

Account struct {
Permissions []string
Policies []Ref
}

Environment struct {
Name string
Permissions []string
Policies []Ref
}

ManagementZone struct {
Environment string
ManagementZone string
Expand All @@ -83,6 +91,14 @@ type (
Email secret.Email
Groups []Ref
}

ServiceUser struct {
Name string
Description string
Groups []Ref
OriginObjectID string
}

Reference struct {
Id string
}
Expand Down
42 changes: 31 additions & 11 deletions pkg/persistence/account/internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package types

import (
"fmt"
jsonutils "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/json"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"

"github.com/invopop/jsonschema"
"github.com/mitchellh/mapstructure"

jsonutils "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/json"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"
)

const (
Expand All @@ -32,15 +34,19 @@ const (

type (
Resources struct {
Policies map[string]Policy
Groups map[string]Group
Users map[string]User
Policies map[string]Policy
Groups map[string]Group
Users map[string]User
ServiceUsers map[string]ServiceUser
}

File struct {
Policies []Policy `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies to configure for this account."`
Groups []Group `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups to configure for this account."`
Users []User `yaml:"users,omitempty" json:"users,omitempty" jsonschema:"description=Users to configure for this account."`
Policies []Policy `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies to configure for this account."`
Groups []Group `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups to configure for this account."`
Users []User `yaml:"users,omitempty" json:"users,omitempty" jsonschema:"description=Users to configure for this account."`
ServiceUsers []ServiceUser `yaml:"service-users,omitempty" json:"serviceUsers,omitempty" jsonschema:"description=Service users to configure for this account."`
}

Policy struct {
ID string `yaml:"id" json:"id" jsonschema:"required,description=A unique identifier of this policy configuration - this can be freely defined, used by monaco."`
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this policy."`
Expand All @@ -49,10 +55,12 @@ type (
Policy string `yaml:"policy" json:"policy" jsonschema:"required,description=The policy definition."`
OriginObjectID string `yaml:"originObjectId,omitempty" json:"originObjectId,omitempty" jsonschema:"description=The identifier of the policy this config originated from - this is filled when downloading, but can also be set to tie a config to a specific object."`
}

PolicyLevel struct {
Type string `yaml:"type" json:"type" jsonschema:"required,enum=account,enum=environment,description=This defines which level this policy applies to - either the whole 'account' or a specific 'environment'. For environment level, the 'environment' field needs to contain the environment ID."`
Environment string `yaml:"environment,omitempty" json:"environment,omitempty" jsonschema:"The ID of the environment this policy applies to. Required if type is 'environment'."`
}

Group struct {
ID string `yaml:"id" json:"id" jsonschema:"required,description=A unique identifier of this group configuration - this can be freely defined, used by monaco."`
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this group."`
Expand All @@ -66,25 +74,36 @@ type (
ManagementZone []ManagementZone `yaml:"managementZones,omitempty" json:"managementZones,omitempty" jsonschema:"description=ManagementZone level permissions that apply to users in this group."`
OriginObjectID string `yaml:"originObjectId,omitempty" json:"originObjectId,omitempty" jsonschema:"description=The identifier of the group this config originated from - this is filled when downloading, but can also be set to tie a config to a specific object."`
}

Account struct {
Permissions []string `yaml:"permissions,omitempty" json:"permissions,omitempty" jsonschema:"description=Permissions for the whole account."`
Policies ReferenceSlice `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies for the whole account."`
}

Environment struct {
Name string `yaml:"environment" json:"environment" jsonschema:"required,description=Name/identifier of the environment."`
Permissions []string `yaml:"permissions,omitempty" json:"permissions,omitempty" jsonschema:"description=Permissions for this environment."`
Policies ReferenceSlice `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies for this environment."`
}

ManagementZone struct {
Environment string `yaml:"environment" json:"environment" jsonschema:"required,description=Name/identifier of the environment the management zone is in."`
ManagementZone string `yaml:"managementZone" json:"managementZone" jsonschema:"required,description=Identifier of the management zone."`
Permissions []string `yaml:"permissions" json:"permissions" jsonschema:"required,description=Permissions for this management zone."`
}

User struct {
Email secret.Email `yaml:"email" json:"email" jsonschema:"required,description=Email address of this user."`
Groups ReferenceSlice `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups this user is part of - either defined by name directly or as a reference to a group configuration."`
}

ServiceUser struct {
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this service user."`
Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"A description of this service user."`
Groups ReferenceSlice `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups this user is part of - either defined by name directly or as a reference to a group configuration."`
OriginObjectID string `yaml:"originObjectId,omitempty" json:"originObjectId,omitempty" jsonschema:"description=The identifier of the service user this config originated from - this is filled when downloading, but can also be set to tie a config to a specific object."`
}

Reference struct {
Type string `yaml:"type" json:"type" mapstructure:"type" jsonschema:"enum=reference"`
Id string `yaml:"id" json:"id" mapstructure:"id" jsonschema:"description=The 'id' of the account configuration being referenced."`
Expand Down Expand Up @@ -149,7 +168,8 @@ func (_ ReferenceSlice) JSONSchema() *jsonschema.Schema {
}

const (
KeyUsers string = "users"
KeyGroups string = "groups"
KeyPolicies string = "policies"
KeyUsers string = "users"
KeyServiceUsers string = "service-users"
KeyGroups string = "groups"
KeyPolicies string = "policies"
)
19 changes: 15 additions & 4 deletions pkg/persistence/account/loader/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/spf13/afero"
"gopkg.in/yaml.v2"

"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/featureflags"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/files"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log/field"
Expand Down Expand Up @@ -55,14 +56,15 @@ func HasAnyAccountKeyDefined(m map[string]any) bool {
return false
}

return m[persistence.KeyUsers] != nil || m[persistence.KeyGroups] != nil || m[persistence.KeyPolicies] != nil
return m[persistence.KeyUsers] != nil || m[persistence.KeyServiceUsers] != nil || m[persistence.KeyGroups] != nil || m[persistence.KeyPolicies] != nil
}

func findAndLoadResources(fs afero.Fs, rootPath string) (*persistence.Resources, error) {
resources := persistence.Resources{
Policies: make(map[string]persistence.Policy),
Groups: make(map[string]persistence.Group),
Users: make(map[string]persistence.User),
Policies: make(map[string]persistence.Policy),
Groups: make(map[string]persistence.Group),
Users: make(map[string]persistence.User),
ServiceUsers: make(map[string]persistence.ServiceUser),
}

yamlFilePaths, err := files.FindYamlFiles(fs, rootPath)
Expand Down Expand Up @@ -153,5 +155,14 @@ func addResourcesFromFile(res persistence.Resources, file persistence.File) erro
res.Users[u.Email.Value()] = u
}

if featureflags.ServiceUsers.Enabled() {
for _, su := range file.ServiceUsers {
if _, exists := res.ServiceUsers[su.Name]; exists {
return fmt.Errorf("found duplicate service user with name %q", su.Name)
}
res.ServiceUsers[su.Name] = su
}
}

return nil
}
96 changes: 67 additions & 29 deletions pkg/persistence/account/loader/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,63 @@
package loader

import (
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/account"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/maps"
"testing"

"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/featureflags"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/account"
)

func TestLoad(t *testing.T) {

var assertGroupLoadedValidFunc = func(t *testing.T, g account.Group) {
assert.Len(t, g.Account.Policies, 1)
assert.Len(t, g.Account.Permissions, 1)
assert.Len(t, g.Environment, 1)
assert.Len(t, g.Environment[0].Policies, 2)
assert.Len(t, g.Environment[0].Permissions, 1)
assert.Len(t, g.ManagementZone, 1)
assert.Len(t, g.ManagementZone[0].Permissions, 1)
}

t.Run("Load single file", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
loaded, err := Load(afero.NewOsFs(), "testdata/valid.yaml")
assert.NoError(t, err)

assert.Len(t, loaded.Users, 1)
_, exists := loaded.Users["[email protected]"]
assert.True(t, exists, "expected user to exist: [email protected]")
assert.Contains(t, loaded.Users, "[email protected]", "expected user to exist: [email protected]")

assert.Len(t, loaded.Groups, 1)
_, exists = loaded.Groups["my-group"]
g, exists := loaded.Groups["my-group"]
assert.True(t, exists, "expected group to exist: my-group")
assertGroupLoadedValidFunc(t, g)

assert.Len(t, loaded.Policies, 1)
_, exists = loaded.Policies["my-policy"]
assert.True(t, exists, "expected policy to exist: my-policy")
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Policies, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Permissions, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Policies, 2)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Permissions, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone[0].Permissions, 1)
assert.Contains(t, loaded.Policies, "my-policy", "expected policy to exist: my-policy")

assert.Len(t, loaded.ServiceUsers, 1)
assert.Contains(t, loaded.ServiceUsers, "Service User 1", "expected service user to exist: Service User 1")
})

t.Run("Load single file - service user feature flag disabled", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "false")
loaded, err := Load(afero.NewOsFs(), "testdata/valid.yaml")
assert.NoError(t, err)

assert.Len(t, loaded.Users, 1)
assert.Contains(t, loaded.Users, "[email protected]", "expected user to exist: [email protected]")

assert.Len(t, loaded.Groups, 1)
g, exists := loaded.Groups["my-group"]
assert.True(t, exists, "expected group to exist: my-group")
assertGroupLoadedValidFunc(t, g)

assert.Len(t, loaded.Policies, 1)
assert.Contains(t, loaded.Policies, "my-policy", "expected policy to exist: my-policy")
assert.Len(t, loaded.ServiceUsers, 0)
})

t.Run("Load single file - with refs", func(t *testing.T) {
Expand All @@ -66,35 +95,31 @@ func TestLoad(t *testing.T) {
})

t.Run("Load multiple files", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
loaded, err := Load(afero.NewOsFs(), "testdata/multi")
assert.NoError(t, err)
assert.Len(t, loaded.Users, 1)
assert.Len(t, loaded.Groups, 1)
assert.Len(t, loaded.Policies, 1)
assert.Len(t, loaded.ServiceUsers, 1)
})

t.Run("Loads origin objectIDs", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
loaded, err := Load(afero.NewOsFs(), "testdata/valid-origin-object-id.yaml")
assert.NoError(t, err)
assert.Len(t, loaded.Users, 1)
_, exists := loaded.Users["[email protected]"]
assert.True(t, exists, "expected user to exist: [email protected]")
assert.Contains(t, loaded.Users, "[email protected]", "expected user to exist: [email protected]")

assert.Len(t, loaded.Groups, 1)
g, exists := loaded.Groups["my-group"]
assert.True(t, exists, "expected group to exist: my-group")
assertGroupLoadedValidFunc(t, g)
assert.Equal(t, "32952350-5e78-476d-ab1a-786dd9d4fe33", g.OriginObjectID, "expected group to be loaded with originObjectID")

assert.Len(t, loaded.Policies, 1)
p, exists := loaded.Policies["my-policy"]
assert.Equal(t, "2338ebda-4aad-4911-96a2-6f60d7c3d2cb", p.OriginObjectID, "expected policy to be loaded with originObjectID")
assert.True(t, exists, "expected policy to exist: my-policy")
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Policies, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Permissions, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Policies, 2)
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Permissions, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone, 1)
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone[0].Permissions, 1)

})

t.Run("Load multiple files but ignore config files", func(t *testing.T) {
Expand Down Expand Up @@ -136,6 +161,12 @@ func TestLoad(t *testing.T) {
assert.Error(t, err)
})

t.Run("Duplicate service user produces error", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
_, err := Load(afero.NewOsFs(), "testdata/duplicate-service-user.yaml")
assert.Error(t, err)
})

t.Run("Missing environment ID for env-level policy produces error", func(t *testing.T) {
_, err := Load(afero.NewOsFs(), "testdata/policy-missing-env-id.yaml")
assert.Error(t, err)
Expand All @@ -156,12 +187,19 @@ func TestLoad(t *testing.T) {
assert.Error(t, err)
})

t.Run("Partial service user definition produces error", func(t *testing.T) {
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
_, err := Load(afero.NewOsFs(), "testdata/partial-service-user.yaml")
assert.Error(t, err)
})

t.Run("root folder not found", func(t *testing.T) {
result, err := Load(afero.NewOsFs(), "testdata/non-existent-folder")
assert.Equal(t, &account.Resources{
Policies: make(map[string]account.Policy, 0),
Groups: make(map[string]account.Group, 0),
Users: make(map[string]account.User, 0),
Policies: make(map[string]account.Policy, 0),
Groups: make(map[string]account.Group, 0),
Users: make(map[string]account.User, 0),
ServiceUsers: make(map[string]account.ServiceUser, 0),
}, result)
assert.NoError(t, err)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ policies:
policy: |-
ALLOW a:b:c;
service-users:
- name: Service User 1
description: Description
groups:
- type: reference
id: my-group
- Log viewer


configs:
- id: something
omit: other-values
Loading

0 comments on commit a928904

Please sign in to comment.