Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: separate auth into backends #134

Merged
merged 4 commits into from
Jun 1, 2024
Merged
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
34 changes: 34 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM ghcr.io/oras-project/registry:latest

LABEL maintainer="Vanessasaurus <@vsoch>"

RUN apk update && apk add python3 py3-pip git make apache2-utils bash && \
pip install --upgrade pip setuptools

ENV registry_host=localhost
ENV registry_port=5000
ENV with_auth=true
ENV REGISTRY_AUTH: "{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}"
ENV REGISTRY_STORAGE_DELETE_ENABLED="true"
RUN htpasswd -cB -b auth.htpasswd myuser mypass && \
cp auth.htpasswd /etc/docker/registry/auth.htpasswd && \
registry serve /etc/docker/registry/config.yml & sleep 5 && \
echo $PWD && ls $PWD

# Match the default user id for a single system so we aren't root
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000
ENV USERNAME=${USERNAME}
ENV USER_UID=${USER_UID}
ENV USER_GID=${USER_GID}
ENV GO_VERSION=1.21.9


# Add the group and user that match our ids
RUN addgroup -S ${USERNAME} && adduser -S ${USERNAME} -G ${USERNAME} && \
echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers

USER $USERNAME
# make install
# make test
16 changes: 16 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "Oras Python with Auth Development Environment",
"dockerFile": "Dockerfile",
"context": "../",

"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
]
}
},
"postStartCommand": "git config --global --add safe.directory /workspaces/oras-py"
}
2 changes: 1 addition & 1 deletion .github/dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pre-commit
black==24.1.0
black==24.3.0
isort
flake8
mypy==0.961
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
ports:
- 5000:5000
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
Expand Down
7 changes: 1 addition & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,11 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 24.3.0
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
hooks:
- id: mypy
additional_dependencies: ["types-requests"]
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
The versions coincide with releases on pip. Only major versions will be released as tags on Github.

## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x)
- refactor of auth to be provided by backend modules (0.2.0)
- Allow generating a Subject from a pre-existing Manifest (0.1.30)
- add option to not refresh headers during the pushing flow, useful for push with basic auth (0.1.29)
- enable additionalProperties in schema validation (0.1.28)
Expand Down
14 changes: 13 additions & 1 deletion docs/getting_started/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ read [the installation guide](installation.md) you
should do that first. If you want to see a more general user guide with examples
for using the SDK and writing clients, see our [user guide](user-guide.md).


## Running Tests

You'll want to create an environment to install to, and then install:
Expand All @@ -21,6 +20,19 @@ We recommend a local registry without auth for tests.
$ docker run -it --rm -p 5000:5000 ghcr.io/oras-project/registry:latest
```

Zot is a good solution too:

```bash
docker run -d -p 5000:5000 --name oras-quickstart ghcr.io/project-zot/zot-linux-amd64:latest
```

For quick auth, you can use the included Developer container and do:

```bash
make install
make test
```

And then when you run `make test`, the tests will run. This ultimately
runs the file [scripts/test.sh](https://github.com/oras-project/oras-py/blob/main/scripts/test.sh).
If you want to test interactively, add an IPython import statement somewhere in the tests:
Expand Down
77 changes: 25 additions & 52 deletions docs/getting_started/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ $ docker run -it --rm -p 5000:5000 ghcr.io/oras-project/registry:latest
```

And add the `-d` for detached. If you are brave and want to try basic auth:

bash
```bash
# This is an htpassword file, "b" means bcrypt
htpasswd -cB -b auth.htpasswd myuser mypass
Expand Down Expand Up @@ -67,9 +67,8 @@ class MyProvider(oras.client.OrasClient):
pass
```

If you don't use this class, it's recommended to set basic auth with
`self.set_basic_auth(username, password)`, which is provided by `oras.provider.Registry`.
Also note that we currently just have one provider type (the `Registry`) and if you
Authentication is provided by custom modules, and you can read about loading
and using them [here](#authentication). Also note that we currently just have one provider type (the `Registry`) and if you
have an idea for a request or custom provider, please [let us know](https://github.com/oras-project/oras-py/issues).

### Creating OCI Objects
Expand Down Expand Up @@ -204,7 +203,7 @@ You can read more about how registries provide tags [at the distribution spec](h
### Push Interactions

Let's start with a very basic push interaction, and this one
follows [the example here](https://oras.land/cli/1_pushing/).
follows [the example here](https://oras.land/docs/how_to_guides/pushing_and_pulling/#pushing-artifacts-with-single-file).

<details>

Expand Down Expand Up @@ -369,14 +368,11 @@ def get_oras_client():
"""
Consistent method to get an oras client
"""
user = os.environ.get("ORAS_USER")
password = os.environ.get("ORAS_PASS")
reg = Registry()
if user and password:
print("Found username and password for basic auth")
reg.set_basic_auth(user, password)
else:
if not reg.auth.username or not reg.auth.password:
sys.exit("ORAS_USER or ORAS_PASS is missing, and required.")

print("Found username and password for basic auth")
return reg


Expand Down Expand Up @@ -622,72 +618,49 @@ is exposed as above. For earlier versions, you can use `self._download_blob`.

### Authentication

Here is a very basic example of logging in and out of a registry using the default (basic)
provided client:

<details>

<summary>Example using basic auth (click to expand)</summary>
As of oras Python 0.2.0, authentication is handled with modules. We take this approach because
different registries have subtle different logic and it led to many errors.

By default, you will get a bearer token setup that takes an initial set of basic credentials and then makes
requests for tokens. This is set by way of defining the "auth_backend" variable. For example,
here is asking for the default.

```python
import oras.client
client = oras.client.OrasClient()
client = oras.client.OrasClient(auth_backend="token")
client.login(password="myuser", username="myuser", insecure=True)
```

And logout!
If you wanted to always maintain the basic auth you might do:

```python
client.logout("localhost:5000")
import oras.client
client = oras.client.OrasClient(auth_backend="basic")
client.login(password="myuser", username="myuser", insecure=True)
```

</details>

Here is an example of getting a GitHub user and token from the environment, and
then doing a pull.
Here is a very basic example of logging in and out of a registry using the default (basic)
provided client:

<details>

<summary>Example setting and using GitHub credentials (click to expand)</summary>
<summary>Example using basic auth (click to expand)</summary>

Given that you are pushing to GitHub packages (which has support for ORAS)
and perhaps are running in a GitHub action, you might want to get these credentials from the environment:

```python
# We will need GitHub personal access token or token
token = os.environ.get("GITHUB_TOKEN")
password = os.environ.get("GITHUB_USER")

if not password or not user:
sys.exit("GITHUB_TOKEN and GITHUB_USER are required in the environment.")
import oras.client
client = oras.client.OrasClient(hostname="ghcr.io")
client.login(password="myuser", username="myuser")
```

Then you can run your custom functions that use these user and password credentials,
either inspecting a particular unique resource identifier or using
your lookup of archives (paths and media types) to push:
And logout!

```python
# Pull Example
reg = MyProvider()
reg.set_basic_auth(user, password)
reg.inspect("ghcr.io/wolfv/conda-forge/linux-64/xtensor:0.9.0-0")

# Push Example
reg = Registry()
reg.set_basic_auth(user, token)
archives = {
"/tmp/pakages-tmp.q6amnrkq/pakages-0.0.16.tar.gz": "application/vnd.oci.image.layer.v1.tar+gzip",
"/tmp/pakages-tmp.q6amnrkq/sbom.json": "application/vnd.cyclonedx"}
reg.push("ghcr.io/vsoch/excellent-dinosaur:latest", archives)
client.logout("localhost:5000")
```

</details>

The above examples supplement our official [examples folder](https://github.com/oras-project/oras-py/tree/main/examples).
Please let us know if you need an additional example or help with your client!


### Debugging

> Can I see more debug information?
Expand Down
1 change: 0 additions & 1 deletion examples/conda-mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def _organize_layers(self, manifest: dict) -> dict:

def main():
mirror = CondaMirror()
mirror.set_basic_auth(user, token)
mirror.inspect("ghcr.io/wolfv/conda-forge/linux-64/xtensor:0.9.0-0")


Expand Down
1 change: 1 addition & 0 deletions examples/follow-image-index.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Follow homebrew image index to get the 'hello' bottle specific to your platform
"""

import re

import oras.client
Expand Down
3 changes: 1 addition & 2 deletions examples/simple/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def main(args):
5. We first try using the docker-py login.
6. If it fails we fall back to custom setting of credentials.
"""
client = oras.client.OrasClient()
client = oras.client.OrasClient(insecre=args.insecure)
print(client.version())

# Other ways to handle login:
Expand All @@ -95,7 +95,6 @@ def main(args):
username=args.username,
config_path=args.config,
hostname=args.hostname,
insecure=args.insecure,
password_stdin=args.password_stdin,
)
logger.info(result)
Expand Down
8 changes: 3 additions & 5 deletions examples/simple/pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@ def main(args):
client = oras.client.OrasClient(insecure=args.insecure)
print(client.version())
try:
if args.username and args.password:
client.set_basic_auth(args.username, args.password)
client.pull(
config_path=args.config,
allowed_media_type=args.allowed_media_type
if not args.allow_all_media_types
else [],
allowed_media_type=(
args.allowed_media_type if not args.allow_all_media_types else []
),
overwrite=not args.keep_old_files,
outdir=args.output,
target=args.target,
Expand Down
2 changes: 0 additions & 2 deletions examples/simple/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ def main(args):
)
client = oras.client.OrasClient(insecure=args.insecure)
try:
if args.username and args.password:
client.set_basic_auth(args.username, args.password)
client.push(
config_path=args.config,
disable_path_validation=args.disable_path_validation,
Expand Down
17 changes: 17 additions & 0 deletions oras/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import requests

from oras.logger import logger

from .basic import BasicAuth
from .token import TokenAuth

auth_backends = {"token": TokenAuth, "basic": BasicAuth}


def get_auth_backend(name="token", session=None, **kwargs):
backend = auth_backends.get(name)
if not backend:
logger.exit(f"Authentication backend {backend} is not known.")
backend = backend(**kwargs)
backend.session = session or requests.Session()
return backend
Loading
Loading