mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: Check permissions endpoint (#1389)
* feat: Check permissions endpoint Allows FE to query backend for permission capabilities. Batch requests supported
This commit is contained in:
@ -250,6 +250,8 @@ func New(options *Options) (http.Handler, func()) {
|
||||
r.Put("/roles", api.putUserRoles)
|
||||
r.Get("/roles", api.userRoles)
|
||||
|
||||
r.Post("/authorization", api.checkPermissions)
|
||||
|
||||
r.Post("/keys", api.postAPIKey)
|
||||
r.Route("/organizations", func(r chi.Router) {
|
||||
r.Post("/", api.postOrganizationsByUser)
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
@ -24,6 +25,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/fullsailor/pkcs7"
|
||||
"github.com/golang-jwt/jwt"
|
||||
@ -212,14 +215,14 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirst
|
||||
}
|
||||
|
||||
// CreateAnotherUser creates and authenticates a new user.
|
||||
func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uuid.UUID) *codersdk.Client {
|
||||
func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, roles ...string) *codersdk.Client {
|
||||
req := codersdk.CreateUserRequest{
|
||||
Email: namesgenerator.GetRandomName(1) + "@coder.com",
|
||||
Username: randomUsername(),
|
||||
Password: "testpass",
|
||||
OrganizationID: organizationID,
|
||||
}
|
||||
_, err := client.CreateUser(context.Background(), req)
|
||||
user, err := client.CreateUser(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
@ -230,6 +233,40 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui
|
||||
|
||||
other := codersdk.New(client.URL)
|
||||
other.SessionToken = login.SessionToken
|
||||
|
||||
if len(roles) > 0 {
|
||||
// Find the roles for the org vs the site wide roles
|
||||
orgRoles := make(map[string][]string)
|
||||
var siteRoles []string
|
||||
|
||||
for _, roleName := range roles {
|
||||
roleName := roleName
|
||||
orgID, ok := rbac.IsOrgRole(roleName)
|
||||
if ok {
|
||||
orgRoles[orgID] = append(orgRoles[orgID], roleName)
|
||||
} else {
|
||||
siteRoles = append(siteRoles, roleName)
|
||||
}
|
||||
}
|
||||
// Update the roles
|
||||
for _, r := range user.Roles {
|
||||
siteRoles = append(siteRoles, r.Name)
|
||||
}
|
||||
// TODO: @emyrk switch "other" to "client" when we support updating other
|
||||
// users.
|
||||
_, err := other.UpdateUserRoles(context.Background(), user.ID, codersdk.UpdateRoles{Roles: siteRoles})
|
||||
require.NoError(t, err, "update site roles")
|
||||
|
||||
// Update org roles
|
||||
for orgID, roles := range orgRoles {
|
||||
organizationID, err := uuid.Parse(orgID)
|
||||
require.NoError(t, err, fmt.Sprintf("parse org id %q", orgID))
|
||||
// TODO: @Emyrk add the member to the organization if they do not already belong.
|
||||
_, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID,
|
||||
codersdk.UpdateRoles{Roles: append(roles, rbac.RoleOrgMember(organizationID))})
|
||||
require.NoError(t, err, "update org membership roles")
|
||||
}
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,49 @@ func (*api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(rw, http.StatusOK, convertRoles(roles))
|
||||
}
|
||||
|
||||
func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) {
|
||||
roles := httpmw.UserRoles(r)
|
||||
user := httpmw.UserParam(r)
|
||||
if user.ID != roles.ID {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
// TODO: @Emyrk in the future we could have an rbac check here.
|
||||
// If the user can masquerade/impersonate as the user passed in,
|
||||
// we could allow this or something like that.
|
||||
Message: "only allowed to check permissions on yourself",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var params codersdk.UserPermissionCheckRequest
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
response := make(codersdk.UserPermissionCheckResponse)
|
||||
for k, v := range params.Checks {
|
||||
if v.Object.ResourceType == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "'resource_type' must be defined",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if v.Object.OwnerID == "me" {
|
||||
v.Object.OwnerID = roles.ID.String()
|
||||
}
|
||||
err := api.Authorizer.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.Action(v.Action),
|
||||
rbac.Object{
|
||||
ResourceID: v.Object.ResourceID,
|
||||
Owner: v.Object.OwnerID,
|
||||
OrgID: v.Object.OrganizationID,
|
||||
Type: v.Object.ResourceType,
|
||||
})
|
||||
response[k] = err == nil
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func convertRole(role rbac.Role) codersdk.Role {
|
||||
return codersdk.Role{
|
||||
DisplayName: role.DisplayName,
|
||||
|
@ -12,6 +12,91 @@ import (
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func TestPermissionCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
// Create admin, member, and org admin
|
||||
admin := coderdtest.CreateFirstUser(t, client)
|
||||
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID))
|
||||
|
||||
// With admin, member, and org admin
|
||||
const (
|
||||
allUsers = "read-all-users"
|
||||
readOrgWorkspaces = "read-org-workspaces"
|
||||
myself = "read-myself"
|
||||
myWorkspace = "read-my-workspace"
|
||||
)
|
||||
params := map[string]codersdk.UserPermissionCheck{
|
||||
allUsers: {
|
||||
Object: codersdk.UserPermissionCheckObject{
|
||||
ResourceType: "users",
|
||||
},
|
||||
Action: "read",
|
||||
},
|
||||
myself: {
|
||||
Object: codersdk.UserPermissionCheckObject{
|
||||
ResourceType: "users",
|
||||
OwnerID: "me",
|
||||
},
|
||||
Action: "read",
|
||||
},
|
||||
myWorkspace: {
|
||||
Object: codersdk.UserPermissionCheckObject{
|
||||
ResourceType: "workspaces",
|
||||
OwnerID: "me",
|
||||
},
|
||||
Action: "read",
|
||||
},
|
||||
readOrgWorkspaces: {
|
||||
Object: codersdk.UserPermissionCheckObject{
|
||||
ResourceType: "workspaces",
|
||||
OrganizationID: admin.OrganizationID.String(),
|
||||
},
|
||||
Action: "read",
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Client *codersdk.Client
|
||||
Check codersdk.UserPermissionCheckResponse
|
||||
}{
|
||||
{
|
||||
Name: "Admin",
|
||||
Client: client,
|
||||
Check: map[string]bool{
|
||||
allUsers: true, myself: true, myWorkspace: true, readOrgWorkspaces: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Member",
|
||||
Client: member,
|
||||
Check: map[string]bool{
|
||||
allUsers: false, myself: true, myWorkspace: true, readOrgWorkspaces: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OrgAdmin",
|
||||
Client: orgAdmin,
|
||||
Check: map[string]bool{
|
||||
allUsers: false, myself: true, myWorkspace: true, readOrgWorkspaces: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp, err := c.Client.CheckPermissions(context.Background(), codersdk.UserPermissionCheckRequest{Checks: params})
|
||||
require.NoError(t, err, "check perms")
|
||||
require.Equal(t, resp, c.Check)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -20,19 +105,7 @@ func TestListRoles(t *testing.T) {
|
||||
// Create admin, member, and org admin
|
||||
admin := coderdtest.CreateFirstUser(t, client)
|
||||
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
|
||||
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
orgAdminUser, err := orgAdmin.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO: @emyrk switch this to the admin when getting non-personal users is
|
||||
// supported. `client.UpdateOrganizationMemberRoles(...)`
|
||||
_, err = orgAdmin.UpdateOrganizationMemberRoles(ctx, admin.OrganizationID, orgAdminUser.ID,
|
||||
codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleOrgMember(admin.OrganizationID), rbac.RoleOrgAdmin(admin.OrganizationID)},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err, "update org member roles")
|
||||
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID))
|
||||
|
||||
otherOrg, err := client.CreateOrganization(ctx, admin.UserID, codersdk.CreateOrganizationRequest{
|
||||
Name: "other",
|
||||
|
Reference in New Issue
Block a user