A GUI for aptly using Angular and Flask
25 Sep 2016At DARIAH we are using aptly to manage our repository of debian packages coming out of our Jenkins builds.
Our release process involves copying a specific package from the snapshot component of the repository to the release component. While this can be managed by command line arguments, we decided to give aptly’s API a go. To provide management of the repository through a web application, we built a simple AngularJS 1 app that shows the repository contents and allows to copy and delete individual packages.
Initially we put the app and the API behind an all-or-nothing authentication. But things got more complicated once we wanted to have public read-only access and more fine-grained control.
This is how we did it.
The aptly API supports all calls we need to make. However, it does not have any authentication built in. So we put the API behind an authentication protected Apache config which also served a basic Angular single page app to handle the calls to the API. Once the app did what we needed, we started to build authenticated API access in between our Angular app and aptly.
We chose flask to build the API. There is an existing flask plugin flask-sso that can plug into our AAI’s shibboleth authentication.
We just plug in the shibboleth attributes to use
SSO_ATTRIBUTES = {
'eppn': (True, 'username'),
'cn': (True, 'fullname'),
'mail': (True, 'email'),
'isMemberOf': (False, 'isMemberOf')
}
app.config.setdefault('SSO_ATTRIBUTE_MAP', SSO_ATTRIBUTES)
app.config.setdefault('SSO_LOGIN_URL', '/login')
ext = SSO(app=app)
and define the login and logout handler
@ext.login_handler
def login(user_info):
session['user'] = user_info
return redirect(url_for('index'))
@app.route('/logout')
def logout():
session.pop('user')
return redirect(url_for('index'))
Now all that’s left to do is set up Apache to enforce Shibboleth on the /login
path
<Location /login>
AuthType shibboleth
ShibRequestSetting requireSession true
Require valid-user
</Location>
So much for the easy part. Since the HTML pages are served trough flask, we can include user login state in the rendered output and our aptly app doesn’t care about users at all.
Now for deciding whether or not the authenticated user is allowed to do some API request, we effectively re-implemented all the aptly API calls in our flask app
@app.route('/api/<path:path>',
methods=['GET', 'POST', 'PUT', 'DELETE'])
I couldn’t figure out, how to simply proxy the request if the user allowed to make it and block it otherwise.
So finally, I went for the ugly route by going through each possible path
and check whether the call is allowed or not.
If the call is valid, flask receives the request and then calls the aptly API with a new request on its own.
The return of this call is then returned by flask to the app.
While this seems complicated and ineffective since it means we process all the data, it does have one advantage. We can manipulate the responses and add some detailed info to each repository for whether the user is allowed to write to a certain repo.
'''allow to GET the repo list but add writablility-info'''
response = requests.get(app.config['API_URL']+'/repos')
if response.status_code != 200:
raise InvalidAPIUsage('There was a back end error!',
status_code=response.status_code)
data = []
for item in response.json():
item['Writable']=writeaccesstorepo(item['Name'])
data.append(item)
ret = Response(response=json.dumps(data),
status=200,
mimetype='application/json')
This is basic way we sent things up.
The call to /repos
is always allowed, since we display the repository also publicly.
Flask then calls the aptly API, processes the result and returns the data to the user.
Here writeaccesstorepo()
is the method that decides whether the current user has write access to the repository in question.
This allows the aptly app to decide whether to show the buttons for deleting and copying packages.
<button ng-click="deletePackage(currentrepo,package);"
ng-if="writeaccessToRepo(currentrepo)">
<span class="fa fa-trash"></span>
</button>
Here writeaccessToRepo()
is the corresponding javascript function checking the result from the API.
In the future, we could also easily extend this to allow for different permissions to different repositories.
See the GitHub repo for the rest of the code.