Skip to content

Commit

Permalink
adding authenticated views, docstrings, and typing
Browse files Browse the repository at this point in the history
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
  • Loading branch information
vsoch committed May 19, 2022
1 parent cc37d20 commit caf0052
Show file tree
Hide file tree
Showing 22 changed files with 690 additions and 191 deletions.
52 changes: 42 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

OCI Registry as Storage enables client libraries to push OCI Artifacts to [OCI Conformant](https://github.com/opencontainers/oci-conformance) registries. This is a Python client for that.

**under development**

## Usage

Expand Down Expand Up @@ -33,18 +32,22 @@ want to deploy a local testing registry (without auth), you can do:
$ docker run -it --rm -p 5000:5000 ghcr.io/oras-project/registry:latest
```

Or with authentication:

To test token authentication, you can either [set up your own auth server](https://github.com/adigunhammedolalekan/registry-auth)
or just use an actual registry. The most we can do here is set up an example that uses basic auth.

```bash
# This is an htpassword file, "b" means bcrypt
htpasswd -cB -b auth.htpasswd myuser mypass
```

The server below will work to login, but you won't be able to issue tokens.

```bash
# And start the registry with authentication
docker run -it --rm -p 5000:5000 \
-v $(pwd)/auth.htpasswd:/etc/docker/registry/auth.htpasswd \
-e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" \
docker run -it --rm -p 5000:5000 ghcr.io/oras-project/registry:latest
ghcr.io/oras-project/registry:latest
```

### Login
Expand All @@ -55,7 +58,38 @@ Once you create (or already have) a registry, you will want to login. You can do
$ oras-py login -u myuser -p mypass localhost:5000

# or localhost (insecure)
$ oras-py login -u myuser -p mypass -k localhost:5000
$ oras-py login -u myuser -p mypass -k localhost:5000 --insecure
```
```bash
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
```

You can also provide them interactively

```bash
$ oras-py login -k localhost:5000 --insecure
```
```bash
Username: myuser
Password: mypass
Login Succeeded
```

or use `--password-stdin`
```bash
$ echo mypass | oras-py login -u myuser -k localhost:5000 --insecure --password-stdin
```
```bash
Login Succeeded
```

Note that oras-py will not remove content from your docker config files, so
there is no concept of a "logout" unless you are using the client interactively,
and have configs loaded, then you can do:

```bash
$ cli.logout(hostname)
```

### Push
Expand Down Expand Up @@ -94,20 +128,18 @@ $ rm -f artifact.txt # first delete the file
$ oras-py pull localhost:5000/dinosaur/artifact:v1
```bash
$ cat artifact.txt
hello dinosaur
```

## TODO

- add same views with auth
- logout command
- finish all basic commands
- add testing
- add testing (docstring test setup for some)
- isort, mypy, pyflakes black
- should there be a tags function?
- add example (custom) GitHub client
- refactor internals to be more like oras-go (e.g., provider, copy?)
- add schemas for manifest, annotations, etc.
- need to have git commit, state, added to defaults on install/release. See [here](https://github.com/oras-project/oras/blob/main/Makefile).
- quiet should be controller for verbosity
- plain_http, configs, need to be parsed in client
- todo we haven't added path traversal, or cacheRoot to pull
- we should have common function to parse errors in json 'errors' -> list -> message
Expand Down
73 changes: 60 additions & 13 deletions oras/auth.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,83 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import os
import re
import docker
import base64
import oras.utils


def get_basic_auth(username, password):
def load_configs(configs: list[str] = None):
"""
Load one or more configs with credentials from the filesystem.
Arguments
---------
configs : list of configuration paths to load
"""
configs = configs or []
default_config = docker.context.config.find_config_file()

# Add the default docker config
if default_config:
configs.append(default_config)
configs = set(configs)

# Load configs until we find our registry hostname
auths = {}
for config in configs:
if not os.path.exists(config):
logger.warning(f"{config} does not exist.")
continue
cfg = oras.utils.read_json(config)
auths.update(cfg.get("auths", {}))
return auths


def get_basic_auth(username: str, password: str):
"""
Prepare basic auth from a username and password.
Arguments
---------
username : the user account name
password : the user account password
"""
auth_str = "%s:%s" % (username, password)
return base64.b64encode(auth_str.encode("utf-8")).decode("utf-8")


def parse_auth_header(authHeaderRaw):
class authHeader:
def __init__(self, lookup: dict):
"""
Given a dictionary of values, match them to class attributes
Arguments
---------
lookup : dictionary of key,value pairs to parse into auth header
"""
self.service = None
self.realm = None
self.scope = None
for key in lookup:
if key in ["realm", "service", "scope"]:
setattr(self, key, lookup[key])


def parse_auth_header(authHeaderRaw: str) -> authHeader:
"""
Parse authentication header into pieces
Arguments
---------
username : the user account name
password : the user account password
"""
regex = re.compile('([a-zA-z]+)="(.+?)"')
matches = regex.findall(authHeaderRaw)
lookup = dict()
for match in matches:
lookup[match[0]] = match[1]
return authHeader(lookup)


class authHeader:
def __init__(self, lookup):
"""
Given a dictionary of values, match them to class attributes
"""
for key in lookup:
if key in ["realm", "service", "scope"]:
setattr(self, key.capitalize(), lookup[key])
16 changes: 8 additions & 8 deletions oras/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import oras
from oras.logger import setup_logger
Expand Down Expand Up @@ -131,7 +131,7 @@ def get_parser():
for command in push, copy:
command.add_argument("--manifest-config", help="manifest config file")

# TODO this can be a list, we afren't doing anything with it yet
# TODO this can be a list, we aren't doing anything with it yet
for command in login, logout, push, pull, copy:
command.add_argument(
"-c",
Expand Down Expand Up @@ -166,7 +166,7 @@ def get_parser():

def run():
"""
Entrypoint to OCI Python
Entrypoint to ORAS Python
"""
parser = get_parser()

Expand Down Expand Up @@ -217,11 +217,11 @@ def help(return_code=0):

# Pass on to the correct parser
return_code = 0
# try:
main(args=args, parser=parser, extra=extra, subparser=helper)
sys.exit(return_code)
# except UnboundLocalError:
# return_code = 1
try:
main(args=args, parser=parser, extra=extra, subparser=helper)
sys.exit(return_code)
except UnboundLocalError:
return_code = 1

help(return_code)

Expand Down
5 changes: 1 addition & 4 deletions oras/cli/help.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#!/usr/bin/env python

__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

login_help = """
Log in to a remote registry
Expand Down Expand Up @@ -58,5 +56,4 @@
oras-py push localhost:5000/hello:latest hi.txt --insecure
Example - Push file to the HTTP registry:
oras-py push localhost:5000/hello:latest hi.txt --plain-http
"""
6 changes: 3 additions & 3 deletions oras/cli/login.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import oras.client

Expand All @@ -11,8 +11,8 @@ def main(args, parser, extra, subparser):
"""
client = oras.client.OrasClient()
client.login(
args.password,
args.username,
password=args.password,
username=args.username,
config_path=args.config,
hostname=args.hostname,
insecure=args.insecure,
Expand Down
13 changes: 13 additions & 0 deletions oras/cli/logout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
__license__ = "Apache-2.0"

import oras.client


def main(args, parser, extra, subparser):
"""
Main is a light wrapper around the logout command.
"""
client = oras.client.OrasClient()
client.logout(args.hostname)
2 changes: 1 addition & 1 deletion oras/cli/pull.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import oras.client

Expand Down
2 changes: 1 addition & 1 deletion oras/cli/push.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import oras.client

Expand Down
2 changes: 1 addition & 1 deletion oras/cli/shell.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"

import oras.client

Expand Down
2 changes: 1 addition & 1 deletion oras/cli/version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021, Vanessa Sochat"
__license__ = "MIT"
__license__ = "Apache-2.0"


def main(args, parser, extra, subparser):
Expand Down

0 comments on commit caf0052

Please sign in to comment.