Merge pull request #3024 from coreos-inc/manageable-robots
Manageable robots epic
This commit is contained in:
commit
6c43b7ff0d
24 changed files with 411 additions and 131 deletions
|
@ -321,7 +321,7 @@ def require_scope(scope_object):
|
|||
return wrapper
|
||||
|
||||
|
||||
def validate_json_request(schema_name):
|
||||
def validate_json_request(schema_name, optional=False):
|
||||
def wrapper(func):
|
||||
@add_method_metadata('request_schema', schema_name)
|
||||
@wraps(func)
|
||||
|
@ -330,9 +330,10 @@ def validate_json_request(schema_name):
|
|||
try:
|
||||
json_data = request.get_json()
|
||||
if json_data is None:
|
||||
raise InvalidRequest('Missing JSON body')
|
||||
|
||||
validate(json_data, schema)
|
||||
if not optional:
|
||||
raise InvalidRequest('Missing JSON body')
|
||||
else:
|
||||
validate(json_data, schema)
|
||||
return func(self, *args, **kwargs)
|
||||
except ValidationError as ex:
|
||||
raise InvalidRequest(ex.message)
|
||||
|
|
|
@ -2,14 +2,31 @@
|
|||
|
||||
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
|
||||
require_user_admin, require_scope, path_param, parse_args,
|
||||
truthy_bool, query_param)
|
||||
truthy_bool, query_param, validate_json_request)
|
||||
from endpoints.api.robot_models_pre_oci import pre_oci_model as model
|
||||
from endpoints.exception import Unauthorized
|
||||
from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from util.names import format_robot_username
|
||||
from flask import abort
|
||||
from flask import abort, request
|
||||
|
||||
|
||||
CREATE_ROBOT_SCHEMA = {
|
||||
'type': 'object',
|
||||
'description': 'Optional data for creating a robot',
|
||||
'properties': {
|
||||
'description': {
|
||||
'type': 'string',
|
||||
'description': 'Optional text description for the robot',
|
||||
'maxLength': 255,
|
||||
},
|
||||
'unstructured_metadata': {
|
||||
'type': 'object',
|
||||
'description': 'Optional unstructured metadata for the robot',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def robots_list(prefix, include_permissions=False):
|
||||
|
@ -38,6 +55,9 @@ class UserRobotList(ApiResource):
|
|||
'The short name for the robot, without any user or organization prefix')
|
||||
class UserRobot(ApiResource):
|
||||
""" Resource for managing a user's robots. """
|
||||
schemas = {
|
||||
'CreateRobot': CREATE_ROBOT_SCHEMA,
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('getUserRobot')
|
||||
|
@ -45,16 +65,23 @@ class UserRobot(ApiResource):
|
|||
""" Returns the user's robot with the specified name. """
|
||||
parent = get_authenticated_user()
|
||||
robot = model.get_user_robot(robot_shortname, parent)
|
||||
return robot.to_dict()
|
||||
return robot.to_dict(include_metadata=True)
|
||||
|
||||
@require_user_admin
|
||||
@nickname('createUserRobot')
|
||||
@validate_json_request('CreateRobot', optional=True)
|
||||
def put(self, robot_shortname):
|
||||
""" Create a new user robot with the specified name. """
|
||||
parent = get_authenticated_user()
|
||||
robot = model.create_user_robot(robot_shortname, parent)
|
||||
log_action('create_robot', parent.username, {'robot': robot_shortname})
|
||||
return robot.to_dict(), 201
|
||||
create_data = request.get_json() or {}
|
||||
robot = model.create_user_robot(robot_shortname, parent, create_data.get('description'),
|
||||
create_data.get('unstructured_metadata'))
|
||||
log_action('create_robot', parent.username, {
|
||||
'robot': robot_shortname,
|
||||
'description': create_data.get('description'),
|
||||
'unstructured_metadata': create_data.get('unstructured_metadata'),
|
||||
})
|
||||
return robot.to_dict(include_metadata=True), 201
|
||||
|
||||
@require_user_admin
|
||||
@nickname('deleteUserRobot')
|
||||
|
@ -94,6 +121,9 @@ class OrgRobotList(ApiResource):
|
|||
@related_user_resource(UserRobot)
|
||||
class OrgRobot(ApiResource):
|
||||
""" Resource for managing an organization's robots. """
|
||||
schemas = {
|
||||
'CreateRobot': CREATE_ROBOT_SCHEMA,
|
||||
}
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrgRobot')
|
||||
|
@ -102,19 +132,26 @@ class OrgRobot(ApiResource):
|
|||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
robot = model.get_org_robot(robot_shortname, orgname)
|
||||
return robot.to_dict()
|
||||
return robot.to_dict(include_metadata=True)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('createOrgRobot')
|
||||
@validate_json_request('CreateRobot', optional=True)
|
||||
def put(self, orgname, robot_shortname):
|
||||
""" Create a new robot in the organization. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
robot = model.create_org_robot(robot_shortname, orgname)
|
||||
log_action('create_robot', orgname, {'robot': robot_shortname})
|
||||
return robot.to_dict(), 201
|
||||
create_data = request.get_json() or {}
|
||||
robot = model.create_org_robot(robot_shortname, orgname, create_data.get('description'),
|
||||
create_data.get('unstructured_metadata'))
|
||||
log_action('create_robot', orgname, {
|
||||
'robot': robot_shortname,
|
||||
'description': create_data.get('description'),
|
||||
'unstructured_metadata': create_data.get('unstructured_metadata'),
|
||||
})
|
||||
return robot.to_dict(include_metadata=True), 201
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ from collections import namedtuple
|
|||
|
||||
from six import add_metaclass
|
||||
|
||||
from endpoints.api import format_date
|
||||
|
||||
|
||||
class Permission(namedtuple('Permission', ['repository_name', 'repository_visibility_name', 'role_name'])):
|
||||
"""
|
||||
|
@ -36,24 +38,32 @@ class RobotWithPermissions(
|
|||
namedtuple('RobotWithPermissions', [
|
||||
'name',
|
||||
'password',
|
||||
'created',
|
||||
'last_accessed',
|
||||
'teams',
|
||||
'repository_names',
|
||||
'description',
|
||||
])):
|
||||
"""
|
||||
RobotWithPermissions is a list of robot entries.
|
||||
:type name: string
|
||||
:type password: string
|
||||
:type created: datetime|None
|
||||
:type last_accessed: datetime|None
|
||||
:type teams: [Team]
|
||||
:type repository_names: [string]
|
||||
|
||||
:type description: string
|
||||
"""
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'token': self.password,
|
||||
'created': format_date(self.created) if self.created is not None else None,
|
||||
'last_accessed': format_date(self.last_accessed) if self.last_accessed is not None else None,
|
||||
'teams': [team.to_dict() for team in self.teams],
|
||||
'repositories': self.repository_names
|
||||
'repositories': self.repository_names,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
|
@ -61,20 +71,35 @@ class Robot(
|
|||
namedtuple('Robot', [
|
||||
'name',
|
||||
'password',
|
||||
'created',
|
||||
'last_accessed',
|
||||
'description',
|
||||
'unstructured_metadata',
|
||||
])):
|
||||
"""
|
||||
Robot represents a robot entity.
|
||||
:type name: string
|
||||
:type password: string
|
||||
|
||||
:type created: datetime|None
|
||||
:type last_accessed: datetime|None
|
||||
:type description: string
|
||||
:type unstructured_metadata: dict
|
||||
"""
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
def to_dict(self, include_metadata=False):
|
||||
data = {
|
||||
'name': self.name,
|
||||
'token': self.password
|
||||
'token': self.password,
|
||||
'created': format_date(self.created) if self.created is not None else None,
|
||||
'last_accessed': format_date(self.last_accessed) if self.last_accessed is not None else None,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
if include_metadata:
|
||||
data['unstructured_metadata'] = self.unstructured_metadata
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class RobotInterface(object):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from app import avatar
|
||||
from data import model
|
||||
from data.database import User, FederatedLogin, Team as TeamTable, Repository
|
||||
from data.database import User, FederatedLogin, Team as TeamTable, Repository, RobotAccountMetadata
|
||||
from endpoints.api.robot_models_interface import (RobotInterface, Robot, RobotWithPermissions, Team,
|
||||
Permission)
|
||||
|
||||
|
@ -23,14 +23,21 @@ class RobotPreOCIModel(RobotInterface):
|
|||
robot_dict = {
|
||||
'name': robot_name,
|
||||
'token': robot_tuple.get(FederatedLogin.service_ident),
|
||||
'created': robot_tuple.get(User.creation_date),
|
||||
'last_accessed': robot_tuple.get(User.last_accessed),
|
||||
'description': robot_tuple.get(RobotAccountMetadata.description),
|
||||
'unstructured_metadata': robot_tuple.get(RobotAccountMetadata.unstructured_json),
|
||||
}
|
||||
|
||||
if include_permissions:
|
||||
robot_dict.update({
|
||||
'teams': [],
|
||||
'repositories': []
|
||||
'repositories': [],
|
||||
})
|
||||
robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'])
|
||||
|
||||
robots[robot_name] = Robot(robot_dict['name'], robot_dict['token'], robot_dict['created'],
|
||||
robot_dict['last_accessed'], robot_dict['description'],
|
||||
robot_dict['unstructured_metadata'])
|
||||
if include_permissions:
|
||||
team_name = robot_tuple.get(TeamTable.name)
|
||||
repository_name = robot_tuple.get(Repository.name)
|
||||
|
@ -48,40 +55,52 @@ class RobotPreOCIModel(RobotInterface):
|
|||
if repository_name is not None:
|
||||
if repository_name not in robot_dict['repositories']:
|
||||
robot_dict['repositories'].append(repository_name)
|
||||
robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'], robot_dict['teams'],
|
||||
robot_dict['repositories'])
|
||||
robots[robot_name] = RobotWithPermissions(robot_dict['name'], robot_dict['token'],
|
||||
robot_dict['created'],
|
||||
robot_dict['last_accessed'],
|
||||
robot_dict['teams'],
|
||||
robot_dict['repositories'],
|
||||
robot_dict['description'])
|
||||
|
||||
return robots.values()
|
||||
|
||||
def regenerate_user_robot_token(self, robot_shortname, owning_user):
|
||||
robot, password = model.user.regenerate_robot_token(robot_shortname, owning_user)
|
||||
return Robot(robot.username, password)
|
||||
robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, owning_user)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
metadata.description, metadata.unstructured_json)
|
||||
|
||||
def regenerate_org_robot_token(self, robot_shortname, orgname):
|
||||
parent = model.organization.get_organization(orgname)
|
||||
robot, password = model.user.regenerate_robot_token(robot_shortname, parent)
|
||||
return Robot(robot.username, password)
|
||||
robot, password, metadata = model.user.regenerate_robot_token(robot_shortname, parent)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
metadata.description, metadata.unstructured_json)
|
||||
|
||||
def delete_robot(self, robot_username):
|
||||
model.user.delete_robot(robot_username)
|
||||
|
||||
def create_user_robot(self, robot_shortname, owning_user):
|
||||
robot, password = model.user.create_robot(robot_shortname, owning_user)
|
||||
return Robot(robot.username, password)
|
||||
def create_user_robot(self, robot_shortname, owning_user, description, unstructured_metadata):
|
||||
robot, password = model.user.create_robot(robot_shortname, owning_user, description or '',
|
||||
unstructured_metadata)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
description or '', unstructured_metadata)
|
||||
|
||||
def create_org_robot(self, robot_shortname, orgname):
|
||||
def create_org_robot(self, robot_shortname, orgname, description, unstructured_metadata):
|
||||
parent = model.organization.get_organization(orgname)
|
||||
robot, password = model.user.create_robot(robot_shortname, parent)
|
||||
return Robot(robot.username, password)
|
||||
robot, password = model.user.create_robot(robot_shortname, parent, description or '',
|
||||
unstructured_metadata)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
description or '', unstructured_metadata)
|
||||
|
||||
def get_org_robot(self, robot_shortname, orgname):
|
||||
parent = model.organization.get_organization(orgname)
|
||||
robot, password = model.user.get_robot(robot_shortname, parent)
|
||||
return Robot(robot.username, password)
|
||||
robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, parent)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
metadata.description, metadata.unstructured_json)
|
||||
|
||||
def get_user_robot(self, robot_shortname, owning_user):
|
||||
robot, password = model.user.get_robot(robot_shortname, owning_user)
|
||||
return Robot(robot.username, password)
|
||||
robot, password, metadata = model.user.get_robot_and_metadata(robot_shortname, owning_user)
|
||||
return Robot(robot.username, password, robot.creation_date, robot.last_accessed,
|
||||
metadata.description, metadata.unstructured_json)
|
||||
|
||||
|
||||
pre_oci_model = RobotPreOCIModel()
|
||||
|
|
38
endpoints/api/test/test_robot.py
Normal file
38
endpoints/api/test/test_robot.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import pytest
|
||||
import json
|
||||
|
||||
from data import model
|
||||
from endpoints.api import api
|
||||
from endpoints.api.test.shared import conduct_api_call
|
||||
from endpoints.api.robot import UserRobot, OrgRobot
|
||||
from endpoints.test.shared import client_with_identity
|
||||
|
||||
from test.test_ldap import mock_ldap
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
@pytest.mark.parametrize('endpoint', [
|
||||
UserRobot,
|
||||
OrgRobot,
|
||||
])
|
||||
@pytest.mark.parametrize('body', [
|
||||
{},
|
||||
{'description': 'this is a description'},
|
||||
{'unstructured_metadata': {'foo': 'bar'}},
|
||||
{'description': 'this is a description', 'unstructured_metadata': {'foo': 'bar'}},
|
||||
])
|
||||
def test_create_robot_with_metadata(endpoint, body, client):
|
||||
with client_with_identity('devtable', client) as cl:
|
||||
# Create the robot with the specified body.
|
||||
conduct_api_call(cl, endpoint, 'PUT', {'orgname': 'buynlarge', 'robot_shortname': 'somebot'},
|
||||
body, expected_code=201)
|
||||
|
||||
# Ensure the create succeeded.
|
||||
resp = conduct_api_call(cl, endpoint, 'GET', {
|
||||
'orgname': 'buynlarge',
|
||||
'robot_shortname': 'somebot',
|
||||
})
|
||||
|
||||
body = body or {}
|
||||
assert resp.json['description'] == (body.get('description') or '')
|
||||
assert resp.json['unstructured_metadata'] == (body.get('unstructured_metadata') or {})
|
Reference in a new issue