mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: Rbac more coderd endpoints, unit test to confirm (#1437)
* feat: Enforce authorize call on all endpoints - Make 'request()' exported for running custom requests * Rbac users endpoints * 401 -> 403
This commit is contained in:
159
coderd/users.go
159
coderd/users.go
@ -109,6 +109,11 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) {
|
||||
statusFilter = r.URL.Query().Get("status")
|
||||
)
|
||||
|
||||
// Reading all users across the site
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser) {
|
||||
return
|
||||
}
|
||||
|
||||
paginationParams, ok := parsePagination(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
@ -157,12 +162,24 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Creates a new user.
|
||||
func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
// Create the user on the site
|
||||
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceUser) {
|
||||
return
|
||||
}
|
||||
|
||||
var createUser codersdk.CreateUserRequest
|
||||
if !httpapi.Read(rw, r, &createUser) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create the organization member in the org.
|
||||
if !api.Authorize(rw, r, rbac.ActionCreate,
|
||||
rbac.ResourceOrganizationMember.InOrg(createUser.OrganizationID)) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: @emyrk Authorize the organization create if the createUser will do that.
|
||||
|
||||
_, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
||||
Username: createUser.Username,
|
||||
Email: createUser.Email,
|
||||
@ -180,7 +197,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
organization, err := api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID)
|
||||
_, err = api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "organization does not exist with the provided id",
|
||||
@ -193,23 +210,6 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// Check if the caller has permissions to the organization requested.
|
||||
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "you are not authorized to add members to that organization",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organization member: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, _, err := api.createUser(r.Context(), createUser)
|
||||
if err != nil {
|
||||
@ -228,6 +228,10 @@ func (api *api) userByName(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithID(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
||||
@ -241,6 +245,10 @@ func (api *api) userByName(rw http.ResponseWriter, r *http.Request) {
|
||||
func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithOwner(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
var params codersdk.UpdateUserProfileRequest
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
@ -307,6 +315,11 @@ func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseW
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceUser.WithID(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
if status == database.UserStatusSuspended && user.ID == apiKey.UserID {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "You cannot suspend yourself",
|
||||
@ -344,6 +357,11 @@ func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
||||
user = httpmw.UserParam(r)
|
||||
params codersdk.UpdateUserPasswordRequest
|
||||
)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
@ -371,6 +389,12 @@ func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData.
|
||||
WithOwner(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := codersdk.UserRoles{
|
||||
Roles: user.RBACRoles,
|
||||
@ -386,7 +410,16 @@ func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
for _, mem := range memberships {
|
||||
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
|
||||
err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead,
|
||||
rbac.ResourceOrganizationMember.
|
||||
WithID(user.ID.String()).
|
||||
InOrg(mem.OrganizationID),
|
||||
)
|
||||
|
||||
// If we can read the org member, include the roles
|
||||
if err == nil {
|
||||
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, resp)
|
||||
@ -394,22 +427,41 @@ func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// User is the user to modify
|
||||
// TODO: Until rbac authorize is implemented, only be able to change your
|
||||
// own roles. This also means you can grant yourself whatever roles you want.
|
||||
user := httpmw.UserParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
if apiKey.UserID != user.ID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "modifying other users is not supported at this time",
|
||||
})
|
||||
return
|
||||
}
|
||||
roles := httpmw.UserRoles(r)
|
||||
|
||||
var params codersdk.UpdateRoles
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
has := make(map[string]struct{})
|
||||
for _, exists := range roles.Roles {
|
||||
has[exists] = struct{}{}
|
||||
}
|
||||
|
||||
for _, roleName := range params.Roles {
|
||||
// If the user already has the role assigned, we don't need to check the permission
|
||||
// to reassign it. Only run permission checks on the difference in the set of
|
||||
// roles.
|
||||
if _, ok := has[roleName]; ok {
|
||||
delete(has, roleName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Assigning a role requires the create permission.
|
||||
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceRoleAssignment.WithID(roleName)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Any roles that were removed also need to be checked.
|
||||
for roleName := range has {
|
||||
if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceRoleAssignment.WithID(roleName)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
ID: user.ID,
|
||||
@ -432,6 +484,8 @@ func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(rw, http.StatusOK, convertUser(updatedUser, organizationIDs))
|
||||
}
|
||||
|
||||
// updateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
||||
// If an organization role is included, an error is returned.
|
||||
func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) {
|
||||
// Enforce only site wide roles
|
||||
for _, r := range args.GrantedRoles {
|
||||
@ -454,6 +508,7 @@ func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUse
|
||||
// Returns organizations the parameterized user has access to.
|
||||
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
|
||||
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@ -469,42 +524,38 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
publicOrganizations := make([]codersdk.Organization, 0, len(organizations))
|
||||
for _, organization := range organizations {
|
||||
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
||||
err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead,
|
||||
rbac.ResourceOrganization.
|
||||
WithID(organization.ID.String()).
|
||||
InOrg(organization.ID),
|
||||
)
|
||||
if err == nil {
|
||||
// Only return orgs the user can read
|
||||
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, publicOrganizations)
|
||||
}
|
||||
|
||||
func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
organizationName := chi.URLParam(r, "organizationname")
|
||||
organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no organization found by name %q", organizationName),
|
||||
})
|
||||
// Return unauthorized rather than a 404 to not leak if the organization
|
||||
// exists.
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organization by name: %s", err),
|
||||
})
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("no organization found by name %q", organizationName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organization member: %s", err),
|
||||
})
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead,
|
||||
rbac.ResourceOrganization.
|
||||
InOrg(organization.ID).
|
||||
WithID(organization.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -617,12 +668,8 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
|
||||
// Creates a new session key, used for logging in via the CLI
|
||||
func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
if user.ID != apiKey.UserID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "Keys can only be generated for the authenticated user",
|
||||
})
|
||||
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user