Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion lib/oci/auth/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule OCI.Auth.Adapter do
"""
@type subject_t :: any()

@type error_details_t :: any()
@type error_details_t :: map()

@callback init(config :: map()) :: {:ok, t()} | {:error, term()}

Expand Down
17 changes: 10 additions & 7 deletions lib/oci/auth/static.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,24 @@ defmodule OCI.Auth.Static do
subject = username
{:ok, subject}
else
{:error, :UNAUTHORIZED, "Invalid username or password"}
{:error, :UNAUTHORIZED, %{reason: "Invalid username or password"}}
end

_ ->
{:error, :UNAUTHORIZED,
"Invalid authorization format, should be username:password"}
%{reason: "Invalid authorization format, should be username:password"}}
end

:error ->
{:error, :UNAUTHORIZED,
"Failed to decode authorization, should be base64 encoded username:password"}
%{
reason:
"Failed to decode authorization, should be base64 encoded username:password"
}}
end

_other ->
{:error, :UNSUPPORTED, "Unsupported authentication scheme: #{scheme}"}
{:error, :UNSUPPORTED, %{reason: "Unsupported authentication scheme: #{scheme}"}}
end
end

Expand All @@ -73,18 +76,18 @@ defmodule OCI.Auth.Static do

case required_action(ctx.method, ctx.endpoint) do
nil ->
{:error, :DENIED}
{:error, :DENIED, %{repo: ctx.repo, method: ctx.method}}

action ->
if action in repo_perms do
:ok
else
{:error, :DENIED}
{:error, :DENIED, %{repo: ctx.repo, action: action}}
end
end

_ ->
{:error, :DENIED}
{:error, :DENIED, %{subject: ctx.subject}}
end
end

Expand Down
9 changes: 4 additions & 5 deletions lib/oci/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ defmodule OCI.Plug do
end

def call(conn, _opts) do
error_resp(conn, :UNSUPPORTED, "OCI Registry must be mounted at /#{Registry.api_version()}")
error_resp(conn, :UNSUPPORTED, %{
reason: "OCI Registry must be mounted at /#{Registry.api_version()}"
})
end

def authenticate(%{private: %{oci_registry: registry}} = conn) do
Expand Down Expand Up @@ -64,16 +66,13 @@ defmodule OCI.Plug do
:ok ->
conn

{:error, reason} ->
error_resp(conn, reason, nil)

{:error, reason, details} ->
error_resp(conn, reason, details)
end
end

defp authorize(_) do
{:error, :UNAUTHORIZED}
{:error, :UNAUTHORIZED, %{}}
end

defp challenge_resp(conn) do
Expand Down
1 change: 1 addition & 0 deletions lib/oci/plug/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule OCI.Plug.Context do

def init(opts), do: opts

# credo:disable-for-next-line Credo.Check.Refactor.ABCSize
def call(conn, _opts \\ []) do
segments = conn.path_info |> Enum.reverse()

Expand Down
38 changes: 13 additions & 25 deletions lib/oci/plug/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@ defmodule OCI.Plug.Handler do
%Plug.Conn{} = conn ->
conn

{:error, oci_error_status} ->
you_suck_and_are_a_bad_person_messsage = """
You suck and are a bad person.

Please return error details for #{oci_error_status} in #{inspect(ctx)}
"""

error_resp(conn, oci_error_status, you_suck_and_are_a_bad_person_messsage)

{:error, oci_error_status, details} ->
error_resp(conn, oci_error_status, details)
end
Expand All @@ -55,7 +46,7 @@ defmodule OCI.Plug.Handler do
repo :: String.t(),
id :: String.t(),
ctx :: OCI.Context.t()
) :: Plug.Conn.t() | {:error, atom()} | {:error, atom(), String.t()}
) :: Plug.Conn.t() | {:error, atom(), map() | String.t()}
defp dispatch(%{method: "GET"} = conn, :tags_list, registry, repo, _id, ctx) do
pag = pagination(conn.query_params)

Expand Down Expand Up @@ -179,7 +170,7 @@ defmodule OCI.Plug.Handler do
error_resp(
conn,
:BLOB_UPLOAD_INVALID,
"Content-Range header is required for PATCH requests"
%{reason: "Content-Range header is required for PATCH requests"}
)

_ ->
Expand Down Expand Up @@ -277,21 +268,12 @@ defmodule OCI.Plug.Handler do

defp dispatch(%{method: "PUT"} = conn, :manifests, registry, repo, reference, ctx) do
manifest = conn.params
raw_manifest = conn.assigns[:oci_raw_manifest]
manifest_digest = conn.assigns[:oci_digest]

with :ok <- Registry.store_manifest(registry, repo, reference, manifest, manifest_digest, ctx) do
maybe_set_oci_subject = fn conn ->
case get_in(conn.params, ["subject", "digest"]) do
nil ->
conn

subject_digest ->
put_resp_header(conn, "oci-subject", subject_digest)
end
end

with :ok <- Registry.store_manifest(registry, repo, reference, manifest, raw_manifest, manifest_digest, ctx) do
conn
|> maybe_set_oci_subject.()
|> maybe_set_oci_subject(manifest)
|> put_resp_header("location", Registry.manifests_reference_path(repo, reference))
|> send_resp(201, "")
end
Expand Down Expand Up @@ -325,7 +307,14 @@ defmodule OCI.Plug.Handler do
method = conn.method
path = conn.request_path

{:error, :UNSUPPORTED, "Unsupported [#{method}] #{path}"}
{:error, :UNSUPPORTED, %{method: method, path: path}}
end

defp maybe_set_oci_subject(conn, manifest) do
case get_in(manifest, ["subject", "digest"]) do
nil -> conn
subject_digest -> put_resp_header(conn, "oci-subject", subject_digest)
end
end

defp pagination(params) do
Expand Down Expand Up @@ -366,7 +355,6 @@ defmodule OCI.Plug.Handler do
ctx
) do
{:ok, _, _} -> :ok
{:error, reason} -> {:error, reason}
{:error, reason, details} -> {:error, reason, details}
end
end
Expand Down
50 changes: 18 additions & 32 deletions lib/oci/plug/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,15 @@ defmodule OCI.Plug.Parser do
## Content Types Handled

### application/octet-stream
Handles binary blob uploads by reading the full body and storing
it in the connection assigns under the `:oci_blob_chunk` key.
Reads the full body and stores it in `conn.assigns[:oci_blob_chunk]`.

### application/vnd.oci.image.manifest.v1+json
Handles OCI image manifest uploads by:
1. Reading the full body
2. Computing its SHA256 digest
3. Storing the digest in the connection assigns
4. Decoding the JSON manifest
### OCI manifest types
Reads the full body, computes its SHA256 digest, decodes the JSON, and stores
the raw body and digest in conn assigns (`:oci_raw_manifest` and `:oci_digest`).
The decoded manifest is returned as params.

### Other Content Types
Passes through to the next parser in the chain.

## Parameters
- conn: The Plug.Conn struct
- type: The content type
- subtype: The content subtype
- headers: The request headers
- opts: Parser options containing a :json_decoder key for manifest parsing

## Returns
- For octet-stream: `{:ok, %{}, conn}` on successful parsing
- For manifest: `{:ok, manifest, conn}` on successful parsing
- For other types: `{:next, conn}` to pass to the next parser
- `{:error, reason}` on failure
- Raises `Plug.Parsers.ParseError` on JSON decode failure for manifests
Passes through to the next parser via `{:next, conn}`.
"""
def parse(conn, "application", "octet-stream", _headers, opts) do
read_full_body(conn, opts, "")
Expand All @@ -65,22 +48,25 @@ defmodule OCI.Plug.Parser do
end
end

def parse(conn, "application", "vnd.oci.image.manifest.v1+json", headers, opts) do
parse_oci_manifest(conn, headers, opts)
def parse(conn, "application", "vnd.oci.image.manifest.v1+json", _headers, opts) do
read_oci_manifest(conn, opts)
end

def parse(conn, "application", "vnd.oci.image.index.v1+json", headers, opts) do
parse_oci_manifest(conn, headers, opts)
def parse(conn, "application", "vnd.oci.image.index.v1+json", _headers, opts) do
read_oci_manifest(conn, opts)
end

def parse(conn, _type, _subtype, _headers, _opts), do: {:next, conn}

defp parse_oci_manifest(conn, _headers, opts) do
read_full_body(conn, opts, "")
|> case do
defp read_oci_manifest(conn, opts) do
case read_full_body(conn, opts, "") do
{:ok, full_body, conn} ->
digest = :crypto.hash(:sha256, full_body) |> Base.encode16(case: :lower)
conn = Plug.Conn.assign(conn, :oci_digest, "sha256:#{digest}")

conn =
conn
|> Plug.Conn.assign(:oci_digest, "sha256:#{digest}")
|> Plug.Conn.assign(:oci_raw_manifest, full_body)

decoder = Keyword.fetch!(opts, :json_decoder)

Expand All @@ -92,7 +78,7 @@ defmodule OCI.Plug.Parser do
raise Plug.Parsers.ParseError, exception: %Plug.Parsers.BadEncodingError{}
end

err ->
{:error, _} = err ->
err
end
end
Expand Down
Loading
Loading