Skip to content

Commit eaead32

Browse files
committed
feat: implement patch and get api methods for role sync
1 parent 7139374 commit eaead32

File tree

6 files changed

+260
-18
lines changed

6 files changed

+260
-18
lines changed

coderd/idpsync/idpsync.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ type IDPSync interface {
5454
SiteRoleSyncEnabled() bool
5555
// RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for
5656
// rational.
57-
RoleSyncSettings() runtimeconfig.RuntimeEntry[*RoleSyncSettings]
57+
RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error)
58+
UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error
5859
// ParseRoleClaims takes claims from an OIDC provider, and returns the params
5960
// for role syncing. Most of the logic happens in SyncRoles.
6061
ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError)

coderd/idpsync/role.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/coder/v2/coderd/rbac/rolestore"
1717
"github.com/coder/coder/v2/coderd/runtimeconfig"
1818
"github.com/coder/coder/v2/coderd/util/slice"
19+
"github.com/coder/coder/v2/codersdk"
1920
)
2021

2122
type RoleParams struct {
@@ -41,8 +42,26 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool {
4142
return false
4243
}
4344

44-
func (s AGPLIDPSync) RoleSyncSettings() runtimeconfig.RuntimeEntry[*RoleSyncSettings] {
45-
return s.Role
45+
func (s AGPLIDPSync) UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error {
46+
orgResolver := s.Manager.OrganizationResolver(db, orgID)
47+
err := s.SyncSettings.Role.SetRuntimeValue(ctx, orgResolver, &settings)
48+
if err != nil {
49+
return xerrors.Errorf("update role sync settings: %w", err)
50+
}
51+
52+
return nil
53+
}
54+
55+
func (s AGPLIDPSync) RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error) {
56+
rlv := s.Manager.OrganizationResolver(db, orgID)
57+
settings, err := s.Role.Resolve(ctx, rlv)
58+
if err != nil {
59+
if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) {
60+
return nil, xerrors.Errorf("resolve role sync settings: %w", err)
61+
}
62+
return &RoleSyncSettings{}, nil
63+
}
64+
return settings, nil
4665
}
4766

4867
func (s AGPLIDPSync) ParseRoleClaims(_ context.Context, _ jwt.MapClaims) (RoleParams, *HTTPError) {
@@ -85,15 +104,12 @@ func (s AGPLIDPSync) SyncRoles(ctx context.Context, db database.Store, user data
85104
allExpected := make([]rbac.RoleIdentifier, 0)
86105
for _, member := range orgMemberships {
87106
orgID := member.OrganizationMember.OrganizationID
88-
orgResolver := s.Manager.OrganizationResolver(tx, orgID)
89-
settings, err := s.RoleSyncSettings().Resolve(ctx, orgResolver)
107+
settings, err := s.RoleSyncSettings(ctx, orgID, tx)
90108
if err != nil {
91-
if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) {
92-
return xerrors.Errorf("resolve group sync settings: %w", err)
93-
}
94109
// No entry means no role syncing for this organization
95110
continue
96111
}
112+
97113
if settings.Field == "" {
98114
// Explicitly disabled role sync for this organization
99115
continue
@@ -261,14 +277,7 @@ func (AGPLIDPSync) RolesFromClaim(field string, claims jwt.MapClaims) ([]string,
261277
return parsedRoles, nil
262278
}
263279

264-
type RoleSyncSettings struct {
265-
// Field selects the claim field to be used as the created user's
266-
// groups. If the group field is the empty string, then no group updates
267-
// will ever come from the OIDC provider.
268-
Field string `json:"field"`
269-
// Mapping maps from an OIDC group --> Coder organization role
270-
Mapping map[string][]string `json:"mapping"`
271-
}
280+
type RoleSyncSettings codersdk.RoleSyncSettings
272281

273282
func (s *RoleSyncSettings) Set(v string) error {
274283
return json.Unmarshal([]byte(v), s)

codersdk/idpsync.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,40 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re
6060
var resp GroupSyncSettings
6161
return resp, json.NewDecoder(res.Body).Decode(&resp)
6262
}
63+
64+
type RoleSyncSettings struct {
65+
// Field selects the claim field to be used as the created user's
66+
// groups. If the group field is the empty string, then no group updates
67+
// will ever come from the OIDC provider.
68+
Field string `json:"field"`
69+
// Mapping maps from an OIDC group --> Coder organization role
70+
Mapping map[string][]string `json:"mapping"`
71+
}
72+
73+
func (c *Client) RoleIDPSyncSettings(ctx context.Context, orgID string) (RoleSyncSettings, error) {
74+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles", orgID), nil)
75+
if err != nil {
76+
return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err)
77+
}
78+
defer res.Body.Close()
79+
80+
if res.StatusCode != http.StatusOK {
81+
return RoleSyncSettings{}, ReadBodyAsError(res)
82+
}
83+
var resp RoleSyncSettings
84+
return resp, json.NewDecoder(res.Body).Decode(&resp)
85+
}
86+
87+
func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req RoleSyncSettings) (RoleSyncSettings, error) {
88+
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles", orgID), req)
89+
if err != nil {
90+
return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err)
91+
}
92+
defer res.Body.Close()
93+
94+
if res.StatusCode != http.StatusOK {
95+
return RoleSyncSettings{}, ReadBodyAsError(res)
96+
}
97+
var resp RoleSyncSettings
98+
return resp, json.NewDecoder(res.Body).Decode(&resp)
99+
}

enterprise/coderd/coderd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,18 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
286286
r.Post("/organizations/{organization}/members/roles", api.postOrgRoles)
287287
r.Put("/organizations/{organization}/members/roles", api.putOrgRoles)
288288
r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole)
289+
})
290+
291+
r.Group(func(r chi.Router) {
292+
r.Use(
293+
apiKeyMiddleware,
294+
httpmw.ExtractOrganizationParam(api.Database),
295+
)
289296
r.Route("/organizations/{organization}/settings", func(r chi.Router) {
290297
r.Get("/idpsync/groups", api.groupIDPSyncSettings)
291298
r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings)
299+
r.Get("/idpsync/roles", api.roleIDPSyncSettings)
300+
r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings)
292301
})
293302
})
294303

enterprise/coderd/idpsync.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
// @Produce json
1818
// @Tags Enterprise
1919
// @Param organization path string true "Organization ID" format(uuid)
20-
// @Success 200 {object} idpsync.GroupSyncSettings
20+
// @Success 200 {object} codersdk.GroupSyncSettings
2121
// @Router /organizations/{organization}/settings/idpsync/groups [get]
2222
func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
2323
ctx := r.Context()
@@ -45,7 +45,7 @@ func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
4545
// @Produce json
4646
// @Tags Enterprise
4747
// @Param organization path string true "Organization ID" format(uuid)
48-
// @Success 200 {object} idpsync.GroupSyncSettings
48+
// @Success 200 {object} codersdk.GroupSyncSettings
4949
// @Router /organizations/{organization}/settings/idpsync/groups [patch]
5050
func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
5151
ctx := r.Context()
@@ -77,3 +77,70 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques
7777

7878
httpapi.Write(ctx, rw, http.StatusOK, settings)
7979
}
80+
81+
// @Summary Get role IdP Sync settings by organization
82+
// @ID get-role-idp-sync-settings-by-organization
83+
// @Security CoderSessionToken
84+
// @Produce json
85+
// @Tags Enterprise
86+
// @Param organization path string true "Organization ID" format(uuid)
87+
// @Success 200 {object} codersdk.RoleSyncSettings
88+
// @Router /organizations/{organization}/settings/idpsync/roles [get]
89+
func (api *API) roleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
90+
ctx := r.Context()
91+
org := httpmw.OrganizationParam(r)
92+
93+
if !api.Authorize(r, policy.ActionRead, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) {
94+
httpapi.Forbidden(rw)
95+
return
96+
}
97+
98+
//nolint:gocritic // Requires system context to read runtime config
99+
sysCtx := dbauthz.AsSystemRestricted(ctx)
100+
settings, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, api.Database)
101+
if err != nil {
102+
httpapi.InternalServerError(rw, err)
103+
return
104+
}
105+
106+
httpapi.Write(ctx, rw, http.StatusOK, settings)
107+
}
108+
109+
// @Summary Update role IdP Sync settings by organization
110+
// @ID update-role-idp-sync-settings-by-organization
111+
// @Security CoderSessionToken
112+
// @Produce json
113+
// @Tags Enterprise
114+
// @Param organization path string true "Organization ID" format(uuid)
115+
// @Success 200 {object} codersdk.RoleSyncSettings
116+
// @Router /organizations/{organization}/settings/idpsync/roles [patch]
117+
func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) {
118+
ctx := r.Context()
119+
org := httpmw.OrganizationParam(r)
120+
121+
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) {
122+
httpapi.Forbidden(rw)
123+
return
124+
}
125+
126+
var req idpsync.RoleSyncSettings
127+
if !httpapi.Read(ctx, rw, r, &req) {
128+
return
129+
}
130+
131+
//nolint:gocritic // Requires system context to update runtime config
132+
sysCtx := dbauthz.AsSystemRestricted(ctx)
133+
err := api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, req)
134+
if err != nil {
135+
httpapi.InternalServerError(rw, err)
136+
return
137+
}
138+
139+
settings, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, api.Database)
140+
if err != nil {
141+
httpapi.InternalServerError(rw, err)
142+
return
143+
}
144+
145+
httpapi.Write(ctx, rw, http.StatusOK, settings)
146+
}

enterprise/coderd/idpsync_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,122 @@ func TestPostGroupSyncConfig(t *testing.T) {
170170
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
171171
})
172172
}
173+
174+
func TestGetRoleSyncConfig(t *testing.T) {
175+
t.Parallel()
176+
177+
t.Run("OK", func(t *testing.T) {
178+
t.Parallel()
179+
180+
dv := coderdtest.DeploymentValues(t)
181+
dv.Experiments = []string{
182+
string(codersdk.ExperimentCustomRoles),
183+
string(codersdk.ExperimentMultiOrganization),
184+
}
185+
186+
owner, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
187+
Options: &coderdtest.Options{
188+
DeploymentValues: dv,
189+
},
190+
LicenseOptions: &coderdenttest.LicenseOptions{
191+
Features: license.Features{
192+
codersdk.FeatureCustomRoles: 1,
193+
codersdk.FeatureMultipleOrganizations: 1,
194+
},
195+
},
196+
})
197+
orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.ScopedRoleOrgAdmin(user.OrganizationID))
198+
199+
ctx := testutil.Context(t, testutil.WaitShort)
200+
settings, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{
201+
Field: "august",
202+
Mapping: map[string][]string{
203+
"foo": {"bar"},
204+
},
205+
})
206+
require.NoError(t, err)
207+
require.Equal(t, "august", settings.Field)
208+
require.Equal(t, map[string][]string{"foo": {"bar"}}, settings.Mapping)
209+
210+
settings, err = orgAdmin.RoleIDPSyncSettings(ctx, user.OrganizationID.String())
211+
require.NoError(t, err)
212+
require.Equal(t, "august", settings.Field)
213+
require.Equal(t, map[string][]string{"foo": {"bar"}}, settings.Mapping)
214+
})
215+
}
216+
217+
func TestPostRoleSyncConfig(t *testing.T) {
218+
t.Parallel()
219+
220+
t.Run("OK", func(t *testing.T) {
221+
t.Parallel()
222+
223+
dv := coderdtest.DeploymentValues(t)
224+
dv.Experiments = []string{
225+
string(codersdk.ExperimentCustomRoles),
226+
string(codersdk.ExperimentMultiOrganization),
227+
}
228+
229+
owner, user := coderdenttest.New(t, &coderdenttest.Options{
230+
Options: &coderdtest.Options{
231+
DeploymentValues: dv,
232+
},
233+
LicenseOptions: &coderdenttest.LicenseOptions{
234+
Features: license.Features{
235+
codersdk.FeatureCustomRoles: 1,
236+
codersdk.FeatureMultipleOrganizations: 1,
237+
},
238+
},
239+
})
240+
241+
orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.ScopedRoleOrgAdmin(user.OrganizationID))
242+
243+
// Test as org admin
244+
ctx := testutil.Context(t, testutil.WaitShort)
245+
settings, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{
246+
Field: "august",
247+
})
248+
require.NoError(t, err)
249+
require.Equal(t, "august", settings.Field)
250+
251+
fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, user.OrganizationID.String())
252+
require.NoError(t, err)
253+
require.Equal(t, "august", fetchedSettings.Field)
254+
})
255+
256+
t.Run("NotAuthorized", func(t *testing.T) {
257+
t.Parallel()
258+
259+
dv := coderdtest.DeploymentValues(t)
260+
dv.Experiments = []string{
261+
string(codersdk.ExperimentCustomRoles),
262+
string(codersdk.ExperimentMultiOrganization),
263+
}
264+
265+
owner, user := coderdenttest.New(t, &coderdenttest.Options{
266+
Options: &coderdtest.Options{
267+
DeploymentValues: dv,
268+
},
269+
LicenseOptions: &coderdenttest.LicenseOptions{
270+
Features: license.Features{
271+
codersdk.FeatureCustomRoles: 1,
272+
codersdk.FeatureMultipleOrganizations: 1,
273+
},
274+
},
275+
})
276+
277+
member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID)
278+
279+
ctx := testutil.Context(t, testutil.WaitShort)
280+
_, err := member.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{
281+
Field: "august",
282+
})
283+
var apiError *codersdk.Error
284+
require.ErrorAs(t, err, &apiError)
285+
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
286+
287+
_, err = member.RoleIDPSyncSettings(ctx, user.OrganizationID.String())
288+
require.ErrorAs(t, err, &apiError)
289+
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
290+
})
291+
}

0 commit comments

Comments
 (0)