Skip to content

Commit

Permalink
* Add support for TLS
Browse files Browse the repository at this point in the history
* Add support for refreshing the database periodically in the background
* Add support to set refresh interval used for periodical refresh and for how often it should be refreshed on demand
  • Loading branch information
BlackVoid authored and jemrobinson committed May 30, 2024
1 parent ff7ed85 commit e7e3cdb
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ COPY ./run.py .
RUN chmod ugo+x ./entrypoint.sh

# Open appropriate ports
EXPOSE 1389
EXPOSE 1389, 1636

# Run the server
ENTRYPOINT ["./entrypoint.sh"]
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ from the `docker` directory.
You can use a Redis server to store generated `uidNumber` and `gidNumber` values in a more persistent way.
To do this, you will need to provide the `--redis-host` and `--redis-port` arguments to `run.py`.

### Configure background refresh [Optional]

By default Apricot will refresh on demand when the data is older than 60 seconds.
If it takes a long time to fetch all users and groups, or you want to ensure that each request prompty gets a respose, you may want to configure background refresh to have it periodically be refreshed in the background.

This is enabled with the `--background-refresh` flag, which uses the `--refresh-interval=60` parameter as the interval to refresh the ldap database.

### Using TLS [Optional]

You can set up a TLS listener to communicate with encryption enabled over the configured port.
To enable it you need to configure the tls port ex. `--tls-port=1636`, and provide a path to the pem files for the certificate `--tls-certificate=<path>` and the private key `--tls-private-key=<path>`.

## Outputs

This will create an LDAP tree that looks like this:
Expand Down
30 changes: 25 additions & 5 deletions apricot/apricot_server.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import inspect
import sys
from typing import Any, cast
from typing import Any, cast, Optional

from twisted.internet import reactor
from twisted.internet.endpoints import serverFromString
from twisted.internet import reactor, task
from twisted.internet.endpoints import serverFromString, quoteStringArgument
from twisted.internet.interfaces import IReactorCore, IStreamServerEndpoint
from twisted.python import log

Expand All @@ -21,10 +21,15 @@ def __init__(
domain: str,
port: int,
*,
background_refresh: bool = False,
debug: bool = False,
enable_mirrored_groups: bool = True,
redis_host: str | None = None,
redis_port: int | None = None,
refresh_interval: int = 60,
tls_port: Optional[int] = None,
tls_certificate: Optional[str] = None,
tls_private_key: Optional[str] = None,
**kwargs: Any,
) -> None:
self.debug = debug
Expand Down Expand Up @@ -66,15 +71,30 @@ def __init__(
if self.debug:
log.msg("Creating an LDAPServerFactory.")
factory = OAuthLDAPServerFactory(
domain, oauth_client, enable_mirrored_groups=enable_mirrored_groups
domain, oauth_client, background_refresh=background_refresh, enable_mirrored_groups=enable_mirrored_groups, refresh_interval=refresh_interval
)

if background_refresh:
if self.debug:
log.msg(f"Starting background refresh (interval={factory.adaptor.refresh_interval})")
loop = task.LoopingCall(factory.adaptor.refresh)
loop.start(factory.adaptor.refresh_interval)

# Attach a listening endpoint
if self.debug:
log.msg("Attaching a listening endpoint.")
log.msg("Attaching a listening endpoint (plain).")
endpoint: IStreamServerEndpoint = serverFromString(reactor, f"tcp:{port}")
endpoint.listen(factory)

# Attach a listening endpoint
if tls_port:
if not (tls_certificate or tls_private_key):
raise ValueError("No TLS certificate or private key provided. Make sure you provide these parameters or disable TLS by not providing the TLS port")
if self.debug:
log.msg("Attaching a listening endpoint (TLS).")
ssl_endpoint: IStreamServerEndpoint = serverFromString(reactor, f"ssl:{tls_port}:privateKey={quoteStringArgument(tls_private_key)}:certKey={quoteStringArgument(tls_certificate)}")
ssl_endpoint.listen(factory)

# Load the Twisted reactor
self.reactor = cast(IReactorCore, reactor)

Expand Down
4 changes: 2 additions & 2 deletions apricot/ldap/oauth_ldap_server_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class OAuthLDAPServerFactory(ServerFactory):
def __init__(
self, domain: str, oauth_client: OAuthClient, *, enable_mirrored_groups: bool
self, domain: str, oauth_client: OAuthClient, *, background_refresh: bool, enable_mirrored_groups: bool, refresh_interval: int,
):
"""
Initialise an LDAPServerFactory
Expand All @@ -18,7 +18,7 @@ def __init__(
"""
# Create an LDAP lookup tree
self.adaptor = OAuthLDAPTree(
domain, oauth_client, enable_mirrored_groups=enable_mirrored_groups
domain, oauth_client, background_refresh=background_refresh, enable_mirrored_groups=enable_mirrored_groups, refresh_interval=refresh_interval
)

def __repr__(self) -> str:
Expand Down
16 changes: 11 additions & 5 deletions apricot/ldap/oauth_ldap_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ def __init__(
domain: str,
oauth_client: OAuthClient,
*,
background_refresh: bool,
enable_mirrored_groups: bool,
refresh_interval: int = 60,
refresh_interval,
) -> None:
"""
Initialise an OAuthLDAPTree
Expand All @@ -28,13 +29,14 @@ def __init__(
@param oauth_client: An OAuth client used to construct the LDAP tree
@param refresh_interval: Interval in seconds after which the tree must be refreshed
"""
self.background_refresh = background_refresh
self.debug = oauth_client.debug
self.domain = domain
self.enable_mirrored_groups = enable_mirrored_groups
self.last_update = time.monotonic()
self.oauth_client = oauth_client
self.refresh_interval = refresh_interval
self.root_: OAuthLDAPEntry | None = None
self.enable_mirrored_groups = enable_mirrored_groups

@property
def dn(self) -> DistinguishedName:
Expand All @@ -47,9 +49,14 @@ def root(self) -> OAuthLDAPEntry:
@return: An OAuthLDAPEntry for the tree
"""
if not self.background_refresh:
self.refresh()
return self.root_

def refresh(self):
if (
not self.root_
or (time.monotonic() - self.last_update) > self.refresh_interval
not self.root_
or (time.monotonic() - self.last_update) > self.refresh_interval
):
# Update users and groups from the OAuth server
log.msg("Retrieving OAuth data.")
Expand Down Expand Up @@ -104,7 +111,6 @@ def root(self) -> OAuthLDAPEntry:
# Set last updated time
log.msg("Finished building LDAP tree.")
self.last_update = time.monotonic()
return self.root_

def __repr__(self) -> str:
return f"{self.__class__.__name__} with backend {self.oauth_client.__class__.__name__}"
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ services:
REDIS_HOST: "redis"
ports:
- "1389:1389"
- "1636:1636"
restart: always

redis:
Expand Down
22 changes: 22 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,32 @@ if [ -n "${DISABLE_MIRRORED_GROUPS}" ]; then
EXTRA_OPTS="${EXTRA_OPTS} --disable-mirrored-groups"
fi

if [ -n "${BACKGROUND_REFRESH}" ]; then
EXTRA_OPTS="${EXTRA_OPTS} --background-refresh"
fi

if [ -n "${REFRESH_INTERVAL}" ]; then
EXTRA_OPTS="${EXTRA_OPTS} --refresh-interval $REFRESH_INTERVAL"
fi

if [ -n "${ENTRA_TENANT_ID}" ]; then
EXTRA_OPTS="${EXTRA_OPTS} --entra-tenant-id $ENTRA_TENANT_ID"
fi



if [ -n "${TLS_PORT}" ]; then
if [ -z "${TLS_CERTIFICATE}" ]; then
echo "$(date +'%Y-%m-%d %H:%M:%S+0000') [-] TLS_CERTIFICATE environment variable is not set"
exit 1
fi
if [ -z "${TLS_PRIVATE_KEY}" ]; then
echo "$(date +'%Y-%m-%d %H:%M:%S+0000') [-] TLS_PRIVATE_KEY environment variable is not set"
exit 1
fi
EXTRA_OPTS="${EXTRA_OPTS} --tls-port $TLS_PORT --tls-certificate $TLS_CERTIFICATE --tls-private-key $TLS_PRIVATE_KEY"
fi

if [ -n "${REDIS_HOST}" ]; then
if [ -z "${REDIS_PORT}" ]; then
REDIS_PORT="6379"
Expand Down
12 changes: 11 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
parser.add_argument("-i", "--client-id", type=str, help="OAuth client ID.")
parser.add_argument("-p", "--port", type=int, default=1389, help="Port to run on.")
parser.add_argument("-s", "--client-secret", type=str, help="OAuth client secret.")
parser.add_argument("--background-refresh", action="store_true", default=False,
help="Refresh in the background instead of as needed per request")
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
parser.add_argument("--disable-mirrored-groups", action="store_false",
dest="enable_mirrored_groups", default=True,
help="Disable creation of mirrored groups.")
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
parser.add_argument("--refresh-interval", type=int, default=60,
help="How often to refresh the database in seconds")

# Options for Microsoft Entra backend
entra_group = parser.add_argument_group("Microsoft Entra")
entra_group.add_argument("-t", "--entra-tenant-id", type=str, help="Microsoft Entra tenant ID.", required=False)
Expand All @@ -32,6 +37,11 @@
redis_group = parser.add_argument_group("Redis")
redis_group.add_argument("--redis-host", type=str, help="Host for Redis server.")
redis_group.add_argument("--redis-port", type=int, help="Port for Redis server.")
# Options for TLS
tls_group = parser.add_argument_group("TLS")
tls_group.add_argument("--tls-certificate", type=str, help="Location of TLS certificate (pem).")
tls_group.add_argument("--tls-port", type=int, default=1636, help="Port to run on with encryption.")
tls_group.add_argument("--tls-private-key", type=str, help="Location of TLS private key (pem).")
# Parse arguments
args = parser.parse_args()

Expand Down

0 comments on commit e7e3cdb

Please sign in to comment.