Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bad99e3
Generate command added
ukumar-ks Nov 12, 2025
43c5a10
Fix asyncio event loop cleanup with SSO auth (#92)
m-k8s Nov 13, 2025
1bede62
push notification cleanup
sk-keeper Nov 14, 2025
b587b88
team approve command added
adeshmukh-ks Nov 14, 2025
2883938
reset password command added
adeshmukh-ks Nov 14, 2025
af5e6ac
verify-records and verify-shared-folder commands added
ukumar-ks Nov 19, 2025
935c9e7
Bug fixes
adeshmukh-ks Nov 19, 2025
a7976d0
KeeperParam and Proto update changes
adeshmukh-ks Nov 20, 2025
7a9859d
Added persistent-login example
adeshmukh-ks Dec 1, 2025
2f5d9a6
changes
adeshmukh-ks Dec 1, 2025
c3e8536
Python SDK bugfixes
ukumar-ks Dec 8, 2025
8271215
Sdk examples Implemented
ukumar-ks Dec 9, 2025
3e3fa92
Record and Folder Share Functionality added to SDK
adeshmukh-ks Dec 10, 2025
ab03090
Corrected constant strings
adeshmukh-ks Dec 11, 2025
a8a4459
Bug Fix and User-Role and Team-Role correlations added
ukumar-ks Dec 16, 2025
2cf735d
Ksm command migration
adeshmukh-ks Dec 18, 2025
e1a9b0b
User report command added
ukumar-ks Dec 18, 2025
896b715
Bugfix SDK - 232 and 233
ukumar-ks Dec 22, 2025
5153b97
Pam gateway commands added
adeshmukh-ks Dec 30, 2025
5a405cc
Security audit report command added
ukumar-ks Jan 5, 2026
6f31469
share-report and share-records-report command added
adeshmukh-ks Jan 9, 2026
c9a960c
PEDM: approval extend
sk-keeper Dec 26, 2025
8ba5f65
Recover from invalid device error
sk-keeper Jan 11, 2026
b94cac2
KEPM: expiration parser
sk-keeper Jan 11, 2026
5906516
Add "certificate_check" property
sk-keeper Jan 11, 2026
1f0538a
KEPM: last seen
sk-keeper Jan 11, 2026
e705305
Aging report and action-report commands added
ukumar-ks Jan 15, 2026
26f42c5
move command fix
sk-keeper Jan 16, 2026
9a1b302
2fa commands added
adeshmukh-ks Jan 19, 2026
3466bb1
enterprise-push command added
adeshmukh-ks Feb 16, 2026
1e046e1
Compliance commands added
adeshmukh-ks Feb 20, 2026
b724808
breachwatch report command added
adeshmukh-ks Feb 20, 2026
b4cef45
External shares report command added
ukumar-ks Feb 23, 2026
0fb6c46
Added SSO MFA login support in example and fixed import errors in yub…
adeshmukh-ks Feb 27, 2026
b3e4cb3
Added persistent flow to example (#144)
adeshmukh-ks Feb 27, 2026
1103a73
SDK release 1.1.0
sk-keeper Feb 27, 2026
0ba6bcc
Fix unite tests
sk-keeper Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 122 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ The Keeper SDK uses a configuration storage system to manage authentication sett

#### **Requirement for client**

If you are accessing keepersdk from a new device, you need to ensure that there is a config.json file present from which the sdk reads credentials. This ensures that the client doesn't contain any hardcoded credentials. Create the .json file in .keeper folder of current user, you might need to create a .keeper folder. A sample showing the structure of the config.json needed is shown below:
If you are accessing keepersdk from a new device, you need to ensure that there is a config.json file present from which the sdk reads credentials. This ensures that the client doesn't contain any hardcoded credentials. Create the .json file in .keeper folder of current user, you might need to create a .keeper folder.

Alternatively you can run the sample login script to give username and password during execution as an alternate to keeping it stored. This will turn on persistent login and would not require re-login for the timeout duration.

A sample showing the structure of the config.json needed is shown below:

```
{
Expand Down Expand Up @@ -170,25 +174,128 @@ If you are accessing keepersdk from a new device, you need to ensure that there
}
```

### SDK Persistent Login Flow
The persistent login flow allows you to authenticate once and remain logged in for a specified timeout period without requiring session refresh. This is particularly useful for automated scripts and long-running applications.

**Key Features:**
- **One-time setup**: Configure persistent login on a new device with a single execution
- **Automatic session management**: No need to re-authenticate during the timeout period
- **Configurable timeout**: Default is 30 days, but can be customized
- **Device registration**: Registers the device's data key for secure authentication

**When to Use:**
- Automated scripts and background services
- Long-running applications that need continuous access
- Development and testing environments
- Applications where user interaction is not always possible

**Important Notes:**
- Persistent login must be enabled on first-time device setup
- The device data key registration is a one-time operation per device
- Enterprise policies may restrict persistent login usage
- Always follow your organization's security policies when using persistent login

**Example: Setting Up Persistent Login**

This example demonstrates how to enable persistent login on a new device. Run this script once to configure persistent login for subsequent sessions:

```python
import getpass

from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth
from keepersdk.constants import KEEPER_PUBLIC_HOSTS

# Initialize configuration and authentication context
config = configuration.JsonConfigurationStorage()
if not config.get().last_server:
print("Available server options:")
for region, host in KEEPER_PUBLIC_HOSTS.items():
print(f" {region}: {host}")
server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com'

config.get().last_server = server
else:
server = config.get().last_server
keeper_endpoint = endpoint.KeeperEndpoint(config, server)
login_auth_context = login_auth.LoginAuth(keeper_endpoint)

# Authenticate user
username = None
if config.get().last_login:
username = config.get().last_login
if not username:
username = input('Enter username: ')
login_auth_context.resume_session = True
login_auth_context.login(username)

while not login_auth_context.login_step.is_final():
if isinstance(login_auth_context.login_step, login_auth.LoginStepDeviceApproval):
login_auth_context.login_step.send_push(login_auth.DeviceApprovalChannel.KeeperPush)
print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume")
input()
elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword):
password = getpass.getpass('Enter password: ')
login_auth_context.login_step.verify_password(password)
elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor):
channel = login_auth_context.login_step.get_channels()[0]
code = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')
login_auth_context.login_step.send_code(channel.channel_uid, code)
else:
raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}")

# Check if login was successful
if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected):
# Obtain authenticated session
keeper_auth_context = login_auth_context.login_step.take_keeper_auth()

# Enable persistent login and register data key for device for the first time
keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1')
keeper_auth.register_data_key_for_device(keeper_auth_context)
mins_per_day = 60*24
timeout_in_minutes = mins_per_day*30 # 30 days
keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes))

print("Persistent login turned on successfully and device registered")

keeper_auth_context.close()
```

### SDK Usage Example

Below is a complete example demonstrating authentication, vault synchronization, and record retrieval:

```python
import sqlite3
import getpass
import sqlite3

from keepersdk.authentication import login_auth, configuration, endpoint
from keepersdk.constants import KEEPER_PUBLIC_HOSTS
from keepersdk.vault import sqlite_storage, vault_online, vault_record

# Initialize configuration and authentication context
config = configuration.JsonConfigurationStorage()
keeper_endpoint = endpoint.KeeperEndpoint(config)
if not config.get().last_server:
print("Available server options:")
for region, host in KEEPER_PUBLIC_HOSTS.items():
print(f" {region}: {host}")
server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com'

config.get().last_server = server
else:
server = config.get().last_server
keeper_endpoint = endpoint.KeeperEndpoint(config, server)
login_auth_context = login_auth.LoginAuth(keeper_endpoint)

# Authenticate user
login_auth_context.login(config.get().users()[0].username, config.get().users()[0].password)

username = None
if config.get().last_login:
username = config.get().last_login
if not username:
username = input('Enter username: ')
login_auth_context.resume_session = True
login_auth_context.login(username)

logged_in_with_persistent = True
while not login_auth_context.login_step.is_final():
if isinstance(login_auth_context.login_step, login_auth.LoginStepDeviceApproval):
login_auth_context.login_step.send_push(login_auth.DeviceApprovalChannel.KeeperPush)
Expand All @@ -202,24 +309,28 @@ while not login_auth_context.login_step.is_final():
code = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')
login_auth_context.login_step.send_code(channel.channel_uid, code)
else:
raise NotImplementedError()
raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}")
logged_in_with_persistent = False

if logged_in_with_persistent:
print("Succesfully logged in with persistent login")

# Check if login was successful
if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected):
# Obtain authenticated session
keeper_auth = login_auth_context.login_step.take_keeper_auth()
keeper_auth_context = login_auth_context.login_step.take_keeper_auth()

# Set up vault storage (using SQLite in-memory database)
conn = sqlite3.Connection('file::memory:', uri=True)
vault_storage = sqlite_storage.SqliteVaultStorage(
lambda: conn,
vault_owner=bytes(keeper_auth.auth_context.username, 'utf-8')
vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8')
)

# Initialize vault and synchronize with Keeper servers
vault = vault_online.VaultOnline(keeper_auth, vault_storage)
vault = vault_online.VaultOnline(keeper_auth_context, vault_storage)
vault.sync_down()

# Access and display vault records
print("Vault Records:")
print("-" * 50)
Expand All @@ -239,7 +350,7 @@ if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected):

print("-" * 50)
vault.close()
keeper_auth.close()
keeper_auth_context.close()
```

**Important Security Notes:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,31 @@
import logging

from keepercli.commands.audit_alert import AuditAlertAdd
from keepercli.params import KeeperParams
from keepercli.params import KeeperParams, KeeperConfig
from keepercli.login import LoginFlow


logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)


def get_default_config_path() -> str:
"""
Get the default config file path following the same logic as JsonFileLoader.

First checks if 'config.json' exists in the current directory.
If not, uses ~/.keeper/config.json.
"""
file_name = 'config.json'
if os.path.isfile(file_name):
return os.path.abspath(file_name)
else:
keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper')
if not os.path.exists(keeper_dir):
os.mkdir(keeper_dir)
return os.path.join(keeper_dir, file_name)


def login_to_keeper_with_config(filename: str) -> KeeperParams:
"""
Login to Keeper with a configuration file.
Expand All @@ -38,20 +57,18 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams:
raise FileNotFoundError(f'Config file {filename} not found')
with open(filename, 'r') as f:
config_data = json.load(f)
username = config_data.get('user', config_data.get('username'))
password = config_data.get('password', '')
if not username:
raise ValueError('Username not found in config file')
context = KeeperParams(config_filename=filename, config=config_data)
if username:
context.username = username
if password:
context.password = password
logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username))
if not logged_in:

keeper_config = KeeperConfig(config_filename=filename, config=config_data)
auth = LoginFlow.login(keeper_config)
if not auth:
raise Exception('Failed to authenticate with Keeper')

context = KeeperParams(keeper_config=keeper_config)
context.set_auth(auth)

return context


def execute_audit_alert_add(context: KeeperParams, **kwargs):
"""
Execute audit alert add command.
Expand All @@ -66,6 +83,7 @@ def execute_audit_alert_add(context: KeeperParams, **kwargs):
except Exception as e:
raise Exception(f'Error: {str(e)}')


if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Add new audit alert using Keeper SDK',
Expand All @@ -76,10 +94,11 @@ def execute_audit_alert_add(context: KeeperParams, **kwargs):
'''
)

default_config_path = get_default_config_path()
parser.add_argument(
'-c', '--config',
default='myconfig.json',
help='Configuration file (default: myconfig.json)'
default=default_config_path,
help=f'Configuration file (default: {default_config_path})'
)

args = parser.parse_args()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,31 @@
import logging

from keepercli.commands.audit_alert import AuditAlertDelete
from keepercli.params import KeeperParams
from keepercli.params import KeeperParams, KeeperConfig
from keepercli.login import LoginFlow


logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)


def get_default_config_path() -> str:
"""
Get the default config file path following the same logic as JsonFileLoader.

First checks if 'config.json' exists in the current directory.
If not, uses ~/.keeper/config.json.
"""
file_name = 'config.json'
if os.path.isfile(file_name):
return os.path.abspath(file_name)
else:
keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper')
if not os.path.exists(keeper_dir):
os.mkdir(keeper_dir)
return os.path.join(keeper_dir, file_name)


def login_to_keeper_with_config(filename: str) -> KeeperParams:
"""
Login to Keeper with a configuration file.
Expand All @@ -38,18 +57,15 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams:
raise FileNotFoundError(f'Config file {filename} not found')
with open(filename, 'r') as f:
config_data = json.load(f)
username = config_data.get('user', config_data.get('username'))
password = config_data.get('password', '')
if not username:
raise ValueError('Username not found in config file')
context = KeeperParams(config_filename=filename, config=config_data)
if username:
context.username = username
if password:
context.password = password
logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username))
if not logged_in:

keeper_config = KeeperConfig(config_filename=filename, config=config_data)
auth = LoginFlow.login(keeper_config)
if not auth:
raise Exception('Failed to authenticate with Keeper')

context = KeeperParams(keeper_config=keeper_config)
context.set_auth(auth)

return context

def execute_audit_alert_delete(context: KeeperParams, **kwargs):
Expand All @@ -66,6 +82,7 @@ def execute_audit_alert_delete(context: KeeperParams, **kwargs):
except Exception as e:
raise Exception(f'Error: {str(e)}')


if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Delete audit alert using Keeper SDK',
Expand All @@ -76,10 +93,11 @@ def execute_audit_alert_delete(context: KeeperParams, **kwargs):
'''
)

default_config_path = get_default_config_path()
parser.add_argument(
'-c', '--config',
default='myconfig.json',
help='Configuration file (default: myconfig.json)'
default=default_config_path,
help=f'Configuration file (default: {default_config_path})'
)

args = parser.parse_args()
Expand Down
Loading
Loading