Skip to content

Commit

Permalink
lxd: Clarify authentication/authorization for viewing/exporting images.
Browse files Browse the repository at this point in the history
Signed-off-by: Mark Laing <[email protected]>
  • Loading branch information
markylaing committed Jul 9, 2024
1 parent 9df996e commit b71f344
Showing 1 changed file with 109 additions and 43 deletions.
152 changes: 109 additions & 43 deletions lxd/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ func imagesPost(d *Daemon, r *http.Request) response.Response {

projectName := request.ProjectParam(r)

// If the client is not authenticated, CheckPermission will return false.
var userCanCreateImages bool
err := s.Authorizer.CheckPermission(r.Context(), entity.ProjectURL(projectName), auth.EntitlementCanCreateImages)
if err != nil && !auth.IsDeniedError(err) {
Expand Down Expand Up @@ -1631,14 +1632,12 @@ func imagesGet(d *Daemon, r *http.Request) response.Response {

request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName)

// Check if the caller is authenticated via the request context.
trusted, err := request.GetCtxValue[bool](r.Context(), request.CtxTrusted)
if err != nil {
return response.InternalError(fmt.Errorf("Failed getting authentication status: %w", err))
}
// If the caller is not trusted, we only want to list public images.
publicOnly := !auth.IsTrusted(r.Context())

// Get a permission checker. If the caller is not authenticated, the permission checker will deny all.
// However, the permission checker will not be called for public images.
// However, the permission checker is only called when an image is private. Both trusted and untrusted clients will
// still see public images.
canViewImage, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeImage)
if err != nil {
return response.SmartError(err)
Expand All @@ -1651,7 +1650,7 @@ func imagesGet(d *Daemon, r *http.Request) response.Response {

var result any
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, !trusted, clauses, canViewImage)
result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, publicOnly, clauses, canViewImage)
if err != nil {
return err
}
Expand Down Expand Up @@ -2993,37 +2992,65 @@ func imageGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

// Get the image (expand partial fingerprints).
trusted := auth.IsTrusted(r.Context())
secret := r.FormValue("secret")

// Unauthenticated clients that do not provide a secret may only view public images.
publicOnly := !trusted && secret == ""

// Get the image. We need to do this before the permission check because the URL in the permission check will not
// work with partial fingerprints.
var info *api.Image
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
info, err = doImageGet(ctx, tx, projectName, fingerprint, false)
info, err = doImageGet(ctx, tx, projectName, fingerprint, publicOnly)
if err != nil {
return err
}

return nil
})
if err != nil {
if err != nil && api.StatusErrorCheck(err, http.StatusNotFound) {
// Return a generic not found. This is so that the caller cannot determine the existence of an image by the
// contents of the error message.
return response.NotFound(nil)
} else if err != nil {
return response.SmartError(err)
}

// Access check.
var userCanViewImage bool
err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, info.Fingerprint), auth.EntitlementCanView)
if err != nil && !auth.IsDeniedError(err) {
return response.SmartError(err)
} else if err == nil {
userCanViewImage = true
}
if secret != "" {
// If a secret was provided, validate it regardless of whether the image is public or the caller has sufficient
// privilege. This is to ensure the image token operation is cancelled.
op, err := imageValidSecret(s, r, projectName, info.Fingerprint, secret)
if err != nil {
return response.SmartError(err)
}

secret := r.FormValue("secret")
// If an operation was found the caller has access, otherwise continue to other access checks.
if op != nil {
userCanViewImage = true
}
}

op, err := imageValidSecret(s, r, projectName, info.Fingerprint, secret)
if err != nil {
return response.SmartError(err)
// No operation found for the secret. Perform other access checks.
if !userCanViewImage {
if info.Public {
// If the image is public any client can view it.
userCanViewImage = true
} else {
// Otherwise perform an access check with the full image fingerprint.
err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, info.Fingerprint), auth.EntitlementCanView)
if err != nil && !auth.IsDeniedError(err) {
return response.SmartError(err)
} else if err == nil {
userCanViewImage = true
}
}
}

// If the caller does not have permission to view the image and the secret was invalid, return generic not found.
if !info.Public && !userCanViewImage && op == nil {
// If the client still cannot view the image, return a generic not found error.
if !userCanViewImage {
return response.NotFound(nil)
}

Expand Down Expand Up @@ -4010,43 +4037,82 @@ func imageExport(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

// Get the image (expand the fingerprint).
isDevLXDQuery := r.RemoteAddr == "@devlxd"
secret := r.FormValue("secret")
trusted := auth.IsTrusted(r.Context())

// Unauthenticated clients that do not provide a secret may only view public images.
// Devlxd queries can be for private images but only if they are cached.
publicOnly := !trusted && secret == "" && !isDevLXDQuery

// Get the image. We need to do this before the permission check because the URL in the permission check will not
// work with partial fingerprints.
var imgInfo *api.Image
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
// Get the image (expand the fingerprint).
_, imgInfo, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName})
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
filter := dbCluster.ImageFilter{Project: &projectName}
if publicOnly {
filter.Public = &publicOnly
}

_, imgInfo, err = tx.GetImage(ctx, fingerprint, filter)
return err
})
if err != nil {
if err != nil && api.StatusErrorCheck(err, http.StatusNotFound) {
// Return a generic not found. This is so that the caller cannot determine the existence of an image by the
// contents of the error message.
return response.NotFound(nil)
} else if err != nil {
return response.SmartError(err)
}

// Access control.
var userCanViewImage bool
err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, imgInfo.Fingerprint), auth.EntitlementCanView)
if err != nil && !auth.IsDeniedError(err) {
return response.SmartError(err)
} else if err == nil {
userCanViewImage = true
}

secret := r.FormValue("secret")

if r.RemoteAddr == "@devlxd" {
if !imgInfo.Public && !imgInfo.Cached {
return response.NotFound(fmt.Errorf("Image %q not found", fingerprint))
}
} else {
if secret != "" {
// If a secret was provided, validate it regardless of whether the image is public or the caller has sufficient
// privilege. This is to ensure the image token operation is cancelled.
op, err := imageValidSecret(s, r, projectName, imgInfo.Fingerprint, secret)
if err != nil {
return response.SmartError(err)
}

// If the image is not public and the caller cannot view it, return a generic not found error.
if !imgInfo.Public && !userCanViewImage && op == nil {
// If an operation was found the caller has access, otherwise continue to other access checks.
if op != nil {
userCanViewImage = true
}
}

if isDevLXDQuery {
// A devlxd query must contain the full fingerprint of the image (no partials).
if fingerprint != imgInfo.Fingerprint {
return response.NotFound(nil)
}

// It must also be for a public or cached image.
if !(imgInfo.Public || imgInfo.Cached) {
return response.NotFound(nil)
}

userCanViewImage = true
}

if !userCanViewImage {
if imgInfo.Public {
// If the image is public any client can view it.
userCanViewImage = true
} else {
// Otherwise perform an access check with the full image fingerprint.
err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, imgInfo.Fingerprint), auth.EntitlementCanView)
if err != nil && !auth.IsDeniedError(err) {
return response.SmartError(err)
} else if err == nil {
userCanViewImage = true
}
}
}

// If the client still cannot view the image, return a generic not found error.
if !userCanViewImage {
return response.NotFound(nil)
}

var address string
Expand Down

0 comments on commit b71f344

Please sign in to comment.