package coderd import ( "context" "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/codersdk" ) // postOrgRoles will allow creating a custom organization role // // @Summary Insert a custom organization role // @ID insert-a-custom-organization-role // @Security CoderSessionToken // @Accept json // @Produce json // @Param organization path string true "Organization ID" format(uuid) // @Param request body codersdk.CustomRoleRequest true "Insert role request" // @Tags Members // @Success 200 {array} codersdk.Role // @Router /organizations/{organization}/members/roles [post] func (api *API) postOrgRoles(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() db = api.Database auditor = api.AGPL.Auditor.Load() organization = httpmw.OrganizationParam(r) aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionCreate, OrganizationID: organization.ID, }) ) defer commitAudit() var req codersdk.CustomRoleRequest if !httpapi.Read(ctx, rw, r, &req) { return } if !validOrganizationRoleRequest(ctx, req, rw) { return } inserted, err := db.InsertCustomRole(ctx, database.InsertCustomRoleParams{ Name: req.Name, DisplayName: req.DisplayName, OrganizationID: uuid.NullUUID{ UUID: organization.ID, Valid: true, }, SitePermissions: db2sdk.List(req.SitePermissions, sdkPermissionToDB), OrgPermissions: db2sdk.List(req.OrganizationPermissions, sdkPermissionToDB), UserPermissions: db2sdk.List(req.UserPermissions, sdkPermissionToDB), }) 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 } aReq.New = inserted httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(inserted)) } // patchRole will allow creating a custom organization role // // @Summary Upsert a custom organization role // @ID upsert-a-custom-organization-role // @Security CoderSessionToken // @Accept json // @Produce json // @Param organization path string true "Organization ID" format(uuid) // @Param request body codersdk.CustomRoleRequest true "Upsert role request" // @Tags Members // @Success 200 {array} codersdk.Role // @Router /organizations/{organization}/members/roles [put] func (api *API) putOrgRoles(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() db = api.Database auditor = api.AGPL.Auditor.Load() organization = httpmw.OrganizationParam(r) aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionWrite, OrganizationID: organization.ID, }) ) defer commitAudit() var req codersdk.CustomRoleRequest if !httpapi.Read(ctx, rw, r, &req) { return } if !validOrganizationRoleRequest(ctx, req, rw) { return } originalRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { Name: req.Name, OrganizationID: organization.ID, }, }, ExcludeOrgRoles: false, // Linter requires all fields to be set. This field is not actually required. OrganizationID: organization.ID, }) // If it is a 404 (not found) error, ignore it. if err != nil && !httpapi.Is404Error(err) { httpapi.InternalServerError(rw, err) return } if len(originalRoles) == 1 { // For auditing changes to a role. aReq.Old = originalRoles[0] } updated, err := db.UpdateCustomRole(ctx, database.UpdateCustomRoleParams{ Name: req.Name, DisplayName: req.DisplayName, OrganizationID: uuid.NullUUID{ UUID: organization.ID, Valid: true, }, SitePermissions: db2sdk.List(req.SitePermissions, sdkPermissionToDB), OrgPermissions: db2sdk.List(req.OrganizationPermissions, sdkPermissionToDB), UserPermissions: db2sdk.List(req.UserPermissions, sdkPermissionToDB), }) 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 } aReq.New = updated httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(updated)) } // deleteOrgRole will remove a custom role from an organization // // @Summary Delete a custom organization role // @ID delete-a-custom-organization-role // @Security CoderSessionToken // @Produce json // @Param organization path string true "Organization ID" format(uuid) // @Param roleName path string true "Role name" // @Tags Members // @Success 200 {array} codersdk.Role // @Router /organizations/{organization}/members/roles/{roleName} [delete] func (api *API) deleteOrgRole(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() auditor = api.AGPL.Auditor.Load() organization = httpmw.OrganizationParam(r) aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionDelete, OrganizationID: organization.ID, }) ) defer commitAudit() rolename := chi.URLParam(r, "roleName") roles, err := api.Database.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { Name: rolename, OrganizationID: organization.ID, }, }, ExcludeOrgRoles: false, // Linter requires all fields to be set. This field is not actually required. OrganizationID: organization.ID, }) if err != nil { httpapi.InternalServerError(rw, err) return } if len(roles) == 0 { httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ Message: fmt.Sprintf("No custom role with the name %s found", rolename), Detail: "no role found", Validations: nil, }) return } if len(roles) > 1 { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: fmt.Sprintf("Multiple roles with the name %s found", rolename), Detail: "multiple roles found, this should never happen", Validations: nil, }) return } aReq.Old = roles[0] err = api.Database.DeleteCustomRole(ctx, database.DeleteCustomRoleParams{ Name: rolename, OrganizationID: uuid.NullUUID{ UUID: organization.ID, Valid: true, }, }) if httpapi.IsUnauthorizedError(err) { httpapi.Forbidden(rw) return } if err != nil { httpapi.InternalServerError(rw, err) return } aReq.New = database.CustomRole{} httpapi.Write(ctx, rw, http.StatusNoContent, nil) } func sdkPermissionToDB(p codersdk.Permission) database.CustomRolePermission { return database.CustomRolePermission{ Negate: p.Negate, ResourceType: string(p.ResourceType), Action: policy.Action(p.Action), } } func validOrganizationRoleRequest(ctx context.Context, req codersdk.CustomRoleRequest, rw http.ResponseWriter) bool { // This check is not ideal, but we cannot enforce a unique role name in the db against // the built-in role names. if rbac.ReservedRoleName(req.Name) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Reserved role name", Detail: fmt.Sprintf("%q is a reserved role name, and not allowed to be used", req.Name), }) return false } if err := codersdk.NameValid(req.Name); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid role name", Detail: err.Error(), }) return false } // Only organization permissions are allowed to be granted if len(req.SitePermissions) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid request, not allowed to assign site wide permissions for an organization role.", Detail: "organization scoped roles may not contain site wide permissions", }) return false } if len(req.UserPermissions) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid request, not allowed to assign user permissions for an organization role.", Detail: "organization scoped roles may not contain user permissions", }) return false } return true }