feat: Implement list roles & enforce authorize examples (#1273)

This commit is contained in:
Steven Masley
2022-05-03 16:10:19 -05:00
committed by GitHub
parent 0f9e30e54f
commit d0293e4d33
13 changed files with 627 additions and 5 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken" "google.golang.org/api/idtoken"
chitrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5" chitrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5"
@ -23,6 +24,7 @@ import (
"github.com/coder/coder/coderd/gitsshkey" "github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/coderd/turnconn"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/site" "github.com/coder/coder/site"
@ -48,6 +50,7 @@ type Options struct {
SecureAuthCookie bool SecureAuthCookie bool
SSHKeygenAlgorithm gitsshkey.Algorithm SSHKeygenAlgorithm gitsshkey.Algorithm
TURNServer *turnconn.Server TURNServer *turnconn.Server
Authorizer *rbac.RegoAuthorizer
} }
// New constructs the Coder API into an HTTP handler. // New constructs the Coder API into an HTTP handler.
@ -61,6 +64,15 @@ func New(options *Options) (http.Handler, func()) {
if options.APIRateLimit == 0 { if options.APIRateLimit == 0 {
options.APIRateLimit = 512 options.APIRateLimit = 512
} }
if options.Authorizer == nil {
var err error
options.Authorizer, err = rbac.NewAuthorizer()
if err != nil {
// This should never happen, as the unit tests would fail if the
// default built in authorizer failed.
panic(xerrors.Errorf("rego authorize panic: %w", err))
}
}
api := &api{ api := &api{
Options: options, Options: options,
} }
@ -68,6 +80,13 @@ func New(options *Options) (http.Handler, func()) {
Github: options.GithubOAuth2Config, Github: options.GithubOAuth2Config,
}) })
// TODO: @emyrk we should just move this into 'ExtractAPIKey'.
authRolesMiddleware := httpmw.ExtractUserRoles(options.Database)
authorize := func(f http.HandlerFunc, actions rbac.Action) http.HandlerFunc {
return httpmw.Authorize(api.Logger, api.Authorizer, actions)(f).ServeHTTP
}
r := chi.NewRouter() r := chi.NewRouter()
r.Use( r.Use(
@ -119,6 +138,7 @@ func New(options *Options) (http.Handler, func()) {
r.Use( r.Use(
apiKeyMiddleware, apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database), httpmw.ExtractOrganizationParam(options.Database),
authRolesMiddleware,
) )
r.Get("/", api.organization) r.Get("/", api.organization)
r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization) r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization)
@ -138,6 +158,10 @@ func New(options *Options) (http.Handler, func()) {
}) })
}) })
r.Route("/members", func(r chi.Router) { r.Route("/members", func(r chi.Router) {
r.Route("/roles", func(r chi.Router) {
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole))
r.Get("/", authorize(api.assignableOrgRoles, rbac.ActionRead))
})
r.Route("/{user}", func(r chi.Router) { r.Route("/{user}", func(r chi.Router) {
r.Use( r.Use(
httpmw.ExtractUserParam(options.Database), httpmw.ExtractUserParam(options.Database),
@ -200,20 +224,28 @@ func New(options *Options) (http.Handler, func()) {
}) })
}) })
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware) r.Use(
apiKeyMiddleware,
authRolesMiddleware,
)
r.Post("/", api.postUser) r.Post("/", api.postUser)
r.Get("/", api.users) r.Get("/", api.users)
// These routes query information about site wide roles.
r.Route("/roles", func(r chi.Router) {
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole))
r.Get("/", authorize(api.assignableSiteRoles, rbac.ActionRead))
})
r.Route("/{user}", func(r chi.Router) { r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database)) r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.userByName) r.Get("/", api.userByName)
r.Put("/profile", api.putUserProfile) r.Put("/profile", api.putUserProfile)
r.Put("/suspend", api.putUserSuspend) r.Put("/suspend", api.putUserSuspend)
// TODO: @emyrk Might want to move these to a /roles group instead of /user.
// As we include more roles like org roles, it makes less sense to scope these here.
r.Put("/roles", api.putUserRoles)
r.Get("/roles", api.userRoles)
r.Get("/organizations", api.organizationsByUser) r.Get("/organizations", api.organizationsByUser)
r.Post("/organizations", api.postOrganizationsByUser) r.Post("/organizations", api.postOrganizationsByUser)
// These roles apply to the site wide permissions.
r.Put("/roles", api.putUserRoles)
r.Get("/roles", api.userRoles)
r.Post("/keys", api.postAPIKey) r.Post("/keys", api.postAPIKey)
r.Route("/organizations", func(r chi.Router) { r.Route("/organizations", func(r chi.Router) {
r.Post("/", api.postOrganizationsByUser) r.Post("/", api.postOrganizationsByUser)

View File

@ -245,6 +245,38 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
return tmp, nil return tmp, nil
} }
func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (database.GetAllUserRolesRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var user *database.User
roles := make([]string, 0)
for _, u := range q.users {
if u.ID == userID {
u := u
roles = append(roles, u.RBACRoles...)
user = &u
break
}
}
for _, mem := range q.organizationMembers {
if mem.UserID == userID {
roles = append(roles, mem.Roles...)
}
}
if user == nil {
return database.GetAllUserRolesRow{}, sql.ErrNoRows
}
return database.GetAllUserRolesRow{
ID: userID,
Username: user.Username,
Roles: roles,
}, nil
}
func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) { func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) {
q.mutex.RLock() q.mutex.RLock()
defer q.mutex.RUnlock() defer q.mutex.RUnlock()

View File

@ -21,6 +21,7 @@ type querier interface {
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
GetAllUserRoles(ctx context.Context, userID uuid.UUID) (GetAllUserRolesRow, error)
// GetAuditLogsBefore retrieves `limit` number of audit logs before the provided // GetAuditLogsBefore retrieves `limit` number of audit logs before the provided
// ID. // ID.
GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error) GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error)

View File

@ -2014,6 +2014,31 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe
return err return err
} }
const getAllUserRoles = `-- name: GetAllUserRoles :one
SELECT
-- username is returned just to help for logging purposes
id, username, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
FROM
users
LEFT JOIN organization_members
ON id = user_id
WHERE
id = $1
`
type GetAllUserRolesRow struct {
ID uuid.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Roles []string `db:"roles" json:"roles"`
}
func (q *sqlQuerier) GetAllUserRoles(ctx context.Context, userID uuid.UUID) (GetAllUserRolesRow, error) {
row := q.db.QueryRowContext(ctx, getAllUserRoles, userID)
var i GetAllUserRolesRow
err := row.Scan(&i.ID, &i.Username, pq.Array(&i.Roles))
return i, err
}
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles id, email, username, hashed_password, created_at, updated_at, status, rbac_roles

View File

@ -122,3 +122,15 @@ SET
updated_at = $3 updated_at = $3
WHERE WHERE
id = $1 RETURNING *; id = $1 RETURNING *;
-- name: GetAllUserRoles :one
SELECT
-- username is returned just to help for logging purposes
id, username, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
FROM
users
LEFT JOIN organization_members
ON id = user_id
WHERE
id = @user_id;

122
coderd/httpmw/authorize.go Normal file
View File

@ -0,0 +1,122 @@
package httpmw
import (
"context"
"net/http"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
)
// Authorize will enforce if the user roles can complete the action on the AuthObject.
// The organization and owner are found using the ExtractOrganization and
// ExtractUser middleware if present.
func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
roles := UserRoles(r)
object := rbacObject(r)
if object.Type == "" {
panic("developer error: auth object has no type")
}
// First extract the object's owner and organization if present.
unknownOrg := r.Context().Value(organizationParamContextKey{})
if organization, castOK := unknownOrg.(database.Organization); unknownOrg != nil {
if !castOK {
panic("developer error: organization param middleware not provided for authorize")
}
object = object.InOrg(organization.ID)
}
unknownOwner := r.Context().Value(userParamContextKey{})
if owner, castOK := unknownOwner.(database.User); unknownOwner != nil {
if !castOK {
panic("developer error: user param middleware not provided for authorize")
}
object = object.WithOwner(owner.ID.String())
}
err := auth.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object)
if err != nil {
internalError := new(rbac.UnauthorizedError)
if xerrors.As(err, internalError) {
logger = logger.With(slog.F("internal", internalError.Internal()))
}
// Log information for debugging. This will be very helpful
// in the early days if we over secure endpoints.
logger.Warn(r.Context(), "unauthorized",
slog.F("roles", roles.Roles),
slog.F("user_id", roles.ID),
slog.F("username", roles.Username),
slog.F("route", r.URL.Path),
slog.F("action", action),
slog.F("object", object),
)
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: err.Error(),
})
return
}
next.ServeHTTP(rw, r)
})
}
}
type authObjectKey struct{}
// APIKey returns the API key from the ExtractAPIKey handler.
func rbacObject(r *http.Request) rbac.Object {
obj, ok := r.Context().Value(authObjectKey{}).(rbac.Object)
if !ok {
panic("developer error: auth object middleware not provided")
}
return obj
}
// WithRBACObject sets the object for 'Authorize()' for all routes handled
// by this middleware. The important field to set is 'Type'
func WithRBACObject(object rbac.Object) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), authObjectKey{}, object)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
// User roles are the 'subject' field of Authorize()
type userRolesKey struct{}
// UserRoles returns the API key from the ExtractUserRoles handler.
func UserRoles(r *http.Request) database.GetAllUserRolesRow {
apiKey, ok := r.Context().Value(userRolesKey{}).(database.GetAllUserRolesRow)
if !ok {
panic("developer error: user roles middleware not provided")
}
return apiKey
}
// ExtractUserRoles requires authentication using a valid API key.
func ExtractUserRoles(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
apiKey := APIKey(r)
role, err := db.GetAllUserRoles(r.Context(), apiKey.UserID)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "roles not found",
})
return
}
ctx := context.WithValue(r.Context(), userRolesKey{}, role)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -0,0 +1,131 @@
package httpmw_test
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/coder/coder/coderd/rbac"
"github.com/google/uuid"
"github.com/coder/coder/coderd/database"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/httpmw"
)
func TestExtractUserRoles(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
AddUser func(db database.Store) (database.User, []string, string)
}{
{
Name: "Member",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember()}
user, token := addUser(t, db, roles...)
return user, roles, token
},
},
{
Name: "Admin",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember(), rbac.RoleAdmin()}
user, token := addUser(t, db, roles...)
return user, roles, token
},
},
{
Name: "OrgMember",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember()}
user, token := addUser(t, db, roles...)
org, err := db.InsertOrganization(context.Background(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: "testorg",
Description: "test",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
orgRoles := []string{rbac.RoleOrgMember(org.ID)}
_, err = db.InsertOrganizationMember(context.Background(), database.InsertOrganizationMemberParams{
OrganizationID: org.ID,
UserID: user.ID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Roles: orgRoles,
})
require.NoError(t, err)
return user, append(roles, orgRoles...), token
},
},
}
for _, c := range testCases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
var (
db = databasefake.New()
user, expRoles, token = c.AddUser(db)
rw = httptest.NewRecorder()
rtr = chi.NewRouter()
)
rtr.Use(
httpmw.ExtractAPIKey(db, &httpmw.OAuth2Configs{}),
httpmw.ExtractUserRoles(db),
)
rtr.Get("/", func(_ http.ResponseWriter, r *http.Request) {
roles := httpmw.UserRoles(r)
require.ElementsMatch(t, user.ID, roles.ID)
require.ElementsMatch(t, expRoles, roles.Roles)
})
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(&http.Cookie{
Name: httpmw.AuthCookie,
Value: token,
})
rtr.ServeHTTP(rw, req)
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
})
}
}
func addUser(t *testing.T, db database.Store, roles ...string) (database.User, string) {
var (
id, secret = randomAPIKeyParts()
hashed = sha256.Sum256([]byte(secret))
)
user, err := db.InsertUser(context.Background(), database.InsertUserParams{
ID: uuid.New(),
Email: "admin@email.com",
Username: "admin",
RBACRoles: roles,
})
require.NoError(t, err)
_, err = db.InsertAPIKey(context.Background(), database.InsertAPIKeyParams{
ID: id,
UserID: user.ID,
HashedSecret: hashed[:],
LastUsed: database.Now(),
ExpiresAt: database.Now().Add(time.Minute),
})
require.NoError(t, err)
return user, fmt.Sprintf("%s-%s", id, secret)
}

View File

@ -145,6 +145,49 @@ func IsOrgRole(roleName string) (string, bool) {
return "", false return "", false
} }
// OrganizationRoles lists all roles that can be applied to an organization user
// in the given organization. This is the list of available roles,
// and specific to an organization.
//
// This should be a list in a database, but until then we build
// the list from the builtins.
func OrganizationRoles(organizationID uuid.UUID) []string {
var roles []string
for _, roleF := range builtInRoles {
role := roleF(organizationID.String()).Name
_, scope, err := roleSplit(role)
if err != nil {
// This should never happen
continue
}
if scope == organizationID.String() {
roles = append(roles, role)
}
}
return roles
}
// SiteRoles lists all roles that can be applied to a user.
// This is the list of available roles, and not specific to a user
//
// This should be a list in a database, but until then we build
// the list from the builtins.
func SiteRoles() []string {
var roles []string
for _, roleF := range builtInRoles {
role := roleF("random")
_, scope, err := roleSplit(role.Name)
if err != nil {
// This should never happen
continue
}
if scope == "" {
roles = append(roles, role.Name)
}
}
return roles
}
// roleName is a quick helper function to return // roleName is a quick helper function to return
// role_name:scopeID // role_name:scopeID
// If no scopeID is required, only 'role_name' is returned // If no scopeID is required, only 'role_name' is returned

View File

@ -1,6 +1,7 @@
package rbac_test package rbac_test
import ( import (
"fmt"
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
@ -60,3 +61,23 @@ func TestIsOrgRole(t *testing.T) {
}) })
} }
} }
func TestListRoles(t *testing.T) {
t.Parallel()
// If this test is ever failing, just update the list to the roles
// expected from the builtin set.
require.ElementsMatch(t, []string{
"admin",
"member",
"auditor",
},
rbac.SiteRoles())
orgID := uuid.New()
require.ElementsMatch(t, []string{
fmt.Sprintf("organization-admin:%s", orgID.String()),
fmt.Sprintf("organization-member:%s", orgID.String()),
},
rbac.OrganizationRoles(orgID))
}

View File

@ -17,6 +17,13 @@ var (
Type: "template", Type: "template",
} }
// ResourceUserRole might be expanded later to allow more granular permissions
// to modifying roles. For now, this covers all possible roles, so having this permission
// allows granting/deleting **ALL** roles.
ResourceUserRole = Object{
Type: "user_role",
}
// ResourceWildcard represents all resource types // ResourceWildcard represents all resource types
ResourceWildcard = Object{ ResourceWildcard = Object{
Type: WildcardSymbol, Type: WildcardSymbol,

27
coderd/roles.go Normal file
View File

@ -0,0 +1,27 @@
package coderd
import (
"net/http"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
)
// assignableSiteRoles returns all site wide roles that can be assigned.
func (*api) assignableSiteRoles(rw http.ResponseWriter, _ *http.Request) {
// TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the
// role of the user.
roles := rbac.SiteRoles()
httpapi.Write(rw, http.StatusOK, roles)
}
// assignableSiteRoles returns all site wide roles that can be assigned.
func (*api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
// TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the
// role of the user.
organization := httpmw.OrganizationParam(r)
roles := rbac.OrganizationRoles(organization.ID)
httpapi.Write(rw, http.StatusOK, roles)
}

129
coderd/roles_test.go Normal file
View File

@ -0,0 +1,129 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
)
func TestListRoles(t *testing.T) {
t.Parallel()
ctx := context.Background()
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)
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")
otherOrg, err := client.CreateOrganization(ctx, admin.UserID, codersdk.CreateOrganizationRequest{
Name: "other",
})
require.NoError(t, err, "create org")
const unauth = "unauthorized"
const notMember = "not a member of the organization"
testCases := []struct {
Name string
Client *codersdk.Client
APICall func() ([]string, error)
ExpectedRoles []string
AuthorizedError string
}{
{
Name: "MemberListSite",
APICall: func() ([]string, error) {
x, err := member.ListSiteRoles(ctx)
return x, err
},
AuthorizedError: unauth,
},
{
Name: "OrgMemberListOrg",
APICall: func() ([]string, error) {
return member.ListOrganizationRoles(ctx, admin.OrganizationID)
},
AuthorizedError: unauth,
},
{
Name: "NonOrgMemberListOrg",
APICall: func() ([]string, error) {
return member.ListOrganizationRoles(ctx, otherOrg.ID)
},
AuthorizedError: notMember,
},
// Org admin
{
Name: "OrgAdminListSite",
APICall: func() ([]string, error) {
return orgAdmin.ListSiteRoles(ctx)
},
AuthorizedError: unauth,
},
{
Name: "OrgAdminListOrg",
APICall: func() ([]string, error) {
return orgAdmin.ListOrganizationRoles(ctx, admin.OrganizationID)
},
ExpectedRoles: rbac.OrganizationRoles(admin.OrganizationID),
},
{
Name: "OrgAdminListOtherOrg",
APICall: func() ([]string, error) {
return orgAdmin.ListOrganizationRoles(ctx, otherOrg.ID)
},
AuthorizedError: notMember,
},
// Admin
{
Name: "AdminListSite",
APICall: func() ([]string, error) {
return client.ListSiteRoles(ctx)
},
ExpectedRoles: rbac.SiteRoles(),
},
{
Name: "AdminListOrg",
APICall: func() ([]string, error) {
return client.ListOrganizationRoles(ctx, admin.OrganizationID)
},
ExpectedRoles: rbac.OrganizationRoles(admin.OrganizationID),
},
}
for _, c := range testCases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
roles, err := c.APICall()
if c.AuthorizedError != "" {
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
require.Contains(t, apiErr.Message, c.AuthorizedError)
} else {
require.NoError(t, err)
require.ElementsMatch(t, c.ExpectedRoles, roles)
}
})
}
}

40
codersdk/roles.go Normal file
View File

@ -0,0 +1,40 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/google/uuid"
)
// ListSiteRoles lists all available site wide roles.
// This is not user specific.
func (c *Client) ListSiteRoles(ctx context.Context) ([]string, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/roles", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var roles []string
return roles, json.NewDecoder(res.Body).Decode(&roles)
}
// ListOrganizationRoles lists all available roles for a given organization.
// This is not user specific.
func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]string, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/roles/", org.String()), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var roles []string
return roles, json.NewDecoder(res.Body).Decode(&roles)
}