Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d0293e4

Browse files
authored
feat: Implement list roles & enforce authorize examples (#1273)
1 parent 0f9e30e commit d0293e4

File tree

13 files changed

+627
-5
lines changed

13 files changed

+627
-5
lines changed

coderd/coderd.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/go-chi/chi/v5"
1313
"github.com/go-chi/chi/v5/middleware"
1414
"github.com/pion/webrtc/v3"
15+
"golang.org/x/xerrors"
1516
"google.golang.org/api/idtoken"
1617

1718
chitrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5"
@@ -23,6 +24,7 @@ import (
2324
"github.com/coder/coder/coderd/gitsshkey"
2425
"github.com/coder/coder/coderd/httpapi"
2526
"github.com/coder/coder/coderd/httpmw"
27+
"github.com/coder/coder/coderd/rbac"
2628
"github.com/coder/coder/coderd/turnconn"
2729
"github.com/coder/coder/codersdk"
2830
"github.com/coder/coder/site"
@@ -48,6 +50,7 @@ type Options struct {
4850
SecureAuthCookie bool
4951
SSHKeygenAlgorithm gitsshkey.Algorithm
5052
TURNServer *turnconn.Server
53+
Authorizer *rbac.RegoAuthorizer
5154
}
5255

5356
// New constructs the Coder API into an HTTP handler.
@@ -61,13 +64,29 @@ func New(options *Options) (http.Handler, func()) {
6164
if options.APIRateLimit == 0 {
6265
options.APIRateLimit = 512
6366
}
67+
if options.Authorizer == nil {
68+
var err error
69+
options.Authorizer, err = rbac.NewAuthorizer()
70+
if err != nil {
71+
// This should never happen, as the unit tests would fail if the
72+
// default built in authorizer failed.
73+
panic(xerrors.Errorf("rego authorize panic: %w", err))
74+
}
75+
}
6476
api := &api{
6577
Options: options,
6678
}
6779
apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{
6880
Github: options.GithubOAuth2Config,
6981
})
7082

83+
// TODO: @emyrk we should just move this into 'ExtractAPIKey'.
84+
authRolesMiddleware := httpmw.ExtractUserRoles(options.Database)
85+
86+
authorize := func(f http.HandlerFunc, actions rbac.Action) http.HandlerFunc {
87+
return httpmw.Authorize(api.Logger, api.Authorizer, actions)(f).ServeHTTP
88+
}
89+
7190
r := chi.NewRouter()
7291

7392
r.Use(
@@ -119,6 +138,7 @@ func New(options *Options) (http.Handler, func()) {
119138
r.Use(
120139
apiKeyMiddleware,
121140
httpmw.ExtractOrganizationParam(options.Database),
141+
authRolesMiddleware,
122142
)
123143
r.Get("/", api.organization)
124144
r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization)
@@ -138,6 +158,10 @@ func New(options *Options) (http.Handler, func()) {
138158
})
139159
})
140160
r.Route("/members", func(r chi.Router) {
161+
r.Route("/roles", func(r chi.Router) {
162+
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole))
163+
r.Get("/", authorize(api.assignableOrgRoles, rbac.ActionRead))
164+
})
141165
r.Route("/{user}", func(r chi.Router) {
142166
r.Use(
143167
httpmw.ExtractUserParam(options.Database),
@@ -200,20 +224,28 @@ func New(options *Options) (http.Handler, func()) {
200224
})
201225
})
202226
r.Group(func(r chi.Router) {
203-
r.Use(apiKeyMiddleware)
227+
r.Use(
228+
apiKeyMiddleware,
229+
authRolesMiddleware,
230+
)
204231
r.Post("/", api.postUser)
205232
r.Get("/", api.users)
233+
// These routes query information about site wide roles.
234+
r.Route("/roles", func(r chi.Router) {
235+
r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole))
236+
r.Get("/", authorize(api.assignableSiteRoles, rbac.ActionRead))
237+
})
206238
r.Route("/{user}", func(r chi.Router) {
207239
r.Use(httpmw.ExtractUserParam(options.Database))
208240
r.Get("/", api.userByName)
209241
r.Put("/profile", api.putUserProfile)
210242
r.Put("/suspend", api.putUserSuspend)
211-
// TODO: @emyrk Might want to move these to a /roles group instead of /user.
212-
// As we include more roles like org roles, it makes less sense to scope these here.
213-
r.Put("/roles", api.putUserRoles)
214-
r.Get("/roles", api.userRoles)
215243
r.Get("/organizations", api.organizationsByUser)
216244
r.Post("/organizations", api.postOrganizationsByUser)
245+
// These roles apply to the site wide permissions.
246+
r.Put("/roles", api.putUserRoles)
247+
r.Get("/roles", api.userRoles)
248+
217249
r.Post("/keys", api.postAPIKey)
218250
r.Route("/organizations", func(r chi.Router) {
219251
r.Post("/", api.postOrganizationsByUser)

coderd/database/databasefake/databasefake.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,38 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
245245
return tmp, nil
246246
}
247247

248+
func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (database.GetAllUserRolesRow, error) {
249+
q.mutex.RLock()
250+
defer q.mutex.RUnlock()
251+
252+
var user *database.User
253+
roles := make([]string, 0)
254+
for _, u := range q.users {
255+
if u.ID == userID {
256+
u := u
257+
roles = append(roles, u.RBACRoles...)
258+
user = &u
259+
break
260+
}
261+
}
262+
263+
for _, mem := range q.organizationMembers {
264+
if mem.UserID == userID {
265+
roles = append(roles, mem.Roles...)
266+
}
267+
}
268+
269+
if user == nil {
270+
return database.GetAllUserRolesRow{}, sql.ErrNoRows
271+
}
272+
273+
return database.GetAllUserRolesRow{
274+
ID: userID,
275+
Username: user.Username,
276+
Roles: roles,
277+
}, nil
278+
}
279+
248280
func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) {
249281
q.mutex.RLock()
250282
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,15 @@ SET
122122
updated_at = $3
123123
WHERE
124124
id = $1 RETURNING *;
125+
126+
127+
-- name: GetAllUserRoles :one
128+
SELECT
129+
-- username is returned just to help for logging purposes
130+
id, username, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
131+
FROM
132+
users
133+
LEFT JOIN organization_members
134+
ON id = user_id
135+
WHERE
136+
id = @user_id;

coderd/httpmw/authorize.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package httpmw
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"golang.org/x/xerrors"
8+
9+
"cdr.dev/slog"
10+
"github.com/coder/coder/coderd/database"
11+
"github.com/coder/coder/coderd/httpapi"
12+
"github.com/coder/coder/coderd/rbac"
13+
)
14+
15+
// Authorize will enforce if the user roles can complete the action on the AuthObject.
16+
// The organization and owner are found using the ExtractOrganization and
17+
// ExtractUser middleware if present.
18+
func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action) func(http.Handler) http.Handler {
19+
return func(next http.Handler) http.Handler {
20+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
21+
roles := UserRoles(r)
22+
object := rbacObject(r)
23+
24+
if object.Type == "" {
25+
panic("developer error: auth object has no type")
26+
}
27+
28+
// First extract the object's owner and organization if present.
29+
unknownOrg := r.Context().Value(organizationParamContextKey{})
30+
if organization, castOK := unknownOrg.(database.Organization); unknownOrg != nil {
31+
if !castOK {
32+
panic("developer error: organization param middleware not provided for authorize")
33+
}
34+
object = object.InOrg(organization.ID)
35+
}
36+
37+
unknownOwner := r.Context().Value(userParamContextKey{})
38+
if owner, castOK := unknownOwner.(database.User); unknownOwner != nil {
39+
if !castOK {
40+
panic("developer error: user param middleware not provided for authorize")
41+
}
42+
object = object.WithOwner(owner.ID.String())
43+
}
44+
45+
err := auth.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object)
46+
if err != nil {
47+
internalError := new(rbac.UnauthorizedError)
48+
if xerrors.As(err, internalError) {
49+
logger = logger.With(slog.F("internal", internalError.Internal()))
50+
}
51+
// Log information for debugging. This will be very helpful
52+
// in the early days if we over secure endpoints.
53+
logger.Warn(r.Context(), "unauthorized",
54+
slog.F("roles", roles.Roles),
55+
slog.F("user_id", roles.ID),
56+
slog.F("username", roles.Username),
57+
slog.F("route", r.URL.Path),
58+
slog.F("action", action),
59+
slog.F("object", object),
60+
)
61+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
62+
Message: err.Error(),
63+
})
64+
return
65+
}
66+
next.ServeHTTP(rw, r)
67+
})
68+
}
69+
}
70+
71+
type authObjectKey struct{}
72+
73+
// APIKey returns the API key from the ExtractAPIKey handler.
74+
func rbacObject(r *http.Request) rbac.Object {
75+
obj, ok := r.Context().Value(authObjectKey{}).(rbac.Object)
76+
if !ok {
77+
panic("developer error: auth object middleware not provided")
78+
}
79+
return obj
80+
}
81+
82+
// WithRBACObject sets the object for 'Authorize()' for all routes handled
83+
// by this middleware. The important field to set is 'Type'
84+
func WithRBACObject(object rbac.Object) func(http.Handler) http.Handler {
85+
return func(next http.Handler) http.Handler {
86+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
87+
ctx := context.WithValue(r.Context(), authObjectKey{}, object)
88+
next.ServeHTTP(rw, r.WithContext(ctx))
89+
})
90+
}
91+
}
92+
93+
// User roles are the 'subject' field of Authorize()
94+
type userRolesKey struct{}
95+
96+
// UserRoles returns the API key from the ExtractUserRoles handler.
97+
func UserRoles(r *http.Request) database.GetAllUserRolesRow {
98+
apiKey, ok := r.Context().Value(userRolesKey{}).(database.GetAllUserRolesRow)
99+
if !ok {
100+
panic("developer error: user roles middleware not provided")
101+
}
102+
return apiKey
103+
}
104+
105+
// ExtractUserRoles requires authentication using a valid API key.
106+
func ExtractUserRoles(db database.Store) func(http.Handler) http.Handler {
107+
return func(next http.Handler) http.Handler {
108+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
109+
apiKey := APIKey(r)
110+
role, err := db.GetAllUserRoles(r.Context(), apiKey.UserID)
111+
if err != nil {
112+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
113+
Message: "roles not found",
114+
})
115+
return
116+
}
117+
118+
ctx := context.WithValue(r.Context(), userRolesKey{}, role)
119+
next.ServeHTTP(rw, r.WithContext(ctx))
120+
})
121+
}
122+
}

0 commit comments

Comments
 (0)