Skip to content
This repository was archived by the owner on Mar 12, 2020. It is now read-only.

Commit 30f72cf

Browse files
author
Dustin Collins
authored
Merge pull request #7 from conjurinc/possum-cli
Possum CLI
2 parents e74777d + 15e2353 commit 30f72cf

21 files changed

Lines changed: 492 additions & 4 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
generated-policy.yml
12
data_key
23
artifacts
34
pytest.xml

conjur/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,11 @@ def _request(self, method, url, *args, **kwargs):
134134

135135
response = getattr(requests, method.lower())(url, *args, **kwargs)
136136
if check_errors and response.status_code >= 300:
137-
raise ConjurException("Request failed: %d" % response.status_code)
137+
try:
138+
error = response.json()['error']
139+
except:
140+
raise ConjurException("Request failed: %d" % response.status_code)
141+
raise ConjurException("%s : %s" % ( error['code'], error['message'] ))
138142

139143
return response
140144

conjur/cli.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from __future__ import print_function
2+
from netrc import netrc
3+
from urlparse import urlparse
4+
import getpass
5+
import argparse
6+
import sys
7+
import os
8+
import conjur
9+
import inflection
10+
import json
11+
from tabulate import tabulate
12+
13+
def eprint(*args, **kwargs):
14+
print(*args, file=sys.stderr, **kwargs)
15+
16+
def netrc_path():
17+
try:
18+
return os.environ['CONJURRC']
19+
except KeyError:
20+
return os.path.expanduser('~/.netrc')
21+
22+
def touch_netrc():
23+
path = netrc_path()
24+
with open(path, 'a'):
25+
os.utime(path, None)
26+
os.chmod(path, 0o600)
27+
28+
def netrc_str(netrc):
29+
"""Dump the class data in the format of a .netrc file."""
30+
rep = ""
31+
for host in netrc.hosts.keys():
32+
attrs = netrc.hosts[host]
33+
rep = rep + "machine "+ host + "\n\tlogin " + str(attrs[0]) + "\n"
34+
if attrs[1]:
35+
rep = rep + "account " + str(attrs[1])
36+
rep = rep + "\tpassword " + str(attrs[2]) + "\n"
37+
for macro in netrc.macros.keys():
38+
rep = rep + "macdef " + macro + "\n"
39+
for line in netrc.macros[macro]:
40+
rep = rep + line
41+
rep = rep + "\n"
42+
return rep
43+
44+
def credentials_from_netrc():
45+
try:
46+
creds = netrc(netrc_path())
47+
except IOError:
48+
raise Exception("Not logged in. File %s does not exist" % netrc_path())
49+
50+
result = creds.authenticators(conjur.config.url)
51+
try:
52+
return ( result[0], result[2] )
53+
except TypeError:
54+
raise Exception("Conjur URL %s is not in %s, therefore you're not logged in" % ( conjur.config.url, netrc_path() ))
55+
56+
def credentials():
57+
try:
58+
( os.environ['CONJUR_AUTHN_LOGIN'], os.environ['CONJUR_AUTHN_API_KEY'] )
59+
except KeyError:
60+
return credentials_from_netrc()
61+
62+
def connect():
63+
return conjur.new_from_key(*credentials())
64+
65+
def current_roleid():
66+
# Ensure the login is valid
67+
api = connect()
68+
api.authenticate()
69+
username = api.login
70+
account = conjur.config.account
71+
if username.find('/') != -1:
72+
kind, _, id = username.partition('/')
73+
else:
74+
kind, id = ( 'user', username)
75+
return "%s:%s:%s" % ( account, kind, id )
76+
77+
def find_object(kind, id):
78+
api = connect()
79+
if len(id.split(':')) > 1:
80+
return api.resource_qualified(id)
81+
else:
82+
return api.resource(kind, id)
83+
84+
def find_variable(id):
85+
return find_object('variable', id)
86+
87+
def find_policy(id):
88+
return find_object('policy', id)
89+
90+
def interpret_login(role):
91+
tokens = role.split(':', 1)
92+
if len(tokens) == 2:
93+
return tokens
94+
else:
95+
return ( 'user', tokens[0] )
96+
97+
def login_handler(args):
98+
role = args.role
99+
if not role:
100+
eprint("Enter your username to log into Possum: ", end="")
101+
role = raw_input()
102+
kind, identifier = interpret_login(role)
103+
username = '/'.join((kind, identifier))
104+
105+
if args.rotate:
106+
api = connect()
107+
role = conjur.Role.from_roleid(api, ":".join((kind, identifier)))
108+
api_key = role.rotate_api_key()
109+
else:
110+
password = getpass.getpass("Enter password for %s %s (it will not be echoed): " % ( kind, identifier ))
111+
api_key = conjur.new_from_password(username, password).api_key
112+
113+
save_api_key(username, api_key)
114+
print("Logged in")
115+
116+
def save_api_key(username, api_key):
117+
touch_netrc()
118+
logins = netrc(netrc_path())
119+
logins.hosts[conjur.config.url] = ( username, None, api_key )
120+
with open(netrc_path(), 'w') as f:
121+
f.write(netrc_str(logins))
122+
123+
def authenticate_handler(args):
124+
import base64
125+
token = connect().authenticate()
126+
if args.H:
127+
token = base64.b64encode(token)
128+
print(token)
129+
130+
def whoami_handler(args):
131+
print(current_roleid())
132+
133+
def rotate_api_key_handler(args):
134+
api = connect()
135+
role = conjur.Role.from_roleid(api, args.role or current_roleid())
136+
api_key = role.rotate_api_key()
137+
if not args.role:
138+
save_api_key(api.login, api_key)
139+
print(api_key)
140+
141+
def list_handler(args):
142+
def flatten_record(record):
143+
result = [ record['id'], record['owner'] ]
144+
if 'policy' in record.keys():
145+
result.append(record['policy'])
146+
return result
147+
148+
api = connect()
149+
resources = [ flatten_record(resource) for resource in api.get(api._resources_url(kind=args.kind)).json() ]
150+
print(tabulate(resources, headers=['Id', 'Owner', 'Policy']))
151+
152+
def show_handler(args):
153+
api = connect()
154+
resource = api.resource_qualified(args.id)
155+
resource = resource.api.get(resource.url()).json()
156+
keys = resource.keys()
157+
keys.sort()
158+
def format_value(value):
159+
if isinstance(value, basestring):
160+
return value
161+
else:
162+
return json.dumps(value)
163+
164+
data = [ ( inflection.camelize(key), format_value(resource[key]) ) for key in keys ]
165+
print(tabulate(data))
166+
167+
def policy_load_handler(args):
168+
if args.policy == '-':
169+
value = sys.stdin.read()
170+
else:
171+
value = args.policy
172+
policy = find_policy(args.id)
173+
url = '/'.join([
174+
policy.api.config.url,
175+
'policies',
176+
policy.api.config.account,
177+
'policy',
178+
conjur.util.urlescape(policy.identifier)
179+
])
180+
181+
response = policy.api.post(url, data=value).json()
182+
created_roles = response['created_roles']
183+
print("")
184+
print("Loaded policy version %s" % response['version'])
185+
if len(created_roles) > 0:
186+
print("Created %s roles" % len(created_roles))
187+
print("")
188+
print(tabulate([ ( record['id'], record['api_key'] ) for record in created_roles.values() ], ("Id", "API Key")))
189+
print("")
190+
191+
def fetch_handler(args):
192+
value = find_variable(args.id).secret(args.version)
193+
if value:
194+
print(value)
195+
else:
196+
sys.exit(1)
197+
198+
def store_handler(args):
199+
if args.value == '-':
200+
value = sys.stdin.read()
201+
else:
202+
value = args.value
203+
find_variable(args.id).add_secret(value)
204+
print("Value added")
205+
206+
parser = argparse.ArgumentParser(description='Possum command-line interface.')
207+
subparsers = parser.add_subparsers()
208+
209+
login = subparsers.add_parser('login')
210+
login.add_argument('-r', '--role')
211+
login.add_argument('--rotate', action='store_true')
212+
login.set_defaults(func=login_handler)
213+
214+
whoami = subparsers.add_parser('whoami')
215+
whoami.set_defaults(func=whoami_handler)
216+
217+
authenticate = subparsers.add_parser('authenticate')
218+
authenticate.add_argument('-H', action='store_true')
219+
authenticate.set_defaults(func=authenticate_handler)
220+
221+
rotate_api_key = subparsers.add_parser('rotate_api_key')
222+
rotate_api_key.add_argument('-r', '--role')
223+
rotate_api_key.set_defaults(func=rotate_api_key_handler)
224+
225+
list_ = subparsers.add_parser('list')
226+
list_.add_argument('-k', '--kind', help='Resource kind')
227+
list_.set_defaults(func=list_handler)
228+
229+
show = subparsers.add_parser('show')
230+
show.add_argument('id')
231+
show.set_defaults(func=show_handler)
232+
233+
policy_load = subparsers.add_parser('policy:load')
234+
policy_load.add_argument('id')
235+
policy_load.add_argument('policy')
236+
policy_load.set_defaults(func=policy_load_handler)
237+
238+
store = subparsers.add_parser('store')
239+
store.add_argument('id')
240+
store.add_argument('value')
241+
store.set_defaults(func=store_handler)
242+
243+
fetch = subparsers.add_parser('fetch')
244+
fetch.add_argument('id')
245+
fetch.add_argument('-V', '--version', help='Variable version')
246+
fetch.set_defaults(func=fetch_handler)
247+
248+
args = parser.parse_args()
249+
args.func(args)

conjur/util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ def urlescape(s):
3030
return quote(s, '')
3131

3232

33+
def login_kind(login):
34+
tokens = login.split('/', 1)
35+
if len(tokens) == 2:
36+
return tokens[0]
37+
else:
38+
return 'user'
39+
40+
def login_identifier(login):
41+
tokens = login.split('/', 1)
42+
if len(tokens) == 2:
43+
return tokens[1]
44+
else:
45+
return tokens[0]
46+
3347
def authzid(obj, kind, with_account=True):
3448
if isinstance(obj, (str, unicode)): # noqa F821 (flake8 doesn't know about unicode)
3549
if not with_account:

demo/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Possum Demo
2+
3+
## Description
4+
5+
See [https://conjurinc.github.io/possum/demo.html](https://conjurinc.github.io/possum/demo.html) for a detailed walkthrough.
6+
7+
The policies used in the demo are available in this directory.
8+
9+
## Running
10+
11+
To run a Possum server and command-line client in Docker containers, simply run `./start.sh`:
12+
13+
```sh-session
14+
$ ./start.sh
15+
pg uses an image, skipping
16+
possum uses an image, skipping
17+
Step 1 : FROM python:2.7-slim
18+
---> 4947dfe5e830
19+
...
20+
Creating demo_pg_1
21+
Creating demo_possum_1
22+
root@5c2b6208380e:/app# possum -h
23+
usage: possum [-h]
24+
{login,whoami,authenticate,rotate_api_key,list,show,policy:load,store,fetch}
25+
...
26+
27+
Possum command-line interface.
28+
```

demo/bootstrap.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
- !group security_admin
2+
3+
- !policy
4+
id: people
5+
owner: !group security_admin
6+
body:
7+
- !group
8+
id: frontend
9+
10+
- !group
11+
id: operations
12+
13+
- !policy
14+
id: prod
15+
owner: !group security_admin
16+
body:
17+
- !policy
18+
id: frontend
19+
20+
- !policy
21+
id: database
22+
owner: !group ../people/operations
23+
24+
- !permit
25+
role: !group people/frontend
26+
privilege: [ read, execute ]
27+
resource: !policy prod/frontend
28+
29+
- !permit
30+
role: !group people/operations
31+
privilege: [ read, execute ]
32+
resource: !policy prod/database

demo/database.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- &variables
2+
- !variable password
3+
4+
- !permit
5+
role: !layer ../frontend
6+
privilege: [ read, execute ]
7+
resource: *variables

demo/docker-compose.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
pg:
2+
image: postgres:9.3
3+
4+
possum:
5+
image: conjurinc/possum
6+
command: server -a demo -f /var/lib/possum/initial_bootstrap.yml
7+
environment:
8+
DATABASE_URL: postgres://postgres@pg/postgres
9+
POSSUM_ADMIN_PASSWORD: secret
10+
POSSUM_DATA_KEY:
11+
volumes:
12+
- .:/var/lib/possum
13+
links:
14+
- pg:pg
15+
16+
client:
17+
build: ..
18+
entrypoint: bash
19+
env_file: env
20+
volumes:
21+
- ..:/app
22+
links:
23+
- possum:possum

demo/env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CONJUR_URL=http://possum
2+
CONJUR_ACCOUNT=demo
3+
PYTHONPATH=/app

demo/frontend.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
- !layer
2+
3+
- &hosts
4+
- !host frontend-01
5+
- !host frontend-02
6+
7+
- !grant
8+
role: !layer
9+
members: *hosts

0 commit comments

Comments
 (0)