summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErnst Widerberg <ernst@sunet.se>2022-01-13 18:10:22 +0100
committerErnst Widerberg <ernst@sunet.se>2022-01-13 18:10:22 +0100
commitbfe891000c2d6bb2c73bdc635d22640a3e89e729 (patch)
tree7d56b8af24102823f4976319641d8a977ffdc8ff
parent386f3bd73383368facd9807f737e26478b0302f3 (diff)
Add read/write permissions to JWTs based on YAML
- Uses Linus's YAML code, except with password stuff removed since auth-server-poc uses htpasswd. - The collector checks JWT on API endpoints get, get/{key}, and delete/{key}, but not on add.
-rw-r--r--auth-server-poc/requirements.txt34
-rw-r--r--auth-server-poc/src/app.py39
-rwxr-xr-xauth-server-poc/src/authn.py (renamed from src/authn.py)61
-rw-r--r--auth-server-poc/src/userdb.yaml23
-rw-r--r--demo/wsgi_demo_users.yaml15
-rwxr-xr-xquickstart.sh5
-rwxr-xr-xsrc/main.py63
7 files changed, 164 insertions, 76 deletions
diff --git a/auth-server-poc/requirements.txt b/auth-server-poc/requirements.txt
index 9927acb..fc8fc53 100644
--- a/auth-server-poc/requirements.txt
+++ b/auth-server-poc/requirements.txt
@@ -248,3 +248,37 @@ zipp==3.6.0 \
# via
# -r requirements.txt
# importlib-metadata
+pyyaml==6.0 \
+ --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+ --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+ --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+ --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+ --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+ --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+ --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+ --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+ --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+ --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+ --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+ --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+ --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+ --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+ --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+ --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+ --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+ --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+ --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+ --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+ --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+ --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+ --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+ --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+ --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+ --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+ --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+ --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+ --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+ --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+ --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+ --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+ --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
diff --git a/auth-server-poc/src/app.py b/auth-server-poc/src/app.py
index 443eded..c7ba0d1 100644
--- a/auth-server-poc/src/app.py
+++ b/auth-server-poc/src/app.py
@@ -3,40 +3,51 @@ from flask_restful import Api, Resource
from flask_jwt_extended import create_access_token, JWTManager
from flask_cors import CORS
+import authn
+
app = Flask(__name__)
cors = CORS(
app,
resources={r"/api/*": {"origins": "*"}},
expose_headers=["Content-Type", "Authorization", "X-Total-Count"],
)
-api = Api(app, prefix='/api/v1.0')
+api = Api(app, prefix="/api/v1.0")
jwt = JWTManager(app)
-PEM_PRIVATE = '/opt/auth-server-poc/cert/private.pem'
-PEM_PUBLIC = '/opt/auth-server-poc/cert/public.pem'
+PEM_PRIVATE = "/opt/auth-server-poc/cert/private.pem"
+PEM_PUBLIC = "/opt/auth-server-poc/cert/public.pem"
-app.config['JWT_PRIVATE_KEY'] = open(PEM_PRIVATE).read()
-app.config['JWT_PUBLIC_KEY'] = open(PEM_PUBLIC).read()
-app.config['JWT_ALGORITHM'] = 'ES256'
-app.config['JWT_IDENTITY_CLAIM'] = 'sub'
-app.config['JWT_ACCESS_TOKEN_EXPIRES'] = False
+app.config["JWT_PRIVATE_KEY"] = open(PEM_PRIVATE).read()
+app.config["JWT_PUBLIC_KEY"] = open(PEM_PUBLIC).read()
+app.config["JWT_ALGORITHM"] = "ES256"
+app.config["JWT_IDENTITY_CLAIM"] = "sub"
+app.config["JWT_ACCESS_TOKEN_EXPIRES"] = False
class AuthApi(Resource):
def post(self):
- additional_claims = {"type": "access", "domains": ["sunet.se"]}
+
+ identity = request.environ.get("REMOTE_USER")
+ db = authn.UserDB("userdb.yaml")
+ additional_claims = {
+ "type": "access",
+ "read": db.read_perms(identity),
+ "write": db.write_perms(identity),
+ }
+
access_token = create_access_token(
- identity=request.environ.get('REMOTE_USER'),
+ identity=identity,
additional_claims=additional_claims,
)
- return {'access_token': access_token}, 200
+
+ return {"access_token": access_token}, 200
-@app.route('/')
+@app.route("/")
def index():
return "<p>Username: {}</p><p>Auth type: {}</p>".format(
- request.environ.get('REMOTE_USER'), request.environ.get('AUTH_TYPE')
+ request.environ.get("REMOTE_USER"), request.environ.get("AUTH_TYPE")
)
-api.add_resource(AuthApi, '/auth')
+api.add_resource(AuthApi, "/auth")
diff --git a/src/authn.py b/auth-server-poc/src/authn.py
index e90118a..8b32cdc 100755
--- a/src/authn.py
+++ b/auth-server-poc/src/authn.py
@@ -12,26 +12,24 @@ class Authz:
return "{}: {}".format(self._org, self._perms)
def read_p(self):
- return 'r' in self._perms
+ return "r" in self._perms
def write_p(self):
- return 'w' in self._perms
+ return "w" in self._perms
class User:
- def __init__(self, username, pw, authz):
+ def __init__(self, username, authz):
self._username = username
- self._password = pw
self._authz = {}
for org, perms in authz.items():
self._authz[org] = Authz(org, perms)
def dump(self):
- return ["{}/{}: {}".format(self._username, self._password, auth.dump())
- for auth in self._authz.values()]
-
- def authn_p(self, pw):
- return pw == self._password
+ return [
+ "{}: {}".format(self._username, auth.dump())
+ for auth in self._authz.values()
+ ]
def orgnames(self):
return [x for x in self._authz.keys()]
@@ -55,58 +53,45 @@ class UserDB:
def __init__(self, yamlfile):
self._users = {}
for u, d in yaml.safe_load(open(yamlfile)).items():
- self._users[u] = User(u, d['pw'], d['authz'])
+ self._users[u] = User(u, d["authz"])
def dump(self):
return [u.dump() for u in self._users.values()]
- def user_authn_p(self, username, password):
- user = self._users.get(username)
- if not user:
- return False
- return user.authn_p(password)
-
def orgs_for_user(self, username):
return self._users.get(username).orgnames()
- def read_perms(self, username, password):
+ def read_perms(self, username):
user = self._users.get(username)
if not user:
return None
- if not user.authn_p(password):
- return None
return user.read_perms()
- def write_perms(self, username, password):
+ def write_perms(self, username):
user = self._users.get(username)
if not user:
return None
- if not user.authn_p(password):
- return None
return user.write_perms()
def self_test():
- db = UserDB('userdb.yaml')
+ db = UserDB("userdb.yaml")
print(db.dump())
- orgs = db.orgs_for_user('user3')
- assert('sunet.se' in orgs)
- assert('su.se' in orgs)
- assert(len(orgs) == 2)
-
- assert(db.user_authn_p('user3', 'pw3') == True)
- assert(db.user_authn_p('user3', 'wrongpw') == False)
+ orgs = db.orgs_for_user("user3")
+ assert "sunet.se" in orgs
+ assert "su.se" in orgs
+ assert len(orgs) == 2
- rp = db.read_perms('user3', 'pw3')
- assert(len(rp) == 2)
- assert('sunet.se' in rp)
- assert('su.se' in rp)
+ rp = db.read_perms("user3", "pw3")
+ assert len(rp) == 2
+ assert "sunet.se" in rp
+ assert "su.se" in rp
- wp = db.write_perms('user3', 'pw3')
- assert(len(wp) == 1)
- assert('sunet.se' in wp)
+ wp = db.write_perms("user3", "pw3")
+ assert len(wp) == 1
+ assert "sunet.se" in wp
-if __name__ == '__main__':
+if __name__ == "__main__":
self_test()
diff --git a/auth-server-poc/src/userdb.yaml b/auth-server-poc/src/userdb.yaml
new file mode 100644
index 0000000..c55773b
--- /dev/null
+++ b/auth-server-poc/src/userdb.yaml
@@ -0,0 +1,23 @@
+user1:
+ authz:
+ sunet.se: r
+ su.se: r
+ kth.se: r
+
+user2:
+ authz:
+ sunet.se: w
+ su.se: w
+ kth.se: w
+
+user3:
+ authz:
+ sunet.se: rw
+ su.se: rw
+ kth.se: rw
+
+user4:
+ authz:
+ sunet.se: rw
+ su.se: r
+ kth.se: w
diff --git a/demo/wsgi_demo_users.yaml b/demo/wsgi_demo_users.yaml
deleted file mode 100644
index 49c4795..0000000
--- a/demo/wsgi_demo_users.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-user1:
- pw: pw1
- authz:
- sunet.se: r
-
-user2:
- pw: pw2
- authz:
- su.se: r
-
-user3:
- pw: pw3
- authz:
- sunet.se: rw
- su.se: r
diff --git a/quickstart.sh b/quickstart.sh
index d46a791..6e566de 100755
--- a/quickstart.sh
+++ b/quickstart.sh
@@ -27,7 +27,10 @@ fi
# Generate a default htpasswd file with a user "usr:pwd".
if [ ! -f ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd ]; then
- htpasswd -b -c ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd usr pwd
+ htpasswd -b -c ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd user1 pwd
+ htpasswd -b ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd user2 pwd
+ htpasswd -b ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd user3 pwd
+ htpasswd -b ${DOCKER_JWT_HTPASSWD_PATH}/.htpasswd user4 pwd
fi
# Launch the containers.
diff --git a/src/main.py b/src/main.py
index f95a09c..9beace0 100755
--- a/src/main.py
+++ b/src/main.py
@@ -116,13 +116,16 @@ async def get(key=None, limit=25, skip=0, ip=None, port=None,
data = []
raw_jwt = Authorize.get_raw_jwt()
- if 'domains' not in raw_jwt:
- return JSONResponse(content={"status": "error",
- "message": "Could not find domains" +
- "claim in JWT token"},
- status_code=400)
+ if "read" not in raw_jwt:
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "Could not find read claim in JWT token",
+ },
+ status_code=400,
+ )
else:
- domains = raw_jwt['domains']
+ domains = raw_jwt["read"]
for domain in domains:
data.extend(get_data(key, limit, skip, ip, port, asn, domain))
@@ -135,10 +138,30 @@ async def get_key(key=None, Authorize: AuthJWT = Depends()):
Authorize.jwt_required()
- # TODO: Use JWT authz and check e.g. domain here
+ raw_jwt = Authorize.get_raw_jwt()
+
+ if "read" not in raw_jwt:
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "Could not find read claim in JWT token",
+ },
+ status_code=400,
+ )
+ else:
+ allowed_domains = raw_jwt["read"]
data = get_data(key)
+ if data["domain"] not in allowed_domains:
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "User not authorized to view this object",
+ },
+ status_code=400,
+ )
+
return JSONResponse(content={"status": "success", "docs": data})
@@ -161,12 +184,36 @@ async def delete(key, Authorize: AuthJWT = Depends()):
Authorize.jwt_required()
+ raw_jwt = Authorize.get_raw_jwt()
+
+ if "write" not in raw_jwt:
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "Could not find write claim in JWT token",
+ },
+ status_code=400,
+ )
+ else:
+ allowed_domains = raw_jwt["write"]
+
+ data = get_data(key)
+
+ if data["domain"] not in allowed_domains:
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "User not authorized to delete this object",
+ },
+ status_code=400,
+ )
+
if db.delete(key) is None:
return JSONResponse(content={"status": "error",
"message": "Document not found"},
status_code=400)
- return JSONResponse(content={"status": "success", "docs": {}})
+ return JSONResponse(content={"status": "success", "docs": data})
def main(standalone=False):