diff --git a/coderd/coderd.go b/coderd/coderd.go index a3ff71788e633..191d3339aa2b1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -12,6 +12,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/pion/webrtc/v3" + "golang.org/x/xerrors" "google.golang.org/api/idtoken" 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/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/codersdk" "github.com/coder/coder/site" @@ -48,6 +50,7 @@ type Options struct { SecureAuthCookie bool SSHKeygenAlgorithm gitsshkey.Algorithm TURNServer *turnconn.Server + Authorizer *rbac.RegoAuthorizer } // New constructs the Coder API into an HTTP handler. @@ -61,6 +64,15 @@ func New(options *Options) (http.Handler, func()) { if options.APIRateLimit == 0 { 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{ Options: options, } @@ -68,6 +80,13 @@ func New(options *Options) (http.Handler, func()) { 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.Use( @@ -119,6 +138,7 @@ func New(options *Options) (http.Handler, func()) { r.Use( apiKeyMiddleware, httpmw.ExtractOrganizationParam(options.Database), + authRolesMiddleware, ) r.Get("/", api.organization) 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("/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.Use( httpmw.ExtractUserParam(options.Database), @@ -200,20 +224,28 @@ func New(options *Options) (http.Handler, func()) { }) }) r.Group(func(r chi.Router) { - r.Use(apiKeyMiddleware) + r.Use( + apiKeyMiddleware, + authRolesMiddleware, + ) r.Post("/", api.postUser) 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.Use(httpmw.ExtractUserParam(options.Database)) r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) 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.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.Route("/organizations", func(r chi.Router) { r.Post("/", api.postOrganizationsByUser) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 725e729ed1df2..5726c9f7befe5 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -245,6 +245,38 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams 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) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 8058cedeb7bb8..db2f3f0d49987 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -21,6 +21,7 @@ type querier interface { DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error DeleteParameterValueByID(ctx context.Context, id uuid.UUID) 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 // ID. GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 1034a3a0d00a6..f91070915c097 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2014,6 +2014,31 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe 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 SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index aa94117ec2aff..2f0ecb3709b9b 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -122,3 +122,15 @@ SET updated_at = $3 WHERE 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; \ No newline at end of file diff --git a/coderd/httpmw/authorize.go b/coderd/httpmw/authorize.go new file mode 100644 index 0000000000000..2eb221f1893eb --- /dev/null +++ b/coderd/httpmw/authorize.go @@ -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)) + }) + } +} diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go new file mode 100644 index 0000000000000..59ee50c80d9be --- /dev/null +++ b/coderd/httpmw/authorize_test.go @@ -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) +} diff --git a/coderd/rbac/builtin.go b/coderd/rbac/builtin.go index 4d659e6e8e39f..26ec0a1afa1cc 100644 --- a/coderd/rbac/builtin.go +++ b/coderd/rbac/builtin.go @@ -145,6 +145,49 @@ func IsOrgRole(roleName string) (string, bool) { 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 // role_name:scopeID // If no scopeID is required, only 'role_name' is returned diff --git a/coderd/rbac/builtin_test.go b/coderd/rbac/builtin_test.go index 849179dc78893..d8b937f78ac53 100644 --- a/coderd/rbac/builtin_test.go +++ b/coderd/rbac/builtin_test.go @@ -1,6 +1,7 @@ package rbac_test import ( + "fmt" "testing" "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)) +} diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 1e86165f24cf4..a4be9b1edab5b 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -17,6 +17,13 @@ var ( 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 = Object{ Type: WildcardSymbol, diff --git a/coderd/roles.go b/coderd/roles.go new file mode 100644 index 0000000000000..64bd7353b4293 --- /dev/null +++ b/coderd/roles.go @@ -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) +} diff --git a/coderd/roles_test.go b/coderd/roles_test.go new file mode 100644 index 0000000000000..523439883e33d --- /dev/null +++ b/coderd/roles_test.go @@ -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) + } + }) + } +} diff --git a/codersdk/roles.go b/codersdk/roles.go new file mode 100644 index 0000000000000..d7d6d0fe2b8bc --- /dev/null +++ b/codersdk/roles.go @@ -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) +}