in ad-joining/register-computer/main.py [0:0]
def __register_computer(request):
"""
Create a computer account for the joining computer.
"""
# Authenticate HTTP request
auth_info = __authenticate_request(request)
# Connect to Active Directory so that we can authorize the request.
try:
ad_site = request.args.get("ad_site")
ad_connection = __connect_to_activedirectory(ad_site)
except Exception as e:
logging.exception("Connecting to Active Directory failed")
return flask.abort(HTTP_BAD_GATEWAY, description="CONNECT_TO_AD_FAILED")
# Authorize the request. This entails two checks:
# (1) Check that the project allows AD to manage computer accounts for it.
# This is to prevent users from joining machines without the project
# owner authorizing it.
# (2) Check that AD allows the project to join machines. This is to prevent
# rogue/unauthorized projects from joining machines.
# Authorize, Part 1: Check that we have read access to the project's VM.
# Read access implies that a project owner or security admin of the project
# is OK with us managing computer accounts for the project's machines.
#
# Access is also required to scavenge stale computer accounts - checking it now
# reduces the risk of us not being able to scavenge later because of lacking
# permissions.
try:
gce_instance = gcp.project.Project(auth_info.get_project_id()).get_instance(
auth_info.get_instance_name(),
auth_info.get_zone())
if gce_instance is None:
raise Exception("Instance does not exist")
# Read the hostname, it might be different from the instance name.
if "hostname" in gce_instance:
computer_name = gce_instance["hostname"]
if "." in computer_name:
computer_name = computer_name.split(".")[0] # Strip domain
else:
computer_name = auth_info.get_instance_name()
logging.info("Successfully read GCE instance data for '%s' (hostname: '%s'), authorized (1/2)" %
(auth_info.get_instance_name(), computer_name))
except Exception as e:
logging.exception("Checking project access to '%s' failed" % auth_info.get_project_id())
return flask.abort(HTTP_ACCESS_DENIED, description="PROJECT_ACCESS_FAILED")
# Authorize, Part 2: Check that there is a OU with the same name as the
# project id. The existence of such a OU implies that the owner of the
# domain is OK with joining machines from that project.
# This check is ignored if the service is configured to use a custom OU.
# For custom OU, users will need to use an external way to make sure rogue/unauthorized
# projects cannot access the service, for example, by using a Shared VPC with Service projects.
computer_ou = None
if "PROJECTS_DN" in os.environ and "CUSTOM_OU_ROOT_DN" in os.environ:
logging.error("Cannot have both PROJECTS_DN and CUSTOM_OU_ROOT_DN environment variables configured. Please make sure only one is present.")
return flask.abort(HTTP_CONFLICT, description="BAD_ROOT_OU_CONFIGURATION")
elif "PROJECTS_DN" in os.environ:
try:
matches = ad_connection.find_ou(__read_required_setting("PROJECTS_DN"), auth_info.get_project_id())
if len(matches) == 0:
logging.error("No OU with name '%s' found in directory" % auth_info.get_project_id())
return flask.abort(HTTP_ACCESS_DENIED, description="MISSING_PROJECT_OU")
elif len(matches) > 1:
logging.error("Found multiple OUs with name '%s' in directory" % auth_info.get_project_id())
return flask.abort(HTTP_ACCESS_DENIED, description="MULTIPLE_PROJECT_OUS")
else:
# There is an OU. That means the request is fine and we also know which
# OU to create a computer account in.
project_ou = matches[0].get_dn()
logging.info("Found OU '%s', authorized (2/2)" % project_ou)
logging.info("Computer will be created in a project OU: '%s'" % project_ou)
computer_ou = project_ou
except Exception as e:
logging.exception("Looking up OU '%s' in Active Directory failed" % auth_info.get_project_id())
return flask.abort(HTTP_INTERNAL_SERVER_ERROR, description="PROJECT_OU_UNKNOWN_ERROR")
# If custom OU is being used, then extract it from the compute instance
elif "CUSTOM_OU_ROOT_DN" in os.environ:
try:
custom_ou = __get_custom_ou_for_computer(ad_connection, gce_instance, auth_info.get_instance_name(), auth_info.get_project_id())
if not custom_ou:
return flask.abort(HTTP_BAD_REQUEST, description="BAD_CUSTOM_OU")
logging.info("Found the OU '%s' that is a descendant of the custom root OU, authorized (2/2)" % custom_ou)
logging.info("Computer will be created in a custom OU: '%s'" % custom_ou)
computer_ou = custom_ou
except Exception:
return flask.abort(HTTP_INTERNAL_SERVER_ERROR, description="UNKNOWN_CUSTOM_OU_ERROR")
else:
logging.error("Could not find PROJECTS_DN nor CUSTOM_OU_ROOT_DN in the environment variables. Failed to configure OU root.")
return flask.abort(HTTP_INTERNAL_SERVER_ERROR, description="BAD_ROOT_OU_CONFIGURATION")
original_computer_name = computer_name
if len(computer_name) > MAX_NETBIOS_COMPUTER_NAME_LENGTH:
# Try to shorten the computer name
computer_name = __shorten_computer_name(computer_name, gce_instance)
logging.info("Computer name was shortened from %s to %s" % (original_computer_name, computer_name))
# The request is now properly authorized, so we are all set to create
# a computer account in the domain.
domain = __read_required_setting("AD_DOMAIN")
try:
computer_upn = "%s$@%s" % (computer_name, domain)
# Create computer and add metadata to trace its connection to the
# GCE VM instance. We also add a temporary UPN to the computer
# so that we can reset its password via Kerberos.
try:
computer_account_dn = ad_connection.add_computer(
computer_ou,
computer_name,
computer_upn,
auth_info.get_project_id(),
auth_info.get_zone(),
auth_info.get_instance_name())
new_computer_account = True
except ad.domain.AlreadyExistsException:
# Computer already exists. If this is the same instance and project name
# then assume this is a re-imaged VM, and continue
computer_account_dn = ("CN=%s,%s" % (computer_name, computer_ou))
try:
computer_accounts = ad_connection.find_computer(computer_account_dn)
computer_account = computer_accounts[0]
# Validate this is the same project and instance name
is_same_computer = (computer_account.get_instance_name() == auth_info.get_instance_name()
and computer_account.get_project_id() == auth_info.get_project_id())
if is_same_computer:
# Account found in AD is in the same project and has
# the same name as the given instance so we can reuse it
logging.info("Account '%s' already exists, reusing" % computer_name)
# We need to add a temporary UPN to the computer
# so that we can reset its password via Kerberos
ad_connection.set_computer_upn(computer_ou, computer_name, computer_upn)
if (computer_account.get_zone() != auth_info.get_zone()):
# The instance we have was created in a different zone
# than then AD account. We need to update the zone attribute.
logging.info("Account '%s' is listed in a different zone (%s). Updating to zone %s"
% (computer_name, computer_account.get_zone(), auth_info.get_zone()))
ad_connection.set_computer_zone(computer_ou, computer_name, auth_info.get_zone())
new_computer_account = False
else:
logging.error("Account '%s' already exists in OU '%s' with different attributes. Current attributes are (instance='%s', project='%s'), and requested attributes are (instance='%s', project='%s')"
% (computer_name, computer_ou, computer_account.get_instance_name(), computer_account.get_project_id(), auth_info.get_instance_name(), auth_info.get_project_id()))
flask.abort(HTTP_CONFLICT, description="SIMILAR_COMPUTER_ACCOUNT_EXISTS_IN_AD")
except ad.domain.NoSuchObjectException as e:
logging.error("Account '%s' from project '%s' already exists, but cannot be found in OU '%s'. It probably belongs to a different project or is configured to use a different OU" %
(computer_name, auth_info.get_project_id(), computer_ou))
flask.abort(HTTP_CONFLICT, description="SIMILAR_COMPUTER_ACCOUNT_EXISTS_IN_AD")
# Check if the instance is part of a Managed Instance Group (MIG)
mig_info = __get_managed_instance_group_for_instance(gce_instance)
mig_name = mig_info.get("name") if mig_info else None
# New instances of MIGs are added to AD groups named after the MIGs.
# Having an AD group with the MIG's computers is useful for managing
# Access control in the domain
if new_computer_account and mig_name:
# Add the computer to an AD group containing all the MIG's computers
# This is only relevant to newly added computers
# as previously added computers were already added to the group
logging.info("Instance '%s' is part of Managed Instance Group '%s'. Account will be added to a matching group"
% (auth_info.get_instance_name(), mig_name))
# Find if the MIG already has an AD group
mig_dn = ("CN=%s,%s" % (mig_name, computer_ou))
try:
mig_ad_group = ad_connection.find_group(mig_dn)
logging.info("AD Group '%s' found. Adding computer '%s' to the group"
%(mig_name, auth_info.get_instance_name()))
except ad.domain.NoSuchObjectException as e:
# Group does not exists, create it.
try:
logging.info("AD Group '%s' not found. Attempting to create it" % (mig_name))
ad_connection.add_group(computer_ou, mig_name, auth_info.get_project_id(), mig_info["zone"], mig_info["region"])
except ad.domain.AlreadyExistsException:
# Two options why group already exists:
# 1. Group was just created by a parallel process adding another computer from the same MIG
# 2. Group by this name already exists in a different project
mig_ad_group = ad_connection.find_group(mig_dn)
if len(mig_ad_group) == 0:
# This error should raise a flag, as AD creates each group with a unique SAM account name, therefore
# we shouldn't get groups with the same ID in other OUs.
logging.error("Failed adding AD Group for MIG '%s' in project '%s'. There is probably a MIG by this name in another OU"
% (mig_name, auth_info.get_project_id()))
flask.abort(HTTP_CONFLICT, "GROUP_ALREADY_EXISTS_IN_AD")
else:
# Group added in the same project, safe to proceed
logging.info("AD Group '%s' found while creating. Assuming it was added by another computer joining in parallel" % (mig_name))
# Add the computer account to group
ad_connection.add_member_to_group(computer_ou, mig_name, computer_account_dn)
# Assign a random password via Kerberos. Using Kerberos instead of
# LDAP avoids having to use Secure LDAP.
kerberos_client = kerberos.password.KerberosPasswordClient(
domain,
ad_connection.get_domain_controller(),
ad_connection.get_domain_controller(),
ad_connection.get_upn_by_samaccountname(ad_connection.get_user()),
__read_ad_password())
set_password_attempt = 0
while True:
set_password_attempt += 1
try:
computer_password = __generate_password()
kerberos_client.set_password(
computer_upn,
computer_password)
break
except kerberos.password.KerberosException as e:
if e.get_error_code() == 1:
# Error is related to the agent (AD user). No point trying again
logging.error("Setting password for '%s' failed. Unrecoverable error" % (computer_upn))
raise e
if set_password_attempt <= PASSWORD_RESET_RETRIES:
# Setting the password might fail, so try again using a new password.
# Password setting is sent by AD to all DCs. Failure can occur in AD
# with multiple DCs, if replication did not yet happen, and some DCs
# are not aware to the new computer account.
logging.warning("Setting password for '%s' failed (attempt #%d), retrying with different password" %
(computer_upn, set_password_attempt))
time.sleep(2)
else:
# Give up
raise e
# Remove the temporary UPN.
ad_connection.remove_computer_upn(computer_ou, computer_name)
logging.info("Created computer account '%s'" % (computer_account_dn))
except werkzeug.exceptions.Conflict as e:
# Re-throw HTTP 409 (Conflict) that is used throughout this try/except,
# to avoid it being replaced by the HTTP 500 for general exceptions
raise e
except Exception as e:
logging.exception("Creating computer account for '%s' in '%s' failed" %
(computer_name, computer_ou))
return flask.abort(HTTP_INTERNAL_SERVER_ERROR, description="UNKNOWN_ERROR_CREATE_COMPUTER_ACCOUNT")
# Return credentials so that the computer can use them to join.
return flask.jsonify(
OriginalComputerName=original_computer_name,
ComputerName=computer_name,
ComputerPassword=computer_password,
OrgUnitPath=computer_ou,
Domain=domain,
DomainController=ad_connection.get_domain_controller())