Skip to content

Commit a928904

Browse files
committed
feat: Persistence for service users
1 parent f01c659 commit a928904

File tree

17 files changed

+533
-83
lines changed

17 files changed

+533
-83
lines changed

pkg/account/resources.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ func NewAccountManagementResources() *Resources {
2828
}
2929

3030
type (
31-
PolicyId = string
32-
GroupId = string
33-
UserId = string
34-
PolicyLevel = any // either PolicyLevelAccount or PolicyLevelEnvironment is allowed
31+
PolicyId = string
32+
GroupId = string
33+
UserId = string
34+
ServiceUserId = string
35+
PolicyLevel = any // either PolicyLevelAccount or PolicyLevelEnvironment is allowed
3536

3637
Resources struct {
37-
Policies map[PolicyId]Policy
38-
Groups map[GroupId]Group
39-
Users map[UserId]User
38+
Policies map[PolicyId]Policy
39+
Groups map[GroupId]Group
40+
Users map[UserId]User
41+
ServiceUsers map[ServiceUserId]ServiceUser
4042
}
43+
4144
Policy struct {
4245
ID string
4346
Name string
@@ -46,9 +49,11 @@ type (
4649
Policy string
4750
OriginObjectID string
4851
}
52+
4953
PolicyLevelAccount struct {
5054
Type string
5155
}
56+
5257
PolicyLevelEnvironment struct {
5358
Type string
5459
Environment string
@@ -64,15 +69,18 @@ type (
6469
ManagementZone []ManagementZone
6570
OriginObjectID string
6671
}
72+
6773
Account struct {
6874
Permissions []string
6975
Policies []Ref
7076
}
77+
7178
Environment struct {
7279
Name string
7380
Permissions []string
7481
Policies []Ref
7582
}
83+
7684
ManagementZone struct {
7785
Environment string
7886
ManagementZone string
@@ -83,6 +91,14 @@ type (
8391
Email secret.Email
8492
Groups []Ref
8593
}
94+
95+
ServiceUser struct {
96+
Name string
97+
Description string
98+
Groups []Ref
99+
OriginObjectID string
100+
}
101+
86102
Reference struct {
87103
Id string
88104
}

pkg/persistence/account/internal/types/types.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ package types
1818

1919
import (
2020
"fmt"
21-
jsonutils "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/json"
22-
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"
21+
2322
"github.com/invopop/jsonschema"
2423
"github.com/mitchellh/mapstructure"
24+
25+
jsonutils "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/json"
26+
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"
2527
)
2628

2729
const (
@@ -32,15 +34,19 @@ const (
3234

3335
type (
3436
Resources struct {
35-
Policies map[string]Policy
36-
Groups map[string]Group
37-
Users map[string]User
37+
Policies map[string]Policy
38+
Groups map[string]Group
39+
Users map[string]User
40+
ServiceUsers map[string]ServiceUser
3841
}
42+
3943
File struct {
40-
Policies []Policy `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies to configure for this account."`
41-
Groups []Group `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups to configure for this account."`
42-
Users []User `yaml:"users,omitempty" json:"users,omitempty" jsonschema:"description=Users to configure for this account."`
44+
Policies []Policy `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies to configure for this account."`
45+
Groups []Group `yaml:"groups,omitempty" json:"groups,omitempty" jsonschema:"description=Groups to configure for this account."`
46+
Users []User `yaml:"users,omitempty" json:"users,omitempty" jsonschema:"description=Users to configure for this account."`
47+
ServiceUsers []ServiceUser `yaml:"service-users,omitempty" json:"serviceUsers,omitempty" jsonschema:"description=Service users to configure for this account."`
4348
}
49+
4450
Policy struct {
4551
ID string `yaml:"id" json:"id" jsonschema:"required,description=A unique identifier of this policy configuration - this can be freely defined, used by monaco."`
4652
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this policy."`
@@ -49,10 +55,12 @@ type (
4955
Policy string `yaml:"policy" json:"policy" jsonschema:"required,description=The policy definition."`
5056
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."`
5157
}
58+
5259
PolicyLevel struct {
5360
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."`
5461
Environment string `yaml:"environment,omitempty" json:"environment,omitempty" jsonschema:"The ID of the environment this policy applies to. Required if type is 'environment'."`
5562
}
63+
5664
Group struct {
5765
ID string `yaml:"id" json:"id" jsonschema:"required,description=A unique identifier of this group configuration - this can be freely defined, used by monaco."`
5866
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this group."`
@@ -66,25 +74,36 @@ type (
6674
ManagementZone []ManagementZone `yaml:"managementZones,omitempty" json:"managementZones,omitempty" jsonschema:"description=ManagementZone level permissions that apply to users in this group."`
6775
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."`
6876
}
77+
6978
Account struct {
7079
Permissions []string `yaml:"permissions,omitempty" json:"permissions,omitempty" jsonschema:"description=Permissions for the whole account."`
7180
Policies ReferenceSlice `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies for the whole account."`
7281
}
82+
7383
Environment struct {
7484
Name string `yaml:"environment" json:"environment" jsonschema:"required,description=Name/identifier of the environment."`
7585
Permissions []string `yaml:"permissions,omitempty" json:"permissions,omitempty" jsonschema:"description=Permissions for this environment."`
7686
Policies ReferenceSlice `yaml:"policies,omitempty" json:"policies,omitempty" jsonschema:"description=Policies for this environment."`
7787
}
88+
7889
ManagementZone struct {
7990
Environment string `yaml:"environment" json:"environment" jsonschema:"required,description=Name/identifier of the environment the management zone is in."`
8091
ManagementZone string `yaml:"managementZone" json:"managementZone" jsonschema:"required,description=Identifier of the management zone."`
8192
Permissions []string `yaml:"permissions" json:"permissions" jsonschema:"required,description=Permissions for this management zone."`
8293
}
94+
8395
User struct {
8496
Email secret.Email `yaml:"email" json:"email" jsonschema:"required,description=Email address of this user."`
8597
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."`
8698
}
8799

100+
ServiceUser struct {
101+
Name string `yaml:"name" json:"name" jsonschema:"required,description=The name of this service user."`
102+
Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"A description of this service user."`
103+
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."`
104+
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."`
105+
}
106+
88107
Reference struct {
89108
Type string `yaml:"type" json:"type" mapstructure:"type" jsonschema:"enum=reference"`
90109
Id string `yaml:"id" json:"id" mapstructure:"id" jsonschema:"description=The 'id' of the account configuration being referenced."`
@@ -149,7 +168,8 @@ func (_ ReferenceSlice) JSONSchema() *jsonschema.Schema {
149168
}
150169

151170
const (
152-
KeyUsers string = "users"
153-
KeyGroups string = "groups"
154-
KeyPolicies string = "policies"
171+
KeyUsers string = "users"
172+
KeyServiceUsers string = "service-users"
173+
KeyGroups string = "groups"
174+
KeyPolicies string = "policies"
155175
)

pkg/persistence/account/loader/load.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/spf13/afero"
2323
"gopkg.in/yaml.v2"
2424

25+
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/featureflags"
2526
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/files"
2627
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log"
2728
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log/field"
@@ -55,14 +56,15 @@ func HasAnyAccountKeyDefined(m map[string]any) bool {
5556
return false
5657
}
5758

58-
return m[persistence.KeyUsers] != nil || m[persistence.KeyGroups] != nil || m[persistence.KeyPolicies] != nil
59+
return m[persistence.KeyUsers] != nil || m[persistence.KeyServiceUsers] != nil || m[persistence.KeyGroups] != nil || m[persistence.KeyPolicies] != nil
5960
}
6061

6162
func findAndLoadResources(fs afero.Fs, rootPath string) (*persistence.Resources, error) {
6263
resources := persistence.Resources{
63-
Policies: make(map[string]persistence.Policy),
64-
Groups: make(map[string]persistence.Group),
65-
Users: make(map[string]persistence.User),
64+
Policies: make(map[string]persistence.Policy),
65+
Groups: make(map[string]persistence.Group),
66+
Users: make(map[string]persistence.User),
67+
ServiceUsers: make(map[string]persistence.ServiceUser),
6668
}
6769

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

158+
if featureflags.ServiceUsers.Enabled() {
159+
for _, su := range file.ServiceUsers {
160+
if _, exists := res.ServiceUsers[su.Name]; exists {
161+
return fmt.Errorf("found duplicate service user with name %q", su.Name)
162+
}
163+
res.ServiceUsers[su.Name] = su
164+
}
165+
}
166+
156167
return nil
157168
}

pkg/persistence/account/loader/load_test.go

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,63 @@
1818
package loader
1919

2020
import (
21-
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/account"
21+
"testing"
22+
2223
"github.com/spf13/afero"
2324
"github.com/stretchr/testify/assert"
24-
"golang.org/x/exp/maps"
25-
"testing"
25+
26+
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/featureflags"
27+
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/account"
2628
)
2729

2830
func TestLoad(t *testing.T) {
31+
32+
var assertGroupLoadedValidFunc = func(t *testing.T, g account.Group) {
33+
assert.Len(t, g.Account.Policies, 1)
34+
assert.Len(t, g.Account.Permissions, 1)
35+
assert.Len(t, g.Environment, 1)
36+
assert.Len(t, g.Environment[0].Policies, 2)
37+
assert.Len(t, g.Environment[0].Permissions, 1)
38+
assert.Len(t, g.ManagementZone, 1)
39+
assert.Len(t, g.ManagementZone[0].Permissions, 1)
40+
}
41+
2942
t.Run("Load single file", func(t *testing.T) {
43+
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
3044
loaded, err := Load(afero.NewOsFs(), "testdata/valid.yaml")
3145
assert.NoError(t, err)
46+
3247
assert.Len(t, loaded.Users, 1)
33-
_, exists := loaded.Users["monaco@dynatrace.com"]
34-
assert.True(t, exists, "expected user to exist: monaco@dynatrace.com")
48+
assert.Contains(t, loaded.Users, "monaco@dynatrace.com", "expected user to exist: monaco@dynatrace.com")
49+
3550
assert.Len(t, loaded.Groups, 1)
36-
_, exists = loaded.Groups["my-group"]
51+
g, exists := loaded.Groups["my-group"]
3752
assert.True(t, exists, "expected group to exist: my-group")
53+
assertGroupLoadedValidFunc(t, g)
54+
3855
assert.Len(t, loaded.Policies, 1)
39-
_, exists = loaded.Policies["my-policy"]
40-
assert.True(t, exists, "expected policy to exist: my-policy")
41-
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Policies, 1)
42-
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Permissions, 1)
43-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment, 1)
44-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Policies, 2)
45-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Permissions, 1)
46-
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone, 1)
47-
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone[0].Permissions, 1)
56+
assert.Contains(t, loaded.Policies, "my-policy", "expected policy to exist: my-policy")
57+
58+
assert.Len(t, loaded.ServiceUsers, 1)
59+
assert.Contains(t, loaded.ServiceUsers, "Service User 1", "expected service user to exist: Service User 1")
60+
})
61+
62+
t.Run("Load single file - service user feature flag disabled", func(t *testing.T) {
63+
t.Setenv(featureflags.ServiceUsers.EnvName(), "false")
64+
loaded, err := Load(afero.NewOsFs(), "testdata/valid.yaml")
65+
assert.NoError(t, err)
66+
67+
assert.Len(t, loaded.Users, 1)
68+
assert.Contains(t, loaded.Users, "monaco@dynatrace.com", "expected user to exist: monaco@dynatrace.com")
4869

70+
assert.Len(t, loaded.Groups, 1)
71+
g, exists := loaded.Groups["my-group"]
72+
assert.True(t, exists, "expected group to exist: my-group")
73+
assertGroupLoadedValidFunc(t, g)
74+
75+
assert.Len(t, loaded.Policies, 1)
76+
assert.Contains(t, loaded.Policies, "my-policy", "expected policy to exist: my-policy")
77+
assert.Len(t, loaded.ServiceUsers, 0)
4978
})
5079

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

6897
t.Run("Load multiple files", func(t *testing.T) {
98+
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
6999
loaded, err := Load(afero.NewOsFs(), "testdata/multi")
70100
assert.NoError(t, err)
71101
assert.Len(t, loaded.Users, 1)
72102
assert.Len(t, loaded.Groups, 1)
73103
assert.Len(t, loaded.Policies, 1)
104+
assert.Len(t, loaded.ServiceUsers, 1)
74105
})
75106

76107
t.Run("Loads origin objectIDs", func(t *testing.T) {
108+
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
77109
loaded, err := Load(afero.NewOsFs(), "testdata/valid-origin-object-id.yaml")
78110
assert.NoError(t, err)
79-
assert.Len(t, loaded.Users, 1)
80-
_, exists := loaded.Users["monaco@dynatrace.com"]
81-
assert.True(t, exists, "expected user to exist: monaco@dynatrace.com")
111+
assert.Contains(t, loaded.Users, "monaco@dynatrace.com", "expected user to exist: monaco@dynatrace.com")
112+
82113
assert.Len(t, loaded.Groups, 1)
83114
g, exists := loaded.Groups["my-group"]
84115
assert.True(t, exists, "expected group to exist: my-group")
116+
assertGroupLoadedValidFunc(t, g)
85117
assert.Equal(t, "32952350-5e78-476d-ab1a-786dd9d4fe33", g.OriginObjectID, "expected group to be loaded with originObjectID")
118+
86119
assert.Len(t, loaded.Policies, 1)
87120
p, exists := loaded.Policies["my-policy"]
88121
assert.Equal(t, "2338ebda-4aad-4911-96a2-6f60d7c3d2cb", p.OriginObjectID, "expected policy to be loaded with originObjectID")
89122
assert.True(t, exists, "expected policy to exist: my-policy")
90-
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Policies, 1)
91-
assert.Len(t, maps.Values(loaded.Groups)[0].Account.Permissions, 1)
92-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment, 1)
93-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Policies, 2)
94-
assert.Len(t, maps.Values(loaded.Groups)[0].Environment[0].Permissions, 1)
95-
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone, 1)
96-
assert.Len(t, maps.Values(loaded.Groups)[0].ManagementZone[0].Permissions, 1)
97-
98123
})
99124

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

164+
t.Run("Duplicate service user produces error", func(t *testing.T) {
165+
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
166+
_, err := Load(afero.NewOsFs(), "testdata/duplicate-service-user.yaml")
167+
assert.Error(t, err)
168+
})
169+
139170
t.Run("Missing environment ID for env-level policy produces error", func(t *testing.T) {
140171
_, err := Load(afero.NewOsFs(), "testdata/policy-missing-env-id.yaml")
141172
assert.Error(t, err)
@@ -156,12 +187,19 @@ func TestLoad(t *testing.T) {
156187
assert.Error(t, err)
157188
})
158189

190+
t.Run("Partial service user definition produces error", func(t *testing.T) {
191+
t.Setenv(featureflags.ServiceUsers.EnvName(), "true")
192+
_, err := Load(afero.NewOsFs(), "testdata/partial-service-user.yaml")
193+
assert.Error(t, err)
194+
})
195+
159196
t.Run("root folder not found", func(t *testing.T) {
160197
result, err := Load(afero.NewOsFs(), "testdata/non-existent-folder")
161198
assert.Equal(t, &account.Resources{
162-
Policies: make(map[string]account.Policy, 0),
163-
Groups: make(map[string]account.Group, 0),
164-
Users: make(map[string]account.User, 0),
199+
Policies: make(map[string]account.Policy, 0),
200+
Groups: make(map[string]account.Group, 0),
201+
Users: make(map[string]account.User, 0),
202+
ServiceUsers: make(map[string]account.ServiceUser, 0),
165203
}, result)
166204
assert.NoError(t, err)
167205
})

pkg/persistence/account/loader/testdata/configs-accounts-mixed.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ policies:
4040
policy: |-
4141
ALLOW a:b:c;
4242
43+
service-users:
44+
- name: Service User 1
45+
description: Description
46+
groups:
47+
- type: reference
48+
id: my-group
49+
- Log viewer
50+
51+
4352
configs:
4453
- id: something
4554
omit: other-values

0 commit comments

Comments
 (0)