03-working-with-credentials

command
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2024 License: Apache-2.0 Imports: 23 Imported by: 0

README

Working with Credentials

This tour illustrates the basic handling of credentials using the OCM library. The library provides an extensible framework to bring together credential providers and credential consunmers in a technology-agnostic way.

It covers four basic scenarios:

  • basic Writing to a repository with directly specified credentials.
  • context Using credentials via the credential management to publish a component version.
  • read Read the previously created component version using the credential management.
  • credrepo Providing credentials via credential repositories.

Running the example

You can call the main program with a config file option (--config <file>) and the name of the scenario. The config file should have content similar to:

repository: ghcr.io/mandelsoft/ocm
username:
password:

Set your favorite OCI registry and don't forget to add the repository prefix for your OCM repository hosted in this registry.

Walkthrough

Writing to a repository with directly specified credentials.

As usual, we start with getting access to an OCM context object.

	ctx := ocm.DefaultContext()

So far, we just used memory or file system based OCM repositories to create component versions. If we want to store something in a remotely accessible repository typically some credentials are required for write access.

The OCM library uses a generic abstraction for credentials. It is just set of properties. To offer various credential sources there is an interface credentials.Credentials provided, whose implementations provide access to those properties. A simple property based implementation is `credentials.DirectCredentials.

The most simple use case is to provide the credentials directly for the repository access creation. The example config file provides such credentials for an OCI registry.

	creds := ociidentity.SimpleCredentials(cfg.Username, cfg.Password)

Now, we can use the OCI repository access creation from the first tour, but we pass the credentials as additional parameter. To give you the chance to specify your own registry, the URL is taken from the config file.

	spec := ocireg.NewRepositorySpec(cfg.Repository, nil)

	repo, err := ctx.RepositoryForSpec(spec, creds)
	if err != nil {
		return err
	}
	defer repo.Close()

If registry name and credentials are fine, we should be able now to add a new component version to this repository using the coding from the previous examples, but now we use a public repository, instead of a memory or file system based one. This coding is in function addVersion in common.go (It is shared by the other examples, also).

	cv, err := repo.NewComponentVersion(name, version)
	if err != nil {
		return errors.Wrapf(err, "cannot create new version")
	}
	defer cv.Close()

	err = setupVersion(cv)
	if err != nil {
		return errors.Wrapf(err, "cannot setup new version")
	}

	// finally, wee add the new version to the repository.
	fmt.Printf("adding component version\n")
	err = repo.AddComponentVersion(cv)
	if err != nil {
		return errors.Wrapf(err, "cannot save version")
	}

In contrast to our first tour we cannot list components, here. OCI registries do not support component listers, therefore we just look up the actually added version to verify the result.

	cv, err := repo.LookupComponentVersion("acme.org/example03", "v0.1.0")
	if err != nil {
		return errors.Wrapf(err, "added version not found")
	}
	defer cv.Close()
	return errors.Wrapf(describeVersion(cv), "describe failed")

The coding for describeVersion is similar to the one shown in the first tour.

Using the Credential Management

Passing credentials directly at the repository is fine, as long only the component version will be accessed. But as soon as described resource content will be read, the required credentials and credential types are dependent on the concrete component version, because it might contain any kind of access method referring to any kind of resource repository type.

To solve this problem of passing any set of credentials the OCM context object is used to store credentials. This is handled by a sub context, the Credentials context.

As usual, we start with the default OCM context.

	ctx := ocm.DefaultContext()

It is now used to gain access to the appropriate credential context.

	credctx := ctx.CredentialsContext()

The credentials context brings together providers of credentials, for example a Vault or a local Docker config.json and credential consumers like GitHub or OCI registries. It must be able to distinguish various kinds of consumers. This is done by identifying a dedicated consumer with a set of properties called credentials.ConsumerId. It consists at least of a consumer type property and a consumer type specific set of properties describing the concrete instance of such a consumer, for example an OCI artifact in an OCI registry is identified by a host and a repository path.

A credential provider like a vault just provides named credential sets and typically does not know anything about the use case for these sets. The task of the credential context is to provide credentials for a dedicated consumer. Therefore, it maintains a configurable mapping of credential sources (credentials in a credential repository) and a dedicated consumer.

This mapping defines a use case, also based on a property set and dedicated credentials. If credentials are required for a dedicated consumer, it matches the defined mappings and returned the best matching entry.

Matching? Let's take the GitHub OCI registry as an example. There are different owners for different repository paths (the GitHub org/user). Therefore, different credentials need to be provided for different repository paths. For example, credentials for ghcr.io/acme can be used for a repository ghcr.io/acme/ocm/myimage.

To start with the credentials context we just provide an explicit mapping for our use case.

First, we create our credentials object as before.

	creds := ociidentity.SimpleCredentials(cfg.Username, cfg.Password)

Then we determine the consumer id for our use case. The repository implementation provides a function for this task. It provides the most general property set (no repository path) for an OCI based OCM repository.

	id, err := oci.GetConsumerIdForRef(cfg.Repository)
	if err != nil {
		return errors.Wrapf(err, "invalid consumer")
	}

The used functions above are just convenience wrappers around the core type ConsumerId, which might be provided for dedicated repository/consumer technologies. Everything can be done directly with the core interface and property name constants provided by the dedicted technologies.

Once we have the id we can finally set the credentials for this id.

	credctx.SetCredentialsForConsumer(id, creds)

Now, the context is prepared to provide credentials for any usage of our OCI registry Let's test, whether it could provide credentials for storing our component version.

First, we get the repository object for our OCM repository.

	spec := ocireg.NewRepositorySpec(cfg.Repository, nil)
	repo, err := ctx.RepositoryForSpec(spec, creds)
	if err != nil {
		return err
	}
	defer repo.Close()

Second, we determine the consumer id for our intended repository acccess. A credential consumer may provide consumer id information for a dedicated sub user context. This is supported by the OCM repo implementation for OCI registries. The usage context is here the component name.

	id = credentials.GetProvidedConsumerId(repo, credentials.StringUsageContext("acme.org/example03"))
	if id == nil {
		return fmt.Errorf("repository does not support consumer id queries")
	}
	fmt.Printf("usage context: %s\n", id)

Third, we ask the credential context for appropriate credentials. The basic context method credctx.GetCredentialsForConsumer returns a credentials source interface able to provide credentials for a changing credentials source. Here, we use a convenience function, which directly provides a credentials interface for the actually valid credentials. An error is only provided if something went wrong while determining the credentials. Delivering NO credentials is a valid result. The returned interface then offers access to the credential properties. via various methods.

	creds, err = credentials.CredentialsForConsumer(credctx, id, ociidentity.IdentityMatcher)
	if err != nil {
		return errors.Wrapf(err, "no credentials")
	}
	if creds == nil {
		return fmt.Errorf("no credentials found")
	}
	fmt.Printf("credentials: %s\n", obfuscate(creds.Properties()))

Now, we can continue with our basic component version composition from the last example, or we just display the content.

The following code snipped shows the code for the context variant creating a new version, the read variant just omits the version creation. The rest of the example is identical.

	if create {
		// now we create a component version in this repository.
		err = addVersion(repo, "acme.org/example03", "v0.1.0")
		if err != nil {
			return err
		}
	}

Let's verify the created content and list the versions as known from tour 1. OCI registries do not support component listers, therefore we just get and describe the actually added version.

	cv, err := repo.LookupComponentVersion("acme.org/example03", "v0.1.0")
	if err != nil {
		return errors.Wrapf(err, "added version not found")
	}
	defer cv.Close()

	err = describeVersion(cv)
	if err != nil {
		return errors.Wrapf(err, "describe failed")
	}

As we can see in the resource list, our image artifact has been uploaded to the OCI registry as OCI artifact and the access method has be changed to ociArtifact. It is not longer a local blob.

	res, err := cv.GetResourcesByName("ocmcli")
	if err != nil {
		return errors.Wrapf(err, "accessing ocmcli resource")
	}
	if len(res) != 1 {
		return fmt.Errorf("oops, there are %d entries for ocmcli", len(res))
	}
	meth, err := res[0].AccessMethod()
	if err != nil {
		return errors.Wrapf(err, "cannot get access method")
	}
	defer meth.Close()

	fmt.Printf("accessing oci image now with %s\n", meth.AccessSpec().Describe(ctx))

This resource access effectively points to the same OCI registry, but a completely different repository. If you are using ghcr.io, this freshly created repo is private, therefore, we need credentials for accessing the content. An access method also acts as credential consumer, which tries to get required credentials from the credential context. Optionally, an access method can act as provider for a consumer id, so that it is possible to query the used consumer id from the method object.

	id = credentials.GetProvidedConsumerId(meth, credentials.StringUsageContext("acme.org/example3"))
	if id == nil {
		fmt.Printf("no consumer id info for access method\n")
	} else {
		fmt.Printf("usage context: %s\n", id)
	}

Because the credentials context now knows the required credentials, the access method as credential consumer can access the blob.

	writer, err := os.OpenFile("/tmp/example3", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
	if err != nil {
		return errors.Wrapf(err, "cannot write output file")
	}
	defer writer.Name()

	reader, err := meth.Reader()
	if err != nil {
		return errors.Wrapf(err, "cannot get reader")
	}
	defer reader.Close()
	n, err := io.Copy(writer, reader)
	if err != nil {
		return errors.Wrapf(err, "cannot copy content")
	}
	fmt.Printf("blob has %d bytes\n", n)
Providing credentials via credential repositories

The OCM toolset embraces multiple storage backend technologies, for OCM metadata as well as for artifacts described by a component version. All those technologies typically have their own way to configure credentials for command line tools or servers.

The credential management provides so-called credential repositories. Such a repository is able to provide any number of named credential sets. This way any special credential store can be connected to the OCM credential management just by providing an own implementation for the repository interface.

One such case is the docker config json, a config file used by docker login to store credentials for dedicated OCI registries.

We start again by providing access to the OCM context and the connected credential context.

	ctx := ocm.DefaultContext()
	credctx := ctx.CredentialsContext()

In package .../contexts/credentials/repositories you can find packages for predefined implementations for some standard credential repositories, for example dockerconfig.

	dspec := dockerconfig.NewRepositorySpec("~/.docker/config.json")

There are general credential stores, like a HashiCorp Vault or type-specific ones, like the docker config json used to configure credentials for the docker client. (working with OCI registries). Those specialized repository implementations are not only able to provide credential sets, they also know about the usage context of the provided credentials. Therefore, such repository implementations are also able to provide credential mappings for consumer ids. This is supported by the credential repository API provided by this library.

The docker config is such a case, so we can instruct the repository to automatically propagate appropriate the consumer id mappings. This feature is typically enabled by a dedicated specfication option.

	dspec = dspec.WithConsumerPropagation(true)

Implementations for more generic credential repositories can also use this feature, if the repository allows adding arbitrary metadata. This is for example used by the vault implementation. It uses dedicated attributes to allow the user to configure intended consumer id properties.

Now, we can just add the repository for this specification to the credential context by getting the repository object for our specification.

	_, err := credctx.RepositoryForSpec(dspec)
	if err != nil {
		return errors.Wrapf(err, "invalid credential repository")
	}

We are not interested in the repository object, so we just ignore the result.

So, if you have done the appropriate docker login for your OCI registry, it should be possible now to get the credentials for the configured repository.

We first query the consumer id for the repository, again.

	id, err := oci.GetConsumerIdForRef(cfg.Repository)
	if err != nil {
		return errors.Wrapf(err, "invalid consumer")
	}

and then get the credentials from the credentials context like in the previous example.

	creds, err := credentials.CredentialsForConsumer(credctx, id, ociidentity.IdentityMatcher)
	if err != nil {
		return errors.Wrapf(err, "no credentials")
	}
	// an error is only provided if something went wrong while determining
	// the credentials. Delivering NO credentials is a valid result.
	if creds == nil {
		return fmt.Errorf("no credentials found")
	}
	fmt.Printf("credentials: %s\n", obfuscate(creds.Properties()))

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL