upload/koji: add support for GSSAPI/Kerberos auth
Prior this commit we only had support for username/password authentication in the koji integration. This wasn't particularly useful because this auth type isn't used in any production instance. This commit adds the support for GSSAPI/Kerberos authentication. The implementation uses kerby library which is very lightweight wrapper around C gssapi library. Also, the koji unit test and the run-koji-container script were modified so the GSSAPI auth is fully tested.
This commit is contained in:
parent
ecc7340570
commit
05fd221bd4
21 changed files with 1637 additions and 31 deletions
|
|
@ -4,14 +4,17 @@ import (
|
|||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/adler32"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/kolo/xmlrpc"
|
||||
"github.com/ubccr/kerby/khttp"
|
||||
)
|
||||
|
||||
type Koji struct {
|
||||
|
|
@ -106,26 +109,17 @@ type CGImportResult struct {
|
|||
BuildID int `xmlrpc:"build_id"`
|
||||
}
|
||||
|
||||
func Login(server, user, password string, transport http.RoundTripper) (*Koji, error) {
|
||||
// Create a temporary xmlrpc client.
|
||||
// The API doesn't require sessionID, sessionKey and callnum yet,
|
||||
// so there's no need to use the custom Koji RoundTripper,
|
||||
// let's just use the one that the called passed in.
|
||||
loginClient, err := xmlrpc.NewClient(server, http.DefaultTransport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type GSSAPICredentials struct {
|
||||
Principal string
|
||||
KeyTab string
|
||||
}
|
||||
|
||||
args := []interface{}{user, password}
|
||||
var reply struct {
|
||||
SessionID int64 `xmlrpc:"session-id"`
|
||||
SessionKey string `xmlrpc:"session-key"`
|
||||
}
|
||||
err = loginClient.Call("login", args, &reply)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type loginReply struct {
|
||||
SessionID int64 `xmlrpc:"session-id"`
|
||||
SessionKey string `xmlrpc:"session-key"`
|
||||
}
|
||||
|
||||
func newKoji(server string, transport http.RoundTripper, reply loginReply) (*Koji, error) {
|
||||
// Create the final xmlrpc client with our custom RoundTripper handling
|
||||
// sessionID, sessionKey and callnum
|
||||
kojiTransport := &Transport{
|
||||
|
|
@ -134,7 +128,6 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e
|
|||
callnum: 0,
|
||||
transport: transport,
|
||||
}
|
||||
|
||||
client, err := xmlrpc.NewClient(server, kojiTransport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -147,6 +140,55 @@ func Login(server, user, password string, transport http.RoundTripper) (*Koji, e
|
|||
}, nil
|
||||
}
|
||||
|
||||
// NewFromPlain creates a new Koji sessions =authenticated using the plain
|
||||
// username/password method. If you want to speak to a public koji instance,
|
||||
// you probably cannot use this method.
|
||||
func NewFromPlain(server, user, password string, transport http.RoundTripper) (*Koji, error) {
|
||||
// Create a temporary xmlrpc client.
|
||||
// The API doesn't require sessionID, sessionKey and callnum yet,
|
||||
// so there's no need to use the custom Koji RoundTripper,
|
||||
// let's just use the one that the called passed in.
|
||||
loginClient, err := xmlrpc.NewClient(server, http.DefaultTransport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []interface{}{user, password}
|
||||
var reply loginReply
|
||||
err = loginClient.Call("login", args, &reply)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newKoji(server, transport, reply)
|
||||
}
|
||||
|
||||
// NewFromGSSAPI creates a new Koji session authenticated using GSSAPI.
|
||||
// Principal and keytab used for the session is passed using credentials
|
||||
// parameter.
|
||||
func NewFromGSSAPI(server string, credentials *GSSAPICredentials, transport http.RoundTripper) (*Koji, error) {
|
||||
// Create a temporary xmlrpc client with kerberos transport.
|
||||
// The API doesn't require sessionID, sessionKey and callnum yet,
|
||||
// so there's no need to use the custom Koji RoundTripper,
|
||||
// let's just use the one that the called passed in.
|
||||
loginClient, err := xmlrpc.NewClient(server+"/ssllogin", &khttp.Transport{
|
||||
KeyTab: credentials.KeyTab,
|
||||
Principal: credentials.Principal,
|
||||
Next: transport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reply loginReply
|
||||
err = loginClient.Call("sslLogin", nil, &reply)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newKoji(server, transport, reply)
|
||||
}
|
||||
|
||||
// GetAPIVersion gets the version of the API of the remote Koji instance
|
||||
func (k *Koji) GetAPIVersion() (int, error) {
|
||||
var version int
|
||||
|
|
@ -309,3 +351,17 @@ func (rt *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
|
||||
return rt.transport.RoundTrip(rClone)
|
||||
}
|
||||
|
||||
func GSSAPICredentialsFromEnv() (*GSSAPICredentials, error) {
|
||||
principal, principalExists := os.LookupEnv("OSBUILD_COMPOSER_KOJI_PRINCIPAL")
|
||||
keyTab, keyTabExists := os.LookupEnv("OSBUILD_COMPOSER_KOJI_KEYTAB")
|
||||
|
||||
if !principalExists || !keyTabExists {
|
||||
return nil, errors.New("Both OSBUILD_COMPOSER_KOJI_PRINCIPAL and OSBUILD_COMPOSER_KOJI_KEYTAB must be set")
|
||||
}
|
||||
|
||||
return &GSSAPICredentials{
|
||||
Principal: principal,
|
||||
KeyTab: keyTab,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ package koji_test
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
|
@ -22,17 +24,36 @@ import (
|
|||
|
||||
func TestKojiImport(t *testing.T) {
|
||||
// define constants
|
||||
server := "http://localhost:8080/kojihub"
|
||||
user := "osbuild"
|
||||
password := "osbuildpass"
|
||||
server := "https://localhost/kojihub"
|
||||
filename := "image.qcow2"
|
||||
filesize := 1024
|
||||
shareDir := "/tmp/osbuild-composer-koji-test"
|
||||
// you cannot create two build with a same name, let's create a random one each time
|
||||
buildName := "osbuild-image-" + uuid.Must(uuid.NewRandom()).String()
|
||||
// koji needs to specify a directory to which the upload should happen, let's reuse the build name
|
||||
uploadDirectory := buildName
|
||||
|
||||
k, err := koji.Login(server, user, password, http.DefaultTransport)
|
||||
// base our transport on the default one
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
// use the self-signed certificate generated by run-koji-container
|
||||
certPool := x509.NewCertPool()
|
||||
cert, err := ioutil.ReadFile(shareDir + "/ca-crt.pem")
|
||||
require.NoError(t, err)
|
||||
|
||||
ok := certPool.AppendCertsFromPEM(cert)
|
||||
require.True(t, ok)
|
||||
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
RootCAs: certPool,
|
||||
}
|
||||
|
||||
// login
|
||||
credentials := &koji.GSSAPICredentials{
|
||||
Principal: "osbuild-krb@LOCAL",
|
||||
KeyTab: shareDir + "/client.keytab",
|
||||
}
|
||||
k, err := koji.NewFromGSSAPI(server, credentials, transport)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
|
|
@ -111,9 +132,9 @@ func TestKojiImport(t *testing.T) {
|
|||
cmd := exec.Command(
|
||||
"koji",
|
||||
"--server", server,
|
||||
"--user", user,
|
||||
"--password", password,
|
||||
"--authtype", "password",
|
||||
"-c", "../../../.github/koji.conf",
|
||||
"--keytab", credentials.KeyTab,
|
||||
"--principal", credentials.Principal,
|
||||
"list-builds",
|
||||
"--buildid", strconv.Itoa(result.BuildID),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
SHARE_DIR=/tmp/osbuild-composer-koji-test
|
||||
|
||||
koji_stop () {
|
||||
echo "Shutting down containers, please wait..."
|
||||
|
||||
${CONTAINER_RUNTIME} stop org.osbuild.koji.koji || true
|
||||
${CONTAINER_RUNTIME} rm org.osbuild.koji.koji || true
|
||||
|
||||
${CONTAINER_RUNTIME} stop org.osbuild.koji.kdc || true
|
||||
${CONTAINER_RUNTIME} rm org.osbuild.koji.kdc || true
|
||||
|
||||
${CONTAINER_RUNTIME} stop org.osbuild.koji.postgres || true
|
||||
${CONTAINER_RUNTIME} rm org.osbuild.koji.postgres || true
|
||||
|
||||
${CONTAINER_RUNTIME} network rm -f org.osbuild.koji || true
|
||||
|
||||
rm -rf "${SHARE_DIR}" || true
|
||||
}
|
||||
|
||||
koji_clean_up_bad_start () {
|
||||
# remember the exit code, so we can report it later
|
||||
EXIT_CODE=$?
|
||||
echo "Start failed, removing containers."
|
||||
|
||||
|
|
@ -22,23 +30,80 @@ koji_clean_up_bad_start () {
|
|||
exit $EXIT_CODE
|
||||
}
|
||||
|
||||
|
||||
# helper to simplify sql queries to the postgres instance
|
||||
psql_cmd () {
|
||||
${CONTAINER_RUNTIME} exec org.osbuild.koji.postgres psql -U koji -d koji "$@"
|
||||
}
|
||||
|
||||
# helper to simplify running commands in the kdc container
|
||||
kdc_exec() {
|
||||
${CONTAINER_RUNTIME} exec org.osbuild.koji.kdc "$@"
|
||||
}
|
||||
|
||||
koji_start() {
|
||||
trap koji_clean_up_bad_start EXIT
|
||||
|
||||
# create a share directory which is used to share files between the host and containers
|
||||
mkdir "${SHARE_DIR}"
|
||||
|
||||
# generate self-signed certificates in the share directory
|
||||
openssl req -new -nodes -x509 -days 365 -keyout "${SHARE_DIR}/ca-key.pem" -out "${SHARE_DIR}/ca-crt.pem" -subj "/CN=osbuild.org"
|
||||
openssl genrsa -out "${SHARE_DIR}/key.pem" 2048
|
||||
openssl req -new -sha256 -key "${SHARE_DIR}/key.pem" -out "${SHARE_DIR}/csr.pem" -subj "/CN=localhost"
|
||||
openssl x509 -req -in "${SHARE_DIR}/csr.pem" -CA "${SHARE_DIR}/ca-crt.pem" -CAkey "${SHARE_DIR}/ca-key.pem" -CAcreateserial -out "${SHARE_DIR}/crt.pem"
|
||||
|
||||
${CONTAINER_RUNTIME} network create org.osbuild.koji
|
||||
|
||||
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.postgres --network org.osbuild.koji \
|
||||
-e POSTGRES_USER=koji \
|
||||
-e POSTGRES_PASSWORD=kojipass \
|
||||
-e POSTGRES_DB=koji \
|
||||
docker.io/library/postgres:12-alpine
|
||||
|
||||
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.kdc \
|
||||
--network org.osbuild.koji \
|
||||
-v "${SHARE_DIR}:/share:z" \
|
||||
-p 88:88/udp \
|
||||
quay.io/osbuild/kdc:v1
|
||||
|
||||
# initialize krb pricipals and create keytabs for them
|
||||
# HTTP/localhost@LOCAL for kojihub
|
||||
kdc_exec kadmin.local -r LOCAL add_principal -randkey HTTP/localhost@LOCAL
|
||||
kdc_exec kadmin.local -r LOCAL ktadd -k /share/koji.keytab HTTP/localhost@LOCAL
|
||||
kdc_exec chmod 644 /share/koji.keytab
|
||||
|
||||
# osbuild-krb@LOCAL for koji clients
|
||||
kdc_exec kadmin.local -r LOCAL add_principal -randkey osbuild-krb@LOCAL
|
||||
kdc_exec kadmin.local -r LOCAL ktadd -k /share/client.keytab osbuild-krb@LOCAL
|
||||
kdc_exec chmod 644 /share/client.keytab
|
||||
|
||||
${CONTAINER_RUNTIME} run -d --name org.osbuild.koji.koji --network org.osbuild.koji \
|
||||
-p 8080:80 \
|
||||
-v "${SHARE_DIR}:/share:z" \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-e POSTGRES_USER=koji \
|
||||
-e POSTGRES_PASSWORD=kojipass \
|
||||
-e POSTGRES_DB=koji \
|
||||
-e POSTGRES_HOST=org.osbuild.koji.postgres \
|
||||
quay.io/osbuild/ghci-koji:v1
|
||||
quay.io/osbuild/koji:v1
|
||||
|
||||
# TODO: we need to wait for the database to be initialized here. A better method should be used.
|
||||
sleep 2
|
||||
|
||||
# create koji users
|
||||
# kojiadmin/kojipass - admin
|
||||
# osbuild/osbuildpass - regular user
|
||||
# osbuild-krb: - regular user authenticated with Kerberos principal osbuild-krb@LOCAL
|
||||
psql_cmd -c "insert into users (name, password, status, usertype) values ('kojiadmin', 'kojipass', 0, 0)" >/dev/null
|
||||
psql_cmd -c "insert into user_perms (user_id, perm_id, creator_id) values (1, 1, 1)" >/dev/null
|
||||
psql_cmd -c "insert into users (name, password, status, usertype) values ('osbuild', 'osbuildpass', 0, 0)" >/dev/null
|
||||
psql_cmd -c "insert into users (name, status, usertype) values ('osbuild-krb', 0, 0)" >/dev/null
|
||||
psql_cmd -c "insert into user_krb_principals (user_id, krb_principal) values (3, 'osbuild-krb@LOCAL')" >/dev/null
|
||||
|
||||
# create content generator osbuild, give osbuild and osbuild-krb users access to it
|
||||
psql_cmd -c "insert into content_generator (name) values ('osbuild')" >/dev/null
|
||||
psql_cmd -c "insert into cg_users (cg_id, user_id, creator_id, active) values (1, 2, 1, true), (1, 3, 1, true)" >/dev/null
|
||||
|
||||
echo "Containers are running, to stop them use:"
|
||||
echo "$0 stop"
|
||||
|
|
@ -46,6 +111,7 @@ koji_start() {
|
|||
trap - EXIT
|
||||
}
|
||||
|
||||
# check arguments
|
||||
if [[ $# -ne 1 || ( "$1" != "start" && "$1" != "stop" ) ]]; then
|
||||
cat <<DOC
|
||||
usage: $0 start|stop
|
||||
|
|
@ -56,11 +122,13 @@ DOC
|
|||
exit 3
|
||||
fi
|
||||
|
||||
# this script must be run as root
|
||||
if [ $UID != 0 ]; then
|
||||
echo This script must be run as root.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# decide whether podman or docker should be used
|
||||
if which podman 2>/dev/null >&2; then
|
||||
CONTAINER_RUNTIME=podman
|
||||
elif which docker 2>/dev/null >&2; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue