Skip to content

Commit 3fd0fc9

Browse files
CENG-569: Add warning and error out on specific cases to avoid repo lockouts (#155)
* Add warning and error out on specific cases to avoid repo lockouts
1 parent a2cacae commit 3fd0fc9

File tree

4 files changed

+166
-25
lines changed

4 files changed

+166
-25
lines changed

cloudsmith/data_source_repository_privileges_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ resource "cloudsmith_service" "test" {
3939
role = "Member"
4040
}
4141
42+
data "cloudsmith_user_self" "current" {}
43+
4244
resource "cloudsmith_repository_privileges" "test" {
4345
organization = cloudsmith_repository.test.namespace
4446
repository = cloudsmith_repository.test.slug
@@ -47,6 +49,12 @@ resource "cloudsmith_repository_privileges" "test" {
4749
privilege = "Read"
4850
slug = cloudsmith_service.test.slug
4951
}
52+
53+
# Include the authenticated account explicitly to satisfy lockout safeguard.
54+
user {
55+
privilege = "Admin"
56+
slug = data.cloudsmith_user_self.current.slug
57+
}
5058
}
5159
5260
data "cloudsmith_repository_privileges" "test_data" {

cloudsmith/resource_repository_privileges.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cloudsmith
33
import (
44
"context"
55
"fmt"
6+
"log"
67
"strings"
78
"time"
89

@@ -20,6 +21,44 @@ var (
2021
}
2122
)
2223

24+
// containsAccountSlug returns true if any privilege entry contains the provided slug
25+
// either as a user or service.
26+
func containsAccountSlug(privs []cloudsmith.RepositoryPrivilegeDict, slug string) bool {
27+
for _, p := range privs {
28+
if p.HasUser() && p.GetUser() == slug {
29+
return true
30+
}
31+
if p.HasService() && p.GetService() == slug {
32+
return true
33+
}
34+
}
35+
return false
36+
}
37+
38+
// containsTeam returns true if any privilege entry references a team.
39+
func containsTeam(privs []cloudsmith.RepositoryPrivilegeDict) bool {
40+
for _, p := range privs {
41+
if p.HasTeam() {
42+
return true
43+
}
44+
}
45+
return false
46+
}
47+
48+
// setContainsSlug returns true if the *schema.Set contains an element whose slug matches key.
49+
func setContainsSlug(set *schema.Set, key string) bool {
50+
if set == nil {
51+
return false
52+
}
53+
for _, x := range set.List() {
54+
m := x.(map[string]interface{})
55+
if m["slug"].(string) == key {
56+
return true
57+
}
58+
}
59+
return false
60+
}
61+
2362
// expandRepositoryPrivilegeServices extracts "services" from TF state as a *schema.Set and converts to
2463
// a slice of structs we can use when interacting with the Cloudsmith API.
2564
func expandRepositoryPrivilegeServices(d *schema.ResourceData) []cloudsmith.RepositoryPrivilegeDict {
@@ -149,12 +188,31 @@ func resourceRepositoryPrivilegesCreateUpdate(d *schema.ResourceData, m interfac
149188
privileges = append(privileges, expandRepositoryPrivilegeTeams(d)...)
150189
privileges = append(privileges, expandRepositoryPrivilegeUsers(d)...)
151190

191+
// Only return an error if the authenticated account is NOT present in any user/service block
192+
// AND there are NO team blocks defined. If team blocks are present, emit a warning only.
193+
userReq := pc.APIClient.UserApi.UserSelf(pc.Auth)
194+
userSelf, _, err := pc.APIClient.UserApi.UserSelfExecute(userReq)
195+
if err != nil {
196+
return fmt.Errorf("error retrieving authenticated account for lockout prevention: %w", err)
197+
}
198+
currentSlug := userSelf.GetSlug()
199+
200+
if !containsAccountSlug(privileges, currentSlug) {
201+
if !containsTeam(privileges) {
202+
return fmt.Errorf(
203+
"repository_privileges (%s.%s): configuration must include authenticated account slug '%s' (user or service block) OR at least one team block to avoid potential lockout",
204+
organization, repository, currentSlug,
205+
)
206+
}
207+
log.Printf("[WARN] repository_privileges (%s.%s): authenticated account slug '%s' not explicitly included via user/service; ensure access via configured teams to avoid lockout.", organization, repository, currentSlug)
208+
}
209+
152210
req := pc.APIClient.ReposApi.ReposPrivilegesUpdate(pc.Auth, organization, repository)
153211
req = req.Data(cloudsmith.RepositoryPrivilegeInputRequest{
154212
Privileges: privileges,
155213
})
156214

157-
_, err := pc.APIClient.ReposApi.ReposPrivilegesUpdateExecute(req)
215+
_, err = pc.APIClient.ReposApi.ReposPrivilegesUpdateExecute(req)
158216
if err != nil {
159217
return err
160218
}
@@ -256,6 +314,47 @@ func resourceRepositoryPrivileges() *schema.Resource {
256314
Update: resourceRepositoryPrivilegesCreateUpdate,
257315
Delete: resourceRepositoryPrivilegesDelete,
258316

317+
// Plan-time validation to surface lockout risk earlier than apply. We still
318+
// keep the apply-time safety net in Create/Update for defense in depth.
319+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
320+
pc := meta.(*providerConfig)
321+
userReq := pc.APIClient.UserApi.UserSelf(pc.Auth)
322+
userSelf, _, err := pc.APIClient.UserApi.UserSelfExecute(userReq)
323+
if err != nil {
324+
// If we cannot determine the current user, defer to apply-time logic.
325+
return nil
326+
}
327+
currentSlug := userSelf.GetSlug()
328+
329+
var userSet *schema.Set
330+
if v, ok := d.GetOk("user"); ok {
331+
userSet = v.(*schema.Set)
332+
}
333+
var serviceSet *schema.Set
334+
if v, ok := d.GetOk("service"); ok {
335+
serviceSet = v.(*schema.Set)
336+
}
337+
var teamSet *schema.Set
338+
if v, ok := d.GetOk("team"); ok {
339+
teamSet = v.(*schema.Set)
340+
}
341+
342+
hasUserOrService := setContainsSlug(userSet, currentSlug) || setContainsSlug(serviceSet, currentSlug)
343+
teamCount := 0
344+
if teamSet != nil {
345+
teamCount = teamSet.Len()
346+
}
347+
348+
if !hasUserOrService {
349+
if teamCount == 0 {
350+
return fmt.Errorf("repository_privileges: authenticated account slug '%s' must be included (user or service block) OR at least one team block must be defined to avoid potential lockout", currentSlug)
351+
}
352+
log.Printf("[WARN] repository_privileges (plan): authenticated account slug '%s' not explicitly included via user/service; ensure team-based access is sufficient to avoid lockout.", currentSlug)
353+
}
354+
355+
return nil
356+
},
357+
259358
Importer: &schema.ResourceImporter{
260359
StateContext: importRepositoryPrivileges,
261360
},

cloudsmith/resource_repository_privileges_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ resource "cloudsmith_service" "test" {
8383
role = "Member"
8484
}
8585
86+
data "cloudsmith_user_self" "current" {}
87+
8688
resource "cloudsmith_repository_privileges" "test" {
8789
organization = cloudsmith_repository.test.namespace
8890
repository = cloudsmith_repository.test.slug
@@ -91,6 +93,12 @@ resource "cloudsmith_repository_privileges" "test" {
9193
privilege = "Read"
9294
slug = cloudsmith_service.test.slug
9395
}
96+
97+
# Include the authenticated account explicitly to satisfy lockout safeguard.
98+
user {
99+
privilege = "Admin"
100+
slug = data.cloudsmith_user_self.current.slug
101+
}
94102
}
95103
`, os.Getenv("CLOUDSMITH_NAMESPACE"))
96104

@@ -106,6 +114,8 @@ resource "cloudsmith_service" "test" {
106114
role = "Member"
107115
}
108116
117+
data "cloudsmith_user_self" "current" {}
118+
109119
resource "cloudsmith_repository_privileges" "test" {
110120
organization = cloudsmith_repository.test.namespace
111121
repository = cloudsmith_repository.test.slug
@@ -114,6 +124,12 @@ resource "cloudsmith_repository_privileges" "test" {
114124
privilege = "Write"
115125
slug = cloudsmith_service.test.slug
116126
}
127+
128+
# Include the authenticated account explicitly to satisfy lockout safeguard.
129+
user {
130+
privilege = "Admin"
131+
slug = data.cloudsmith_user_self.current.slug
132+
}
117133
}
118134
`, os.Getenv("CLOUDSMITH_NAMESPACE"))
119135

docs/resources/repository_privileges.md

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ The repository privileges resource allows the management of privileges for a giv
44

55
Note that while users can be added to repositories in this manner, since Terraform does not (and cannot currently) manage those user accounts, you may encounter issues if the users change or are deleted outside of Terraform.
66

7+
> [!WARNING] Important: When a repository is first created in Cloudsmith, the creating account (user or service account that owns the API key) is automatically granted an implicit Admin privilege.
8+
When you later manage privileges via this resource, you must explicitly include that account (using a `user` or `service` block with the appropriate `slug`). Otherwise, the provider will refuse to apply the change to prevent locking you out. You will still be able to apply changes if a `team` block is present. However, you must make sure that the account has sufficient permission within the team, or lockout can still occur.
9+
710
See [docs.cloudsmith.com](https://docs.cloudsmith.com/repositories/repository-settings#repository-privileges) for full permissions documentation.
811

912
## Example Usage
@@ -25,38 +28,53 @@ resource "cloudsmith_repository" "my_repository" {
2528
}
2629
2730
resource "cloudsmith_team" "my_team" {
28-
organization = data.cloudsmith_organization.my_organization.slug_perm
29-
name = "My Team"
31+
organization = data.cloudsmith_organization.my_organization.slug_perm
32+
name = "My Team"
3033
}
3134
3235
resource "cloudsmith_team" "my_other_team" {
33-
organization = data.cloudsmith_organization.my_organization.slug_perm
34-
name = "My Other Team"
36+
organization = data.cloudsmith_organization.my_organization.slug_perm
37+
name = "My Other Team"
3538
}
3639
3740
resource "cloudsmith_service" "my_service" {
38-
name = "My Service"
39-
organization = data.cloudsmith_organization.my_organization.slug_perm
41+
name = "My Service"
42+
organization = data.cloudsmith_organization.my_organization.slug_perm
4043
}
4144
45+
# This will return a slug for either service or user
46+
data "cloudsmith_user_self" "current" {}
47+
4248
resource "cloudsmith_repository_privileges" "privs" {
4349
organization = data.cloudsmith_organization.my_organization.slug
4450
repository = cloudsmith_repository.my_repository.slug
4551
46-
service {
47-
privilege = "Write"
48-
slug = cloudsmith_service.my_service.slug
49-
}
52+
### Always include the authenticated account to avoid lockout (see note above)
53+
54+
# user {
55+
# privilege = "Admin"
56+
# slug = data.cloudsmith_user_self.current.slug
57+
# }
58+
59+
# service {
60+
# privilege = "Admin"
61+
# slug = data.cloudsmith_user_self.current.slug
62+
# }
63+
64+
service {
65+
privilege = "Write"
66+
slug = cloudsmith_service.my_service.slug
67+
}
5068
51-
team {
52-
privilege = "Write"
53-
slug = cloudsmith_team.my_team.slug
54-
}
69+
team {
70+
privilege = "Write"
71+
slug = cloudsmith_team.my_team.slug
72+
}
5573
56-
team {
57-
privilege = "Read"
58-
slug = cloudsmith_team.my_other_team.slug
59-
}
74+
team {
75+
privilege = "Read"
76+
slug = cloudsmith_team.my_other_team.slug
77+
}
6078
6179
user {
6280
privilege = "Read"
@@ -72,14 +90,14 @@ The following arguments are supported:
7290
* `organization` - (Required) Organization to which this repository belongs.
7391
* `repository` - (Required) Repository to which these privileges apply.
7492
* `service` - (Optional) Variable number of blocks containing service accounts that should have repository privileges.
75-
* `privilege` - (Required) The service's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
76-
* `slug` - (Required) The slug/identifier of the service.
93+
* `privilege` - (Required) The service's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
94+
* `slug` - (Required) The slug/identifier of the service.
7795
* `team` - (Optional) Variable number of blocks containing teams that should have repository privileges.
78-
* `privilege` - (Required) The team's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
79-
* `slug` - (Required) The slug/identifier of the team.
96+
* `privilege` - (Required) The team's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
97+
* `slug` - (Required) The slug/identifier of the team.
8098
* `user` - (Optional) Variable number of blocks containing users that should have repository privileges.
81-
* `privilege` - (Required) The user's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
82-
* `slug` - (Required) The slug/identifier of the user.
99+
* `privilege` - (Required) The user's privilege level in the repository. Must be one of `Admin`, `Write`, or `Read`.
100+
* `slug` - (Required) The slug/identifier of the user.
83101

84102
## Import
85103

0 commit comments

Comments
 (0)