Merge pull request #522 from dod-ccpo/csp-env-integration

CSP Interface: Environments
This commit is contained in:
patricksmithdds 2019-01-08 16:44:08 -05:00 committed by GitHub
commit 657b17c1ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 7 deletions

View File

@ -0,0 +1,28 @@
"""Add cloud_id column to user
Revision ID: 91bd9482ce23
Revises: b3fa1493e0a9
Create Date: 2019-01-08 10:18:23.764179
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '91bd9482ce23'
down_revision = 'b3fa1493e0a9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('cloud_id', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'cloud_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""Add cloud_id column to environments
Revision ID: b3fa1493e0a9
Revises: 6172ac7b8b26
Create Date: 2019-01-04 14:28:59.660309
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b3fa1493e0a9'
down_revision = '6172ac7b8b26'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('environments', sa.Column('cloud_id', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('environments', 'cloud_id')
# ### end Alembic commands ###

View File

@ -1,9 +1,11 @@
from .cloud import MockCloudProvider
from .files import RackspaceFileProvider from .files import RackspaceFileProvider
from .reports import MockReportingProvider from .reports import MockReportingProvider
class MockCSP: class MockCSP:
def __init__(self, app): def __init__(self, app):
self.cloud = MockCloudProvider()
self.files = RackspaceFileProvider(app) self.files = RackspaceFileProvider(app)
self.reports = MockReportingProvider() self.reports = MockReportingProvider()

66
atst/domain/csp/cloud.py Normal file
View File

@ -0,0 +1,66 @@
from uuid import uuid4
class CloudProviderInterface:
def create_application(self, name): # pragma: no cover
"""Create an application in the cloud with the provided name. Returns
the ID of the created object.
"""
raise NotImplementedError()
def create_user(self, user): # pragma: no cover
"""Create an account in the CSP for specified user. Returns the ID of
the created user.
"""
raise NotImplementedError()
def create_role(self, environment_role): # pragma: no cover
"""Takes an `atst.model.EnvironmentRole` object and allows the
specified user access to the specified cloud entity.
This _does not_ return a token, but is intended to perform any necessary
setup to allow a token to be generated in the future (for example,
add the user to the cloud entity in some fashion).
"""
raise NotImplementedError()
def delete_role(self, environment_role): # pragma: no cover
"""Takes an `atst.model.EnvironmentRole` object and performs any action
necessary in the CSP to remove the specified user from the specified
environment. This method does not return anything.
"""
raise NotImplementedError()
def get_access_token(self, environment_role): # pragma: no cover
"""Takes an `atst.model.EnvironmentRole` object and returns a federated
access token that gives the specified user access to the specified
environment with the proper permissions.
"""
raise NotImplementedError()
class MockCloudProvider(CloudProviderInterface):
def create_application(self, name):
"""Returns an id that represents what would be an application in the
cloud."""
return uuid4().hex
def create_user(self, user):
"""Returns an id that represents what would be an user in the cloud."""
return uuid4().hex
def create_role(self, environment_role):
# Currently, there is nothing to mock out, so just do nothing.
pass
def delete_role(self, environment_role):
# Currently nothing to do.
pass
def get_access_token(self, environment_role):
# for now, just create a mock token using the user and environment
# cloud IDs and the name of the role in the environment
user_id = environment_role.user.cloud_id or ""
env_id = environment_role.environment.cloud_id or ""
role_details = environment_role.role
return "::".join([user_id, env_id, role_details])

View File

@ -1,8 +1,18 @@
from flask import current_app as app
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from atst.database import db from atst.database import db
class EnvironmentRoles(object): class EnvironmentRoles(object):
@classmethod
def create(cls, user, environment, role):
env_role = EnvironmentRole(user=user, environment=environment, role=role)
if not user.cloud_id:
user.cloud_id = app.csp.cloud.create_user(user)
app.csp.cloud.create_role(env_role)
return env_role
@classmethod @classmethod
def get(cls, user_id, environment_id): def get(cls, user_id, environment_id):
existing_env_role = ( existing_env_role = (
@ -19,6 +29,7 @@ class EnvironmentRoles(object):
def delete(cls, user_id, environment_id): def delete(cls, user_id, environment_id):
existing_env_role = EnvironmentRoles.get(user_id, environment_id) existing_env_role = EnvironmentRoles.get(user_id, environment_id)
if existing_env_role: if existing_env_role:
app.csp.cloud.delete_role(existing_env_role)
db.session.delete(existing_env_role) db.session.delete(existing_env_role)
db.session.commit() db.session.commit()
return True return True

View File

@ -1,3 +1,4 @@
from flask import current_app as app
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
@ -15,6 +16,7 @@ class Environments(object):
@classmethod @classmethod
def create(cls, project, name): def create(cls, project, name):
environment = Environment(project=project, name=name) environment = Environment(project=project, name=name)
environment.cloud_id = app.csp.cloud.create_application(environment.name)
db.session.add(environment) db.session.add(environment)
db.session.commit() db.session.commit()
return environment return environment
@ -23,7 +25,7 @@ class Environments(object):
def create_many(cls, project, names): def create_many(cls, project, names):
environments = [] environments = []
for name in names: for name in names:
environment = Environment(project=project, name=name) environment = Environments.create(project, name)
environments.append(environment) environments.append(environment)
db.session.add_all(environments) db.session.add_all(environments)
@ -31,7 +33,7 @@ class Environments(object):
@classmethod @classmethod
def add_member(cls, environment, user, role): def add_member(cls, environment, user, role):
environment_user = EnvironmentRole( environment_user = EnvironmentRoles.create(
user=user, environment=environment, role=role user=user, environment=environment, role=role
) )
db.session.add(environment_user) db.session.add(environment_user)
@ -86,7 +88,7 @@ class Environments(object):
updated = True updated = True
db.session.add(env_role) db.session.add(env_role)
elif not env_role: elif not env_role:
env_role = EnvironmentRole( env_role = EnvironmentRoles.create(
user=workspace_role.user, environment=environment, role=new_role user=workspace_role.user, environment=environment, role=new_role
) )
updated = True updated = True

View File

@ -15,6 +15,8 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
project_id = Column(ForeignKey("projects.id"), nullable=False) project_id = Column(ForeignKey("projects.id"), nullable=False)
project = relationship("Project") project = relationship("Project")
cloud_id = Column(String)
@property @property
def users(self): def users(self):
return [r.user for r in self.roles] return [r.user for r in self.roles]

View File

@ -29,6 +29,8 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
provisional = Column(Boolean) provisional = Column(Boolean)
cloud_id = Column(String)
REQUIRED_FIELDS = [ REQUIRED_FIELDS = [
"email", "email",
"dod_id", "dod_id",

View File

@ -1,6 +1,15 @@
from flask import render_template, request as http_request, g, redirect, url_for from flask import (
current_app as app,
g,
redirect,
render_template,
request as http_request,
url_for,
)
from . import workspaces_bp from . import workspaces_bp
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import UnauthorizedError
from atst.domain.projects import Projects from atst.domain.projects import Projects
from atst.domain.workspaces import Workspaces from atst.domain.workspaces import Workspaces
from atst.forms.project import NewProjectForm, ProjectForm from atst.forms.project import NewProjectForm, ProjectForm
@ -76,3 +85,15 @@ def update_project(workspace_id, project_id):
project=project, project=project,
form=form, form=form,
) )
@workspaces_bp.route("/workspaces/<workspace_id>/environments/<environment_id>/access")
def access_environment(workspace_id, environment_id):
env_role = EnvironmentRoles.get(g.current_user.id, environment_id)
if not env_role:
raise UnauthorizedError(
g.current_user, "access environment {}".format(environment_id)
)
else:
token = app.csp.cloud.get_access_token(env_role)
return redirect(url_for("atst.csp_environment_access", token=token))

View File

@ -34,7 +34,7 @@
<ul> <ul>
{% for environment in project.environments %} {% for environment in project.environments %}
<li class='block-list__item project-list-item__environment'> <li class='block-list__item project-list-item__environment'>
<a href='{{ url_for("atst.csp_environment_access")}}' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'> <a href='{{ url_for("workspaces.access_environment", workspace_id=workspace.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'>
{{ Icon('link') }} {{ Icon('link') }}
<span>{{ environment.name }}</span> <span>{{ environment.name }}</span>
</a> </a>

View File

@ -2,7 +2,36 @@ from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.workspace_roles import WorkspaceRoles from atst.domain.workspace_roles import WorkspaceRoles
from tests.factories import UserFactory, WorkspaceFactory from tests.factories import ProjectFactory, UserFactory, WorkspaceFactory
def test_create_environments():
project = ProjectFactory.create()
environments = Environments.create_many(project, ["Staging", "Production"])
for env in environments:
assert env.cloud_id is not None
def test_create_environment_role_creates_cloud_id(session):
owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[{"name": "project1", "environments": [{"name": "project1 prod"}]}],
)
env = workspace.projects[0].environments[0]
new_role = [{"id": env.id, "role": "developer"}]
workspace_role = workspace.members[0]
assert not workspace_role.user.cloud_id
assert Environments.update_environment_roles(
owner, workspace, workspace_role, new_role
)
assert workspace_role.user.cloud_id is not None
def test_update_environment_roles(): def test_update_environment_roles():

View File

@ -1,6 +1,13 @@
from flask import url_for from flask import url_for
from tests.factories import UserFactory, WorkspaceFactory from tests.factories import (
UserFactory,
WorkspaceFactory,
WorkspaceRoleFactory,
EnvironmentRoleFactory,
EnvironmentFactory,
ProjectFactory,
)
from atst.domain.projects import Projects from atst.domain.projects import Projects
from atst.domain.workspaces import Workspaces from atst.domain.workspaces import Workspaces
from atst.models.workspace_role import Status as WorkspaceRoleStatus from atst.models.workspace_role import Status as WorkspaceRoleStatus
@ -125,3 +132,42 @@ def test_user_without_permission_cannot_update_project(client, user_session):
assert response.status_code == 404 assert response.status_code == 404
assert project.name == "Great Project" assert project.name == "Great Project"
assert project.description == "Cool stuff happening here!" assert project.description == "Cool stuff happening here!"
def create_environment(user):
workspace = WorkspaceFactory.create()
workspace_role = WorkspaceRoleFactory.create(workspace=workspace, user=user)
project = ProjectFactory.create(workspace=workspace)
return EnvironmentFactory.create(project=project, name="new environment!")
def test_environment_access_with_env_role(client, user_session):
user = UserFactory.create()
environment = create_environment(user)
env_role = EnvironmentRoleFactory.create(
user=user, environment=environment, role="developer"
)
user_session(user)
response = client.get(
url_for(
"workspaces.access_environment",
workspace_id=environment.workspace.id,
environment_id=environment.id,
)
)
assert response.status_code == 302
assert "csp-environment-access" in response.location
def test_environment_access_with_no_role(client, user_session):
user = UserFactory.create()
environment = create_environment(user)
user_session(user)
response = client.get(
url_for(
"workspaces.access_environment",
workspace_id=environment.workspace.id,
environment_id=environment.id,
)
)
assert response.status_code == 404