Skip to content

Commit

Permalink
add support for additional SSH keys
Browse files Browse the repository at this point in the history
  • Loading branch information
ib-steffen committed Feb 25, 2019
1 parent 716d983 commit 10e48a1
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 10 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ Some of InfraBox' features are:
- [Set resource limits (CPU and memory) for each task](https://github.com/SAP/infrabox-examples)
- [GitHub integration](docs/install/configure/github.md)
- [Gerrit integration](docs/install/configure/gerrit.md)
- GitLab (coming soon)
- [LDAP support](docs/install/configure/ldap.md)
- [Periodically schedule builds](docs/cronjobs.md)
- [and many more, see our examples](https://github.com/SAP/infrabox-examples)
Expand Down
122 changes: 122 additions & 0 deletions src/api/handlers/projects/sshkeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import re

from flask import request, g, abort
from flask_restplus import Resource, fields

from pyinfrabox.utils import validate_uuid
from pyinfraboxutils.ibflask import OK
from pyinfraboxutils.ibrestplus import api, response_model

from croniter import croniter

ns = api.namespace('SSHKeys',
path='/api/v1/projects/<project_id>/sshkeys',
description='SSH Key related operations')

sshkey_model = api.model('CronJob', {
'name': fields.String(required=True),
'id': fields.String(required=True),
'secret': fields.String(required=True),
})

add_sshkey_model = api.model('AddCronJob', {
'name': fields.String(required=True, max_length=255),
'secret': fields.String(required=True, max_length=255),
})

@ns.route('/')
@api.doc(responses={403: 'Not Authorized'})
class SSHKeys(Resource):

name_pattern = re.compile('^[a-zA-Z0-9_]+$')

@api.marshal_list_with(sshkey_model)
def get(self, project_id):
'''
Returns project's sshkeys
'''
p = g.db.execute_many_dict('''
SELECT k.id, k.name, s.name as secret
FROM sshkey k
JOIN secret s
ON s.id = k.secret_id
WHERE s.project_id = %s
AND k.project_id = %s
''', [project_id, project_id])
return p

@api.expect(add_sshkey_model)
@api.response(200, 'Success', response_model)
def post(self, project_id):
'''
Add new sshkey
'''
b = request.get_json()

if not CronJobs.name_pattern.match(b['name']):
abort(400, 'CronJob name must be not empty alphanumeric string.')

result = g.db.execute_one_dict("""
SELECT id
FROM secret
WHERE project_id = %s
AND name = %s
""", [project_id, b['secret']])

if not result:
abort(400, 'Secret does not exist')

secret_id = result['id']

result = g.db.execute_one_dict("""
SELECT COUNT(*) as cnt
FROM sshkey
WHERE project_id = %s
""", [project_id])

if result['cnt'] > 50:
abort(400, 'Too many sshkeys.')

r = g.db.execute_one("""
SELECT count(*)
FROM sshkey
WHERE project_id = %s
AND name = %s
""", [project_id, b['name']])

if r[0] > 0:
abort(400, 'SSH Key with this name already exist')

g.db.execute('''
INSERT INTO sshkey (project_id, name, secret_id) VALUES(%s, %s, %s)
''', [project_id, b['name'], secret_id])

g.db.commit()

return OK('Successfully added SSH Key')

@ns.route('/<sshkey_id>')
@api.doc(responses={403: 'Not Authorized'})
class CronJob(Resource):
@api.response(200, 'Success', response_model)
def delete(self, project_id, sshkey_id):
'''
Delete a sshkey
'''
if not validate_uuid(sshkey_id):
abort(400, "Invalid sshkey uuid.")

num_sshkeys = g.db.execute_one("""
SELECT COUNT(*) FROM sshkey
WHERE project_id = %s and id = %s
""", [project_id, sshkey_id])[0]

if num_sshkeys == 0:
return abort(400, 'SSH Key does not exist.')

g.db.execute("""
DELETE FROM sshkey WHERE project_id = %s and id = %s
""", [project_id, sshkey_id])
g.db.commit()

return OK('Successfully deleted SSH Key.')
97 changes: 97 additions & 0 deletions src/dashboard-client/src/components/project/SSHKeys.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<template>
<div class="m-sm full-height">
<md-card md-theme="white" class="full-height clean-card">
<md-card-header>
<md-card-header-text class="setting-list">
<md-icon>security</md-icon>
<span>SSHKeys</span>
</md-card-header-text>
</md-card-header>
<md-card-area>
<md-list class="m-t-md m-b-md md-double-line">
<md-list-item>
<div class="md-list-text-container">
<span>
<md-input-container>
<label>Name</label>
<md-input @keyup.enter.native="addSSHKey" required v-model="name"></md-input>
</md-input-container>
</span>
</div>

<div class="md-list-text-container">
<span>
<md-input-container>
<label for="secret_select">Secret</label>
<md-select name="secret_select" id="secret_select" v-model="secret">
<md-option v-for="s in project.secrets" :value=r :key="s.name" class="bg-white">{{s.name}}</md-option>
</md-select>
</md-input-container>
</span>
</div>

<md-button class="md-icon-button md-list-action" @click="addSSHKey()">
<md-icon md-theme="running" class="md-primary">add_circle</md-icon>
<md-tooltip>Add SSH Key</md-tooltip>
</md-button>
</md-list-item>
<md-list-item v-for="k in project.sshkeys" :key="k.id">
<md-input-container class="m-l-sm">
{{ k.name }}
</md-input-container>
<md-input-container class="m-l-sm">
{{ k.secret }}
</md-input-container>
<md-button class="md-icon-button md-list-action" @click="project.removeSSHKey(co)">
<md-icon class="md-primary">delete</md-icon>
<md-tooltip>Remove sshkey</md-tooltip>
</md-button>
</md-list-item>
</md-list>
</md-card-area>
</md-card>
</div>
</template>

<script>
export default {
props: ['project'],
data: () => {
return {
'name': '',
'secret': ''
}
},
created () {
this.project._loadSSHKeys()
},
methods: {
deleteSSHKEY (id) {
NewAPIService.delete(`projects/${this.project.id}/sshkeys/${id}`)
.then((response) => {
NotificationService.$emit('NOTIFICATION', new Notification(response))
this.project._reloadSSHKeys()
})
.catch((err) => {
NotificationService.$emit('NOTIFICATION', new Notification(err))
})
},
addCronJob () {
const d = {
name: this.name,
secret: this.secret
}
NewAPIService.post(`projects/${this.project.id}/sshkeys`, d)
.then((response) => {
NotificationService.$emit('NOTIFICATION', new Notification(response))
this.name = ''
this.minute = ''
this.project._reloadSSHKeys()
})
.catch((err) => {
NotificationService.$emit('NOTIFICATION', new Notification(err))
})
}
}
}
</script>
13 changes: 9 additions & 4 deletions src/dashboard-client/src/components/project/Settings.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<template>
<md-layout md-row>
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="33" md-flex-xlarge="33" class="b-r thin-border">
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="50" md-flex-xlarge="50" class="b-r thin-border">
<ib-project-secrets :project="project"></ib-project-secrets>
</md-layout>
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="33" md-flex-xlarge="33" class="b-r thin-border">
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="50" md-flex-xlarge="50" class="b-r thin-border">
<ib-project-collaborators :project="project"></ib-project-collaborators>
</md-layout>
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="33" md-flex-xlarge="33">
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="50" md-flex-xlarge="50">
<ib-project-tokens :project="project"></ib-project-tokens>
</md-layout>
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="50" md-flex-medium="50" md-flex-large="50" md-flex-xlarge="50">
<ib-project-sshkeys :project="project"></ib-project-sshkeys>
</md-layout>
<md-layout md-column md-gutter md-flex-xsmall="100" md-flex-small="100" md-flex-medium="100" md-flex-large="100" md-flex-xlarge="100">
<ib-project-cronjobs :project="project"></ib-project-cronjobs>
</md-layout>
Expand All @@ -24,6 +27,7 @@ import ProjectTokens from './Tokens'
import ProjectCollaborators from './Collaborators'
import ProjectBadges from './Badges'
import ProjectCronJobs from './Cron'
import ProjectSSHKeys from './SSHKeys'
export default {
props: ['project'],
Expand All @@ -32,7 +36,8 @@ export default {
'ib-project-tokens': ProjectTokens,
'ib-project-collaborators': ProjectCollaborators,
'ib-project-badges': ProjectBadges,
'ib-project-cronjobs': ProjectCronJobs
'ib-project-cronjobs': ProjectCronJobs,
'ib-project-sshkeys': ProjectSSHKeys
}
}
</script>
Expand Down
17 changes: 17 additions & 0 deletions src/dashboard-client/src/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,23 @@ export default class Project {
})
}

_loadSSHKeys () {
if (this.cronjobs) {
return
}

this._reloadSSHKeys()
}

_reloadSSHKeys () {
return NewAPIService.get(`projects/${this.id}/sshkeys`)
.then((sshkeys) => {
store.commit('setSSHKeys', { project: this, sshkeys: sshkeys })
})
.catch((err) => {
NotificationService.$emit('NOTIFICATION', new Notification(err))
})
}
_loadRoles () {
if (this.roles) {
return
Expand Down
7 changes: 7 additions & 0 deletions src/dashboard-client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ function addJobs (state, jobs) {
}
}

function setSSHKeys (state, data) {
const project = data.project
const sshkeys = data.sshkeys
project.sshkeys = sshkeys
}

function setCronJobs (state, data) {
const project = data.project
const cronjobs = data.cronjobs
Expand Down Expand Up @@ -308,6 +314,7 @@ const mutations = {
addJobs,
setSecrets,
setCronJobs,
setSSHKeys,
setCollaborators,
setRoles,
setTokens,
Expand Down
2 changes: 1 addition & 1 deletion src/db/migrations/00028.sql
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ALTER TABLE "commit" ADD COLUMN gerrit_change_id varchar;
ALTER TABLE "commit" ADD COLUMN gerrit_change_id varchar;
9 changes: 9 additions & 0 deletions src/db/migrations/00029.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE sshkey (
project_id uuid NOT NULL,
id uuid DEFAULT gen_random_uuid() NOT NULL,
name character varying(255) NOT NULL,
secret_id uuid NOT NULL
);

ALTER TABLE ONLY sshkey
ADD CONSTRAINT sshkey_pkey PRIMARY KEY (id);
37 changes: 37 additions & 0 deletions src/openpolicyagent/policies/projects_sshkeys.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package infrabox

import input as api

import data.infrabox.collaborators.collaborators
import data.infrabox.projects.projects
import data.infrabox.roles

projects_sshkeys_administrator([user, project]){
collaborators[i].project_id = project
collaborators[i].user_id = user
roles[collaborators[i].role] >= 20
}

# Allow GET access to /api/v1/projects/<project_id>/sshkeys for project administrators
allow {
api.method = "GET"
api.path = ["api", "v1", "projects", project_id, "sshkeys"]
api.token.type = "user"
projects_sshkeys_administrator([api.token.user.id, project_id])
}

# Allow POST access to /api/v1/projects/<project_id>/sshkeys for project administrators
allow {
api.method = "POST"
api.path = ["api", "v1", "projects", project_id, "sshkeys"]
api.token.type = "user"
projects_sshkeys_administrator([api.token.user.id, project_id])
}

# Allow DELETE access to /api/v1/projects/<project_id>/sshkeys/<cronjob_id> for project administrators
allow {
api.method = "DELETE"
api.path = ["api", "v1", "projects", project_id, "sshkeys", cronjob_id]
api.token.type = "user"
projects_sshkeys_administrator([api.token.user.id, project_id])
}
Loading

0 comments on commit 10e48a1

Please sign in to comment.