From fb7adf5580ce6bf0d14055cadc098a0f37a41150 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 09:18:46 -0500 Subject: [PATCH 01/10] feat: Check permissions endpoint Allows FE to query backend for permission capabilities. Batch requests supported --- coderd/coderd.go | 4 ++ coderd/roles.go | 43 ++++++++++++++++++++ coderd/roles_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++++ codersdk/roles.go | 13 ++++++ codersdk/users.go | 23 +++++++++++ 5 files changed, 180 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index 67e7b0eaeae3f..1ddf492c07e8c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -250,6 +250,10 @@ func New(options *Options) (http.Handler, func()) { r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) + r.Route("/permissions", func(r chi.Router) { + r.Post("/check", api.checkPermissions) + }) + r.Post("/keys", api.postAPIKey) r.Route("/organizations", func(r chi.Router) { r.Post("/", api.postOrganizationsByUser) diff --git a/coderd/roles.go b/coderd/roles.go index cad7430dcb9d4..8eb1db70c19d2 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -27,6 +27,49 @@ func (*api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertRoles(roles)) } +func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) { + roles := httpmw.UserRoles(r) + user := httpmw.UserParam(r) + if user.ID != roles.ID { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + // TODO: @Emyrk in the future we could have an rbac check here. + // If the user can masquerade/impersonate as the user passed in, + // we could allow this or something like that. + Message: "only allowed to check permissions on yourself", + }) + return + } + + var params codersdk.UserPermissionCheckRequest + if !httpapi.Read(rw, r, ¶ms) { + return + } + + response := make(codersdk.UserPermissionCheckResponse) + for k, v := range params.Checks { + if v.Object.ResourceType == "" { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "'resource_type' must be defined", + }) + return + } + + if v.Object.OwnerID == "me" { + v.Object.OwnerID = roles.ID.String() + } + err := api.Authorizer.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, v.Action, + rbac.Object{ + ResourceID: v.Object.ResourceID, + Owner: v.Object.OwnerID, + OrgID: v.Object.OrganizationID, + Type: v.Object.ResourceType, + }) + response[k] = err == nil + } + + httpapi.Write(rw, http.StatusOK, response) +} + func convertRole(role rbac.Role) codersdk.Role { return codersdk.Role{ DisplayName: role.DisplayName, diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 2c42d129cec4f..56ecc4f5e2a8f 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -12,6 +12,103 @@ import ( "github.com/coder/coder/codersdk" ) +func TestPermissionCheck(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") + + // With admin, member, and org admin + const ( + allUsers = "read-all-users" + readOrgWorkspaces = "read-org-workspaces" + myself = "read-myself" + myWorkspace = "read-my-workspace" + ) + params := map[string]codersdk.UserPermissionCheck{ + allUsers: { + Object: codersdk.UserPermissionCheckObject{ + ResourceType: "users", + }, + Action: "read", + }, + myself: { + Object: codersdk.UserPermissionCheckObject{ + ResourceType: "users", + OwnerID: "me", + }, + Action: "read", + }, + myWorkspace: { + Object: codersdk.UserPermissionCheckObject{ + ResourceType: "workspaces", + OwnerID: "me", + }, + Action: "read", + }, + readOrgWorkspaces: { + Object: codersdk.UserPermissionCheckObject{ + ResourceType: "workspaces", + OrganizationID: admin.OrganizationID.String(), + }, + Action: "read", + }, + } + + testCases := []struct { + Name string + Client *codersdk.Client + Check codersdk.UserPermissionCheckResponse + }{ + { + Name: "Admin", + Client: client, + Check: map[string]bool{ + allUsers: true, myself: true, myWorkspace: true, readOrgWorkspaces: true, + }, + }, + { + Name: "Member", + Client: member, + Check: map[string]bool{ + allUsers: false, myself: true, myWorkspace: true, readOrgWorkspaces: false, + }, + }, + { + Name: "OrgAdmin", + Client: orgAdmin, + Check: map[string]bool{ + allUsers: false, myself: true, myWorkspace: true, readOrgWorkspaces: true, + }, + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Name, func(t *testing.T) { + resp, err := c.Client.CheckPermissions(context.Background(), codersdk.UserPermissionCheckRequest{Checks: params}) + require.NoError(t, err, "check perms") + require.Equal(t, resp, c.Check) + }) + } +} + func TestListRoles(t *testing.T) { t.Parallel() diff --git a/codersdk/roles.go b/codersdk/roles.go index 727c78e2256c1..fd9ee8387666a 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -43,3 +43,16 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Ro var roles []Role return roles, json.NewDecoder(res.Body).Decode(&roles) } + +func (c *Client) CheckPermissions(ctx context.Context, checks UserPermissionCheckRequest) (UserPermissionCheckResponse, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/permissions/check", uuidOrMe(Me)), checks) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var roles UserPermissionCheckResponse + return roles, json.NewDecoder(res.Body).Decode(&roles) +} diff --git a/codersdk/users.go b/codersdk/users.go index 693277608d5b8..232ca31f043ce 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -8,6 +8,8 @@ import ( "time" "github.com/google/uuid" + + "github.com/coder/coder/coderd/rbac" ) // Me is used as a replacement for your own ID. @@ -76,6 +78,27 @@ type UserRoles struct { OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"` } +type UserPermissionCheckObject struct { + ResourceType string `json:"resource_type,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` + ResourceID string `json:"resource_id,omitempty"` +} + +type UserPermissionCheckResponse map[string]bool + +// UserPermissionCheckRequest is a structure instead of a map because +// go-playground/validate can only validate structs. If you attempt to pass +// a map into 'httpapi.Read', you will get an invalid type error. +type UserPermissionCheckRequest struct { + Checks map[string]UserPermissionCheck `json:"checks"` +} + +type UserPermissionCheck struct { + Object UserPermissionCheckObject `json:"object"` + Action rbac.Action `json:"action"` +} + // LoginWithPasswordRequest enables callers to authenticate with email and password. type LoginWithPasswordRequest struct { Email string `json:"email" validate:"required,email"` From e584a163153148175531db004e6fe4f1100071c7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 09:21:04 -0500 Subject: [PATCH 02/10] Make gen --- site/src/api/typesGenerated.ts | 50 ++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e30c94e8663e6..07b06ace6bef5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -12,7 +12,7 @@ export interface AgentGitSSHKey { readonly private_key: string } -// From codersdk/users.go:100:6 +// From codersdk/users.go:123:6 export interface AuthMethods { readonly password: boolean readonly github: boolean @@ -30,7 +30,7 @@ export interface BuildInfoResponse { readonly version: string } -// From codersdk/users.go:41:6 +// From codersdk/users.go:43:6 export interface CreateFirstUserRequest { readonly email: string readonly username: string @@ -38,13 +38,13 @@ export interface CreateFirstUserRequest { readonly organization: string } -// From codersdk/users.go:49:6 +// From codersdk/users.go:51:6 export interface CreateFirstUserResponse { readonly user_id: string readonly organization_id: string } -// From codersdk/users.go:95:6 +// From codersdk/users.go:118:6 export interface CreateOrganizationRequest { readonly name: string } @@ -77,7 +77,7 @@ export interface CreateTemplateVersionRequest { readonly parameter_values: CreateParameterRequest[] } -// From codersdk/users.go:54:6 +// From codersdk/users.go:56:6 export interface CreateUserRequest { readonly email: string readonly username: string @@ -101,7 +101,7 @@ export interface CreateWorkspaceRequest { readonly parameter_values: CreateParameterRequest[] } -// From codersdk/users.go:91:6 +// From codersdk/users.go:114:6 export interface GenerateAPIKeyResponse { readonly key: string } @@ -119,13 +119,13 @@ export interface GoogleInstanceIdentityToken { readonly json_web_token: string } -// From codersdk/users.go:80:6 +// From codersdk/users.go:103:6 export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:86:6 +// From codersdk/users.go:109:6 export interface LoginWithPasswordResponse { readonly session_token: string } @@ -273,17 +273,17 @@ export interface UpdateActiveTemplateVersion { readonly id: string } -// From codersdk/users.go:70:6 +// From codersdk/users.go:72:6 export interface UpdateRoles { readonly roles: string[] } -// From codersdk/users.go:66:6 +// From codersdk/users.go:68:6 export interface UpdateUserPasswordRequest { readonly password: string } -// From codersdk/users.go:61:6 +// From codersdk/users.go:63:6 export interface UpdateUserProfileRequest { readonly email: string readonly username: string @@ -304,7 +304,7 @@ export interface UploadResponse { readonly hash: string } -// From codersdk/users.go:31:6 +// From codersdk/users.go:33:6 export interface User { readonly id: string readonly email: string @@ -315,13 +315,33 @@ export interface User { readonly roles: Role[] } -// From codersdk/users.go:74:6 +// From codersdk/users.go:97:6 +export interface UserPermissionCheck { + readonly object: UserPermissionCheckObject + // This is likely an enum in an external package ("github.com/coder/coder/coderd/rbac.Action") + readonly action: string +} + +// From codersdk/users.go:81:6 +export interface UserPermissionCheckObject { + readonly resource_type?: string + readonly owner_id?: string + readonly organization_id?: string + readonly resource_id?: string +} + +// From codersdk/users.go:93:6 +export interface UserPermissionCheckRequest { + readonly checks: Record +} + +// From codersdk/users.go:76:6 export interface UserRoles { readonly roles: string[] readonly organization_roles: Record } -// From codersdk/users.go:23:6 +// From codersdk/users.go:25:6 export interface UsersRequest extends Pagination { readonly search: string readonly status: string @@ -422,7 +442,7 @@ export type ParameterScope = "organization" | "template" | "user" | "workspace" // From codersdk/provisionerdaemons.go:26:6 export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" -// From codersdk/users.go:16:6 +// From codersdk/users.go:18:6 export type UserStatus = "active" | "suspended" // From codersdk/workspaceresources.go:15:6 From 8530cdf0fb9819df39f13408d393e7821a6d80eb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 09:33:44 -0500 Subject: [PATCH 03/10] test: Easy method for assigning roles in unit tests --- coderd/coderdtest/coderdtest.go | 41 +++++++++++++++++++++++++++++++-- coderd/roles_test.go | 30 +++--------------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index a77f6bde5575a..f09d94e8a3b86 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -13,6 +13,7 @@ import ( "encoding/base64" "encoding/json" "encoding/pem" + "fmt" "io" "math/big" "net" @@ -24,6 +25,8 @@ import ( "testing" "time" + "github.com/coder/coder/coderd/rbac" + "cloud.google.com/go/compute/metadata" "github.com/fullsailor/pkcs7" "github.com/golang-jwt/jwt" @@ -197,14 +200,14 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirst } // CreateAnotherUser creates and authenticates a new user. -func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uuid.UUID) *codersdk.Client { +func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, roles ...string) *codersdk.Client { req := codersdk.CreateUserRequest{ Email: namesgenerator.GetRandomName(1) + "@coder.com", Username: randomUsername(), Password: "testpass", OrganizationID: organizationID, } - _, err := client.CreateUser(context.Background(), req) + user, err := client.CreateUser(context.Background(), req) require.NoError(t, err) login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{ @@ -215,6 +218,40 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui other := codersdk.New(client.URL) other.SessionToken = login.SessionToken + + if len(roles) > 0 { + // Find the roles for the org vs the site wide roles + orgRoles := make(map[string][]string) + var siteRoles []string + + for _, roleName := range roles { + roleName := roleName + orgID, ok := rbac.IsOrgRole(roleName) + if ok { + orgRoles[orgID] = append(orgRoles[orgID], roleName) + } else { + siteRoles = append(siteRoles, roleName) + } + } + // Update the roles + for _, r := range user.Roles { + siteRoles = append(siteRoles, r.Name) + } + // TODO: @emyrk switch "other" to "client" when we support updating other + // users. + _, err := other.UpdateUserRoles(context.Background(), user.ID, codersdk.UpdateRoles{Roles: siteRoles}) + require.NoError(t, err, "update site roles") + + // Update org roles + for orgID, roles := range orgRoles { + organizationID, err := uuid.Parse(orgID) + require.NoError(t, err, fmt.Sprintf("parse org id %q", orgID)) + // TODO: @Emyrk add the member to the organization if they do not already belong. + _, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID, + codersdk.UpdateRoles{Roles: append(roles, rbac.RoleOrgMember(organizationID))}) + require.NoError(t, err, "update org membership roles") + } + } return other } diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 56ecc4f5e2a8f..b4d79ae6760c4 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -15,24 +15,11 @@ import ( func TestPermissionCheck(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") + orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID)) // With admin, member, and org admin const ( @@ -102,6 +89,7 @@ func TestPermissionCheck(t *testing.T) { for _, c := range testCases { c := c t.Run(c.Name, func(t *testing.T) { + t.Parallel() resp, err := c.Client.CheckPermissions(context.Background(), codersdk.UserPermissionCheckRequest{Checks: params}) require.NoError(t, err, "check perms") require.Equal(t, resp, c.Check) @@ -117,19 +105,7 @@ func TestListRoles(t *testing.T) { // 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") + orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID)) otherOrg, err := client.CreateOrganization(ctx, admin.UserID, codersdk.CreateOrganizationRequest{ Name: "other", From 85dc6a7fe73d6acf7d92998c862ab440527118c1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 09:54:50 -0500 Subject: [PATCH 04/10] Make actions just a string --- codersdk/users.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/codersdk/users.go b/codersdk/users.go index 232ca31f043ce..212a7c16fab46 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -8,8 +8,6 @@ import ( "time" "github.com/google/uuid" - - "github.com/coder/coder/coderd/rbac" ) // Me is used as a replacement for your own ID. @@ -96,7 +94,7 @@ type UserPermissionCheckRequest struct { type UserPermissionCheck struct { Object UserPermissionCheckObject `json:"object"` - Action rbac.Action `json:"action"` + Action string `json:"action"` } // LoginWithPasswordRequest enables callers to authenticate with email and password. From d74eeb421591177733561b4f0bab37ee83b81334 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 10:04:16 -0500 Subject: [PATCH 05/10] Handle action type to string --- coderd/roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/roles.go b/coderd/roles.go index 8eb1db70c19d2..5c9f16672dd9b 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -57,7 +57,7 @@ func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) { if v.Object.OwnerID == "me" { v.Object.OwnerID = roles.ID.String() } - err := api.Authorizer.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, v.Action, + err := api.Authorizer.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.Action(v.Action), rbac.Object{ ResourceID: v.Object.ResourceID, Owner: v.Object.OwnerID, From 79c088617910eb4fdb2bed97bd5606bc0cc01462 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 10:41:52 -0500 Subject: [PATCH 06/10] Add usage comments --- codersdk/users.go | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/codersdk/users.go b/codersdk/users.go index 212a7c16fab46..e1c45e9796c09 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -76,25 +76,54 @@ type UserRoles struct { OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"` } -type UserPermissionCheckObject struct { - ResourceType string `json:"resource_type,omitempty"` - OwnerID string `json:"owner_id,omitempty"` - OrganizationID string `json:"organization_id,omitempty"` - ResourceID string `json:"resource_id,omitempty"` -} - type UserPermissionCheckResponse map[string]bool // UserPermissionCheckRequest is a structure instead of a map because // go-playground/validate can only validate structs. If you attempt to pass // a map into 'httpapi.Read', you will get an invalid type error. type UserPermissionCheckRequest struct { + // Checks is a map keyed with an arbitrary string to a permission check. + // The key can be any string that is helpful to the caller, and allows + // multiple permission checks to be run in a single request. + // The key ensures that each permission check has the same key in the + // response. Checks map[string]UserPermissionCheck `json:"checks"` } +// UserPermissionCheck is used to check if a user can do a given action +// to a given set of objects. type UserPermissionCheck struct { + // Object can represent a "set" of objects, such as: + // - All workspaces in an organization + // - All workspaces owned by me + // - All workspaces across the entire product + // When defining an object, use the most specific language when possible to + // produce the smallest set. Meaning to set as many fields on 'Object' as + // you can. Example, if you want to check if you can update all workspaces + // owned by 'me', try to also add an 'OrganizationID' to the settings. + // Omitting the 'OrganizationID' could produce the incorrect value, as + // workspaces have both `user` and `organization` owners. Object UserPermissionCheckObject `json:"object"` - Action string `json:"action"` + // Action can be 'create', 'read', 'update', or 'delete' + Action string `json:"action"` +} + +type UserPermissionCheckObject struct { + // ResourceType is the name of the resource. + // './coderd/rbac/object.go' has the list of valid resource types. + ResourceType string `json:"resource_type,omitempty"` + // OwnerID (optional) is a user_id. It adds the set constraint to all resources owned + // by a given user. + OwnerID string `json:"owner_id,omitempty"` + // OrganizationID (optional) is an organization_id. It adds the set constraint to + // all resources owned by a given organization. + OrganizationID string `json:"organization_id,omitempty"` + // ResourceID (optional) reduces the set to a singular resource. This assigns + // a resource ID to the resource type, eg: a single workspace. + // The rbac library will not fetch the resource from the database, so if you + // are using this option, you should also set the 'OwnerID' and 'OrganizationID' + // if possible. Be as specific as possible using all the fields relevant. + ResourceID string `json:"resource_id,omitempty"` } // LoginWithPasswordRequest enables callers to authenticate with email and password. From 899eccc4dbe63051bef2fdaea2a7e0b756b4b9d7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 11 May 2022 14:33:34 -0500 Subject: [PATCH 07/10] Make resource type required on the typescript --- codersdk/users.go | 2 +- site/src/api/typesGenerated.ts | 39 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/codersdk/users.go b/codersdk/users.go index e1c45e9796c09..7172a972c5537 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -111,7 +111,7 @@ type UserPermissionCheck struct { type UserPermissionCheckObject struct { // ResourceType is the name of the resource. // './coderd/rbac/object.go' has the list of valid resource types. - ResourceType string `json:"resource_type,omitempty"` + ResourceType string `json:"resource_type"` // OwnerID (optional) is a user_id. It adds the set constraint to all resources owned // by a given user. OwnerID string `json:"owner_id,omitempty"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 07b06ace6bef5..c8552e786871e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -12,7 +12,7 @@ export interface AgentGitSSHKey { readonly private_key: string } -// From codersdk/users.go:123:6 +// From codersdk/users.go:150:6 export interface AuthMethods { readonly password: boolean readonly github: boolean @@ -30,7 +30,7 @@ export interface BuildInfoResponse { readonly version: string } -// From codersdk/users.go:43:6 +// From codersdk/users.go:41:6 export interface CreateFirstUserRequest { readonly email: string readonly username: string @@ -38,13 +38,13 @@ export interface CreateFirstUserRequest { readonly organization: string } -// From codersdk/users.go:51:6 +// From codersdk/users.go:49:6 export interface CreateFirstUserResponse { readonly user_id: string readonly organization_id: string } -// From codersdk/users.go:118:6 +// From codersdk/users.go:145:6 export interface CreateOrganizationRequest { readonly name: string } @@ -77,7 +77,7 @@ export interface CreateTemplateVersionRequest { readonly parameter_values: CreateParameterRequest[] } -// From codersdk/users.go:56:6 +// From codersdk/users.go:54:6 export interface CreateUserRequest { readonly email: string readonly username: string @@ -101,7 +101,7 @@ export interface CreateWorkspaceRequest { readonly parameter_values: CreateParameterRequest[] } -// From codersdk/users.go:114:6 +// From codersdk/users.go:141:6 export interface GenerateAPIKeyResponse { readonly key: string } @@ -119,13 +119,13 @@ export interface GoogleInstanceIdentityToken { readonly json_web_token: string } -// From codersdk/users.go:103:6 +// From codersdk/users.go:130:6 export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:109:6 +// From codersdk/users.go:136:6 export interface LoginWithPasswordResponse { readonly session_token: string } @@ -273,17 +273,17 @@ export interface UpdateActiveTemplateVersion { readonly id: string } -// From codersdk/users.go:72:6 +// From codersdk/users.go:70:6 export interface UpdateRoles { readonly roles: string[] } -// From codersdk/users.go:68:6 +// From codersdk/users.go:66:6 export interface UpdateUserPasswordRequest { readonly password: string } -// From codersdk/users.go:63:6 +// From codersdk/users.go:61:6 export interface UpdateUserProfileRequest { readonly email: string readonly username: string @@ -304,7 +304,7 @@ export interface UploadResponse { readonly hash: string } -// From codersdk/users.go:33:6 +// From codersdk/users.go:31:6 export interface User { readonly id: string readonly email: string @@ -315,33 +315,32 @@ export interface User { readonly roles: Role[] } -// From codersdk/users.go:97:6 +// From codersdk/users.go:95:6 export interface UserPermissionCheck { readonly object: UserPermissionCheckObject - // This is likely an enum in an external package ("github.com/coder/coder/coderd/rbac.Action") readonly action: string } -// From codersdk/users.go:81:6 +// From codersdk/users.go:111:6 export interface UserPermissionCheckObject { - readonly resource_type?: string + readonly resource_type: string readonly owner_id?: string readonly organization_id?: string readonly resource_id?: string } -// From codersdk/users.go:93:6 +// From codersdk/users.go:84:6 export interface UserPermissionCheckRequest { readonly checks: Record } -// From codersdk/users.go:76:6 +// From codersdk/users.go:74:6 export interface UserRoles { readonly roles: string[] readonly organization_roles: Record } -// From codersdk/users.go:25:6 +// From codersdk/users.go:23:6 export interface UsersRequest extends Pagination { readonly search: string readonly status: string @@ -442,7 +441,7 @@ export type ParameterScope = "organization" | "template" | "user" | "workspace" // From codersdk/provisionerdaemons.go:26:6 export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" -// From codersdk/users.go:18:6 +// From codersdk/users.go:16:6 export type UserStatus = "active" | "suspended" // From codersdk/workspaceresources.go:15:6 From 9f3e8e4fe181498f0db81dfff22966d76ff83f59 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 May 2022 09:08:45 -0500 Subject: [PATCH 08/10] feat: Support map types for codersdk typescript gen --- scripts/apitypings/main.go | 22 ++++++++++++++++++++++ site/src/api/typesGenerated.ts | 3 +++ 2 files changed, 25 insertions(+) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 4afcecc9cd2d3..88eb51fd6391d 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -183,6 +183,28 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { // type string // These are enums. Store to expand later. enums[obj.Name()] = obj + case *types.Map: + // Declared maps that are not structs are still valid codersdk objects. + // Handle them custom by calling 'typescriptType' directly instead of + // iterating through each struct field. + // These types support no json/typescript tags. + // These are **NOT** enums, as a map in Go would never be used for an enum. + ts, err := g.typescriptType(obj.Type().Underlying()) + if err != nil { + return nil, xerrors.Errorf("(map) generate %q: %w", obj.Name(), err) + } + + var str strings.Builder + _, _ = str.WriteString(g.posLine(obj)) + if ts.AboveTypeLine != "" { + str.WriteString(ts.AboveTypeLine) + str.WriteRune('\n') + } + // Use similar output syntax to enums. + str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) + structs[obj.Name()] = str.String() + case *types.Array, *types.Slice: + // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. } case *types.Var: // TODO: Are any enums var declarations? This is also codersdk.Me. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c8552e786871e..81d0747623c16 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -334,6 +334,9 @@ export interface UserPermissionCheckRequest { readonly checks: Record } +// From codersdk/users.go:79:6 +export type UserPermissionCheckResponse = Record + // From codersdk/users.go:74:6 export interface UserRoles { readonly roles: string[] From 210d2c1dab776223f74108f62bd9d6bb3a463de5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 May 2022 11:22:43 -0500 Subject: [PATCH 09/10] Rename to authorization --- coderd/coderd.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 1ddf492c07e8c..4d394cb9362ae 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -250,9 +250,7 @@ func New(options *Options) (http.Handler, func()) { r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) - r.Route("/permissions", func(r chi.Router) { - r.Post("/check", api.checkPermissions) - }) + r.Post("/authorization", api.checkPermissions) r.Post("/keys", api.postAPIKey) r.Route("/organizations", func(r chi.Router) { From 9a66d887e40643e4d59b968b73132b44e9fb9783 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 May 2022 12:24:38 -0500 Subject: [PATCH 10/10] Fix sdk url --- codersdk/roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/roles.go b/codersdk/roles.go index fd9ee8387666a..c67255b435b53 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -45,7 +45,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Ro } func (c *Client) CheckPermissions(ctx context.Context, checks UserPermissionCheckRequest) (UserPermissionCheckResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/permissions/check", uuidOrMe(Me)), checks) + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", uuidOrMe(Me)), checks) if err != nil { return nil, err }