Skip to content

Commit baaa864

Browse files
BlackVoidjemrobinson
authored andcommitted
* 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 4a034d9 commit baaa864

File tree

8 files changed

+85
-14
lines changed

8 files changed

+85
-14
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
*,
24+
background_refresh: bool = False,
2425
debug: bool = False,
2526
enable_mirrored_groups: bool = True,
2627
redis_host: str | None = None,
2728
redis_port: int | None = None,
29+
refresh_interval: int = 60,
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
@@ -66,15 +71,30 @@ def __init__(
6671
if self.debug:
6772
log.msg("Creating an LDAPServerFactory.")
6873
factory = OAuthLDAPServerFactory(
69-
domain, oauth_client, enable_mirrored_groups=enable_mirrored_groups
74+
domain, oauth_client, background_refresh=background_refresh, enable_mirrored_groups=enable_mirrored_groups, refresh_interval=refresh_interval
7075
)
7176

77+
if background_refresh:
78+
if self.debug:
79+
log.msg(f"Starting background refresh (interval={factory.adaptor.refresh_interval})")
80+
loop = task.LoopingCall(factory.adaptor.refresh)
81+
loop.start(factory.adaptor.refresh_interval)
82+
7283
# Attach a listening endpoint
7384
if self.debug:
74-
log.msg("Attaching a listening endpoint.")
85+
log.msg("Attaching a listening endpoint (plain).")
7586
endpoint: IStreamServerEndpoint = serverFromString(reactor, f"tcp:{port}")
7687
endpoint.listen(factory)
7788

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

apricot/ldap/oauth_ldap_server_factory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class OAuthLDAPServerFactory(ServerFactory):
1111
def __init__(
12-
self, domain: str, oauth_client: OAuthClient, *, enable_mirrored_groups: bool
12+
self, domain: str, oauth_client: OAuthClient, *, background_refresh: bool, enable_mirrored_groups: bool, refresh_interval: int,
1313
):
1414
"""
1515
Initialise an LDAPServerFactory
@@ -18,7 +18,7 @@ def __init__(
1818
"""
1919
# Create an LDAP lookup tree
2020
self.adaptor = OAuthLDAPTree(
21-
domain, oauth_client, enable_mirrored_groups=enable_mirrored_groups
21+
domain, oauth_client, background_refresh=background_refresh, enable_mirrored_groups=enable_mirrored_groups, refresh_interval=refresh_interval
2222
)
2323

2424
def __repr__(self) -> str:

apricot/ldap/oauth_ldap_tree.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ def __init__(
1818
domain: str,
1919
oauth_client: OAuthClient,
2020
*,
21+
background_refresh: bool,
2122
enable_mirrored_groups: bool,
22-
refresh_interval: int = 60,
23+
refresh_interval,
2324
) -> None:
2425
"""
2526
Initialise an OAuthLDAPTree
@@ -28,13 +29,14 @@ def __init__(
2829
@param oauth_client: An OAuth client used to construct the LDAP tree
2930
@param refresh_interval: Interval in seconds after which the tree must be refreshed
3031
"""
32+
self.background_refresh = background_refresh
3133
self.debug = oauth_client.debug
3234
self.domain = domain
35+
self.enable_mirrored_groups = enable_mirrored_groups
3336
self.last_update = time.monotonic()
3437
self.oauth_client = oauth_client
3538
self.refresh_interval = refresh_interval
3639
self.root_: OAuthLDAPEntry | None = None
37-
self.enable_mirrored_groups = enable_mirrored_groups
3840

3941
@property
4042
def dn(self) -> DistinguishedName:
@@ -47,9 +49,14 @@ def root(self) -> OAuthLDAPEntry:
4749
4850
@return: An OAuthLDAPEntry for the tree
4951
"""
52+
if not self.background_refresh:
53+
self.refresh()
54+
return self.root_
55+
56+
def refresh(self):
5057
if (
51-
not self.root_
52-
or (time.monotonic() - self.last_update) > self.refresh_interval
58+
not self.root_
59+
or (time.monotonic() - self.last_update) > self.refresh_interval
5360
):
5461
# Update users and groups from the OAuth server
5562
log.msg("Retrieving OAuth data.")
@@ -104,7 +111,6 @@ def root(self) -> OAuthLDAPEntry:
104111
# Set last updated time
105112
log.msg("Finished building LDAP tree.")
106113
self.last_update = time.monotonic()
107-
return self.root_
108114

109115
def __repr__(self) -> str:
110116
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_MIRRORED_GROUPS}" ]; then
4141
EXTRA_OPTS="${EXTRA_OPTS} --disable-mirrored-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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
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.")
1818
parser.add_argument("-s", "--client-secret", type=str, help="OAuth client secret.")
19+
parser.add_argument("--background-refresh", action="store_true", default=False,
20+
help="Refresh in the background instead of as needed per request")
21+
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
1922
parser.add_argument("--disable-mirrored-groups", action="store_false",
2023
dest="enable_mirrored_groups", default=True,
2124
help="Disable creation of mirrored groups.")
22-
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
25+
parser.add_argument("--refresh-interval", type=int, default=60,
26+
help="How often to refresh the database in seconds")
27+
2328
# Options for Microsoft Entra backend
2429
entra_group = parser.add_argument_group("Microsoft Entra")
2530
entra_group.add_argument("-t", "--entra-tenant-id", type=str, help="Microsoft Entra tenant ID.", required=False)
@@ -32,6 +37,11 @@
3237
redis_group = parser.add_argument_group("Redis")
3338
redis_group.add_argument("--redis-host", type=str, help="Host for Redis server.")
3439
redis_group.add_argument("--redis-port", type=int, help="Port for Redis server.")
40+
# Options for TLS
41+
tls_group = parser.add_argument_group("TLS")
42+
tls_group.add_argument("--tls-certificate", type=str, help="Location of TLS certificate (pem).")
43+
tls_group.add_argument("--tls-port", type=int, default=1636, help="Port to run on with encryption.")
44+
tls_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)