def get_auth_token_gss(account, gsstoken, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via a GSS token. The token lifetime is 1 hour. :param account: Account identifier as a string. :param gsscred: GSS principal@REALM as a string. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :param session: The database session in use. :returns: A dict with token and expires_at entries. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens __delete_expired_tokens_account(account=account, session=session) # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(gsstoken)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, token=token, ip=ip) new_token.save(session=session) return token_dictionary(new_token)
def get_global_account_usage(account, rse_expression, issuer, vo='def'): """ Get the account usage and connect it with (if available) the account limits of the account. :param account: The account to read. :param rse_expression: The rse expression to read (If none, get all). :param issuer: The issuer account. :param vo: The VO to act on. :returns: List of dicts {'rse_id', 'bytes_used', 'files_used', 'bytes_limit'} """ kwargs = {'account': account, 'rse_expression': rse_expression} if not rucio.api.permission.has_permission( issuer=issuer, vo=vo, action='get_global_account_usage', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not list global account usage.' % (issuer)) account = InternalAccount(account, vo=vo) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) return [ api_update_return_dict(d) for d in account_limit_core.get_global_account_usage( account=account, rse_expression=rse_expression) ]
def get_auth_token_x509(account, dn, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via an x509 certificate. The token lifetime is 1 hour. :param account: Account identifier as a string. :param dn: Client certificate distinguished name string, as extracted by Apache/mod_ssl. :param id: The application identifier as a string. :param ipaddr: IP address of the client as a string. :param session: The database session in use. :returns: A models.Token object as saved to the database. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).with_for_update(skip_locked=True).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(dn)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, identity=dn, token=token, ip=ip) new_token.save(session=session) session.expunge(new_token) return new_token
def get_auth_token_saml(account, saml_nameid, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via SAML. The token lifetime is 1 hour. :param account: Account identifier as a string. :param saml_nameid: SAML NameID of the client. :param appid: The application identifier as a string. :param ip: IP address of the client a a string. :param session: The database session in use. :returns: A dict with token and expires_at entries. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens __delete_expired_tokens_account(account=account, session=session) tuid = generate_uuid() # NOQA token = '%(account)s-%(saml_nameid)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, identity=saml_nameid, token=token, ip=ip) new_token.save(session=session) return token_dictionary(new_token)
def get_account_usage(account, rse, issuer): """ Get the account usage and connect it with (if available) the account limits of the account. :param account: The account to read. :param rse: The rse to read (If none, get all). :param issuer: The issuer account. :returns: List of dicts {'rse_id', 'bytes_used', 'files_used', 'bytes_limit'} """ rse_id = None if rse: rse_id = get_rse_id(rse=rse) kwargs = {'account': account, 'rse': rse, 'rse_id': rse_id} if not rucio.api.permission.has_permission( issuer=issuer, action='get_account_usage', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not list account usage.' % (issuer)) account = InternalAccount(account) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) return [ api_update_return_dict(d) for d in account_limit_core.get_account_usage(account=account, rse_id=rse_id) ]
def add_account_identity(identity, type_, account, email, default=False, password=None, session=None): """ Adds a membership association between identity and account. :param identity: The identity key name. For example x509 DN, or a username. :param type_: The type of the authentication (x509, gss, userpass, ssh, saml, oidc). :param account: The account name. :param email: The Email address associated with the identity. :param default: If True, the account should be used by default with the provided identity. :param password: Password if type is userpass. :param session: The database session in use. """ if not account_exists(account, session=session): raise exception.AccountNotFound('Account \'%s\' does not exist.' % account) id_ = session.query(models.Identity).filter_by(identity=identity, identity_type=type_).first() if id_ is None: add_identity(identity=identity, type_=type_, email=email, password=password, session=session) id_ = session.query(models.Identity).filter_by(identity=identity, identity_type=type_).first() iaa = models.IdentityAccountAssociation(identity=id_.identity, identity_type=id_.identity_type, account=account, is_default=default) try: iaa.save(session=session) except IntegrityError as error: if match('.*IntegrityError.*ORA-00001: unique constraint.*violated.*', error.args[0]) \ or match('.*IntegrityError.*UNIQUE constraint failed.*', error.args[0]) \ or match('.*IntegrityError.*1062.*Duplicate entry.*for key.*', error.args[0]) \ or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0]) \ or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0]) \ or match('.*IntegrityError.*columns? .*not unique.*', error.args[0]): raise exception.Duplicate('Identity pair \'%s\',\'%s\' already exists!' % (identity, type_))
def delete_account_limit(account, rse, issuer): """ Delete an account limit.. :param account: The account name. :param rse: The rse name. :param issuer: The issuer account_core. :returns: True if successful; False otherwise. """ rse_id = get_rse_id(rse=rse) kwargs = {'account': account, 'rse': rse, 'rse_id': rse_id} if not rucio.api.permission.has_permission( issuer=issuer, action='delete_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not delete account limits.' % (issuer)) account = InternalAccount(account) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) return account_limit_core.delete_account_limit(account=account, rse_id=rse_id)
def delete_global_account_limit(account, rse_expression, issuer, vo='def'): """ Delete a global account limit.. :param account: The account name. :param rse_expression: The rse expression. :param issuer: The issuer account_core. :param vo: The VO to act on. :returns: True if successful; False otherwise. """ kwargs = {'account': account, 'rse_expression': rse_expression} if not rucio.api.permission.has_permission( issuer=issuer, vo=vo, action='delete_global_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not delete global account limits.' % (issuer)) account = InternalAccount(account, vo=vo) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) return account_limit_core.delete_global_account_limit( account=account, rse_expression=rse_expression)
def get_auth_token_saml(account, saml_nameid, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via SAML. The token lifetime is 1 hour. :param account: Account identifier as a string. :param saml_nameid: SAML NameID of the client. :param appid: The application identifier as a string. :param ip: IP address of the client a a string. :param session: The database session in use. :returns: A models.Token object as saved to the database. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).with_for_update(skip_locked=True).delete() tuid = generate_uuid() # NOQA token = '%(account)s-%(saml_nameid)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, identity=saml_nameid, token=token, ip=ip) new_token.save(session=session) session.expunge(new_token) return new_token
def get_auth_token_x509(account, dn, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via an x509 certificate. The token lifetime is 1 hour. :param account: Account identifier as a string. :param dn: Client certificate distinguished name string, as extracted by Apache/mod_ssl. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :param session: The database session in use. :returns: Authentication token as a variable-length string. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(dn)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, token=token, ip=ip) new_token.save(session=session) return token
def get_ssh_challenge_token(account, appid, ip=None, session=None): """ Prepare a challenge token for subsequent SSH public key authentication. The challenge lifetime is fixed to 10 seconds. :param account: Account identifier as a string. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :returns: A models.Token object as saved to the database. """ # Make sure the account exists if not account_exists(account, session=session): return None # Cryptographically secure random number. # This requires a /dev/urandom like device from the OS rng = random.SystemRandom() crypto_rand = rng.randint(0, sys.maxsize) # give the client 10 seconds max to sign the challenge token expiration = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) expiration_unix = expiration.strftime("%s") challenge_token = 'challenge-%(crypto_rand)s-%(account)s-%(expiration_unix)s' % locals() new_challenge_token = models.Token(account=account, token=challenge_token, ip=ip, expired_at=expiration) new_challenge_token.save(session=session) session.expunge(new_challenge_token) return new_challenge_token
def get_auth_token_gss(account, gsstoken, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via a GSS token. The token lifetime is 1 hour. :param account: Account identifier as a string. :param gsscred: GSS principal@REALM as a string. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :param session: The database session in use. :returns: Authentication token as a variable-length string. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(gsstoken)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, token=token, ip=ip) new_token.save(session=session) return token
def add_account_identity(identity, type, account, email, default=False, password=None, session=None): """ Adds a membership association between identity and account. :param identity: The identity key name. For example x509 DN, or a username. :param type: The type of the authentication (x509, gss, userpass, ssh, saml, oidc). :param account: The account name. :param email: The Email address associated with the identity. :param default: If True, the account should be used by default with the provided identity. :param password: Password if type is userpass. :param session: The database session in use. """ if not account_exists(account, session=session): raise exception.AccountNotFound('Account \'%s\' does not exist.' % account) id = session.query(models.Identity).filter_by(identity=identity, identity_type=type).first() if id is None: add_identity(identity=identity, type=type, email=email, password=password, session=session) id = session.query(models.Identity).filter_by(identity=identity, identity_type=type).first() iaa = models.IdentityAccountAssociation(identity=id.identity, identity_type=id.identity_type, account=account) try: iaa.save(session=session) except IntegrityError: raise exception.Duplicate('Identity pair \'%s\',\'%s\' already exists!' % (identity, type))
def get_auth_token_gss(account, gsstoken, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via a GSS token. The token lifetime is 1 hour. :param account: Account identifier as a string. :param gsscred: GSS principal@REALM as a string. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :param session: The database session in use. :returns: Authentication token as a Python struct .token string .expired_at datetime """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens session.query(models.Token).filter( models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).with_for_update( skip_locked=True).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(gsstoken)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, token=token, ip=ip) new_token.save(session=session) session.expunge(new_token) return new_token
def set_global_account_limit(account, rse_expression, bytes_, issuer, vo='def'): """ Set a global account limit. :param account: The account name. :param rse_expression: The rse expression. :param bytes_: The limit in bytes. :param issuer: The issuer account_core. :param vo: The VO to act on. """ kwargs = { 'account': account, 'rse_expression': rse_expression, 'bytes': bytes_ } if not rucio.api.permission.has_permission( issuer=issuer, vo=vo, action='set_global_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not set account limits.' % (issuer)) account = InternalAccount(account, vo=vo) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) account_limit_core.set_global_account_limit(account=account, rse_expression=rse_expression, bytes_=bytes_)
def set_account_limit(account, rse, bytes, issuer): """ Set an account limit.. :param account: The account name. :param rse: The rse name. :param bytes: The limit in bytes. :param issuer: The issuer account_core. """ rse_id = get_rse_id(rse=rse) kwargs = {'account': account, 'rse': rse, 'rse_id': rse_id, 'bytes': bytes} if not rucio.api.permission.has_permission( issuer=issuer, action='set_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied( 'Account %s can not set account limits.' % (issuer)) account = InternalAccount(account) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound( 'Account %s does not exist' % (account)) account_limit_core.set_account_limit(account=account, rse_id=rse_id, bytes=bytes)
def get_auth_token_x509(account, dn, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via an x509 certificate. The token lifetime is 1 hour. :param account: Account identifier as a string. :param dn: Client certificate distinguished name string, as extracted by Apache/mod_ssl. :param id: The application identifier as a string. :param ipaddr: IP address of the client as a string. :param session: The database session in use. :returns: A dict with token and expires_at entries. """ # Make sure the account exists if not account_exists(account, session=session): return None # remove expired tokens __delete_expired_tokens_account(account=account, session=session) # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(dn)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, identity=dn, token=token, ip=ip) new_token.save(session=session) return token_dictionary(new_token)
def get_auth_token_user_pass(account, username, password, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via username and password. The token lifetime is 1 hour. :param account: Account identifier as a string. :param username: Username as a string. :param password: SHA1 hash of the password as a string. :param appid: The application identifier as a string. :param ip: IP address of the client a a string. :param session: The database session in use. :returns: A models.Token object as saved to the database. """ # Make sure the account exists if not account_exists(account, session=session): return None result = session.query(models.Identity).filter_by( identity=username, identity_type=IdentityType.USERPASS).first() db_salt = result['salt'] db_password = result['password'] salted_password = db_salt + password.encode() if db_password != hashlib.sha256(salted_password).hexdigest(): return None # get account identifier result = session.query(models.IdentityAccountAssociation).filter_by( identity=username, identity_type=IdentityType.USERPASS, account=account).first() db_account = result['account'] # remove expired tokens session.query(models.Token).filter( models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).with_for_update( skip_locked=True).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(username)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=db_account, identity=username, token=token, ip=ip) new_token.save(session=session) session.expunge(new_token) return new_token
def account_exists(account): """ Checks to see if account exists. This procedure does not check it's status. :param account: Name of the account_core. :returns: True if found, otherwise false. """ return account_core.account_exists(account)
def account_exists(account, vo='def'): """ Checks to see if account exists. This procedure does not check it's status. :param account: Name of the account. :param vo: The VO to act on. :returns: True if found, otherwise false. """ account = InternalAccount(account, vo=vo) return account_core.account_exists(account)
def sync_accounts(self, iam_users): for user in iam_users: username = user['userName'] email = user['emails'][0]['value'] if not user['active']: logging.debug( 'Skipped account creation for User {} [not active]'.format( username)) continue # Rucio DB schema restriction if len(username) > 25: logging.debug( 'Skipped account creation for User {} [len(username) > 25]' .format(username)) continue if not account.account_exists(InternalAccount(username)): account.add_account(InternalAccount(username), AccountType.SERVICE, email) logging.debug( 'Created account for User {} ***'.format(username)) # Give account quota for all RSEs for rse_obj in rse.list_rses(): set_local_account_limit(InternalAccount(username), rse_obj['id'], 1000000000000) # Make the user an admin & able to sign URLs try: add_account_attribute(InternalAccount(username), 'admin', 'True') add_account_attribute(InternalAccount(username), 'sign-gcs', 'True') except Exception as e: logging.debug(e) if "groups" in user: for group in user['groups']: group_name = group['display'] if not account.has_account_attribute( InternalAccount(username), group_name): add_account_attribute(InternalAccount(username), group_name, 'True')
def set_account_limit(account, rse, bytes, issuer): """ Set an account limit.. :param account: The account name. :param rse: The rse name. :param bytes: The limit in bytes. :param issuer: The issuer account_core. """ kwargs = {'account': account, 'rse': rse, 'bytes': bytes} if not rucio.api.permission.has_permission(issuer=issuer, action='set_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied('Account %s can not set account limits.' % (issuer)) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound('Account %s does not exist' % (account)) rse_id = get_rse_id(rse=rse) account_limit_core.set_account_limit(account=account, rse_id=rse_id, bytes=bytes)
def delete_account_limit(account, rse, issuer): """ Delete an account limit.. :param account: The account name. :param rse: The rse name. :param issuer: The issuer account_core. :returns: True if successful; False otherwise. """ kwargs = {'account': account, 'rse': rse} if not rucio.api.permission.has_permission(issuer=issuer, action='delete_account_limit', kwargs=kwargs): raise rucio.common.exception.AccessDenied('Account %s can not delete account limits.' % (issuer)) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound('Account %s does not exist' % (account)) rse_id = get_rse_id(rse=rse) return account_limit_core.delete_account_limit(account=account, rse_id=rse_id)
def get_auth_token_user_pass(account, username, password, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via username and password. The token lifetime is 1 hour. :param account: Account identifier as a string. :param username: Username as a string. :param password: SHA1 hash of the password as a string. :param appid: The application identifier as a string. :param ip: IP address of the client a a string. :param session: The database session in use. :returns: Authentication token as a variable-length string. """ # Make sure the account exists if not account_exists(account, session=session): return None result = session.query(models.Identity).filter_by(identity=username, identity_type=IdentityType.USERPASS).first() db_salt = result['salt'] db_password = result['password'] if db_password != hashlib.sha256('%s%s' % (db_salt, password)).hexdigest(): return None # get account identifier result = session.query(models.IdentityAccountAssociation).filter_by(identity=username, identity_type=IdentityType.USERPASS, account=account).first() db_account = result['account'] # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-%(username)s-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=db_account, token=token, ip=ip) new_token.save(session=session) return token
def get_account_usage(account, rse, issuer): """ Get the account usage and connect it with (if available) the account limits of the account. :param account: The account to read. :param rse: The rse to read (If none, get all). :param issuer: The issuer account. :returns: List of dicts {'rse_id', 'bytes_used', 'files_used', 'bytes_limit'} """ kwargs = {'account': account, 'rse': rse} if not rucio.api.permission.has_permission(issuer=issuer, action='get_account_usage', kwargs=kwargs): raise rucio.common.exception.AccessDenied('Account %s can not list account usage.' % (issuer)) if not account_exists(account=account): raise rucio.common.exception.AccountNotFound('Account %s does not exist' % (account)) rse_id = None if rse: rse_id = get_rse_id(rse=rse) return account_limit_core.get_account_usage(account=account, rse_id=rse_id)
def set_local_account_limit(account, rse, bytes_, issuer, vo='def', session=None): """ Set an account limit.. :param account: The account name. :param rse: The rse name. :param bytes_: The limit in bytes. :param issuer: The issuer account_core. :param vo: The VO to act on. :param session: The database session in use. """ rse_id = get_rse_id(rse=rse, vo=vo, session=session) kwargs = {'account': account, 'rse': rse, 'rse_id': rse_id, 'bytes': bytes_} if not rucio.api.permission.has_permission(issuer=issuer, vo=vo, action='set_local_account_limit', kwargs=kwargs, session=session): raise rucio.common.exception.AccessDenied('Account %s can not set account limits.' % (issuer)) account = InternalAccount(account, vo=vo) if not account_exists(account=account, session=session): raise rucio.common.exception.AccountNotFound('Account %s does not exist' % (account)) account_limit_core.set_local_account_limit(account=account, rse_id=rse_id, bytes_=bytes_, session=session)
def delete_local_account_limit(account, rse, issuer, vo='def', session=None): """ Delete an account limit.. :param account: The account name. :param rse: The rse name. :param issuer: The issuer account_core. :param vo: The VO to act on. :param session: The database session in use. :returns: True if successful; False otherwise. """ rse_id = get_rse_id(rse=rse, vo=vo, session=session) kwargs = {'account': account, 'rse': rse, 'rse_id': rse_id} if not rucio.api.permission.has_permission(issuer=issuer, vo=vo, action='delete_local_account_limit', kwargs=kwargs, session=session): raise rucio.common.exception.AccessDenied('Account %s can not delete account limits.' % (issuer)) account = InternalAccount(account, vo=vo) if not account_exists(account=account, session=session): raise rucio.common.exception.AccountNotFound('Account %s does not exist' % (account)) return account_limit_core.delete_local_account_limit(account=account, rse_id=rse_id, session=session)
def get_auth_token_ssh(account, signature, appid, ip=None, session=None): """ Authenticate a Rucio account temporarily via SSH key exchange. The token lifetime is 1 hour. :param account: Account identifier as a string. :param signature: Response to server challenge signed with SSH private key as string. :param appid: The application identifier as a string. :param ip: IP address of the client as a string. :param session: The database session in use. :returns: A models.Token object as saved to the database. """ if not isinstance(signature, bytes): signature = signature.encode() # Make sure the account exists if not account_exists(account, session=session): return None # get all active challenge tokens for the requested account active_challenge_tokens = session.query(models.Token).filter(models.Token.expired_at >= datetime.datetime.utcnow(), models.Token.account == account, models.Token.token.like('challenge-%')).all() # get all identities for the requested account identities = session.query(models.IdentityAccountAssociation).filter_by(identity_type=IdentityType.SSH, account=account).all() # no challenge tokens found if not active_challenge_tokens: return None # try all available SSH identities for the account with the provided signature match = False for identity in identities: pub_k = paramiko.RSAKey(data=b64decode(identity['identity'].split()[1])) for challenge_token in active_challenge_tokens: if pub_k.verify_ssh_sig(str(challenge_token['token']).encode(), paramiko.Message(signature)): match = True break if match: break if not match: return None # remove expired tokens session.query(models.Token).filter(models.Token.expired_at < datetime.datetime.utcnow(), models.Token.account == account).with_for_update(skip_locked=True).delete() # create new rucio-auth-token for account tuid = generate_uuid() # NOQA token = '%(account)s-ssh:pubkey-%(appid)s-%(tuid)s' % locals() new_token = models.Token(account=account, token=token, ip=ip) new_token.save(session=session) session.expunge(new_token) return new_token