Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mirror access control #1085

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

Add mirror access control #1085

wants to merge 8 commits into from

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Jan 3, 2025

Motivation:

Sometimes you need to restrict access to specific repositories without allowing mirroring for all repositories. These repositories may contain sensitive information or code that is only available to a select group of members.

Modifications:

  • Add MirrorAccessController that can allow or disallow access to the remote repositories for mirroring.
    • It is used to check if a scheduled mirroring task or a task triggered by UI is allowed to access.
    • MirrorListener.onCreate() and MirrorListener.onUpdate() events are newly added. The events could be used as an extension point to detect a new remote URL pattern and integrate external systems such as a workflow or an email notification.
  • Add CRUD REST API for administrators
    • GET /api/v1/mirror/access to retrieve all mirror access controls.
    • POST /api/v1/mirror/access to create a mirror access controls.
    • and so on...
  • In addition to the REST API, a mirror access control can be added with MirrorAccessController.allow(...) API which is exposed by PluginContext.mirrorAccessController().
  • Add CrudRepository abstraction to easily create and update a collection of entities in a directory.
    • This API would be useful especially when we implement UI-based CRUD operations.
  • Add mirror access control UI for administrators
    • CRUD operations are supported.

Result:

  • Administrators can restrict access to remote repositories when mirroring.
  • You can receive notifications when a new mirror is created or an existing mirror is updated through MirrorListener.

Motivation:

Sometimes you need to restrict access to specific repositories
without allowing mirroring for all repositories. These repositories may
contain sensitive information or code that is only available to a
select group of members.

Modifications:

- Add `MirrorAccessController` that can allow or disallow access to
  the remote repositories for mirroring.
  - It is used to check a scheduled mirroring task or a task triggered
    by UI is allowed to access.
  - `MirrorListener.onCreate()` and `MirrorListener.onUpdate()` events
    are newly added. The events could be used as an extension point to
    detect a new remote URL pattern and integrate external systems
    such as a workflow or a email notification.
- Add CRUD REST API for administrators
  - GET `/api/v1/mirror/access` to retrieve all mirror access
    controls.
  - POST `/api/v1/mirror/access` to create a mirror access
    controls.
  - and so on...
- In addition to the REST API, a mirror access control can be added
  with `MirrorAccessController.allow(...)` API which is exposed by
  `PluginContext.mirrorAccessController()`.
- Add `CrudRepository` abstraction to easily create and update a
  collection of entities in a directory.
  - This API would be useful especially when we implement UI-based CRUD
    operations.
- Add mirror access control UI for administrators
  - CRUD are supported.

Result:

- Administrators can restrict access to remote repositories when mirroring.
- You can receive notifications when a new mirror is created or an
  existing mirror is updated through `MirrorListener`.
@ikhoon ikhoon added this to the 0.74.0 milestone Jan 3, 2025
@ikhoon
Copy link
Contributor Author

ikhoon commented Jan 3, 2025

Screenshot 2025-01-03 at 5 20 51 PM Screenshot 2025-01-03 at 5 22 03 PM

Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically looks great! Will also review the UI part. 👍

}

@Override
public CompletableFuture<Boolean> allow(String targetPattern, String reason, int order) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason that you encourage using this via PluginContext.mirrorAccessController() instead of the REST API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to use REST API in a Plugin but it can be a hassle.
The plugin needs to manage an access token for each phase.

I was thinking of implementing a plugin to expose a callback as a REST API.
When the callback is invoked, a target pattern will be added to a whitelist or a blacklist via these APIs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to use REST API in a Plugin but it can be a hassle.

What I meant was exposing this API via MirrorAccessControlService. Is there any reason for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MirrorAccessController is going to be used in a plugin to pragmatically add an access control via the API.

logger.info("Allowing the target pattern: {}", accessControl);
// If there is a duplicate target pattern, the order will be considered first.
// If the order is the same, the latest one will be considered first.
return repository().save(accessControl, author).thenApply(unused -> true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we delete this access control later? Do we just need to disallow it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, it isn't supported by MirrorAccessControler API. If necessary, should we use the UI to manage it?

Considering the current usability, I thought the design is sufficient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I was just curious because the ID is automatically generated.

if (acl.isEmpty()) {
// If there is no access control, it is allowed by default.
return Streams.stream(repoUris)
.collect(toImmutableMap(uri -> uri, uri -> true));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Is uri guaranteed to be unique?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. uri is not unique.

Comment on lines 236 to 245
if (!allowed) {
logger.debug("The mirroring from {} is not allowed. mirror: {}",
m.remoteRepoUri(), m);
continue;
}
} catch (Exception e) {
logger.warn("Failed to check the access control. mirror: {}",
m, e);
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Would it be more consistent behavior to notify MirrorListener and log from there if needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MirrorListener.onDisallowed() is added to notify if a mirror is disallowed.
I didn't address the warning log for the exception because it will be raised rarely and the error may be a system-level error.

/**
* Invoked when a new {@link Mirror} is created.
*/
void onCreate(Mirror mirror, MirrorAccessController accessController);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) What do you think of only passing the mirrorId here and instead creating a MirrorContext similar to how PluginContext is defined which contains ProjectManager, CommandExecutor, etc..

CentralDogma could maintain an internal CompositeMirrorListener which holds this context, and pass it on to user-defined child MirrorListeners.

This way

  1. we can actually handle the failure from the extra lookup in when creating/updating a mirror
  2. there is more freedom in MirrorListener to modify/read from the repository

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the extra lookup is useless for the default implementation but the overhead would not be significant.

However, it is not enough to just give a mirror ID. Since a snapshot of when it was created is needed, a revision should also be provided. Instead of adding more contexts to the API, I think the current style is not bad.

It may be possible to give more freedom, but I doubt the features are useful at this point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can actually handle the failure from the extra lookup in when creating/updating a mirror

For more context, I imagined that we will notify users when a mirror is created/updated, but not allowed.
My concern wasn't really about overhead, but rather if there is a way to monitor/apply fallback logic if the callback isn't invoked due to failure from the extra IO.
(I doubt we will be looking at exception logs since we aren't actively monitoring logs at the moment)

I'm fine with the current design, but wanted to make the above point clear

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the async processing is not good from a transaction perspective. If we only pass the mirror ID, there is an advantage that MirrorListener is always called when the commit is pushed.

Initially, I wished to design a simple API. I will consider improving this point in the future.

Copy link

codecov bot commented Jan 7, 2025

Codecov Report

Attention: Patch coverage is 63.15789% with 175 lines in your changes missing coverage. Please review.

Project coverage is 70.19%. Comparing base (f4dda40) to head (63d5399).
Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...rp/centraldogma/internal/api/v1/MirrorRequest.java 44.77% 37 Missing ⚠️
...internal/mirror/DefaultMirrorAccessController.java 54.16% 31 Missing and 2 partials ⚠️
...ernal/api/sysadmin/MirrorAccessControlRequest.java 45.16% 17 Missing ⚠️
...ldogma/server/internal/api/MirroringServiceV1.java 56.25% 13 Missing and 1 partial ⚠️
...erver/internal/mirror/CompositeMirrorListener.java 47.61% 11 Missing ⚠️
...rnal/storage/repository/git/GitCrudRepository.java 75.55% 10 Missing and 1 partial ⚠️
...nternal/storage/repository/DefaultHasRevision.java 33.33% 10 Missing ⚠️
...ma/server/internal/mirror/MirrorAccessControl.java 72.72% 9 Missing ⚠️
...erver/internal/mirror/MirrorSchedulingService.java 50.00% 5 Missing and 1 partial ⚠️
...traldogma/server/internal/mirror/MirrorRunner.java 79.16% 4 Missing and 1 partial ⚠️
... and 8 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1085      +/-   ##
============================================
- Coverage     70.29%   70.19%   -0.11%     
- Complexity     4284     4372      +88     
============================================
  Files           427      441      +14     
  Lines         17258    17622     +364     
  Branches       1915     1939      +24     
============================================
+ Hits          12131    12369     +238     
- Misses         4077     4207     +130     
+ Partials       1050     1046       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@minwoox
Copy link
Contributor

minwoox commented Jan 7, 2025

Sorry for causing the conflict. 😓

@ikhoon
Copy link
Contributor Author

ikhoon commented Jan 7, 2025

No worries. I expected it in advance.

Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! 👍 👍 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants