Ctrl K
ComunidadGuías…

coderd/httpmw/organizationparam.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package httpmw

import (
	"context"
	"fmt"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"

	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/httpapi"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/rbac/policy"
	"github.com/coder/coder/v2/codersdk"
)

type (
	organizationParamContextKey        struct{}
	organizationMemberParamContextKey  struct{}
	organizationMembersParamContextKey struct{}
)

// OrganizationParam returns the organization from the ExtractOrganizationParam handler.
func OrganizationParam(r *http.Request) database.Organization {
	organization, ok := r.Context().Value(organizationParamContextKey{}).(database.Organization)
	if !ok {
		panic("developer error: organization param middleware not provided")
	}
	return organization
}

// OrganizationMemberParam returns the organization membership that allowed the query
// from the ExtractOrganizationParam handler.
func OrganizationMemberParam(r *http.Request) OrganizationMember {
	organizationMember, ok := r.Context().Value(organizationMemberParamContextKey{}).(OrganizationMember)
	if !ok {
		panic("developer error: organization member param middleware not provided")
	}
	return organizationMember
}

func OrganizationMembersParam(r *http.Request) OrganizationMembers {
	organizationMembers, ok := r.Context().Value(organizationMembersParamContextKey{}).(OrganizationMembers)
	if !ok {
		panic("developer error: organization members param middleware not provided")
	}
	return organizationMembers
}

// ExtractOrganizationParam grabs an organization from the "organization" URL parameter.
// This middleware requires the API key middleware higher in the call stack for authentication.
func ExtractOrganizationParam(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) {
			ctx := r.Context()
			arg := chi.URLParam(r, "organization")
			if arg == "" {
				httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
					Message: "\"organization\" must be provided.",
				})
				return
			}

			var organization database.Organization
			var dbErr error

			// If the name is exactly "default" or the nil UUID, then we fetch
			// the default organization. This is a special case to make it
			// easier for single org deployments.
			if arg == codersdk.DefaultOrganization || arg == uuid.Nil.String() {
				organization, dbErr = db.GetDefaultOrganization(ctx)
			} else {
				// Try by name or uuid.
				id, err := uuid.Parse(arg)
				if err == nil {
					organization, dbErr = db.GetOrganizationByID(ctx, id)
				} else {
					organization, dbErr = db.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{
						Name:    arg,
						Deleted: false,
					})
				}
			}
			if httpapi.Is404Error(dbErr) {
				httpapi.ResourceNotFound(rw)
				return
			}
			if dbErr != nil {
				httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
					Message: fmt.Sprintf("Internal error fetching organization %q.", arg),
					Detail:  dbErr.Error(),
				})
				return
			}
			ctx = context.WithValue(ctx, organizationParamContextKey{}, organization)
			next.ServeHTTP(rw, r.WithContext(ctx))
		})
	}
}

// OrganizationMember is the database object plus the Username and Avatar URL. Including these
// in the middleware is preferable to a join at the SQL layer so that we can keep the
// autogenerated database types as they are.
type OrganizationMember struct {
	database.OrganizationMember
	Username  string
	AvatarURL string
}

// ExtractOrganizationMemberParam grabs a user membership from the "organization" and "user" URL parameter.
// This middleware requires the ExtractUser and ExtractOrganization middleware higher in the stack
func ExtractOrganizationMemberParam(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) {
			ctx := r.Context()
			organization := OrganizationParam(r)
			_, members, done := ExtractOrganizationMember(ctx, nil, rw, r, db, organization.ID)
			if done {
				return
			}

			if len(members) != 1 {
				httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
					Message: "Internal error fetching organization member.",
					// This is a developer error and should never happen.
					Detail: fmt.Sprintf("Expected exactly one organization member, but got %d.", len(members)),
				})
				return
			}

			organizationMember := members[0]

			ctx = context.WithValue(ctx, organizationMemberParamContextKey{}, OrganizationMember{
				OrganizationMember: organizationMember.OrganizationMember,
				// Here we're making two exceptions to the rule about not leaking data about the user
				// to the API handler, which is to include the username and avatar URL.
				// If the caller has permission to read the OrganizationMember, then we're explicitly
				// saying here that they also have permission to see the member's username and avatar.
				// This is OK!
				//
				// API handlers need this information for audit logging and returning the owner's
				// username in response to creating a workspace. Additionally, the frontend consumes
				// the Avatar URL and this allows the FE to avoid an extra request.
				Username:  organizationMember.Username,
				AvatarURL: organizationMember.AvatarURL,
			})

			next.ServeHTTP(rw, r.WithContext(ctx))
		})
	}
}

// ExtractOrganizationMember extracts all user memberships from the "user" URL
// parameter. If orgID is uuid.Nil, then it will return all memberships for the
// user, otherwise it will only return memberships to the org.
//
// If `user` is returned, that means the caller can use the data. This is returned because
// it is possible to have a user with 0 organizations. So the user != nil, with 0 memberships.
func ExtractOrganizationMember(ctx context.Context, auth func(r *http.Request, action policy.Action, object rbac.Objecter) bool, rw http.ResponseWriter, r *http.Request, db database.Store, orgID uuid.UUID) (*database.User, []database.OrganizationMembersRow, bool) {
	// We need to resolve the `{user}` URL parameter so that we can get the userID and
	// username.  We do this as SystemRestricted since the caller might have permission
	// to access the OrganizationMember object, but *not* the User object.  So, it is
	// very important that we do not add the User object to the request context or otherwise
	// leak it to the API handler.
	// nolint:gocritic
	user, ok := ExtractUserContext(dbauthz.AsSystemRestricted(ctx), db, rw, r)
	if !ok {
		return nil, nil, true
	}

	organizationMembers, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{
		OrganizationID: orgID,
		UserID:         user.ID,
		IncludeSystem:  true,
		GithubUserID:   0,
	})
	if httpapi.Is404Error(err) {
		httpapi.ResourceNotFound(rw)
		return nil, nil, true
	}
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching organization member.",
			Detail:  err.Error(),
		})
		return nil, nil, true
	}

	// Only return the user data if the caller can read the user object.
	if auth != nil && auth(r, policy.ActionRead, user) {
		return &user, organizationMembers, false
	}

	// If the user cannot be read and 0 memberships exist, throw a 404 to not
	// leak the user existence.
	if len(organizationMembers) == 0 {
		httpapi.ResourceNotFound(rw)
		return nil, nil, true
	}

	return nil, organizationMembers, false
}

type OrganizationMembers struct {
	// User is `nil` if the caller is not allowed access to the site wide
	// user object.
	User *database.User
	// Memberships can only be length 0 if `user != nil`. If `user == nil`, then
	// memberships will be at least length 1.
	Memberships []OrganizationMember
}

func (om OrganizationMembers) UserID() uuid.UUID {
	if om.User != nil {
		return om.User.ID
	}

	if len(om.Memberships) > 0 {
		return om.Memberships[0].UserID
	}
	return uuid.Nil
}

// ExtractOrganizationMembersParam grabs all user organization memberships.
// Only requires the "user" URL parameter.
//
// Use this if you want to grab as much information for a user as you can.
// From an organization context, site wide user information might not available.
func ExtractOrganizationMembersParam(db database.Store, auth func(r *http.Request, action policy.Action, object rbac.Objecter) bool) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			ctx := r.Context()

			// Fetch all memberships
			user, members, done := ExtractOrganizationMember(ctx, auth, rw, r, db, uuid.Nil)
			if done {
				return
			}

			orgMembers := make([]OrganizationMember, 0, len(members))
			for _, organizationMember := range members {
				orgMembers = append(orgMembers, OrganizationMember{
					OrganizationMember: organizationMember.OrganizationMember,
					Username:           organizationMember.Username,
					AvatarURL:          organizationMember.AvatarURL,
				})
			}

			ctx = context.WithValue(ctx, organizationMembersParamContextKey{}, OrganizationMembers{
				User:        user,
				Memberships: orgMembers,
			})
			next.ServeHTTP(rw, r.WithContext(ctx))
		})
	}
}