Skip to content

Commit

Permalink
wiring up service account itegration #10
Browse files Browse the repository at this point in the history
  • Loading branch information
jrmerz committed Apr 15, 2024
1 parent d773f5e commit ac2b71e
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 139 deletions.
32 changes: 31 additions & 1 deletion admin-cli/nodejs/bin/pgfarm-admin-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@ import auth from '../lib/auth.js';
import {config} from '../lib/config.js';
const program = new Command();

let stdin = '';

program.command('login')
.description('Login using UCD CAS Authentication')
.option('-h, --headless', 'Login without local browser (ie you are in a pure shell, no Desktop UI), copy and paste token')
.action(options => {
auth.login(options);
});

program.command('service-account-login <serviceAccountName>')
.description('Login using PG Farm service account')
.option('-f, --file <file>', 'File to read service account secret from')
.option('-e, --env <envName>', 'Environment variable to read service account secret from')
.action((name, options) => {
if( !options.file && !options.env && !stdin ) {
console.error('You must specify a file or env option');
process.exit(1);
}
if( !options.file && !options.env ) {
options.secret = stdin;
}

auth.loginServiceAccount(name, options);
});

program.command('token')
.description('Print current user token')
.option('-j, --jwt', 'Print full JWT token instead of the hash token')
Expand All @@ -33,4 +51,16 @@ program.command('update-service')
auth.updateService();
});

program.parse(process.argv);
if( process.stdin.isTTY ) {
program.parse(process.argv);
} else {
process.stdin.on('readable', () => {
let chunk = this.read();
if (chunk !== null) {
stdin += chunk;
}
});
process.stdin.on('end', () => {
program.parse(process.argv);
});
}
2 changes: 2 additions & 0 deletions admin-cli/nodejs/bin/pgfarm-admin-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ program.command('create')
program.command('add-user <org/instance> <user>')
.description('Add a user to an database (instance admin only)')
.option('-a, --admin', 'Grant admin privileges to the user')
.option('-s, --service-account', 'User is a service account')
.option('-p, --parent <parent>', 'Parent user for service account')
.action((instanceName, user, opts) => {
instance.addUser(instanceName, user, opts);
});
Expand Down
46 changes: 45 additions & 1 deletion admin-cli/nodejs/lib/auth.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import LocalLoginServer from './local-server.js';
import init from 'multi-ini';
import {config} from './config.js';
import {config, save as saveConfig} from './config.js';
import os from 'os';
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import crypto from 'crypto';

class Auth {

Expand All @@ -26,6 +28,48 @@ class Auth {
return localServer.create(opts);
}

async loginServiceAccount(name, opts={}) {
if( opts.file ) {
if( !path.isAbsolute(opts.file) ) {
opts.file = path.join(process.cwd(), opts.file);
}
opts.secret = fs.readFileSync(opts.file, 'utf-8').trim();
} else if( opts.env ) {
opts.secret = process.env[opts.env];
}

if( !opts.secret ) {
console.error('No secret provided');
process.exit(1);
}

let resp = await fetch(`${config.host}/auth/service-account/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: name,
secret: opts.secret
})
});

if( resp.status !== 200 ) {
console.error('Login failed', await resp.text());
process.exit(1);
}

let body = await resp.json();

config.token = body.access_token;
const hash = 'urn:md5:'+crypto.createHash('md5').update(body.access_token).digest('base64');
config.tokenHash = hash;

saveConfig();

this.updateService();
}

updateService() {
let pgService = {};
if( fs.existsSync(this.PG_SERVICE_FILE) ) {
Expand Down
5 changes: 5 additions & 0 deletions admin-cli/nodejs/lib/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class Instances {
let flags = [];
if( opts.admin ) {
flags.push('type=ADMIN');
} else if( opts.serviceAccount ) {
flags.push('type=SERVICE_ACCOUNT');
}
if( opts.parent ) {
flags.push(`parent=${opts.parent}`);
}
flags = flags.length > 0 ? '?'+flags.join('&') : '';

Expand Down
45 changes: 44 additions & 1 deletion admin-cli/python/src/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from .http_login_server import start_http_server
from .config import get_config_value
from .config import get_config_value, get_config, save_config, update_pgservice
import webbrowser
import requests
import urllib.parse
import json
import os
import hashlib
import base64

class Auth:

Expand All @@ -25,6 +28,46 @@ def login(self):

start_http_server()

def service_account_login(self, username, opts={}):
host = get_config_value("host")

if "file" in opts and opts["file"]:
with open(opts["file"], "r") as file:
secret = file.read()
elif "env" in opts and opts["env"]:
secret = os.getenv(opts["env"])

url = f"{host}/auth/service-account/login"

headers = {
"Content-Type": "application/json"
}

data = {
"username": username,
"secret": secret
}

response = requests.post(url, headers=headers, data=json.dumps(data))

if( response.status_code != 200 ):
print(f"Login failed {response.text}")
return

response_data = response.json()
jwt = response_data["access_token"]
md5_hash = hashlib.md5(jwt.encode()).digest()
md5_hash = base64.b64encode(md5_hash).decode()

# Write JWT token as a dot file in the user's home directory
config = get_config()
config["token"] = jwt
config["tokenHash"] = f"urn:md5:{md5_hash}"
save_config(config)

# Update the PGSERVICE file
update_pgservice()

def get_token(self, jwt=False):
if( jwt ):
return get_config_value("token")
Expand Down
13 changes: 13 additions & 0 deletions admin-cli/python/src/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ def login():
auth = Auth()
auth.login()

@auth_cmds.command('service-account-login')
def service_account_login(
username: Annotated[str, typer.Argument(help="Service account username")],
file: Annotated[str, typer.Option('--file', '-f', help="Path to file storing service account secret")] = None,
env: Annotated[str, typer.Option('--env', '-e', help="Environment variable storing service account secret")] = None
):
"""Login to PG Farm"""
auth = Auth()
auth.service_account_login(username, {
'file': file,
'env': env
})

@auth_cmds.command()
def token(jwt: Annotated[bool, typer.Option("--jwt", "-j", help="Print JWT")] = False):
"""Print the access token or JWT token. This token should be used as your password"""
Expand Down
24 changes: 12 additions & 12 deletions devops/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ $DOCKER_BUILD \
push $PG_FARM_SERVICE_IMAGE


echo "Building $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION"
$DOCKER_BUILD \
--tag $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION \
--build-arg PG_VERSION=${PG_VERSION} \
--build-arg PG_FARM_VERSION=${VERSION} \
--build-arg PG_FARM_REPO_TAG=${PG_FARM_TAG} \
--build-arg PG_FARM_REPO_BRANCH=${PG_FARM_BRANCH} \
--build-arg PG_FARM_REPO_HASH=${PG_FARM_SHA} \
--build-arg BUILD_DATETIME=${BUILD_DATETIME} \
--cache-from $PG_FARM_PG_INSTANCE_IMAGE:$PG_FARM_BRANCH \
pg-instance
docker push $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION
# echo "Building $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION"
# $DOCKER_BUILD \
# --tag $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION \
# --build-arg PG_VERSION=${PG_VERSION} \
# --build-arg PG_FARM_VERSION=${VERSION} \
# --build-arg PG_FARM_REPO_TAG=${PG_FARM_TAG} \
# --build-arg PG_FARM_REPO_BRANCH=${PG_FARM_BRANCH} \
# --build-arg PG_FARM_REPO_HASH=${PG_FARM_SHA} \
# --build-arg BUILD_DATETIME=${BUILD_DATETIME} \
# --cache-from $PG_FARM_PG_INSTANCE_IMAGE:$PG_FARM_BRANCH \
# pg-instance
# docker push $PG_FARM_PG_INSTANCE_IMAGE:$PG_VERSION
14 changes: 14 additions & 0 deletions services/administration/schema/002-user.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ CREATE OR REPLACE FUNCTION pgfarm.ensure_user(username_in text)
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION pgfarm.purge_user(username_in text)
RETURNS void AS $$
DECLARE
uid UUID;
BEGIN
SELECT pgfarm.get_user_id(username_in) INTO uid;
DELETE FROM pgfarm.instance_user WHERE user_id = uid;
DELETE FROM pgfarm.user_token WHERE user_id = uid;
DELETE FROM pgfarm.organization_role WHERE user_id = uid;
DELETE FROM pgfarm.user_email WHERE user_id = uid;
DELETE FROM pgfarm.user WHERE user_id = uid;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION pgfarm.add_organization_role(org_in text, username_in text, role_in pgfarm.organization_role_type)
RETURNS void AS $$
DECLARE
Expand Down
3 changes: 2 additions & 1 deletion services/administration/schema/003-instance.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ DO $$ BEGIN
'PUBLIC',
'USER',
'ADMIN',
'PGREST'
'PGREST',
'SERVICE_ACCOUNT'
);
EXCEPTION
WHEN duplicate_object THEN null;
Expand Down
23 changes: 20 additions & 3 deletions services/administration/schema/005-instance-user.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@ CREATE TABLE IF NOT EXISTS pgfarm.instance_user (
user_id UUID NOT NULL REFERENCES pgfarm.user(user_id),
password text NOT NULL,
type instance_user_type NOT NULL,
parent_user_id UUID REFERENCES pgfarm.user(user_id),
created_at timestamp NOT NULL DEFAULT now(),
updated_at timestamp NOT NULL DEFAULT now(),
UNIQUE (instance_id, user_id)
);
CREATE INDEX IF NOT EXISTS instance_user_username_idx ON pgfarm.instance_user(user_id);

CREATE OR REPLACE FUNCTION check_parent_user_id() RETURNS TRIGGER AS $$
BEGIN
IF NEW.type = 'SERVICE_ACCOUNT' AND NEW.parent_user_id IS NULL THEN
RAISE EXCEPTION 'Parent user ID cannot be null for service accounts';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER check_parent_user_id_trigger
BEFORE INSERT OR UPDATE ON pgfarm.instance_user
FOR EACH ROW
EXECUTE FUNCTION check_parent_user_id();

CREATE OR REPLACE FUNCTION get_instance_user_id(username_or_id text, inst_name_or_id text, org_name_or_id text)
RETURNS UUID AS $$
DECLARE
Expand Down Expand Up @@ -48,21 +63,23 @@ CREATE OR REPLACE FUNCTION get_instance_user_id_for_db(username_or_id text, db_n
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION add_instance_user(inst_name_or_id text, org_name_or_id text, username_in text, password_in text, type_in instance_user_type)
CREATE OR REPLACE FUNCTION add_instance_user(inst_name_or_id text, org_name_or_id text, username_in text, password_in text, type_in instance_user_type, parent_in text)
RETURNS UUID AS $$
DECLARE
uid UUID;
iid UUID;
iuid UUID;
puid UUID;
BEGIN

SELECT pgfarm.get_instance_id(inst_name_or_id, org_name_or_id) INTO iid;
SELECT pgfarm.ensure_user(username_in) INTO uid;
SELECT pgfarm.get_user_id(parent_in) INTO puid;

INSERT INTO pgfarm.instance_user
(instance_id, user_id, password, type)
(instance_id, user_id, password, type, parent_user_id)
VALUES
(iid, uid, password_in, type_in)
(iid, uid, password_in, type_in, puid)
RETURNING instance_user_id INTO iuid;

RETURN iuid;
Expand Down
8 changes: 5 additions & 3 deletions services/administration/src/controllers/api/admin/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ router.put('/:organization/:instance/:user',

try {
let {instance, organization} = getOrgAndIsntFromReq(req);
let user = req.params.user.replace(/@.*$/, '');
// let user = req.params.user.replace(/@.*$/, '');
let user = req.params.user;
let parent = req.query.parent;

// do not let api create special users, for now
let type = req.query.type || 'USER';
if( type !== 'USER' && type !== 'ADMIN' ) {
if( type !== 'USER' && type !== 'ADMIN' && type !== 'SERVICE_ACCOUNT') {
throw new Error('Invalid type: '+type);
}

let id = await model.createUser(instance, organization, user, type);
let id = await model.createUser(instance, organization, user, type, {parent});
res.status(204).json({id});
} catch(e) {
handleError(res, e);
Expand Down
2 changes: 2 additions & 0 deletions services/administration/src/controllers/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ function register(app) {
delete loginResp.body.id_token;
}

await pgAdminClient.setUserToken(loginResp.body.access_token);

res
.status(loginResp.status)
.json(loginResp.body);
Expand Down
Loading

0 comments on commit ac2b71e

Please sign in to comment.