Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd0293e4

Browse files
authored
feat: Implement list roles & enforce authorize examples (#1273)
1 parent0f9e30e commitd0293e4

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
SecureAuthCookiebool
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
ifoptions.APIRateLimit==0 {
6265
options.APIRateLimit=512
6366
}
67+
ifoptions.Authorizer==nil {
68+
varerrerror
69+
options.Authorizer,err=rbac.NewAuthorizer()
70+
iferr!=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+
returnhttpmw.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
returntmp,nil
246246
}
247247

248+
func (q*fakeQuerier)GetAllUserRoles(_ context.Context,userID uuid.UUID) (database.GetAllUserRolesRow,error) {
249+
q.mutex.RLock()
250+
deferq.mutex.RUnlock()
251+
252+
varuser*database.User
253+
roles:=make([]string,0)
254+
for_,u:=rangeq.users {
255+
ifu.ID==userID {
256+
u:=u
257+
roles=append(roles,u.RBACRoles...)
258+
user=&u
259+
break
260+
}
261+
}
262+
263+
for_,mem:=rangeq.organizationMembers {
264+
ifmem.UserID==userID {
265+
roles=append(roles,mem.Roles...)
266+
}
267+
}
268+
269+
ifuser==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
deferq.mutex.RUnlock()

‎coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing 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 aboutcustomizing 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+
funcAuthorize(logger slog.Logger,auth*rbac.RegoAuthorizer,action rbac.Action)func(http.Handler) http.Handler {
19+
returnfunc(next http.Handler) http.Handler {
20+
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
21+
roles:=UserRoles(r)
22+
object:=rbacObject(r)
23+
24+
ifobject.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+
iforganization,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+
ifowner,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+
iferr!=nil {
47+
internalError:=new(rbac.UnauthorizedError)
48+
ifxerrors.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+
typeauthObjectKeystruct{}
72+
73+
// APIKey returns the API key from the ExtractAPIKey handler.
74+
funcrbacObject(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+
returnobj
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+
funcWithRBACObject(object rbac.Object)func(http.Handler) http.Handler {
85+
returnfunc(next http.Handler) http.Handler {
86+
returnhttp.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+
typeuserRolesKeystruct{}
95+
96+
// UserRoles returns the API key from the ExtractUserRoles handler.
97+
funcUserRoles(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+
returnapiKey
103+
}
104+
105+
// ExtractUserRoles requires authentication using a valid API key.
106+
funcExtractUserRoles(db database.Store)func(http.Handler) http.Handler {
107+
returnfunc(next http.Handler) http.Handler {
108+
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
109+
apiKey:=APIKey(r)
110+
role,err:=db.GetAllUserRoles(r.Context(),apiKey.UserID)
111+
iferr!=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)

[8]ページ先頭

©2009-2025 Movatter.jp