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

Introduce Role-Based Authentication for Repository Management #1060

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

Conversation

minwoox
Copy link
Member

@minwoox minwoox commented Nov 14, 2024

Motivation:
To address the migration of mirroring configurations from projects to repositories and to simplify access management,
we decided to:

  • Hide the meta repository.
  • Replace permission-based authentication with a role-based system using RepositoryRole (READ, WRITE, ADMIN). The ADMIN role has access to the configurations.

This change resolves the issue where users with WRITE permission for the meta repository were only allowed to creating mirroring configurations.

With RepositoryRole, access becomes more structured and extensible for future enhancements (e.g., introducing custom roles). While permission-based authentication is replaced here, this does not preclude the future coexistence of role-based and permission-based systems.

Modifications:

  • Added RepositoryRole with hierarchical roles (READ, WRITE, ADMIN).
  • Replaced RequiresPermission annotations with RequiresRepositoryRole.
  • Renamed RequiresRole to RequiresProjectRole.
    • Updated it to accept a single ProjectRole, utilizing the hierarchical model to avoid redundant role specifications.
  • Removed APIs for managing permissions, replacing them with role management APIs.

Result:

  • Role-based access simplifies management and aligns with repository-specific mirroring configurations.
  • (Breaking) APIs for managing permissions are removed.

To-do:

  • Update the documentation to reflect the new systme, including updated screenshots

Migration plan:

  • Deploy PR, which supports deserialization of both legacy and new metadata format.
  • Deploy intermediate commit supporting both permission and role APIs, ensuring metadata is stored in the new format.
    • The commit also migrate the legacy format to new format.
  • Deploy this commit, which exclusively supports role-based APIs.

@minwoox minwoox added this to the 0.72.0 milestone Nov 14, 2024
minwoox added a commit to minwoox/centraldogma that referenced this pull request Nov 14, 2024
Motivation
To ensure smooth compatibility with line#1060, the `PerRolePermissions` and `RepositoryMetadata` structure need an update to support changes.

Modifications:
- Enhanced the `PerRolePermissions` and `RepositoryMetadata` deserializers to accept both an array or permissions and a permission.
- Added `REPO_ADMIN` `Permission`.
- Removed `MetadataApiService.updateSpecificUserPermission` and `updateSpecificTokenPermission`.
  - They are not used in the UI and we don't even have test cases for that.
  - Will add those APIs later when we need it.

Result:
- The `PerRolePermissions` and `RepositoryMetadata` structures now support the upcoming changes in line#1060.
@minwoox minwoox changed the title [WIP] Use the highest authority permission Simplify Permission Configuration by Using Highest Authority Nov 18, 2024
@minwoox minwoox changed the title Simplify Permission Configuration by Using Highest Authority [WIP] Simplify Permission Configuration by Using Highest Authority Nov 21, 2024
@RequiresRole(roles = { ProjectRole.OWNER, ProjectRole.MEMBER })
public CompletableFuture<ProjectMetadata> getProjectMetadata(
@Param String projectName,
@Param("checkPermissionOnly") @Default("false") boolean isCheckPermissionOnly) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This parameter was introduced here but has not been used anywhere. Let me just remove that.

@minwoox minwoox changed the title [WIP] Simplify Permission Configuration by Using Highest Authority Introduce Role-Based Authentication for Repository Management Nov 22, 2024
// }
// }
// }
final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/roles/projects");
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that this code will be applied after the migration is complete.

// }
// }
// }
final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/roles/projects");
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: It would be nice to have a DSL/builder to build a JSON pointer type-safely. Let's consider it later.

JsonPointers
  .meta()
  .repos(repoName)
  .roles()
  .projects()
  .compile();

JsonPointers
  .meta()
  .members(repoName)
  .compile();

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good suggestion. By the way, this PR is in WIP and I forgot to tell you about it.
Would you mind reviewing #1064, beforehand?

return repositoryMetadata.perRolePermissions().guest();
@Nullable
private static RepositoryRole repositoryRole(Roles roles, @Nullable RepositoryRole repositoryRole,
@Nullable ProjectRole projectRole) {
Copy link
Contributor

Choose a reason for hiding this comment

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

projectRole seems not null always in the current usage.

Suggested change
@Nullable ProjectRole projectRole) {
ProjectRole projectRole) {

if (memberOrGuestRole == null) {
return repositoryRole;
}
if (memberOrGuestRole == RepositoryRole.WRITE || repositoryRole == RepositoryRole.WRITE) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's say a-member-user is granted to READ role and projects.member is granted to WRITE. Is WRITE given for a-member-user?

{
  "repos": {
    "my-repo": {
      "name": "my-repo",
      "roles": {
        "users": {
          "a-member-user": "READ"
        }
        "projects": {
          "member": "WRITE",
          ...
       }
      }
    }
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Is WRITE given for a-member-user?

That is correct, as similar to how GitHub handles it.
The role with the highest authority will take precedence.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a little surprising - I was expecting the repository role to override the project role.
If that's how Github is doing it, then I guess the current approach is fine.

@minwoox minwoox changed the title Introduce Role-Based Authentication for Repository Management [WIP] Introduce Role-Based Authentication for Repository Management Dec 2, 2024
@minwoox minwoox modified the milestones: 0.72.0, 0.73.0 Dec 4, 2024
minwoox added a commit that referenced this pull request Dec 16, 2024
Motivation
To ensure smooth compatibility with #1060, the `RepositoryMetadata` structure needs an update to support changes.
AS-IS:
```json
{
  "name": "minu-test",
  "perRolePermissions": {
    "owner": [
      "READ",
      "WRITE"
    ],
    "member": ["READ"],
    "guest": []
  },
  "perUserPermissions": {
    "[email protected]": [
      "READ"
    ],
    "[email protected]": [
      "READ",
      "WRITE"
    ]
  },
  "perTokenPermissions": {
    "goodman": [
      "READ"
    ]
  },
  "creation": {
    "user": "[email protected]",
    "timestamp": "2024-08-19T02:47:23.370762417Z"
  }
}
```

TO-BE:
```json
{
  "name": "minu-test",
  "roles": {
    "projects": {
      "member": "READ",
      "guest": null
    }
    "users": {
      "[email protected]": "READ",
      "[email protected]": "WRITE"
    },
    "tokens": {
      "goodman": "READ"
    }
  },
  "creation": {
    "user": "[email protected]",
    "timestamp": "2024-08-19T02:47:23.370762417Z"
  }
}
```

Modifications:
- Enhanced the `RepositoryMetadata` deserializers to accept both legacy and new formats.
- Added `RepositoryRole`.
  - It will be used for repository instead of `Permission`.
- Removed `MetadataApiService.updateSpecificUserPermission` and `updateSpecificTokenPermission`.
  - They are not used in the UI and we don't even have test cases for that.
  - Will add those APIs later with better API design when we need it.

Result:
- The `RepositoryMetadata` structure now supports the upcoming changes in #1060.
@minwoox minwoox changed the title [WIP] Introduce Role-Based Authentication for Repository Management Introduce Role-Based Authentication for Repository Management Dec 17, 2024
@minwoox
Copy link
Member Author

minwoox commented Dec 17, 2024

This PR is ready. PTAL. 🙇

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Apart from MetadataService#repositoryRole, I understood that the rest of changes are simple renaming/refactoring.

return repositoryMetadata.perRolePermissions().member();
default:
return repositoryMetadata.perRolePermissions().guest();
@Nullable
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional) What do you think of adding a RepositoryRole.NONE? It doesn't seem intuitive that a method returning an enum is nullable.

Suggested change
@Nullable

if (memberOrGuestRole == null) {
return repositoryRole;
}
if (memberOrGuestRole == RepositoryRole.WRITE || repositoryRole == RepositoryRole.WRITE) {
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a little surprising - I was expecting the repository role to override the project role.
If that's how Github is doing it, then I guess the current approach is fine.

default:
return repositoryMetadata.perRolePermissions().guest();
@Nullable
private static RepositoryRole repositoryRole(Roles roles, @Nullable RepositoryRole repositoryRole,
Copy link
Contributor

Choose a reason for hiding this comment

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

optional) the logic in this class was not easy to follow at first glance. To me, the following flow makes more sense:

    @Nullable
    private static RepositoryRole repositoryRole(Roles roles, @Nullable RepositoryRole repositoryRole,
                                                 ProjectRole projectRole) {
        final RepositoryRole projectRepositoryRole;
        if (projectRole == ProjectRole.MEMBER) {
            projectRepositoryRole = roles.projectRoles().member();
        } else {
            projectRepositoryRole = roles.projectRoles().guest();
        }
        if (repositoryRole == RepositoryRole.ADMIN || projectRepositoryRole == RepositoryRole.ADMIN) {
            return RepositoryRole.ADMIN;
        }
        if (repositoryRole == RepositoryRole.WRITE || projectRepositoryRole == RepositoryRole.WRITE) {
            return RepositoryRole.ADMIN;
        }
        if (repositoryRole == RepositoryRole.READ || projectRepositoryRole == RepositoryRole.READ) {
            return RepositoryRole.READ;
        }
        return null;
    }

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