10000 feat: add api for patching custom org roles by Emyrk · Pull Request #13357 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: add api for patching custom org roles #13357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 29, 2024
Prev Previous commit
Next Next commit
fixup test to assign org role instead of site
  • Loading branch information
Emyrk committed May 28, 2024
commit d231cdf02de606c41f9a6d8335db04e0b89ba459
9 changes: 6 additions & 3 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,9 +826,12 @@ func New(options *Options) *API {
})
})
r.Route("/members", func(r chi.Router) {
r.Get("/roles", api.assignableOrgRoles)
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).
Patch("/roles", api.patchOrgRoles)
r.Route("/roles", func(r chi.Router) {
r.Get("/", api.assignableOrgRoles)
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).
Patch("/", api.patchOrgRoles)
})

r.Route("/{user}", func(r chi.Router) {
r.Use(
httpmw.ExtractOrganizationMemberParam(options.Database),
Expand Down
13 changes: 12 additions & 1 deletion coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,14 +600,25 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
customRoles := make([]string, 0)
// Validate that the roles being assigned are valid.
for _, r := range grantedRoles {
_, isOrgRole := rbac.IsOrgRole(r)
roleOrgIDStr, isOrgRole := rbac.IsOrgRole(r)
if shouldBeOrgRoles && !isOrgRole {
return xerrors.Errorf("Must only update org roles")
}
if !shouldBeOrgRoles && isOrgRole {
return xerrors.Errorf("Must only update site wide roles")
}

if shouldBeOrgRoles {
roleOrgID, err := uuid.Parse(roleOrgIDStr)
if err != nil {
return xerrors.Errorf("role %q has invalid uuid for org: %w", r, err)
}

if roleOrgID != *orgID {
return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, (*orgID).String())
}
}

// All roles should be valid roles
if _, err := rbac.RoleByName(r); err != nil {
customRoles = append(customRoles, r)
Expand Down
37 changes: 1 addition & 36 deletions coderd/members.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package coderd

import (
"context"
"net/http"

"github.com/google/uuid"

"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/rbac"

Expand Down Expand Up @@ -48,7 +43,7 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
return
}

updatedUser, err := api.updateOrganizationMemberRoles(ctx, database.UpdateMemberRolesParams{
updatedUser, err := api.Database.UpdateMemberRoles(ctx, database.UpdateMemberRolesParams{
GrantedRoles: params.Roles,
UserID: member.UserID,
OrgID: organization.ID,
Expand All @@ -63,36 +58,6 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, convertOrganizationMember(updatedUser))
}

func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
// Enforce only site wide roles
for _, r := range args.GrantedRoles {
// Must be an org role for the org in the args
orgID, ok := rbac.IsOrgRole(r)
if !ok {
return database.OrganizationMember{}, xerrors.Errorf("must only update organization roles")
}

roleOrg, err := uuid.Parse(orgID)
if err != nil {
return database.OrganizationMember{}, xerrors.Errorf("Role must have proper UUIDs for organization, %q does not", r)
}

if roleOrg != args.OrgID {
return database.OrganizationMember{}, xerrors.Errorf("Must only pass roles for org %q", args.OrgID.String())
}

if _, err := rbac.RoleByName(r); err != nil {
return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported organization role", r)
}
}

updatedUser, err := api.Database.UpdateMemberRoles(ctx, args)
if err != nil {
return database.OrganizationMember{}, xerrors.Errorf("Update site roles: %w", err)
}
return updatedUser, nil
}
Comment on lines -66 to -94
Copy link
Member Author
@Emyrk Emyrk May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all moved to dbauthz. The same is done for site wide roles in dbauthz already.


func convertOrganizationMember(mem database.OrganizationMember) codersdk.OrganizationMember {
convertedMember := codersdk.OrganizationMember{
UserID: mem.UserID,
Expand Down
18 changes: 15 additions & 3 deletions codersdk/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,21 @@ type Role struct {
UserPermissions []Permission `json:"user_permissions" table:"user_permissions"`
}

// PatchRole will upsert a custom site wide role
func (c *Client) PatchRole(ctx context.Context, req Role) (Role, error) {
res, err := c.Request(ctx, http.MethodPatch, "/api/v2/users/roles", req)
// FullName returns the role name scoped to the organization ID. This is useful if
// printing a set of roles from different scopes, as duplicated names across multiple
// scopes will become unique.
// In practice, this is primarily used in testing.
func (r Role) FullName() string {
if r.OrganizationID == "" {
return r.Name
}
return r.Name + ":" + r.OrganizationID
}

// PatchOrganizationRole will upsert a custom organization role
func (c *Client) PatchOrganizationRole(ctx context.Context, organizationID uuid.UUID, req Role) (Role, error) {
res, err := c.Request(ctx, http.MethodPatch,
fmt.Sprintf("/api/v2/organizations/%s/members/roles", organizationID.String()), req)
if err != nil {
return Role{}, err
}
Expand Down
16 changes: 0 additions & 16 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,22 +327,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
})
})

r.Route("/users/roles", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
)
r.Group(func(r chi.Router) {
r.Use(
api.customRolesEnabledMW,
)
r.Patch("/", api.patchRole)
})
// Unfortunate, but this r.Route overrides the AGPL roles route.
// The AGPL does not have the entitlements to block the licensed
// routes, so we need to duplicate the AGPL here.
r.Get("/", api.AGPL.AssignableSiteRoles)
})
Comment on lines -330 to -344
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why that interface was created. This was moved to the /organizations route, and would require duplicating all the routes. So instead the code lives in AGPL and enterprise just patches the interface.


r.Route("/users/{user}/quiet-hours", func(r chi.Router) {
r.Use(
api.autostopRequirementEnabledMW,
Expand Down
77 changes: 0 additions & 77 deletions enterprise/coderd/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,80 +107,3 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context,

return db2sdk.Role(convertedInsert), true
}

// patchRole will allow creating a custom role
//
// @Summary Upsert a custom site-wide role
// @ID upsert-a-custom-site-wide-role
// @Security CoderSessionToken
// @Produce json
// @Tags Members
// @Success 200 {array} codersdk.Role
// @Router /users/roles [patch]
func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var req codersdk.Role
if !httpapi.Read(ctx, rw, r, &req) {
return
}

if err := httpapi.NameValid(req.Name); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid role name",
Detail: err.Error(),
})
return
}

if len(req.OrganizationPermissions) > 0 {
// Org perms should be assigned only in org specific roles. Otherwise,
// it gets complicated to keep track of who can do what.
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request, not allowed to assign organization permissions for a site wide role.",
Detail: "site wide roles may not contain organization specific permissions",
})
return
}

// Make sure all permissions inputted are valid according to our policy.
rbacRole := db2sdk.RoleToRBAC(req)
args, err := rolestore.ConvertRoleToDB(rbacRole)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request",
Detail: err.Error(),
})
return
}

inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
Name: args.Name,
DisplayName: args.DisplayName,
SitePermissions: args.SitePermissions,
OrgPermissions: args.OrgPermissions,
UserPermissions: args.UserPermissions,
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to update role permissions",
Detail: err.Error(),
})
return
}

convertedInsert, err := rolestore.ConvertDBRole(inserted)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Permissions were updated, unable to read them back out of the database.",
Detail: err.Error(),
})
return
}

httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(convertedInsert))
}
Loading
0