Skip to content

Commit 518039b

Browse files
committed
* Add support for TLS
* 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
1 parent 85aa4cd commit 518039b

File tree

8 files changed

+84
-12
lines changed

8 files changed

+84
-12
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ COPY ./run.py .
2020
RUN chmod ugo+x ./entrypoint.sh
2121

2222
# Open appropriate ports
23-
EXPOSE 1389
23+
EXPOSE 1389, 1636
2424

2525
# Run the server
2626
ENTRYPOINT ["./entrypoint.sh"]

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ from the `docker` directory.
2424
You can use a Redis server to store generated `uidNumber` and `gidNumber` values in a more persistent way.
2525
To do this, you will need to provide the `--redis-host` and `--redis-port` arguments to `run.py`.
2626

27+
### Configure background refresh [Optional]
28+
29+
By default Apricot will refresh on demand when the data is older than 60 seconds.
30+
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.
31+
32+
This is enabled with the `--background-refresh` flag, which uses the `--refresh-interval=60` parameter as the interval to refresh the ldap database.
33+
34+
### Using TLS [Optional]
35+
36+
You can set up a TLS listener to communicate with encryption enabled over the configured port.
37+
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>`.
38+
2739
## Outputs
2840

2941
This will create an LDAP tree that looks like this:

apricot/apricot_server.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import inspect
22
import sys
3-
from typing import Any, cast
3+
from typing import Any, cast, Optional
44

5-
from twisted.internet import reactor
6-
from twisted.internet.endpoints import serverFromString
5+
from twisted.internet import reactor, task
6+
from twisted.internet.endpoints import serverFromString, quoteStringArgument
77
from twisted.internet.interfaces import IReactorCore, IStreamServerEndpoint
88
from twisted.python import log
99

@@ -21,10 +21,15 @@ def __init__(
2121
domain: str,
2222
port: int,
2323
enable_group_of_groups: bool,
24+
refresh_interval: int,
25+
background_refresh: bool,
2426
*,
2527
debug: bool = False,
2628
redis_host: str | None = None,
2729
redis_port: int | None = None,
30+
tls_port: Optional[int] = None,
31+
tls_certificate: Optional[str] = None,
32+
tls_private_key: Optional[str] = None,
2833
**kwargs: Any,
2934
) -> None:
3035
self.debug = debug
@@ -63,14 +68,29 @@ def __init__(
6368
# Create an LDAPServerFactory
6469
if self.debug:
6570
log.msg("Creating an LDAPServerFactory.")
66-
factory = OAuthLDAPServerFactory(domain, oauth_client, enable_group_of_groups)
71+
factory = OAuthLDAPServerFactory(domain, oauth_client, enable_group_of_groups, refresh_interval, background_refresh)
72+
73+
if background_refresh:
74+
if self.debug:
75+
log.msg(f"Starting background refresh (interval={factory.adaptor.refresh_interval})")
76+
loop = task.LoopingCall(factory.adaptor.refresh)
77+
loop.start(factory.adaptor.refresh_interval)
6778

6879
# Attach a listening endpoint
6980
if self.debug:
70-
log.msg("Attaching a listening endpoint.")
81+
log.msg("Attaching a listening endpoint (plain).")
7182
endpoint: IStreamServerEndpoint = serverFromString(reactor, f"tcp:{port}")
7283
endpoint.listen(factory)
7384

85+
# Attach a listening endpoint
86+
if tls_port:
87+
if not (tls_certificate or tls_private_key):
88+
raise ValueError("No TLS certificate or private key provided. Make sure you provide these parameters or disable TLS by not providing the TLS port")
89+
if self.debug:
90+
log.msg("Attaching a listening endpoint (TLS).")
91+
ssl_endpoint: IStreamServerEndpoint = serverFromString(reactor, f"ssl:{tls_port}:privateKey={quoteStringArgument(tls_private_key)}:certKey={quoteStringArgument(tls_certificate)}")
92+
ssl_endpoint.listen(factory)
93+
7494
# Load the Twisted reactor
7595
self.reactor = cast(IReactorCore, reactor)
7696

apricot/ldap/oauth_ldap_server_factory.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99

1010
class OAuthLDAPServerFactory(ServerFactory):
11-
def __init__(self, domain: str, oauth_client: OAuthClient, enable_group_of_groups: bool):
11+
def __init__(self, domain: str, oauth_client: OAuthClient, enable_group_of_groups: bool, refresh_interval: int,
12+
background_refresh: bool):
1213
"""
1314
Initialise an LDAPServerFactory
1415
1516
@param oauth_client: An OAuth client used to construct the LDAP tree
1617
"""
1718
# Create an LDAP lookup tree
18-
self.adaptor = OAuthLDAPTree(domain, oauth_client, enable_group_of_groups)
19+
self.adaptor = OAuthLDAPTree(domain, oauth_client, enable_group_of_groups, refresh_interval, background_refresh)
1920

2021
def __repr__(self) -> str:
2122
return f"{self.__class__.__name__} using adaptor {self.adaptor}"

apricot/ldap/oauth_ldap_tree.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
class OAuthLDAPTree:
1515

1616
def __init__(
17-
self, domain: str, oauth_client: OAuthClient, enable_group_of_groups: bool, refresh_interval: int = 60
17+
self, domain: str, oauth_client: OAuthClient, enable_group_of_groups: bool, refresh_interval: int = 60,
18+
background_refresh: bool = False
1819
) -> None:
1920
"""
2021
Initialise an OAuthLDAPTree
@@ -30,6 +31,7 @@ def __init__(
3031
self.refresh_interval = refresh_interval
3132
self.root_: OAuthLDAPEntry | None = None
3233
self.enable_group_of_groups = enable_group_of_groups
34+
self.background_refresh = background_refresh
3335

3436
@property
3537
def dn(self) -> DistinguishedName:
@@ -42,9 +44,14 @@ def root(self) -> OAuthLDAPEntry:
4244
4345
@return: An OAuthLDAPEntry for the tree
4446
"""
47+
if not self.background_refresh:
48+
self.refresh()
49+
return self.root_
50+
51+
def refresh(self):
4552
if (
46-
not self.root_
47-
or (time.monotonic() - self.last_update) > self.refresh_interval
53+
not self.root_
54+
or (time.monotonic() - self.last_update) > self.refresh_interval
4855
):
4956
# Update users and groups from the OAuth server
5057
log.msg("Retrieving OAuth data.")
@@ -81,7 +88,6 @@ def root(self) -> OAuthLDAPEntry:
8188
# Set last updated time
8289
log.msg("Finished building LDAP tree.")
8390
self.last_update = time.monotonic()
84-
return self.root_
8591

8692
def __repr__(self) -> str:
8793
return f"{self.__class__.__name__} with backend {self.oauth_client.__class__.__name__}"

docker/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ services:
1414
REDIS_HOST: "redis"
1515
ports:
1616
- "1389:1389"
17+
- "1636:1636"
1718
restart: always
1819

1920
redis:

docker/entrypoint.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,32 @@ if [ -n "${DISABLE_GROUP_OF_GROUPS}" ]; then
4141
EXTRA_OPTS="${EXTRA_OPTS} --disable-group-of-groups"
4242
fi
4343

44+
if [ -n "${BACKGROUND_REFRESH}" ]; then
45+
EXTRA_OPTS="${EXTRA_OPTS} --background-refresh"
46+
fi
47+
48+
if [ -n "${REFRESH_INTERVAL}" ]; then
49+
EXTRA_OPTS="${EXTRA_OPTS} --refresh-interval $REFRESH_INTERVAL"
50+
fi
51+
4452
if [ -n "${ENTRA_TENANT_ID}" ]; then
4553
EXTRA_OPTS="${EXTRA_OPTS} --entra-tenant-id $ENTRA_TENANT_ID"
4654
fi
4755

56+
57+
58+
if [ -n "${TLS_PORT}" ]; then
59+
if [ -z "${TLS_CERTIFICATE}" ]; then
60+
echo "$(date +'%Y-%m-%d %H:%M:%S+0000') [-] TLS_CERTIFICATE environment variable is not set"
61+
exit 1
62+
fi
63+
if [ -z "${TLS_PRIVATE_KEY}" ]; then
64+
echo "$(date +'%Y-%m-%d %H:%M:%S+0000') [-] TLS_PRIVATE_KEY environment variable is not set"
65+
exit 1
66+
fi
67+
EXTRA_OPTS="${EXTRA_OPTS} --tls-port $TLS_PORT --tls-certificate $TLS_CERTIFICATE --tls-private-key $TLS_PRIVATE_KEY"
68+
fi
69+
4870
if [ -n "${REDIS_HOST}" ]; then
4971
if [ -z "${REDIS_PORT}" ]; then
5072
REDIS_PORT="6379"

run.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
parser.add_argument("-d", "--domain", type=str, help="Which domain users belong to.")
1616
parser.add_argument("-i", "--client-id", type=str, help="OAuth client ID.")
1717
parser.add_argument("-p", "--port", type=int, default=1389, help="Port to run on.")
18+
parser.add_argument("--tls-port", type=int, default=None, help="Port to run on with encryption.")
1819
parser.add_argument("-s", "--client-secret", type=str, help="OAuth client secret.")
1920
parser.add_argument("--disable-group-of-groups", action="store_false",
2021
dest="enable_group_of_groups", default=True,
2122
help="Disable creation of group-of-groups.")
2223
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
24+
parser.add_argument("--refresh-interval", type=int, default=60,
25+
help="How often to refresh the database in seconds")
26+
parser.add_argument("--background-refresh", action="store_true", default=False,
27+
help="Refresh in the background instead of as needed per request")
28+
2329
# Options for Microsoft Entra backend
2430
entra_group = parser.add_argument_group("Microsoft Entra")
2531
entra_group.add_argument("-t", "--entra-tenant-id", type=str, help="Microsoft Entra tenant ID.", required=False)
@@ -32,6 +38,10 @@
3238
redis_group = parser.add_argument_group("Redis")
3339
redis_group.add_argument("--redis-host", type=str, help="Host for Redis server.")
3440
redis_group.add_argument("--redis-port", type=int, help="Port for Redis server.")
41+
# Options for TLS
42+
redis_group = parser.add_argument_group("TLS")
43+
redis_group.add_argument("--tls-certificate", type=str, help="Location of TLS certificate (pem).")
44+
redis_group.add_argument("--tls-private-key", type=str, help="Location of TLS private key (pem).")
3545
# Parse arguments
3646
args = parser.parse_args()
3747

0 commit comments

Comments
 (0)