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

feat: Implement list roles & enforce authorize examples#1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 16 commits intomainfromstevenmasley/list_roles
May 3, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
16 commits
Select commitHold shift + click to select a range
30e2031
feat: First draft of adding authorize to an http endpoint
EmyrkMay 3, 2022
2161f84
WIP: Using middleware to change auth object params
EmyrkMay 3, 2022
54bc054
feat: Implement basic authorize and unit test
EmyrkMay 3, 2022
d083a7c
Some cleanup
EmyrkMay 3, 2022
95b9a14
Merge remote-tracking branch 'origin/main' into stevenmasley/list_roles
EmyrkMay 3, 2022
1498dcd
Expand 'orgs' to 'organizations' in func namings
EmyrkMay 3, 2022
f36ae37
Renamings
EmyrkMay 3, 2022
b831260
Use rbac.object directly
EmyrkMay 3, 2022
db04d67
Fix broken tests
EmyrkMay 3, 2022
b76f373
Add some comments
EmyrkMay 3, 2022
117f838
Linting
EmyrkMay 3, 2022
42b42ab
Handle out of order lists
EmyrkMay 3, 2022
0efe72c
Add unit test
EmyrkMay 3, 2022
dba617d
Add unit test for mw
EmyrkMay 3, 2022
190940f
parallel unit test
EmyrkMay 3, 2022
c86c67c
style order
EmyrkMay 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Expand All@@ -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"
Expand All@@ -48,6 +50,7 @@ type Options struct {
SecureAuthCookiebool
SSHKeygenAlgorithm gitsshkey.Algorithm
TURNServer*turnconn.Server
Authorizer*rbac.RegoAuthorizer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Does this need to be exposed via options? It doesn't seem to be used outside of here.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I was thinking of making it an interface that is "denyall" or something to trigger in unit tests. But for now, we don't, so I'll drop it fromOptions

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Ah wait yes.@kylecarbs this will be needed if you decide to do the rbacAuthorize() check manually inside the function, instead of the middleware. So this is required.

Copy link
Member

@kylecarbskylecarbsMay 3, 2022
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This could go onapi instead, that way tests couldn't mistakenly pass it viaOptions.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Right now nothing else is on the api only like that.

coder/coderd/coderd.go

Lines 333 to 338 indb04d67

typeapistruct {
*Options
websocketWaitMutex sync.Mutex
websocketWaitGroup sync.WaitGroup
}

I don't think it'd be bad to pass one in via options 🤷‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Oh my bad, I thought there was precedent for this 🤦

If you feel like it's fine I'll defer, but I generally think it's hasty to expose a value unless it needs to be used elsewhere. It's really easy to expose a parameter, but much harder to hide one.

}

// New constructs the Coder API into an HTTP handler.
Expand All@@ -61,13 +64,29 @@ func New(options *Options) (http.Handler, func()) {
ifoptions.APIRateLimit==0 {
options.APIRateLimit=512
}
ifoptions.Authorizer==nil {
varerrerror
options.Authorizer,err=rbac.NewAuthorizer()
iferr!=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,
}
apiKeyMiddleware:=httpmw.ExtractAPIKey(options.Database,&httpmw.OAuth2Configs{
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 {
returnhttpmw.Authorize(api.Logger,api.Authorizer,actions)(f).ServeHTTP
}

r:=chi.NewRouter()

r.Use(
Expand DownExpand Up@@ -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)
Expand All@@ -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),
Expand DownExpand Up@@ -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)
Expand Down
32 changes: 32 additions & 0 deletionscoderd/database/databasefake/databasefake.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -245,6 +245,38 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
returntmp,nil
}

func (q*fakeQuerier)GetAllUserRoles(_ context.Context,userID uuid.UUID) (database.GetAllUserRolesRow,error) {
q.mutex.RLock()
deferq.mutex.RUnlock()

varuser*database.User
roles:=make([]string,0)
for_,u:=rangeq.users {
ifu.ID==userID {
u:=u
roles=append(roles,u.RBACRoles...)
user=&u
break
}
}

for_,mem:=rangeq.organizationMembers {
ifmem.UserID==userID {
roles=append(roles,mem.Roles...)
}
}

ifuser==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()
deferq.mutex.RUnlock()
Expand Down
1 change: 1 addition & 0 deletionscoderd/database/querier.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

25 changes: 25 additions & 0 deletionscoderd/database/queries.sql.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

12 changes: 12 additions & 0 deletionscoderd/database/queries/users.sql
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
122 changes: 122 additions & 0 deletionscoderd/httpmw/authorize.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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.
funcAuthorize(logger slog.Logger,auth*rbac.RegoAuthorizer,action rbac.Action)func(http.Handler) http.Handler {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Authorize feels like action, not something that would return a handler.

What do you think about renaming this toEnforceRBAC?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I thinkEnforceRBAC is also weak though. I was thinking the packagehttpmw provides enough context, and it does do the actionAuthorize().

Authorize is the correct word for what is happening, as it's not authentication. I feelEnforceRBAC doesn't indicate theobject andaction are included.

Another word that comes to mind is "Access". Idk,EnforceAccess,EnforcePermissions. MaybeEnforceRBAC isn't that bad, just felt odd to me at first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Fair enough. I'm primarily trying to display that theRBAC package is being leveraged when calling this handle.Enforce is a bit sketchy too.

While it isauthorizing, I'm nervous that this will get conflated with authentication really easily.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

yea this is classic authorization vs authentication. If you aren't familiar with it, it's easy to mix up.

kylecarbs reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Agreed agreed

returnfunc(next http.Handler) http.Handler {
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
roles:=UserRoles(r)
object:=rbacObject(r)

ifobject.Type=="" {
panic("developer error: auth object has no type")
}

// First extract the object's owner and organization if present.
unknownOrg:=r.Context().Value(organizationParamContextKey{})
iforganization,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{})
ifowner,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)
iferr!=nil {
internalError:=new(rbac.UnauthorizedError)
ifxerrors.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)
})
}
}

typeauthObjectKeystruct{}

// APIKey returns the API key from the ExtractAPIKey handler.
funcrbacObject(r*http.Request) rbac.Object {
obj,ok:=r.Context().Value(authObjectKey{}).(rbac.Object)
if!ok {
panic("developer error: auth object middleware not provided")
}
returnobj
}

// WithRBACObject sets the object for 'Authorize()' for all routes handled
// by this middleware. The important field to set is 'Type'
funcWithRBACObject(object rbac.Object)func(http.Handler) http.Handler {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It might be confusing that this is calledWithRBACObject, but the values arerbac.ResourceX. It could be helpful to make these the same names!

returnfunc(next http.Handler) http.Handler {
returnhttp.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()
typeuserRolesKeystruct{}

// UserRoles returns the API key from the ExtractUserRoles handler.
funcUserRoles(r*http.Request) database.GetAllUserRolesRow {
apiKey,ok:=r.Context().Value(userRolesKey{}).(database.GetAllUserRolesRow)
if!ok {
panic("developer error: user roles middleware not provided")
}
returnapiKey
}

// ExtractUserRoles requires authentication using a valid API key.
funcExtractUserRoles(db database.Store)func(http.Handler) http.Handler {
returnfunc(next http.Handler) http.Handler {
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
apiKey:=APIKey(r)
role,err:=db.GetAllUserRoles(r.Context(),apiKey.UserID)
iferr!=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))
})
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp